Skip to main content

Email plugin for HawkAPI — SMTP, SES, SendGrid, Mailgun, Resend, Jinja2, webhooks, persistent outbox

Project description

hawkapi-mail

Email plugin for HawkAPI. SMTP, AWS SES, SendGrid, Mailgun, Resend, Jinja2 templates, persistent outbox with retry, and webhook handlers for delivery/bounce/complaint events.

Install

pip install hawkapi-mail            # SMTP + SendGrid + Mailgun + Resend + outbox
pip install 'hawkapi-mail[ses]'     # adds AWS SES backend

Quickstart

from hawkapi import Depends, HawkAPI
from hawkapi_mail import (
    EmailMessage,
    Mailer,
    SMTPBackend,
    SMTPConfig,
    get_mailer,
    init_mail,
)

app = HawkAPI()
init_mail(
    app,
    backend=SMTPBackend(SMTPConfig(host="smtp.example.com", port=587, start_tls=True,
                                    username="api", password="secret")),
    default_sender="hello@example.com",
)


@app.post("/welcome")
async def welcome(email: str, mail: Mailer = Depends(get_mailer)):
    msg = EmailMessage.build(
        subject="Welcome!", to=email, text="Glad you joined.",
        html="<h1>Glad you joined.</h1>",
    )
    await mail.send(msg)
    return {"ok": True}

Backends

from hawkapi_mail import (
    InMemoryBackend,                                # tests
    SMTPBackend, SMTPConfig,                        # SMTP / SMTPS / STARTTLS
    SESBackend, SESConfig,                          # AWS SES (extras: [ses])
    SendGridBackend, SendGridConfig,                # SendGrid v3 API
    MailgunBackend, MailgunConfig,                  # Mailgun v3 API
    ResendBackend, ResendConfig,                    # Resend HTTP API
)

sendgrid = SendGridBackend(SendGridConfig(api_key="SG.xxx"))
mailgun  = MailgunBackend(MailgunConfig(api_key="key-xxx", domain="mg.example.com"))
resend   = ResendBackend(ResendConfig(api_key="re_xxx"))
ses      = SESBackend(SESConfig(region="eu-west-1"))   # uses boto3 / IAM

All backends share one async send(message) -> SendResult interface; swap them freely.

Templates

from hawkapi_mail import TemplateRenderer

templates = TemplateRenderer(directory="emails/")           # or package=..., or templates={...}
init_mail(app, backend=..., templates=templates, default_sender="hello@example.com")

await mail.send_template(
    "welcome.html",
    text_template="welcome.txt",
    context={"name": "Alice"},
    subject="Welcome",
    to="alice@example.com",
)

Jinja2 with async rendering + HTML autoescape on by default.

Persistent outbox

from hawkapi_mail import SQLiteOutbox, RetryPolicy

outbox = SQLiteOutbox(path="mail.db")
init_mail(
    app,
    backend=sendgrid,
    outbox=outbox,
    retry=RetryPolicy(max_attempts=5, base_seconds=5, max_seconds=3600),
    start_worker=True,     # drains the outbox in the background
)

# Enqueue instead of sending right away:
entry_id = await mail.send(message, deferred=True)

The worker pulls due entries, calls the backend, and on SendError schedules an exponential-backoff retry. After max_attempts the entry is dropped (logged at error level). For tests, swap in MemoryOutbox().

Webhooks

from hawkapi_mail import (
    verify_sendgrid, parse_sendgrid,
    verify_mailgun,  parse_mailgun,
    verify_resend,   parse_resend,
    parse_ses_sns,   confirm_ses_subscription,
)


@app.post("/webhooks/mailgun")
async def mailgun_hook(request):
    form = await request.form()
    verify_mailgun(
        signing_key="…",
        timestamp=form["timestamp"], token=form["token"], signature=form["signature"],
    )
    event = parse_mailgun(await request.body())
    # event.kind ∈ delivered | bounce | complaint | opened | clicked | unsubscribed | other
    return {"ok": True}


@app.post("/webhooks/ses")
async def ses_hook(request):
    body = await request.body()
    if await confirm_ses_subscription(body):       # SNS SubscriptionConfirmation
        return {"confirmed": True}
    for event in parse_ses_sns(body):
        ...
    return {"ok": True}

All providers normalize to a single WebhookEvent(provider, kind, recipient, message_id, timestamp, raw).

Testing

from hawkapi_mail import InMemoryBackend, init_mail

backend = InMemoryBackend()
init_mail(app, backend=backend, default_sender="me@x.com")

# After exercising the app:
assert len(backend.sent) == 1
assert backend.sent[0].subject == "Welcome"
backend.clear()

Development

git clone https://github.com/ashimov/hawkapi-mail.git
cd hawkapi-mail
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/

License

MIT.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hawkapi_mail-0.2.0.tar.gz (42.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hawkapi_mail-0.2.0-py3-none-any.whl (20.6 kB view details)

Uploaded Python 3

File details

Details for the file hawkapi_mail-0.2.0.tar.gz.

File metadata

  • Download URL: hawkapi_mail-0.2.0.tar.gz
  • Upload date:
  • Size: 42.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hawkapi_mail-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3c34dcf8e64a0eb7ae02e4bec5b0f20a0cda5cbf90f54f86481ffd75e930236c
MD5 49c3ef22023f7bcfd823d98c85637347
BLAKE2b-256 d2ed06eb9ded694ceb040ad662cc0e3125eb28adf4d2d366ea5bf6e4070819ba

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_mail-0.2.0.tar.gz:

Publisher: release.yml on ashimov/hawkapi-mail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file hawkapi_mail-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: hawkapi_mail-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 20.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for hawkapi_mail-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b720623c416dac655af5be82d37e0aa8913dc7884dd4b9958a02bfa1ff7e4003
MD5 68ac24ed1c6668420c8a817d4ca3f4ed
BLAKE2b-256 52121bf013e4a2cbd43c5fed8961594bf3fb3e76e1b81a10c48f88eceba84ea5

See more details on using hashes here.

Provenance

The following attestation bundles were made for hawkapi_mail-0.2.0-py3-none-any.whl:

Publisher: release.yml on ashimov/hawkapi-mail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page