litestar-org/litestar-skills/litestar-email

Claude Code and agent skill by litestar-org/litestar-skills

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when working with litestar_email, EmailPlugin, EmailConfig, EmailService, EmailMessage, SMTPConfig, ResendConfig, SendGridConfig, MailgunConfig, SESConfig. Not for marketing APIs — use vendor SDKs.

Install

npx vskill@latest install litestar-org/litestar-skills/litestar-email

SKILL.md

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] with Provide(email_config.provide_service().__aiter__)

(or FromDishka[EmailService] when the project uses Dishka) for direct EmailService handler injection.

  • Pass recipient collections as list[str]. to, cc, bcc, and reply_to

are list fields.

  • Pass attachment content as bytes. Do file I/O before constructing the

message and keep that I/O async.

  • Await send_message() and send_messages(). Both return the count sent.
  • Keep API keys and SMTP credentials in the project's settings layer.

Quick Reference

Install
bash
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[httpx]>=0.4.0" # default HTTP transport
pip install "litestar-email[aiohttp]>=0.4.0" # alternative HTTP transport

The HTTP API backends select httpx by default, but the transport is optional in litestar-email itself. Install the httpx or aiohttp extra (unless the project already provides that dependency), and select aiohttp only when the project standardizes on it.

Configure the Plugin
python
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="noreply@example.com",
from_name="Example App",
)
app = Litestar(plugins=[EmailPlugin(config=email_config)])

EmailConfig fields:

FieldDefaultContract
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_silentlyFalseBackend-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:

python
email_config = EmailConfig(
backend="memory",
email_service_dependency_key="email_service",
email_service_state_key="email_config",
)
Inject EmailService

EmailPlugin.on_app_init() registers:

  • Provide(config.provide_service, sync_to_thread=False) under email_service_dependency_key;
  • config.signature_namespace (BaseEmailBackend, EmailConfig, EmailMessage,

EmailMultiAlternatives, EmailService, MailgunConfig, ResendConfig, SESConfig, SMTPConfig, SendGridConfig) in Litestar's signature namespace;

  • the EmailConfig instance under email_service_state_key in app state.

Because EmailConfig.provide_service() is a synchronous method returning an AsyncServiceProvider instance rather than an async def generator function, Litestar's default Provide(config.provide_service, sync_to_thread=False) injects AsyncServiceProvider rather than EmailService.

To inject an open EmailService with request-scoped connection cleanup, bind Provide(email_config.provide_service().__aiter__) on the router, controller, or handler (or use Dishka's FromDishka[EmailService]):

python
from litestar import post
from litestar.di import NamedDependency, Provide
from litestar_email import EmailConfig, EmailMessage, EmailService
email_config = EmailConfig(backend="memory", from_email="noreply@example.com")
@post(
"/notifications",
dependencies={"mailer": Provide(email_config.provide_service().__aiter__)},
)
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=["recipient@example.com"],
),
)
return {"sent": sent}

If using the plugin's default dependency directly without overriding dependencies, register signature_types=[AsyncServiceProvider], annotate the parameter as mailer: NamedDependency[AsyncServiceProvider], and enter async with mailer as service: inside the handler.

If the project uses Dishka, yield EmailService from a Scope.REQUEST provider and inject FromDishka[EmailService]:

python
from collections.abc import AsyncIterator
from dishka import Provider, Scope
from dishka.integrations.litestar import FromDishka, inject
from litestar import post
from litestar_email import EmailConfig, EmailMessage, EmailService
def build_email_provider(config: EmailConfig) -> Provider:
async def provide_mailer() -> AsyncIterator[EmailService]:
async with config.provide_service() as mailer:
yield mailer
provider = Provider(scope=Scope.REQUEST)
provider.provide(provide_mailer)
return provider
@post("/welcome")
@inject
async def send_welcome(
email_service: FromDishka[EmailService],
) -> dict[str, int]:
sent = await email_service.send_message(
EmailMessage(subject="Welcome", body="Hello!", to=["user@example.com"]),
)
return {"sent": sent}

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.

python
from litestar_email import EmailMessage
message = EmailMessage(
subject="Monthly report",
body="The report is attached.",
from_email="Reports <reports@example.com>",
to=["owner@example.com"],
cc=["audit@example.com"],
bcc=["archive@example.com"],
reply_to=["support@example.com"],
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:

python
from litestar_email import EmailMultiAlternatives
message = EmailMultiAlternatives(
subject="Welcome",
body="Welcome to Example App.",
to=["user@example.com"],
html_body="<p>Welcome to <strong>Example App</strong>.</p>",
)

The message collections have these exact shapes:

FieldType
to, cc, bcc, reply_tolist[str]
headersdict[str, str]
attachmentslist[tuple[str, bytes, str]]
alternativeslist[tuple[str, str]]

recipients() returns to + cc + bcc; it does not include reply_to.

Pick a Backend
Existing project constraintConfigurationExtra
Local output onlybackend="console"None
Unit or integration testsbackend="memory"None
SMTP server or Mailpitbackend=SMTPConfig(...)smtp
Existing Resend accountbackend=ResendConfig(...)httpx or aiohttp
Existing SendGrid accountbackend=SendGridConfig(...)httpx or aiohttp
Existing Mailgun accountbackend=MailgunConfig(...)httpx or aiohttp
Existing AWS SES setupbackend=SESConfig(...)ses plus httpx or aiohttp
Project-owned backendRegistered name or backend-class import pathProject-specific

Backend config fields:

ConfigFields and defaults
SMTPConfighost="localhost", port=25, username=None, password=None, use_tls=False, use_ssl=False, timeout=30
ResendConfigapi_key="", timeout=30, http_transport="httpx"
SendGridConfigapi_key="", timeout=30, http_transport="httpx"
MailgunConfigapi_key="", domain="", region="us", timeout=30, http_transport="httpx"
SESConfigregion="us-east-1", aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, 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 (from litestar_email.transports import HTTPTransport, HTTPResponse, get_transport). Keep the default when the project has no transport preference.

For custom backends, subclass BaseEmailBackend and either pass its dotted import path to EmailConfig(backend="my_app.email.CustomBackend") or register a short name with @email_backend("custom"). Inspect registered names with list_backends() and resolve classes or instances with get_backend_class() or get_backend(). ConsoleBackend also accepts stream: TextIO | None = None when instantiated directly.

Amazon SES Contract

The 0.4.0 SES backend:

  • calls the SES API v2 SendEmail endpoint with Simple content;
  • signs the exact transmitted JSON bytes with botocore SigV4;
  • uses explicit SESConfig credentials when both aws_access_key_id and

aws_secret_access_key are set (with optional aws_session_token);

  • otherwise uses botocore's default credential chain;
  • supports text plus the first text/html alternative;
  • supports to, cc, bcc, and the complete reply_to list;
  • rejects attachments with EmailDeliveryError because Simple content does

not support raw MIME attachments;

  • rejects messages with neither a non-empty text body nor an HTML alternative;
  • always propagates EmailRateLimitError and EmailAuthenticationError, even

when fail_silently=True (all HTTP API backends — Resend, SendGrid, Mailgun, and SES — always propagate EmailRateLimitError).

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
python
from litestar_email import EmailConfig, EmailMessage, SMTPConfig
config = EmailConfig(
backend=SMTPConfig(host="localhost", port=1025),
from_email="noreply@example.com",
)
messages = [
EmailMessage(subject="One", body="First", to=["one@example.com"]),
EmailMessage(subject="Two", body="Second", to=["two@example.com"]),
]
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. When bound via Provide(config.provide_service().__aiter__) (or a Dishka request-scoped async generator), DI enters and closes that context per request.

send_messages([]) returns 0. send_message(message) delegates to send_messages([message]) and returns 0 or 1.

Exception Hierarchy
text
EmailError
├── EmailBackendError
├── EmailDeliveryError
│ ├── EmailConnectionError
│ ├── EmailAuthenticationError
│ └── EmailRateLimitError
└── MissingDependencyError (also inherits ImportError)

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/litestar-org/litestar-skills/litestar-email/badge)](https://verified-skill.com/skills/litestar-org/litestar-skills/litestar-email)

Something wrong with this skill or its page? Tell us.