Guides

Providers

A provider adapter is the only part of MailingKit that knows how a specific email service works. Everything above it, including your code, stays the same when you switch.

Built-in providers

NameSends real emailInstallUse it for
consolenocoreLocal development. The default.
filenocoreWriting .eml files to open in a mail client.
memorynocoreAutomated tests.
smtpyescoreAny SMTP service, Amazon SES SMTP, Mailpit.
resendyesmailingkit[resend]The Resend HTTP API.

Choose one with MAILINGKIT_PROVIDER, or construct it yourself:

python
from mailingkit import MailingKit
from mailingkit.providers import SmtpProvider

mail = MailingKit(
    SmtpProvider("smtp.example.com", username="apikey", password="change-me"),
    default_sender="no-reply@example.com",
)

SMTP

Uses only the standard library. TLS is on by default: starttls on port 587, or ssl for implicit TLS on 465. security="none" is for local tools such as Mailpit, and MailingKit refuses to send credentials over it.

env
MAILINGKIT_PROVIDER=smtp
MAILINGKIT_SMTP_HOST=email-smtp.eu-west-1.amazonaws.com
MAILINGKIT_SMTP_PORT=587
MAILINGKIT_SMTP_SECURITY=starttls
MAILINGKIT_SMTP_USERNAME=...
MAILINGKIT_SMTP_PASSWORD=...

SMTP replies are mapped to MailingKit errors: login failures become AuthenticationError, refused recipients RecipientRejectedError, 4xx replies and dropped connections TemporaryProviderError (retried), and 5xx replies PermanentProviderError.

Resend

bash
pip install "mailingkit[resend]"
env
MAILINGKIT_PROVIDER=resend
MAILINGKIT_RESEND_API_KEY=re_...

Tags, attachments, inline images and custom headers are passed through. Idempotency keys are sent as Resend's Idempotency-Key header, so Resend deduplicates too. A 429 becomes RateLimitError with retry_after read from the response.

Writing your own provider

A provider is one async method. It receives a message that is already rendered, validated and addressed, sends it, and translates failures into MailingKit errors with an honest retryable flag:

acme_provider.py
from mailingkit import (
    AuthenticationError,
    EmailProvider,
    PermanentProviderError,
    ProviderResponse,
    RateLimitError,
    TemporaryProviderError,
)

class AcmeMailProvider(EmailProvider):
    name = "acme"

    def __init__(self, client):
        self.client = client

    async def send(self, message, *, idempotency_key=None):
        response = await self.client.post("/emails", json={
            "from": str(message.sender),
            "to": [str(a) for a in message.to],
            "subject": message.subject,
            "html": message.html,
            "text": message.text,
        })
        if response.status_code == 401:
            raise AuthenticationError("Acme rejected the API key")
        if response.status_code == 429:
            raise RateLimitError("Acme is throttling", retry_after=5)
        if response.status_code >= 500:
            raise TemporaryProviderError(f"Acme returned {response.status_code}")
        if response.status_code >= 400:
            raise PermanentProviderError(f"Acme returned {response.status_code}")
        return ProviderResponse(provider_message_id=response.json()["id"])

    async def close(self):
        await self.client.aclose()

If your provider raises something that is not a MailingKit error, MailingKit wraps it in PermanentProviderError so your application still only deals with one hierarchy.

Selecting it by name

To make MAILINGKIT_PROVIDER=acme work, implement from_env() and register the class, either at runtime:

python
from mailingkit import register_provider

register_provider("acme", AcmeMailProvider)

or, for a reusable package, with an entry point that MailingKit discovers automatically:

pyproject.toml
[project.entry-points."mailingkit.providers"]
acme = "acme_mailingkit:AcmeMailProvider"

Raw MIME

Services that accept a raw RFC 5322 message can reuse MailingKit's builder:

python
from mailingkit.mime import to_mime

raw_bytes = to_mime(message).as_bytes()