Skip to 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="[email protected]", 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

Console (Development)

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', '[email protected]', ['[email protected]'], ) # 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

SMTP (Production)

class MyConfig(DjangoConfig): secret_key: str = "your-secret-key" debug: bool = False email: EmailConfig = EmailConfig( backend="smtp", host="smtp.gmail.com", port=587, username="[email protected]", password="your-app-password", use_tls=True, default_from="[email protected]", timeout=30 )

Generated Django settings:

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = 'smtp.gmail.com' EMAIL_PORT = 587 EMAIL_HOST_USER = '[email protected]' EMAIL_HOST_PASSWORD = 'your-app-password' EMAIL_USE_TLS = True EMAIL_USE_SSL = False DEFAULT_FROM_EMAIL = '[email protected]' EMAIL_TIMEOUT = 30

SMTP Credentials Security Never hardcode SMTP credentials:

  • Use environment variables for username/password
  • Use app-specific passwords (not account passwords)
  • Enable TLS encryption (use_tls=True)
  • Store credentials in secure secrets manager

SendGrid (Production)

class MyConfig(DjangoConfig): secret_key: str = "your-secret-key" debug: bool = False email: EmailConfig = EmailConfig( backend="sendgrid", username="apikey", password="SG.your-sendgrid-api-key", default_from="[email protected]" )

Generated Django settings:

EMAIL_BACKEND = 'sendgrid_backend.SendgridBackend' SENDGRID_API_KEY = 'SG.your-sendgrid-api-key' EMAIL_HOST_USER = 'apikey' DEFAULT_FROM_EMAIL = '[email protected]'

SendGrid Benefits SendGrid provides:

  • High deliverability - Dedicated IP addresses
  • Analytics - Email open/click tracking
  • Templates - Transactional email templates
  • Scalability - Send millions of emails
  • No SMTP management - API-based delivery

Requirements:

  • Install sendgrid-django package
  • Verify sender domain in SendGrid dashboard
  • Use API key (not SMTP credentials)

Provider-Specific Examples

Gmail SMTP

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

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

AWS SES

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="email-smtp.us-east-1.amazonaws.com", port=587, username="YOUR_SES_SMTP_USERNAME", password="YOUR_SES_SMTP_PASSWORD", use_tls=True, default_from="[email protected]" # Must be verified in SES )

AWS SES Configuration Setup Steps:

  1. Verify sender email/domain in AWS SES console
  2. Create SMTP credentials (IAM → SES SMTP credentials)
  3. Request production access (sandbox mode limited to verified emails)
  4. Configure SPF/DKIM records for better deliverability

Benefits:

  • ✅ High deliverability and reputation
  • ✅ Pay-as-you-go pricing ($0.10 per 1000 emails)
  • ✅ Integrated with AWS ecosystem
  • ✅ Detailed sending statistics

Mailgun SMTP

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="smtp.mailgun.org", port=587, username="[email protected]", password="your-mailgun-smtp-password", use_tls=True, default_from="[email protected]" )

Mailgun Setup

Configuration:

  • Use sandbox domain for testing
  • Add custom domain for production
  • Verify domain with DNS records
  • Get SMTP credentials from Mailgun dashboard

Free Tier:

  • 5,000 emails/month free
  • Email validation API included
  • Analytics and tracking

Office 365

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="smtp.office365.com", port=587, username="[email protected]", password="your-password", use_tls=True, default_from="[email protected]" )

Office 365 Notes Requirements:

  • Valid Office 365 subscription
  • FROM address must match authenticated user
  • Modern authentication may require app password

Limitations:

  • 30 messages/minute rate limit
  • 10,000 recipients/day limit
  • Not recommended for bulk sending

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="[email protected]" ) 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="[email protected]" ) 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="[email protected]" # 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="[email protected]") 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="[email protected]", password="your-app-password", use_tls=True, default_from="[email protected]" # Default FROM )

Usage:

from django.core.mail import send_mail # Use default FROM send_mail('Subject', 'Body', None, ['[email protected]']) # Override FROM send_mail('Subject', 'Body', '[email protected]', ['[email protected]'])

Testing

Send Test Email

from django.core.mail import send_mail send_mail( 'Test Email', 'This is a test email from Django-CFG', '[email protected]', ['[email protected]'], 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', '[email protected]', ['[email protected]']) 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.

Environment Variables

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="[email protected]", # ❌ 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') )

TLS/SSL Encryption

2. Enable TLS/SSL

Unencrypted Email Without TLS/SSL encryption:

  • Passwords transmitted in plain text
  • Email content readable by network sniffers
  • Man-in-the-middle attacks possible
  • Compliance violations (GDPR, HIPAA)

❌ Bad:

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="smtp.example.com", port=25, # ❌ Unencrypted port use_tls=False, # ❌ No encryption use_ssl=False, # ❌ No encryption )

✅ Good:

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

App Passwords

3. Use App Passwords

Gmail App Passwords Required Gmail blocks regular passwords for SMTP since May 2022. You must:

  1. Enable 2-Factor Authentication
  2. Generate App Password (16-character code)
  3. Use App Password for SMTP (not account password)

Regular passwords will fail with “Username and Password not accepted” error.

✅ Gmail with App Password:

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="smtp.gmail.com", port=587, username="[email protected]", password="abcd efgh ijkl mnop", # ✅ 16-char App Password use_tls=True )

Domain Verification

4. Validate FROM Address

Domain Reputation Using verified domains improves:

  • Deliverability - Lower spam scores
  • Trust - SPF/DKIM authentication
  • Reputation - Sender reputation tracking

Unverified domains:

  • ❌ High spam scores
  • ❌ Emails blocked by providers
  • ❌ No SPF/DKIM authentication

✅ Good:

class MyConfig(DjangoConfig): email: EmailConfig = EmailConfig( backend="smtp", host="smtp.example.com", port=587, username="user", password="pass", use_tls=True, default_from="[email protected]" # ✅ Verified domain )

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