---
title: Email Models
description: EmailConfig model for SMTP and Console email backends
sidebar_label: Email Models
sidebar_position: 6
---

# Email Models


<Callout type="info">
**Automatic Backend Selection**
Django-CFG provides **type-safe email configuration** with automatic backend selection - Console for development, SMTP for production.

</Callout>

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

## EmailConfig

Type-safe email service configuration.

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `backend` | `Literal["console", "smtp"]` | `"console"` | Email backend type |
| `host` | `str` | `"localhost"` | SMTP server hostname |
| `port` | `int` | `587` | SMTP server port (1-65535) |
| `username` | `str \| None` | `None` | SMTP username |
| `password` | `str \| None` | `None` | SMTP password |
| `use_tls` | `bool` | `True` | Use TLS encryption |
| `use_ssl` | `bool` | `False` | Use SSL encryption |
| `default_from` | `str` | `"noreply@example.com"` | Default FROM email address |
| `timeout` | `int` | `30` | Email send timeout in seconds (≥1) |

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `to_django_config()` | `dict` | Convert to Django email settings |

### Backend Comparison

| Backend | Use Case | Features |
|---------|----------|----------|
| `console` | Development | Prints emails to console, no actual delivery |
| `smtp` | Production | Standard SMTP delivery (Gmail, SES, Mailgun) |

## Usage Examples

### Console (Development)

```python
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:**
```python
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
```

**Usage:**
```python
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
```


<Callout type="info">
**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

</Callout>

### SMTP (Production)

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

    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",
        timeout=30
    )
```

**Generated Django settings:**
```python
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
DEFAULT_FROM_EMAIL = 'noreply@myapp.com'
EMAIL_TIMEOUT = 30
```


<Callout type="warning">
**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

</Callout>

## Provider-Specific Examples

### Gmail SMTP

```python
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"
    )
```


<Callout type="warning">
**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

</Callout>

### AWS SES

```python
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="verified@yourdomain.com"  # Must be verified in SES
    )
```


<Callout type="info">
**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

</Callout>

### Mailgun SMTP

```python
class MyConfig(DjangoConfig):
    email: EmailConfig = EmailConfig(
        backend="smtp",
        host="smtp.mailgun.org",
        port=587,
        username="postmaster@yourdomain.mailgun.org",
        password="your-mailgun-smtp-password",
        use_tls=True,
        default_from="noreply@yourdomain.com"
    )
```


<Callout type="info">
**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

</Callout>

### Office 365

```python
class MyConfig(DjangoConfig):
    email: EmailConfig = EmailConfig(
        backend="smtp",
        host="smtp.office365.com",
        port=587,
        username="your-email@yourdomain.com",
        password="your-password",
        use_tls=True,
        default_from="your-email@yourdomain.com"
    )
```


<Callout type="warning">
**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

</Callout>


## SSL vs TLS

### TLS (Port 587) - Recommended

```python
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)

```python
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

```python
import os

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

    @property
    def email(self) -> EmailConfig:
        if self._environment == "production":
            return EmailConfig(
                backend="smtp",
                host=os.getenv('EMAIL_HOST'),
                port=587,
                username=os.getenv('SMTP_USER'),
                password=os.getenv('SMTP_PASSWORD'),
                use_tls=True,
                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

```bash
# Production (.env or system ENV)
EMAIL__BACKEND="smtp"
EMAIL__HOST="smtp.example.com"
EMAIL__USERNAME="your-email@example.com"
EMAIL__PASSWORD="${EMAIL_PASSWORD}"
EMAIL__DEFAULT_FROM="noreply@myapp.com"

# Development (.env)
EMAIL__BACKEND="console"
```

```python
# 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

```python
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

```python
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:**
```python
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

```python
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

```python
# 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


<Callout type="error">
**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.**

</Callout>

### Environment Variables

### 1. Use Environment Variables


<Callout type="warning">
**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

</Callout>

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

**✅ Good:**
```python
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


<Callout type="error">
**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)

</Callout>

**❌ Bad:**
```python
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:**
```python
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


<Callout type="warning">
**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.

</Callout>

**✅ Gmail with App Password:**
```python
class MyConfig(DjangoConfig):
    email: EmailConfig = EmailConfig(
        backend="smtp",
        host="smtp.gmail.com",
        port=587,
        username="your-email@gmail.com",
        password="abcd efgh ijkl mnop",  # ✅ 16-char App Password
        use_tls=True
    )
```

### Domain Verification

### 4. Validate FROM Address


<Callout type="info">
**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

</Callout>

**✅ Good:**
```python
class MyConfig(DjangoConfig):
    email: EmailConfig = EmailConfig(
        backend="smtp",
        host="smtp.example.com",
        port=587,
        username="user",
        password="pass",
        use_tls=True,
        default_from="noreply@verified-domain.com"  # ✅ Verified domain
    )
```


## Troubleshooting

### Authentication Failed (Gmail)

**Error:**
```
SMTPAuthenticationError: Username and Password not accepted
```

**Solution:**
- Enable 2FA in Gmail
- Generate App Password at https://myaccount.google.com/apppasswords
- Use App Password instead of regular password

### Connection Timeout

**Error:**
```
socket.timeout: timed out
```

**Solution:**
```python
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:**
```python
# 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"
    )
```

## Validation

EmailConfig validates:

- **Backend** - Must be 'console' or 'smtp'
- **Port** - Must be 1-65535
- **Timeout** - Must be ≥ 1 second

**Example validation error:**

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

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

## See Also

- [**DjangoConfig**](./django-settings) - Base configuration class
- [**Configuration Overview**](./) - Configuration system overview
