Skip to main content

Email Models

Automatic Backend Selection

Django-CFG provides type-safe email configuration with automatic backend selection - Console for development, SMTP/SendGrid for production.

Django-CFG provides EmailConfig model for type-safe email configuration with support for SMTP, SendGrid, and Console backends.

EmailConfig

Type-safe email service configuration.

Complete Model

from pydantic import BaseModel, Field, field_validator
from typing import Optional, Literal

class EmailConfig(BaseModel):
"""
Email service configuration for SMTP, SendGrid, or console backend.
"""

backend: Literal["console", "smtp", "sendgrid"] = Field(
default="console",
description="Email backend type"
)
host: str = Field(
default="localhost",
description="SMTP server hostname"
)
port: int = Field(
default=587,
description="SMTP server port",
ge=1,
le=65535
)
username: Optional[str] = Field(
default=None,
description="SMTP username"
)
password: Optional[str] = Field(
default=None,
description="SMTP password"
)
use_tls: bool = Field(
default=True,
description="Use TLS encryption"
)
use_ssl: bool = Field(
default=False,
description="Use SSL encryption"
)
default_from: str = Field(
default="noreply@example.com",
description="Default FROM email address"
)
timeout: int = Field(
default=30,
description="Email send timeout in seconds",
ge=1
)

@field_validator('backend')
@classmethod
def validate_backend(cls, v):
"""Validate email backend"""
if v not in ['console', 'smtp', 'sendgrid']:
raise ValueError("Backend must be 'console', 'smtp', or 'sendgrid'")
return v

def to_django_config(self) -> dict:
"""Convert to Django email settings"""
if self.backend == "console":
return {
'EMAIL_BACKEND': 'django.core.mail.backends.console.EmailBackend'
}
elif self.backend == "sendgrid":
return {
'EMAIL_BACKEND': 'sendgrid_backend.SendgridBackend',
'SENDGRID_API_KEY': self.password,
'EMAIL_HOST_USER': self.username,
'DEFAULT_FROM_EMAIL': self.default_from,
}
else: # smtp
return {
'EMAIL_BACKEND': 'django.core.mail.backends.smtp.EmailBackend',
'EMAIL_HOST': self.host,
'EMAIL_PORT': self.port,
'EMAIL_HOST_USER': self.username,
'EMAIL_HOST_PASSWORD': self.password,
'EMAIL_USE_TLS': self.use_tls,
'EMAIL_USE_SSL': self.use_ssl,
'DEFAULT_FROM_EMAIL': self.default_from,
'EMAIL_TIMEOUT': self.timeout,
}

Usage Examples

from django_cfg import DjangoConfig
from django_cfg.models import EmailConfig

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"
debug: bool = True

email: EmailConfig = EmailConfig(
backend="console" # Prints emails to console
)

Generated Django settings:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

Usage:

from django.core.mail import send_mail

send_mail(
'Subject',
'Message body',
'from@example.com',
['to@example.com'],
)
# Email printed to console instead of sent
Console Backend Use Case

Console backend is ideal for:

  • Development - See email content without sending
  • Testing - Verify email content in unit tests
  • Debugging - Inspect email formatting

Not suitable for:

  • ❌ Production environments
  • ❌ Actual email delivery
  • ❌ User notifications

Provider-Specific Examples

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.gmail.com",
port=587,
username="your-email@gmail.com",
password="your-app-password", # Generate at https://myaccount.google.com/apppasswords
use_tls=True,
default_from="your-email@gmail.com"
)
Gmail Requirements

Before using Gmail SMTP:

  1. ✅ Enable 2-Factor Authentication in Gmail
  2. ✅ Generate App Password at https://myaccount.google.com/apppasswords
  3. ✅ Use App Password (not regular password)
  4. ✅ Verify FROM address matches Gmail account

Common Issues:

  • ❌ "Username and Password not accepted" - Use App Password
  • ❌ Daily sending limit: 500 emails/day for free accounts
  • ❌ Rate limits apply - consider dedicated SMTP for production

SSL vs TLS

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.example.com",
port=587,
use_tls=True, # ✅ TLS encryption
use_ssl=False,
username="user",
password="pass"
)

Advantages:

  • Standard port for STARTTLS
  • More compatible
  • Recommended by most providers

SSL (Port 465)

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.example.com",
port=465,
use_tls=False,
use_ssl=True, # ✅ SSL encryption
username="user",
password="pass"
)

Advantages:

  • Older standard
  • Some providers require it

Environment-Specific Configuration

Using Properties

import os

class MyConfig(DjangoConfig):
secret_key: str = "your-secret-key"

@property
def email(self) -> EmailConfig:
if self._environment == "production":
return EmailConfig(
backend="sendgrid",
username="apikey",
password=os.getenv('SENDGRID_API_KEY'),
default_from="noreply@myapp.com"
)
elif self._environment == "staging":
return EmailConfig(
backend="smtp",
host="smtp.gmail.com",
port=587,
username=os.getenv('SMTP_USER'),
password=os.getenv('SMTP_PASSWORD'),
use_tls=True,
default_from="staging@myapp.com"
)
else: # development
return EmailConfig(
backend="console"
)

Using Environment Variables

# Production (.env or system ENV)
EMAIL__BACKEND="sendgrid"
EMAIL__USERNAME="apikey"
EMAIL__PASSWORD="${SENDGRID_API_KEY}"
EMAIL__DEFAULT_FROM="noreply@myapp.com"

# Development (.env)
EMAIL__BACKEND="console"
# api/environment/loader.py
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

class EmailConfig(BaseSettings):
backend: str = Field(default="console")
host: str = Field(default="localhost")
port: int = Field(default=587)
username: str | None = Field(default=None)
password: str | None = Field(default=None)
use_tls: bool = Field(default=True)
default_from: str = Field(default="noreply@example.com")

model_config = SettingsConfigDict(
env_prefix="EMAIL__",
env_nested_delimiter="__",
)

Advanced Usage

Custom Timeout

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.example.com",
port=587,
username="user",
password="pass",
use_tls=True,
timeout=60 # 60 seconds timeout for slow connections
)

Multiple FROM Addresses

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.gmail.com",
port=587,
username="your-email@gmail.com",
password="your-app-password",
use_tls=True,
default_from="noreply@myapp.com" # Default FROM
)

Usage:

from django.core.mail import send_mail

# Use default FROM
send_mail('Subject', 'Body', None, ['to@example.com'])

# Override FROM
send_mail('Subject', 'Body', 'support@myapp.com', ['to@example.com'])

Testing

Send Test Email

from django.core.mail import send_mail

send_mail(
'Test Email',
'This is a test email from Django-CFG',
'noreply@myapp.com',
['test@example.com'],
fail_silently=False,
)

Test Configuration

# tests/test_email.py
from django.core.mail import send_mail
from django.test import TestCase, override_settings

class EmailTestCase(TestCase):
@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend')
def test_send_email(self):
send_mail('Subject', 'Body', 'from@example.com', ['to@example.com'])

from django.core.mail import outbox
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].subject, 'Subject')

Security Best Practices

Email Security Critical

Email credentials are high-value targets for attackers. Compromised email access can lead to:

  • ❌ Spam/phishing sent from your domain
  • ❌ Reputation damage and blacklisting
  • ❌ Account takeovers via password resets
  • ❌ Data breaches through email access

Always follow ALL security practices below.

1. Use Environment Variables

Never Hardcode Credentials

Hardcoded email credentials can be:

  • Leaked through version control (git history)
  • Exposed in error messages and logs
  • Found via code search tools
  • Stolen if server is compromised

❌ Bad:

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
username="user@example.com", # ❌ EXPOSED
password="actual-password-123", # ❌ LEAKED
)

✅ Good:

import os

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host=os.getenv('EMAIL_HOST'),
port=int(os.getenv('EMAIL_PORT', '587')),
username=os.getenv('EMAIL_USER'),
password=os.getenv('EMAIL_PASSWORD'),
use_tls=True,
default_from=os.getenv('DEFAULT_FROM_EMAIL')
)

Troubleshooting

Authentication Failed (Gmail)

Error:

SMTPAuthenticationError: Username and Password not accepted

Solution:

Connection Timeout

Error:

socket.timeout: timed out

Solution:

class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.example.com",
port=587,
timeout=60, # Increase timeout
username="user",
password="pass"
)

TLS/SSL Errors

Error:

ssl.SSLError: [SSL: WRONG_VERSION_NUMBER]

Solution:

# Try port 465 with SSL instead of 587 with TLS
class MyConfig(DjangoConfig):
email: EmailConfig = EmailConfig(
backend="smtp",
host="smtp.example.com",
port=465,
use_tls=False,
use_ssl=True, # Use SSL instead of TLS
username="user",
password="pass"
)

SendGrid Errors

Error:

HTTP Error 403: Forbidden

Solution:

  • Verify API key is correct
  • Check SendGrid account is active
  • Verify FROM address is verified in SendGrid

Validation

EmailConfig validates:

  • Backend - Must be 'console', 'smtp', or 'sendgrid'
  • Port - Must be 1-65535
  • Timeout - Must be ≥ 1 second

Example validation error:

# ❌ Invalid backend
EmailConfig(
backend="invalid" # Validation error
)

# ❌ Invalid port
EmailConfig(
backend="smtp",
port=99999 # Validation error
)

See Also