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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hawkapi_mail-0.1.0.tar.gz.
File metadata
- Download URL: hawkapi_mail-0.1.0.tar.gz
- Upload date:
- Size: 39.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b48cbd0f128270d252d31c0952e20ce44d6e62ce91dc526baf18859801bd514
|
|
| MD5 |
93cb6bcee4e1dbe2d8c536588bb55c29
|
|
| BLAKE2b-256 |
9626db761920130eea020b421526f40c44e3e7859c11c4de2c102a5cad515329
|
Provenance
The following attestation bundles were made for hawkapi_mail-0.1.0.tar.gz:
Publisher:
release.yml on ashimov/hawkapi-mail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hawkapi_mail-0.1.0.tar.gz -
Subject digest:
6b48cbd0f128270d252d31c0952e20ce44d6e62ce91dc526baf18859801bd514 - Sigstore transparency entry: 1552896305
- Sigstore integration time:
-
Permalink:
ashimov/hawkapi-mail@e3934a4acdae1dd076d3444f18fd888cd76c3c3c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ashimov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3934a4acdae1dd076d3444f18fd888cd76c3c3c -
Trigger Event:
release
-
Statement type:
File details
Details for the file hawkapi_mail-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hawkapi_mail-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a6135d5655d5fa7253cf08a55e2cf135f72db70ea260f2ceb68166abc1f8067
|
|
| MD5 |
7b795871c0a5531ca9bd3ed25970fd22
|
|
| BLAKE2b-256 |
b54ab3754fbeaca2ba3c48dfb40b892d54960d8ecd2b973fa53331594ea64172
|
Provenance
The following attestation bundles were made for hawkapi_mail-0.1.0-py3-none-any.whl:
Publisher:
release.yml on ashimov/hawkapi-mail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hawkapi_mail-0.1.0-py3-none-any.whl -
Subject digest:
0a6135d5655d5fa7253cf08a55e2cf135f72db70ea260f2ceb68166abc1f8067 - Sigstore transparency entry: 1552896331
- Sigstore integration time:
-
Permalink:
ashimov/hawkapi-mail@e3934a4acdae1dd076d3444f18fd888cd76c3c3c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ashimov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3934a4acdae1dd076d3444f18fd888cd76c3c3c -
Trigger Event:
release
-
Statement type: