Litestar Email
Litestar Email

Email plugin for litestar

litestar-email

Email support for Litestar applications. This plugin provides a pluggable email backend system for sending transactional emails with support for multiple providers.

Installation

pip install litestar-email

For production backends, install with the appropriate extra:

# SMTP backend
pip install litestar-email[smtp]

# aiohttp transport (alternative to httpx for API backends)
pip install litestar-email[aiohttp]

# All optional dependencies
pip install litestar-email[all]

Note: API backends (Resend, SendGrid, Mailgun) use httpx which is bundled with Litestar. No extra installation is needed for these backends.

Usage

from litestar import Litestar
from litestar_email import EmailConfig, EmailPlugin

# Development: console output
config = EmailConfig(
    backend="console",
    from_email="noreply@example.com",
    from_name="My App",
)

app = Litestar(plugins=[EmailPlugin(config=config)])

For production, pass a backend config object directly:

from litestar_email import EmailConfig, EmailPlugin, SMTPConfig

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

app = Litestar(plugins=[EmailPlugin(config=config)])

Sending Email

from litestar_email import EmailMessage

message = EmailMessage(
    subject="Welcome!",
    body="Thanks for signing up.",
    to=["user@example.com"],
)

async with config.provide_service() as mailer:
    await mailer.send_message(message)

If message.from_email is omitted, the service uses config.from_email and config.from_name as defaults.

Dependency Injection

The plugin registers a mailer dependency for handlers by default:

from litestar import get
from litestar_email import EmailMessage, EmailService

@get("/welcome/{email:str}")
async def send_welcome(email: str, mailer: EmailService) -> dict[str, str]:
    message = EmailMessage(
        subject="Welcome!",
        body="Thanks for signing up.",
        to=[email],
    )
    await mailer.send_message(message)
    return {"status": "sent"}

This works for both router handlers and controller methods the same way, since the dependency is registered on the app.

If you need a service outside of Litestar (e.g., for a worker), use config.get_service() for a one-off instance or config.provide_service() for batch sending.

Events and Listeners

Event listeners in Litestar execute outside request context and cannot receive DI-injected dependencies. Pass the mailer explicitly:

from litestar import Litestar, get
from litestar.events import listener
from litestar_email import EmailConfig, EmailMessage, EmailPlugin, EmailService, SMTPConfig

config = EmailConfig(
    backend=SMTPConfig(host="localhost", port=1025),
    from_email="noreply@example.com",
    from_name="My App",
)

@get("/register/{email:str}")
async def register(email: str, mailer: EmailService) -> dict[str, str]:
    # Pass the DI-injected mailer to the event
    request.app.emit("user.registered", email, mailer=mailer)
    return {"status": "queued"}

@listener("user.registered")
async def on_user_registered(email: str, mailer: EmailService) -> None:
    # mailer is passed explicitly from emit(), not injected via DI
    await mailer.send_message(
        EmailMessage(subject="Welcome!", body="Thanks for signing up.", to=[email]),
    )

app = Litestar(
    plugins=[EmailPlugin(config=config)],
    listeners=[on_user_registered],
)

You can override the dependency and state keys via EmailConfig if needed: email_service_dependency_key="email_service" and email_service_state_key="email_service".

Standalone (No Litestar)

Use the config helpers directly without Litestar:

from litestar_email import EmailConfig, EmailMessage, SMTPConfig

config = EmailConfig(
    backend=SMTPConfig(host="localhost", port=1025),
    from_email="noreply@example.com",
)
message = EmailMessage(
    subject="Hello!",
    body="This is a standalone send.",
    to=["user@example.com"],
)

async with config.provide_service() as mailer:
    await mailer.send_message(message)

For batch sends, use the context manager to reuse the connection:

async with config.provide_service() as mailer:
    await mailer.send_messages([message1, message2, message3])

HTML Email

from litestar_email import EmailMultiAlternatives

message = EmailMultiAlternatives(
    subject="Welcome!",
    body="Thanks for signing up.",  # Plain text fallback
    html_body="<h1>Welcome!</h1><p>Thanks for signing up.</p>",
    to=["user@example.com"],
)

Available Backends

BackendConfig ClassUse Case
console-Development (prints to stdout)
memory-Testing (stores in memory)
smtpSMTPConfigProduction SMTP servers
resendResendConfigResend HTTP API
sendgridSendGridConfigSendGrid HTTP API
mailgunMailgunConfigMailgun HTTP API

API backends (Resend, SendGrid, Mailgun) support configurable HTTP transports:

from litestar_email import ResendConfig

# Default: uses httpx (bundled with Litestar)
config = ResendConfig(api_key="re_xxx...")

# Alternative: use aiohttp
config = ResendConfig(api_key="re_xxx...", http_transport="aiohttp")

Testing

The InMemoryBackend is designed for testing:

from litestar_email.backends import InMemoryBackend

def test_sends_welcome_email():
    InMemoryBackend.clear()

    # ... code that sends email ...

    assert len(InMemoryBackend.outbox) == 1
    assert InMemoryBackend.outbox[0].subject == "Welcome!"

If you need direct backend access in tests, use config.get_backend():

from litestar_email import EmailConfig, EmailMessage

config = EmailConfig(backend="memory")
backend = config.get_backend()

message = EmailMessage(subject="Hello", body="Body", to=["user@example.com"])
await backend.send_messages([message])

Development

make install    # Install dependencies
make test       # Run tests
make lint       # Run linting
make check-all  # Run all checks

License

MIT