Litestar email
Opinionated first-party agent skills, plugins, subagents, slash commands, and MCP servers for the Litestar framework ecosystem — publishable to Claude Code, Gemini CLI, Codex CLI, Cursor, OpenCode, and VS Code/Copilot from a single repo.
npx -y skills add litestar-org/litestar-skills --skill litestar-emailAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Auto-activate for litestar_email, EmailPlugin, EmailConfig, EmailService, EmailMessage, SMTPConfig, ResendConfig, SendGridConfig, MailgunConfig, SESConfig, or InMemoryBackend. Not for marketing-campaign platforms — use their dedicated SDKs.
SKILL.md
15.7 KB, as published. Nobody here has run it
litestar-email
litestar-email 0.4.0 provides one async sending interface for console, memory,
SMTP, Resend, SendGrid, Mailgun, Amazon SES, and custom backends. Match the
backend already selected by the project; keep message construction independent
from the transport.
Code Style Rules
- Use
NamedDependency[EmailService]for handler injection. The plugin registers a named Litestar dependency, not a global service singleton. - Pass recipient collections as
list[str].to,cc,bcc, andreply_toare list fields. - Pass attachment content as
bytes. Do file I/O before constructing the message and keep that I/O async. - Await
send_message()andsend_messages(). Both return the count sent. - Keep API keys and SMTP credentials in the project's settings layer.
Quick Reference
Install
pip install "litestar-email>=0.4.0"
pip install "litestar-email[smtp]>=0.4.0" # aiosmtplib
pip install "litestar-email[ses]>=0.4.0" # botocore for SigV4
pip install "litestar-email[aiohttp]>=0.4.0" # optional HTTP transport
The HTTP API backends use httpx by default. Select the aiohttp extra only
when the project already standardizes on that transport.
Configure the Plugin
from os import environ
from litestar import Litestar
from litestar_email import EmailConfig, EmailPlugin, SMTPConfig
email_config = EmailConfig(
backend=SMTPConfig(
host="smtp.example.com",
port=587,
username=environ["SMTP_USERNAME"],
password=environ["SMTP_PASSWORD"],
use_tls=True,
),
from_email="[email protected]",
from_name="Example App",
)
app = Litestar(plugins=[EmailPlugin(config=email_config)])
EmailConfig fields:
| Field | Default | Contract |
|---|---|---|
backend | "console" | Registered name, import path, or built-in backend config object |
from_email | "noreply@localhost" | Default sender address |
from_name | "" | Default display name |
fail_silently | False | Backend-specific best-effort delivery behavior |
email_service_dependency_key | "mailer" | Litestar DI key |
email_service_state_key | "mailer" | Key holding the config in app state |
The dependency and state keys occupy separate namespaces. Change them independently when the application already uses either key:
email_config = EmailConfig(
backend="memory",
email_service_dependency_key="email_service",
email_service_state_key="email_config",
)
Inject EmailService
The handler parameter name must match email_service_dependency_key:
from litestar import post
from litestar.di import NamedDependency
from litestar_email import EmailMessage, EmailService
@post("/notifications")
async def send_notification(
mailer: NamedDependency[EmailService],
) -> dict[str, int]:
sent = await mailer.send_message(
EmailMessage(
subject="Notification",
body="You have a new notification.",
to=["[email protected]"],
),
)
return {"sent": sent}
EmailPlugin.on_app_init() registers:
config.provide_serviceunderemail_service_dependency_key;- the public email types in Litestar's signature namespace;
- the
EmailConfiginstance underemail_service_state_keyin app state.
App state does not contain a permanently open EmailService. Use
plugin.get_service(app.state) or config.get_service(app.state) when code
outside handler DI needs a service derived from app state.
Construct Messages
subject and body are required constructor arguments. Recipient lists have
empty-list defaults, so provide at least one delivery recipient before sending.
from litestar_email import EmailMessage
message = EmailMessage(
subject="Monthly report",
body="The report is attached.",
from_email="Reports <[email protected]>",
to=["[email protected]"],
cc=["[email protected]"],
bcc=["[email protected]"],
reply_to=["[email protected]"],
headers={"X-Campaign-ID": "monthly-report"},
)
message.attach(
filename="report.pdf",
content=b"report content",
mimetype="application/pdf",
)
message.attach_alternative(
content="<p>The report is attached.</p>",
mimetype="text/html",
)
EmailMessage does not accept html_body or from_name. Put a per-message
display name in from_email, as shown above. Use
EmailMultiAlternatives.html_body for the HTML convenience constructor:
from litestar_email import EmailMultiAlternatives
message = EmailMultiAlternatives(
subject="Welcome",
body="Welcome to Example App.",
to=["[email protected]"],
html_body="<p>Welcome to <strong>Example App</strong>.</p>",
)
The message collections have these exact shapes:
| Field | Type |
|---|---|
to, cc, bcc, reply_to | list[str] |
headers | dict[str, str] |
attachments | list[tuple[str, bytes, str]] |
alternatives | list[tuple[str, str]] |
recipients() returns to + cc + bcc; it does not include reply_to.
Pick a Backend
| Existing project constraint | Configuration | Extra |
|---|---|---|
| Local output only | backend="console" | None |
| Unit or integration tests | backend="memory" | None |
| SMTP server or Mailpit | backend=SMTPConfig(...) | smtp |
| Existing Resend account | backend=ResendConfig(...) | None |
| Existing SendGrid account | backend=SendGridConfig(...) | None |
| Existing Mailgun account | backend=MailgunConfig(...) | None |
| Existing AWS SES setup | backend=SESConfig(...) | ses |
| Project-owned backend | Registered name or backend-class import path | Project-specific |
Backend config fields:
| Config | Fields and defaults |
|---|---|
SMTPConfig | host="localhost", port=25, username=None, password=None, use_tls=False, use_ssl=False, timeout=30 |
ResendConfig | api_key="", timeout=30, http_transport="httpx" |
SendGridConfig | api_key="", timeout=30, http_transport="httpx" |
MailgunConfig | api_key="", domain="", region="us", timeout=30, http_transport="httpx" |
SESConfig | region="us-east-1", optional AWS credentials, timeout=30, http_transport="httpx" |
For SMTP, use_tls=True performs STARTTLS after connecting; use_ssl=True
uses implicit TLS. Select the mode required by the SMTP server.
For HTTP backends, http_transport accepts "httpx", "aiohttp", or an
HTTPTransport class. Keep the default when the project has no transport
preference.
Amazon SES Contract
The 0.4.0 SES backend:
- calls the SES API v2
SendEmailendpoint withSimplecontent; - signs the exact transmitted JSON bytes with botocore SigV4;
- uses explicit
SESConfigcredentials when both key fields are set; - otherwise uses botocore's default credential chain;
- supports text plus the first
text/htmlalternative; - supports
to,cc,bcc, and the completereply_tolist; - rejects attachments with
EmailDeliveryErrorbecauseSimplecontent does not support raw MIME attachments; - rejects messages with neither a non-empty text body nor an HTML alternative;
- always propagates
EmailRateLimitErrorandEmailAuthenticationError, even whenfail_silently=True.
Use SMTP or another attachment-capable backend when the message includes files. Do not imply that SES 0.4.0 sends raw MIME content.
Service Lifecycle
from litestar_email import EmailConfig, EmailMessage, SMTPConfig
config = EmailConfig(
backend=SMTPConfig(host="localhost", port=1025),
from_email="[email protected]",
)
messages = [
EmailMessage(subject="One", body="First", to=["[email protected]"]),
EmailMessage(subject="Two", body="Second", to=["[email protected]"]),
]
async with config.provide_service() as mailer:
sent = await mailer.send_messages(messages)
Outside a service context, each send_message() or send_messages() call
creates, opens, and closes a backend. Inside config.provide_service() or
async with EmailService(config), calls reuse one open backend until context
exit. Litestar DI consumes the provider as an async iterator and performs the
same cleanup.
send_messages([]) returns 0. send_message(message) delegates to
send_messages([message]) and returns 0 or 1.
Exception Hierarchy
EmailError
├── EmailBackendError
├── EmailDeliveryError
│ ├── EmailConnectionError
│ ├── EmailAuthenticationError
│ └── EmailRateLimitError
└── MissingDependencyError (also inherits ImportError)
EmailRateLimitError.retry_after is int | None. Unknown backend names raise
ValueError; missing optional packages raise MissingDependencyError.
Catch specific delivery failures before EmailDeliveryError:
from litestar_email import (
EmailAuthenticationError,
EmailConnectionError,
EmailDeliveryError,
EmailRateLimitError,
)
try:
await mailer.send_message(message)
except EmailRateLimitError as exc:
await schedule_retry(delay=exc.retry_after or 60)
except EmailAuthenticationError:
await alert_operators("Email credentials were rejected")
except EmailConnectionError:
await schedule_retry(delay=30)
except EmailDeliveryError:
await record_delivery_failure()
In-Memory Testing
InMemoryBackend.outbox is a class-level list shared by every memory backend
instance. Clear it around each test:
from collections.abc import Iterator
import pytest
from litestar_email import EmailConfig, EmailMessage
from litestar_email.backends import InMemoryBackend
@pytest.fixture(autouse=True)
def clear_email_outbox() -> Iterator[None]:
InMemoryBackend.clear()
yield
InMemoryBackend.clear()
@pytest.mark.anyio
async def test_welcome_email() -> None:
config = EmailConfig(backend="memory", from_email="[email protected]")
async with config.provide_service() as mailer:
sent = await mailer.send_message(
EmailMessage(
subject="Welcome",
body="Thanks for signing up.",
to=["[email protected]"],
),
)
assert sent == 1
assert len(InMemoryBackend.outbox) == 1
assert InMemoryBackend.outbox[0].subject == "Welcome"
For direct backend tests, use backend = config.get_backend() and await
backend.send_messages([...]). Never inspect a fictional outbox on
EmailService or EmailConfig.
Workflow
- Inspect the project's existing provider, network policy, and dependency extras. Keep its backend unless the user asks to migrate.
- Build one
EmailConfigwith the selected backend config and default sender. - Register
EmailPlugin(config=...)and inject the configured dependency key withNamedDependency[EmailService]. - Construct
EmailMessagewith plain text. Add HTML throughattach_alternative()orEmailMultiAlternatives. - Load attachment bytes asynchronously, then call
attach(). - Reuse a service context for batches. Let Litestar DI manage request-scoped service cleanup in handlers.
- Use
backend="memory"in tests and clearInMemoryBackend.outboxbetween tests. - For slow or retryable delivery, use the queue system already present in the
project. Choose
litestar-queuesorlitestar-saqonly when it matches the existing stack.
Guardrails
- Do not pass
html_bodytoEmailMessage; onlyEmailMultiAlternativesdefines that field. - Do not pass file paths as attachments. Pass
(filename, content_bytes, mimetype)or callattach(). - Do not pass a string to
reply_to; passlist[str]. - Do not read app state as an open service by default. The plugin stores its
EmailConfigthere and derives services from it. - Do not configure a named API backend separately from its settings. Use
backend=ResendConfig(...),backend=SendGridConfig(...),backend=MailgunConfig(...), orbackend=SESConfig(...). - Do not send SES attachments. Select an attachment-capable backend.
- Do not assume
fail_silently=Truesuppresses every exception. SES authentication and rate-limit failures always propagate. - Do not hard-code API keys, SMTP passwords, or AWS credentials.
- Do not force a provider migration. Match the project's deployed backend and operational constraints.
Validation
-
litestar-email>=0.4.0and the selected backend extra are installed. -
EmailPlugin(config=...)is registered. - The handler name matches
email_service_dependency_key. - Handler injection uses
NamedDependency[EmailService]. -
EmailMessagesuppliessubject,body, and a delivery recipient. - Attachments are byte triples and the selected backend supports them.
- HTML content is stored in
alternatives, not passed toEmailMessage. - SMTP TLS mode matches the server.
- SES messages contain no attachments and contain text or HTML.
- Batch sends reuse a managed service context.
- Tests clear and assert
InMemoryBackend.outbox. - Delivery exceptions are caught from most specific to least specific.
- Secrets come from the project's settings layer.
Example
from dataclasses import dataclass
from html import escape
from litestar import Litestar, post
from litestar.di import NamedDependency
from litestar.params import JSONBody
from litestar_email import (
EmailConfig,
EmailMessage,
EmailPlugin,
EmailService,
)
@dataclass
class Notification:
recipient: str
subject: str
text: str
@post("/notifications")
async def create_notification(
data: JSONBody[Notification],
mailer: NamedDependency[EmailService],
) -> dict[str, int]:
message = EmailMessage(
subject=data.subject,
body=data.text,
to=[data.recipient],
)
message.attach_alternative(
content=f"<p>{escape(data.text)}</p>",
mimetype="text/html",
)
return {"sent": await mailer.send_message(message)}
email_config = EmailConfig(
backend="memory",
from_email="[email protected]",
from_name="Example App",
)
app = Litestar(
route_handlers=[create_notification],
plugins=[EmailPlugin(config=email_config)],
)
</example>
References Index
Official References
- PyPI release 0.4.0
- Message API at v0.4.0
- Configuration API at v0.4.0
- Plugin lifecycle at v0.4.0
- Service lifecycle at v0.4.0
- Exception hierarchy at v0.4.0
- SES backend at v0.4.0
- Tagged tests at v0.4.0