Reference

API reference

Generated from the mailingkit 0.1.0 source. Everything here is importable from mailingkit, except the built-in providers, which live in mailingkit.providers.

Client

The objects your application talks to.

MailingKitclass

MailingKit(
    provider: EmailProvider | None = None,
    *,
    default_sender: AddressInput | None = None,
    templates: str | Path | TemplateRenderer | None = None,
    retry: RetryPolicy | None = None,
    idempotency_store: IdempotencyStore | None = None,
    idempotency_ttl: float = 24 * 60 * 60,
    redirect_to: AddressInput | None = None,
    limits: Limits | None = None,
    hooks: Iterable[Hook] = (),
)

Send email through any provider.

python
mail = MailingKit.from_env()
await mail.send(to="ada@example.com", subject="Welcome", text="Hello Ada")
await mail.send_template("welcome", {"name": "Ada"}, to="ada@example.com")

With no provider configured, MailingKit uses ConsoleProvider, which prints messages and sends nothing.

from_env(environ: Mapping[str, str] | None = None, **overrides: Any) -> Self

Build a client from MAILINGKIT_* environment variables.

Keyword arguments override what the environment says, for example MailingKit.from_env(hooks=[my_hook]).

render(
    template: Template[Any] | str,
    data: object | None = None,
) -> RenderedTemplate

Render a template without sending it. Useful for previews and tests.

validate(message: EmailMessage) -> EmailMessage

Return the exact message that would be handed to the provider, or raise.

Applies the default sender, generates a text body from HTML when there is none, validates everything, then applies redirect_to.

async send(
    *,
    to: Recipients,
    subject: str,
    html: str | None = None,
    text: str | None = None,
    sender: AddressInput | None = None,
    cc: Recipients | None = None,
    bcc: Recipients | None = None,
    reply_to: Recipients | None = None,
    headers: Mapping[str, str] | None = None,
    attachments: Iterable[Attachment] | None = None,
    tags: Mapping[str, str] | None = None,
    idempotency_key: str | None = None,
) -> SendResult

Send a message you have already written.

async send_template(
    template: Template[T] | str,
    data: T | Mapping[str, Any] | None = None,
    *,
    to: Recipients,
    sender: AddressInput | None = None,
    cc: Recipients | None = None,
    bcc: Recipients | None = None,
    reply_to: Recipients | None = None,
    headers: Mapping[str, str] | None = None,
    attachments: Iterable[Attachment] | None = None,
    tags: Mapping[str, str] | None = None,
    idempotency_key: str | None = None,
) -> SendResult

Render a template with data and send it.

async send_message(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> SendResult

Send a prebuilt EmailMessage.

async aclose() -> None

SyncMailingKitclass

SyncMailingKit(provider: EmailProvider | None = None, **options: Any)

A blocking wrapper for code that is not async: Django views, scripts, Celery tasks.

It runs the async client on one private event loop in a background thread, so it is safe to call from any thread, including one that already has its own running event loop.

from_env(environ: Mapping[str, str] | None = None, **overrides: Any) -> Self
wrap(client: MailingKit) -> Self

Wrap an existing async client so sync and async code share one configuration.

render(
    template: Template[Any] | str,
    data: object | None = None,
) -> RenderedTemplate
validate(message: EmailMessage) -> EmailMessage
send(
    *,
    to: Recipients,
    subject: str,
    html: str | None = None,
    text: str | None = None,
    sender: AddressInput | None = None,
    cc: Recipients | None = None,
    bcc: Recipients | None = None,
    reply_to: Recipients | None = None,
    headers: Mapping[str, str] | None = None,
    attachments: Iterable[Attachment] | None = None,
    tags: Mapping[str, str] | None = None,
    idempotency_key: str | None = None,
) -> SendResult
send_template(
    template: Template[T] | str,
    data: T | Mapping[str, Any] | None = None,
    *,
    to: Recipients,
    sender: AddressInput | None = None,
    cc: Recipients | None = None,
    bcc: Recipients | None = None,
    reply_to: Recipients | None = None,
    headers: Mapping[str, str] | None = None,
    attachments: Iterable[Attachment] | None = None,
    tags: Mapping[str, str] | None = None,
    idempotency_key: str | None = None,
) -> SendResult
send_message(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> SendResult
close() -> None

Messages

The provider-independent message model.

EmailMessageclass

EmailMessage(to: tuple[Address, ...] = (), subject: str = '', html: str | None = None, text: str | None = None, sender: Address | None = None, cc: tuple[Address, ...] = (), bcc: tuple[Address, ...] = (), reply_to: tuple[Address, ...] = (), headers: Mapping[str, str], attachments: tuple[Attachment, ...] = (), tags: Mapping[str, str])

A fully rendered message, ready to hand to a provider.

Most applications never build this directly: MailingKit.send and MailingKit.send_template create it. Use build when you do need one, since it accepts plain strings for addresses.

FieldType
totuple[Address, ...] = ()
subjectstr = ''
htmlstr | None = None
textstr | None = None
senderAddress | None = None
cctuple[Address, ...] = ()
bcctuple[Address, ...] = ()
reply_totuple[Address, ...] = ()
headersMapping[str, str]
attachmentstuple[Attachment, ...] = ()
tagsMapping[str, str]
build(
    *,
    to: Recipients | None = None,
    subject: str = '',
    html: str | None = None,
    text: str | None = None,
    sender: AddressInput | None = None,
    cc: Recipients | None = None,
    bcc: Recipients | None = None,
    reply_to: Recipients | None = None,
    headers: Mapping[str, str] | None = None,
    attachments: Iterable[Attachment] | None = None,
    tags: Mapping[str, str] | None = None,
) -> EmailMessage

Create a message from loosely typed input such as plain address strings.

recipients: tuple[Address, ...]

Every envelope recipient: to, cc and bcc.

Addressclass

Address(email: str, name: str | None = None)

A single mailbox, optionally with a display name.

python
>>> Address.parse("Jane Doe <jane@example.com>")
Address(email='jane@example.com', name='Jane Doe')
FieldType
emailstr
namestr | None = None
parse(value: AddressInput) -> Address

Build an address from "a@example.com", "Name <a@example.com>" or an Address.

Attachmentclass

Attachment(filename: str, content: bytes, content_type: str | None = None, content_id: str | None = None)

A file attached to a message.

Set content_id to reference the attachment inline from HTML as <img src="cid:...">.

FieldType
filenamestr
contentbytes
content_typestr | None = None
content_idstr | None = None
from_path(
    path: str | Path,
    *,
    filename: str | None = None,
    content_type: str | None = None,
    content_id: str | None = None,
) -> Attachment

Read a file from disk. filename defaults to the file's own name.

size: int

Limitsclass

Limits(max_recipients: int = 50, max_message_bytes: int = 10 * 1024 * 1024, max_subject_length: int = 998)

Safety limits applied to every message.

The defaults fit every built-in provider. Raise them only if your provider allows it.

FieldType
max_recipientsint = 50
max_message_bytesint = 10 * 1024 * 1024
max_subject_lengthint = 998

Templates

File-based templates with typed data.

Templateclass

Template(name: str, model: type[T] | None = None)

A named template, optionally bound to a data model.

The model can be a dataclass or a Pydantic model. When it is set, MailingKit checks the data against it before rendering:

python
@dataclass
class WelcomeData:
    name: str
    login_url: str

WELCOME = Template("welcome", WelcomeData)
FieldType
namestr
modeltype[T] | None = None

TemplateRendererclass

TemplateRenderer(
    directory: str | Path | Sequence[str | Path] | None = None,
    *,
    environment: jinja2.Environment | None = None,
    globals: Mapping[str, Any] | None = None,
)

Loads templates from one or more directories and renders them.

Pass environment to bring your own configured Jinja2 environment (custom filters, a different loader). Its autoescape and undefined settings are then your responsibility.

exists(name: str) -> bool
render(
    template: Template[Any] | str,
    data: object | None = None,
) -> RenderedTemplate

Render a template. Raises a TemplateError subclass on any problem.

RenderedTemplateclass

RenderedTemplate(subject: str, html: str | None, text: str | None)

The output of rendering a template: what the recipient will see.

FieldType
subjectstr
htmlstr | None
textstr | None

Providers

The adapter contract and the built-in adapters (import them from mailingkit.providers).

EmailProviderclass

class EmailProvider

Base class for provider adapters.

A provider receives a message that is already rendered, validated and addressed. Its only jobs are to hand that message to the service and to translate failures into the mailingkit.errors provider exceptions, setting retryable honestly.

Minimal custom provider:

python
class MyProvider(EmailProvider):
    name = "my-provider"

    async def send(self, message, *, idempotency_key=None):
        provider_id = await my_api.post(...)
        return ProviderResponse(provider_message_id=provider_id)
async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse

Hand message to the service. Raise a ProviderError subclass on failure.

async close() -> None

Release connections or clients. Called by MailingKit.aclose().

from_env(environ: Mapping[str, str]) -> Self

Build the provider from MAILINGKIT_* variables. Override to support it.

ProviderResponseclass

ProviderResponse(provider_message_id: str | None = None)

What a provider returns after accepting a message.

FieldType
provider_message_idstr | None = None

register_providerfunction

def register_provider(name: str, provider_class: type[EmailProvider]) -> None

Make a provider selectable by name at runtime, for example from MAILINGKIT_PROVIDER.

ConsoleProviderclass

ConsoleProvider(stream: TextIO | None = None, *, show_html: bool = False)

Subclass of EmailProvider

Writes each message to a stream (stdout by default). Nothing leaves the machine.

Environment: MAILINGKIT_CONSOLE_SHOW_HTML=true prints the HTML body as well.

from_env(environ: Mapping[str, str]) -> Self
format(message: EmailMessage) -> str
async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse

FileProviderclass

FileProvider(directory: str | Path = '.mailingkit/outbox')

Subclass of EmailProvider

Saves messages to a folder instead of sending them.

Environment: MAILINGKIT_FILE_DIR (default .mailingkit/outbox).

from_env(environ: Mapping[str, str]) -> Self
async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse

MemoryProviderclass

MemoryProvider()

Subclass of EmailProvider

Stores every message in outbox instead of sending it.

python
provider = MemoryProvider()
mail = MailingKit(provider, default_sender="app@example.com")
await mail.send(to="ada@example.com", subject="Hi", text="Hello")
assert provider.last.subject == "Hi"

fail_next queues errors to raise, which is how retry behaviour is tested.

from_env(environ: Mapping[str, str]) -> Self
async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse
fail_next(*errors: ProviderError) -> None

Raise these errors, in order, on the next sends.

clear() -> None
last: EmailMessage
sent_to(email: str) -> list[EmailMessage]

Messages where email is among the recipients (to, cc or bcc).

SmtpProviderclass

SmtpProvider(
    host: str,
    port: int | None = None,
    *,
    username: str | None = None,
    password: str | Secret | None = None,
    security: Security = 'starttls',
    timeout: float = 30.0,
    ssl_context: ssl.SSLContext | None = None,
)

Subclass of EmailProvider

SMTP with TLS on by default.

security is "starttls" (port 587, the default), "ssl" (implicit TLS, port 465) or "none" (plain text, meant for local tools such as Mailpit). Credentials are refused over an unencrypted connection.

Environment: MAILINGKIT_SMTP_HOST (required), MAILINGKIT_SMTP_PORT, MAILINGKIT_SMTP_USERNAME, MAILINGKIT_SMTP_PASSWORD, MAILINGKIT_SMTP_SECURITY, MAILINGKIT_SMTP_TIMEOUT.

from_env(environ: Mapping[str, str]) -> Self
async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse

ResendProviderclass

ResendProvider(
    api_key: str | Secret,
    *,
    base_url: str = DEFAULT_BASE_URL,
    timeout: float = 30.0,
    client: httpx.AsyncClient | None = None,
)

Subclass of EmailProvider

Resend adapter. Idempotency keys are passed through, so Resend deduplicates too.

Environment: MAILINGKIT_RESEND_API_KEY (required), MAILINGKIT_RESEND_BASE_URL, MAILINGKIT_RESEND_TIMEOUT.

from_env(environ: Mapping[str, str]) -> Self
payload(message: EmailMessage) -> dict[str, Any]

The JSON body Resend expects for POST /emails.

async send(
    message: EmailMessage,
    *,
    idempotency_key: str | None = None,
) -> ProviderResponse
async close() -> None

Results and reliability

What a send returns, events for hooks, retries and idempotency.

SendResultclass

SendResult(id: str, provider: str, provider_message_id: str | None, attempts: int, idempotency_key: str | None = None, duplicate: bool = False, redirected: bool = False, accepted_at: datetime)

The outcome of a successful send.

id is MailingKit's own identifier for the send. provider_message_id is the provider's identifier, useful when matching delivery webhooks or support tickets later. duplicate is true when an earlier send with the same idempotency key was reused.

FieldType
idstr
providerstr
provider_message_idstr | None
attemptsint
idempotency_keystr | None = None
duplicatebool = False
redirectedbool = False
accepted_atdatetime

MailEventclass

MailEvent(name: EventName, id: str, provider: str, attempt: int, template: str | None = None, recipient_count: int = 0, error: Exception | None = None, result: SendResult | None = None)

Passed to hooks. Holds no recipients, bodies or template data, so it is safe to log.

FieldType
nameEventName
idstr
providerstr
attemptint
templatestr | None = None
recipient_countint = 0
errorException | None = None
resultSendResult | None = None

RetryPolicyclass

RetryPolicy(max_attempts: int = 3, initial_delay: float = 0.5, max_delay: float = 10.0, multiplier: float = 2.0, jitter: bool = True)

Exponential backoff with full jitter. Only errors with retryable=True are retried.

FieldType
max_attemptsint = 3
initial_delayfloat = 0.5
max_delayfloat = 10.0
multiplierfloat = 2.0
jitterbool = True
should_retry(attempt: int, error: ProviderError) -> bool
delay(attempt: int, error: ProviderError | None = None) -> float

Seconds to wait after the given (1-based) failed attempt.

IdempotencyStoreclass

class IdempotencyStore

Subclass of Protocol

Remembers the result of each idempotent send for ttl seconds.

async get(key: str) -> SendResult | None
async set(key: str, result: SendResult, ttl: float) -> None

InMemoryIdempotencyStoreclass

InMemoryIdempotencyStore(max_entries: int = 10000)

A bounded, process-local store. Oldest entries are evicted first.

async get(key: str) -> SendResult | None
async set(key: str, result: SendResult, ttl: float) -> None

Secretclass

Secret(value: str)

Wraps a credential so it never appears in a repr, a log line or a traceback.

get_secret_value() -> str

Errors

Every error MailingKit raises. Provider errors carry retryable, provider and status_code.

MailingKitErrorclass

class MailingKitError

Subclass of Exception

Base class for every error MailingKit raises.

ConfigurationErrorclass

class ConfigurationError

Subclass of MailingKitError

MailingKit or a provider is configured incorrectly.

ValidationErrorclass

class ValidationError

Subclass of MailingKitError, ValueError

A message failed validation and was not sent.

InvalidAddressErrorclass

class InvalidAddressError

Subclass of ValidationError

An email address is malformed or unsafe.

MessageTooLargeErrorclass

class MessageTooLargeError

Subclass of ValidationError

A message is larger than the configured limit.

TemplateErrorclass

class TemplateError

Subclass of MailingKitError

Base class for template problems.

TemplateNotFoundErrorclass

class TemplateNotFoundError

Subclass of TemplateError

No template exists with the requested name.

TemplateDataErrorclass

class TemplateDataError

Subclass of TemplateError, ValueError

The data passed to a template is missing fields or has the wrong shape.

TemplateRenderErrorclass

class TemplateRenderError

Subclass of TemplateError

A template failed while rendering.

ProviderErrorclass

ProviderError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    retryable: bool | None = None,
)

A provider failed to accept a message.

retryable tells the caller (and MailingKit's own retry loop) whether sending the same message again could succeed. __cause__ holds the provider's original exception for debugging; it is never part of the message text, so secrets in it are not logged.

FieldType
default_retryablebool = False

AuthenticationErrorclass

class AuthenticationError

Subclass of ProviderError

The provider rejected the credentials.

RateLimitErrorclass

RateLimitError(
    message: str,
    *,
    provider: str | None = None,
    status_code: int | None = None,
    retry_after: float | None = None,
)

Subclass of ProviderError

The provider is throttling requests. Safe to retry after retry_after seconds.

RecipientRejectedErrorclass

class RecipientRejectedError

Subclass of ProviderError

The provider refused one or more recipients.

TemporaryProviderErrorclass

class TemporaryProviderError

Subclass of ProviderError

A transient failure such as a timeout, a dropped connection or a 5xx response.

PermanentProviderErrorclass

class PermanentProviderError

Subclass of ProviderError

The provider rejected the message and retrying will not help.