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.
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) -> SelfBuild 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,
) -> RenderedTemplateRender a template without sending it. Useful for previews and tests.
validate(message: EmailMessage) -> EmailMessageReturn 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,
) -> SendResultSend 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,
) -> SendResultRender a template with data and send it.
async send_message(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> SendResultSend a prebuilt EmailMessage.
async aclose() -> NoneSyncMailingKitclass
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) -> Selfwrap(client: MailingKit) -> SelfWrap an existing async client so sync and async code share one configuration.
render(
template: Template[Any] | str,
data: object | None = None,
) -> RenderedTemplatevalidate(message: EmailMessage) -> EmailMessagesend(
*,
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,
) -> SendResultsend_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,
) -> SendResultsend_message(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> SendResultclose() -> NoneMessages
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.
| Field | Type |
|---|---|
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] |
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,
) -> EmailMessageCreate 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.
>>> Address.parse("Jane Doe <jane@example.com>")
Address(email='jane@example.com', name='Jane Doe')| Field | Type |
|---|---|
email | str |
name | str | None = None |
parse(value: AddressInput) -> AddressBuild 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:...">.
| Field | Type |
|---|---|
filename | str |
content | bytes |
content_type | str | None = None |
content_id | str | None = None |
from_path(
path: str | Path,
*,
filename: str | None = None,
content_type: str | None = None,
content_id: str | None = None,
) -> AttachmentRead a file from disk. filename defaults to the file's own name.
size: intLimitsclass
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.
| Field | Type |
|---|---|
max_recipients | int = 50 |
max_message_bytes | int = 10 * 1024 * 1024 |
max_subject_length | int = 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:
@dataclass
class WelcomeData:
name: str
login_url: str
WELCOME = Template("welcome", WelcomeData)| Field | Type |
|---|---|
name | str |
model | type[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) -> boolrender(
template: Template[Any] | str,
data: object | None = None,
) -> RenderedTemplateRender 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.
| Field | Type |
|---|---|
subject | str |
html | str | None |
text | str | None |
Providers
The adapter contract and the built-in adapters (import them from mailingkit.providers).
EmailProviderclass
class EmailProviderBase 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:
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,
) -> ProviderResponseHand message to the service. Raise a ProviderError subclass on failure.
async close() -> NoneRelease connections or clients. Called by MailingKit.aclose().
from_env(environ: Mapping[str, str]) -> SelfBuild 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.
| Field | Type |
|---|---|
provider_message_id | str | None = None |
register_providerfunction
def register_provider(name: str, provider_class: type[EmailProvider]) -> NoneMake 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]) -> Selfformat(message: EmailMessage) -> strasync send(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> ProviderResponseFileProviderclass
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]) -> Selfasync send(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> ProviderResponseMemoryProviderclass
MemoryProvider()Subclass of EmailProvider
Stores every message in outbox instead of sending it.
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]) -> Selfasync send(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> ProviderResponsefail_next(*errors: ProviderError) -> NoneRaise these errors, in order, on the next sends.
clear() -> Nonelast: EmailMessagesent_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]) -> Selfasync send(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> ProviderResponseResendProviderclass
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]) -> Selfpayload(message: EmailMessage) -> dict[str, Any]The JSON body Resend expects for POST /emails.
async send(
message: EmailMessage,
*,
idempotency_key: str | None = None,
) -> ProviderResponseasync close() -> NoneResults 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.
| Field | Type |
|---|---|
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 |
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.
| Field | Type |
|---|---|
name | EventName |
id | str |
provider | str |
attempt | int |
template | str | None = None |
recipient_count | int = 0 |
error | Exception | None = None |
result | SendResult | 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.
| Field | Type |
|---|---|
max_attempts | int = 3 |
initial_delay | float = 0.5 |
max_delay | float = 10.0 |
multiplier | float = 2.0 |
jitter | bool = True |
should_retry(attempt: int, error: ProviderError) -> booldelay(attempt: int, error: ProviderError | None = None) -> floatSeconds to wait after the given (1-based) failed attempt.
IdempotencyStoreclass
class IdempotencyStoreSubclass of Protocol
Remembers the result of each idempotent send for ttl seconds.
async get(key: str) -> SendResult | Noneasync set(key: str, result: SendResult, ttl: float) -> NoneInMemoryIdempotencyStoreclass
InMemoryIdempotencyStore(max_entries: int = 10000)A bounded, process-local store. Oldest entries are evicted first.
async get(key: str) -> SendResult | Noneasync set(key: str, result: SendResult, ttl: float) -> NoneSecretclass
Secret(value: str)Wraps a credential so it never appears in a repr, a log line or a traceback.
get_secret_value() -> strErrors
Every error MailingKit raises. Provider errors carry retryable, provider and status_code.
MailingKitErrorclass
class MailingKitErrorSubclass of Exception
Base class for every error MailingKit raises.
ConfigurationErrorclass
class ConfigurationErrorSubclass of MailingKitError
MailingKit or a provider is configured incorrectly.
ValidationErrorclass
class ValidationErrorSubclass of MailingKitError, ValueError
A message failed validation and was not sent.
InvalidAddressErrorclass
class InvalidAddressErrorSubclass of ValidationError
An email address is malformed or unsafe.
MessageTooLargeErrorclass
class MessageTooLargeErrorSubclass of ValidationError
A message is larger than the configured limit.
TemplateErrorclass
class TemplateErrorSubclass of MailingKitError
Base class for template problems.
TemplateNotFoundErrorclass
class TemplateNotFoundErrorSubclass of TemplateError
No template exists with the requested name.
TemplateDataErrorclass
class TemplateDataErrorSubclass of TemplateError, ValueError
The data passed to a template is missing fields or has the wrong shape.
TemplateRenderErrorclass
class TemplateRenderErrorSubclass 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.
| Field | Type |
|---|---|
default_retryable | bool = False |
AuthenticationErrorclass
class AuthenticationErrorSubclass 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 RecipientRejectedErrorSubclass of ProviderError
The provider refused one or more recipients.
TemporaryProviderErrorclass
class TemporaryProviderErrorSubclass of ProviderError
A transient failure such as a timeout, a dropped connection or a 5xx response.
PermanentProviderErrorclass
class PermanentProviderErrorSubclass of ProviderError
The provider rejected the message and retrying will not help.