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
| Name | Sends real email | Install | Use it for |
|---|---|---|---|
console | no | core | Local development. The default. |
file | no | core | Writing .eml files to open in a mail client. |
memory | no | core | Automated tests. |
smtp | yes | core | Any SMTP service, Amazon SES SMTP, Mailpit. |
resend | yes | mailingkit[resend] | The Resend HTTP API. |
Choose one with MAILINGKIT_PROVIDER, or construct it yourself:
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.
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
pip install "mailingkit[resend]"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:
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:
from mailingkit import register_provider
register_provider("acme", AcmeMailProvider)or, for a reusable package, with an entry point that MailingKit discovers automatically:
[project.entry-points."mailingkit.providers"]
acme = "acme_mailingkit:AcmeMailProvider"Raw MIME
Services that accept a raw RFC 5322 message can reuse MailingKit's builder:
from mailingkit.mime import to_mime
raw_bytes = to_mime(message).as_bytes()