---
name: litestar-email
description: "Auto-activate for litestar_email, EmailPlugin, EmailConfig, EmailService, EmailMessage, SMTPConfig, ResendConfig, SendGridConfig, MailgunConfig, SESConfig, or InMemoryBackend. Not for marketing platforms."
---

# litestar-email

`litestar-email` provides a pluggable email-sending abstraction for Litestar. One config + plugin, swap backends without touching call sites.

Backends:

- `SMTPConfig` — generic SMTP via `aiosmtplib`
- `ResendConfig` — Resend HTTP API
- `SendGridConfig` — SendGrid HTTP API
- `MailgunConfig` — Mailgun HTTP API
- `SESConfig` — Amazon SES API v2
- `backend="memory"` / `InMemoryBackend` — for tests; captures messages in `InMemoryBackend.outbox`
- `backend="console"` — for local development; prints messages

## Code Style Rules

- PEP 604 unions: `T | None`, never `Optional[T]`
- Consumer Litestar app modules MAY use `from __future__ import annotations`
- Async all I/O — `EmailService.send_message` is `async`

## Quick Reference

### Install

```bash
pip install litestar-email
# Optional extras for specific backends:
pip install litestar-email[smtp]
pip install litestar-email[ses]
pip install litestar-email[aiohttp]
```

### Basic Setup

```python
from litestar import Litestar
from litestar_email import EmailPlugin, EmailConfig, SMTPConfig

app = Litestar(plugins=[EmailPlugin(config=EmailConfig(
    backend=SMTPConfig(
        host="smtp.example.com",
        port=587,
        use_tls=True,
        username="user@example.com",
        password="secret",
    ),
    from_email="noreply@example.com",
    from_name="My App",
))])
```

### EmailConfig

| Option | Type | Description |
| --- | --- | --- |
| `backend` | `str \| BackendConfig` | One of `"console"`, `"memory"`, `SMTPConfig`, `ResendConfig`, `SendGridConfig`, `MailgunConfig`, `SESConfig` |
| `from_email` | `str` | Default sender address |
| `from_name` | `str \| None` | Optional display name |

### Backend Configs

#### SMTPConfig

```python
from litestar_email import SMTPConfig

SMTPConfig(
    host="smtp.gmail.com",
    port=587,
    use_tls=True,           # STARTTLS
    use_ssl=False,          # Implicit SSL (port 465)
    username="you@gmail.com",
    password="app-password",
    timeout=10,
)
```

#### ResendConfig

```python
from litestar_email import ResendConfig
ResendConfig(api_key="re_xxxxxxxxxx")
```

#### SendGridConfig

```python
from litestar_email import SendGridConfig
SendGridConfig(api_key="SG.xxxxxxxxxx")
```

#### MailgunConfig

```python
from litestar_email import MailgunConfig
MailgunConfig(api_key="key-xxxxxxxxxx", domain="mg.example.com", region="us")
```

#### Memory backend (testing)

```python
from litestar_email import EmailConfig
from litestar_email.backends import InMemoryBackend

InMemoryBackend.clear()
config = EmailConfig(backend="memory", from_email="test@example.com")
# Stores sent messages in memory; inspect InMemoryBackend.outbox
```

### Dependency Injection

`EmailPlugin.on_app_init` registers an `EmailService` dependency as `mailer` by default. Override `email_service_dependency_key` if the project already standardizes on another parameter name.

```python
from litestar import post
from litestar_email import EmailService, EmailMessage

@post("/send-notification")
async def send_notification(
    mailer: EmailService,
    data: NotificationRequest,
) -> dict:
    await mailer.send_message(EmailMessage(
        to=[data.recipient],
        subject="Notification",
        body="You have a new notification.",
        html_body="<p>You have a new notification.</p>",
    ))
    return {"sent": True}
```

### EmailMessage

```python
from litestar_email import EmailMessage

EmailMessage(
    to=["recipient@example.com"],         # required
    subject="Hello",                       # required
    body="Plain text body",                # optional
    html_body="<p>HTML body</p>",          # optional
    cc=["cc@example.com"],
    bcc=["bcc@example.com"],
    reply_to="reply@example.com",
    from_email="override@example.com",     # overrides EmailConfig default
    from_name="Override Name",
    headers={"X-Custom": "value"},
    attachments=[("/path/to/file.pdf", "application/pdf")],
)
```

### EmailMultiAlternatives

```python
from litestar_email import EmailMultiAlternatives

msg = EmailMultiAlternatives(
    to=["user@example.com"],
    subject="Welcome",
    body="Welcome to our platform.",
    html_body="<p>Welcome to our platform.</p>",
)
await email_service.send_message(msg)
```

### EmailService Methods

| Method | Description |
| --- | --- |
| `send_message(msg)` | Send a single `EmailMessage` |
| `send_messages(msgs)` | Batch send |

Both are `async`.

### Connection Pooling (SMTP)

```python
async with email_service as svc:
    await svc.send_message(msg1)
    await svc.send_message(msg2)
```

### Standalone Usage (no DI)

```python
from litestar_email import EmailConfig, SMTPConfig, EmailMessage

config = EmailConfig(
    backend=SMTPConfig(host="smtp.example.com", port=587, use_tls=True),
    from_email="noreply@example.com",
)

async def main():
    async with config.provide_service() as email_service:
        await email_service.send_message(EmailMessage(
            to=["user@example.com"], subject="Hello", body="World",
        ))
```

### Templating

`litestar-email` does not ship a templating engine. Use Litestar's Jinja2 integration to render `body` / `html_body` strings before constructing `EmailMessage`:

```python
from litestar.template import TemplateEngineProtocol

async def send_welcome(
    mailer: EmailService,
    template_engine: TemplateEngineProtocol,
    user: User,
) -> None:
    html = template_engine.render("emails/welcome.html", {"user": user})
    text = template_engine.render("emails/welcome.txt", {"user": user})
    await mailer.send_message(EmailMessage(
        to=[user.email],
        subject="Welcome!",
        body=text,
        html_body=html,
    ))
```

<workflow>

## Workflow

### Step 1: Install + Pick Backend

| Need | Backend |
| --- | --- |
| Generic SMTP / corporate mail | `SMTPConfig` |
| Modern transactional API | `ResendConfig` (preferred for new projects) |
| Existing SendGrid contract | `SendGridConfig` |
| Mailgun account | `MailgunConfig` |
| Any test environment | `backend="memory"` / `InMemoryBackend` |
| AWS-native transactional mail | `SESConfig` |

### Step 2: Configure Plugin

Build `EmailConfig(backend=..., from_email=..., from_name=...)` and wrap in `EmailPlugin`. Add to `Litestar(plugins=[...])`.

### Step 3: Inject EmailService

In handlers / services, declare `email_service: EmailService` parameter. Litestar's DI provides it.

### Step 4: Construct EmailMessage

Use `EmailMessage` for simple sends. Use `EmailMultiAlternatives` if you need multiple HTML parts. Render templates separately if needed.

### Step 5: Background Send (recommended for slow ops)

For non-interactive flows, enqueue email sending via `litestar-saq` rather than blocking the request. See `../litestar-saq/SKILL.md`.

```python
await task_queues.get("default").enqueue(
    "send_welcome_email",
    user_id=user.id,
    timeout=30,
    retries=2,
    key=f"welcome-{user.id}",
)
```

### Step 6: Test with InMemoryBackend

In test config, swap `backend="memory"`. Clear and assert against `InMemoryBackend.outbox`.

</workflow>

<guardrails>

## Guardrails

- **Use `backend="memory"` in all test environments** — no real network calls; `InMemoryBackend.outbox` captures messages for assertions.
- **Background-queue email sends** — use `litestar-saq` for transactional email. SMTP can be slow; blocking handlers degrades p99.
- **Set `from_email` at the plugin level** — overriding per message is for exceptions, not the default.
- **Use `Resend` or `SendGrid` for high-volume transactional** — direct SMTP scales poorly past ~100/s.
- **Never log passwords/API keys** — sanitize `EmailConfig.backend` before structlog dumps.
- **Validate recipient addresses at the API boundary** — invalid addresses cause backend errors and waste retries.
- **Set timeouts** — `SMTPConfig.timeout` defaults are usually fine; tune if your SMTP host is slow.
- **Don't ship unused extras** — `[smtp]`, `[ses]`, and `[aiohttp]` are opt-in dependencies.

</guardrails>

<validation>

### Validation Checkpoint

Before delivering email-sending code, verify:

- [ ] `EmailPlugin` is in `app.plugins`
- [ ] Backend is appropriate for env (`backend="memory"` in tests, real backend in dev/prod)
- [ ] `from_email` is configured at the `EmailConfig` level
- [ ] Handler injects `EmailService` via DI, usually as `mailer`
- [ ] `EmailMessage` is constructed with required `to` and `subject`
- [ ] Slow / retry-able sends are queued via `litestar-saq` instead of blocking the request
- [ ] Tests assert against `InMemoryBackend.outbox`
- [ ] Secrets (`password`, `api_key`) come from env / settings, not hard-coded

</validation>

<example>

## Example

**Task:** Welcome-email flow that queues a SAQ task to send via Resend; test asserts via `InMemoryBackend`.

```python
# app/config/email.py
from litestar_email import EmailConfig, ResendConfig
from app.lib.settings import get_settings

def get_email_config() -> EmailConfig:
    settings = get_settings()
    if settings.env == "test":
        return EmailConfig(backend="memory", from_email="test@example.com")
    return EmailConfig(
        backend=ResendConfig(api_key=settings.resend.api_key),
        from_email=settings.email.from_email,
        from_name=settings.email.from_name,
    )
```

```python
# app/server/plugins.py
from litestar_email import EmailPlugin
from app.config.email import get_email_config

email = EmailPlugin(config=get_email_config())
```

```python
# app/domain/accounts/tasks.py
from litestar_email import EmailMessage

async def send_welcome_email(ctx: dict, *, user_id: int, email: str, name: str) -> None:
    """Send welcome email as a SAQ background task."""
    email_service = ctx["state"]["email_service"]
    template_engine = ctx["state"]["template_engine"]
    html = template_engine.render("emails/welcome.html", {"name": name})
    await email_service.send_message(EmailMessage(
        to=[email],
        subject=f"Welcome, {name}!",
        body=f"Welcome, {name}!",
        html_body=html,
    ))
```

```python
# app/domain/accounts/controllers.py
from litestar import Controller, post
from litestar_saq import TaskQueues

class AccountController(Controller):
    path = "/api/accounts"

    @post("/")
    async def create_account(self, data: AccountCreate, task_queues: TaskQueues) -> Account:
        user = await self.create(data)
        await task_queues.get("default").enqueue(
            "send_welcome_email",
            user_id=user.id, email=user.email, name=user.name,
            timeout=30, retries=2, key=f"welcome-{user.id}",
        )
        return user
```

```python
# tests/test_accounts.py
async def test_account_creation_queues_welcome_email(client, email_service):
    from litestar_email.backends import InMemoryBackend

    InMemoryBackend.clear()
    resp = await client.post("/api/accounts", json={"email": "alice@example.com", "name": "Alice"})
    assert resp.status_code == 201
    # After SAQ flush in test:
    assert len(InMemoryBackend.outbox) == 1
    assert InMemoryBackend.outbox[0].subject == "Welcome, Alice!"
```

</example>

---

## Cross-References

- **[litestar](../litestar/SKILL.md)** — DI, plugin lifecycle.
- **[litestar-saq](../litestar-saq/SKILL.md)** — Background-queue email sends.
- **[litestar-testing](../litestar-testing/SKILL.md)** — Testing flows that send email.

## Official References

- <https://github.com/litestar-org/litestar-email>

## Shared Styleguide Baseline

- [General Principles](../litestar-styleguide/references/general.md)
- [Python](../litestar-styleguide/references/python.md)
- [Litestar](../litestar-styleguide/references/litestar.md)
