Metadata-Version: 2.4
Name: vanty-mail
Version: 0.3.0
Summary: Vanty App: ESP-agnostic transactional mail, templates, webhooks.
License-Expression: MIT
License-File: LICENSE
Keywords: email,fastapi,mail,resend,smtp,transactional
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: aiosmtplib>=3.0.1
Requires-Dist: cryptography>=43.0.0
Requires-Dist: email-validator>=2.3.0
Requires-Dist: fastapi>=0.135.1
Requires-Dist: httpx>=0.28.1
Requires-Dist: jinja2>=3.1.4
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: taskiq>=0.11
Requires-Dist: tortoise-orm>=1.1.6
Requires-Dist: vanty-core>=0.3.0
Description-Content-Type: text/markdown

# Vanty Mail

[![PyPI](https://img.shields.io/pypi/v/vanty-mail)](https://pypi.org/project/vanty-mail/)
[![Python](https://img.shields.io/pypi/pyversions/vanty-mail)](https://pypi.org/project/vanty-mail/)

ESP-agnostic transactional mail toolkit for FastAPI with Tortoise ORM. One
typed `MailApp` API, swappable backends (Resend, SMTP, Mailgun, SendGrid,
Postmark, Memory), Jinja2 templates with safe filters, normalized provider
webhooks, and tenant-scoped storage.

Inspired by [`django-anymail`](https://anymail.dev/) — re-implemented for
FastAPI + Tortoise ORM + httpx with [`vanty-core`](../vanty-core/) events.

## Installation

```bash
pip install vanty-mail
# or
uv pip install vanty-mail
```

## Quick start

```python
from contextlib import asynccontextmanager

from fastapi import FastAPI

from vanty_mail import MailSettings, mount_mail_router

settings = MailSettings(
    database_url="sqlite://./vanty-mail.db",
    default_backend="resend",
    resend_api_key="re_...",
    default_from_email="hello@example.com",
)

app = FastAPI()
kit = mount_mail_router(app, settings=settings)
app.include_router(kit.admin_router, prefix="/admin/mail")


@asynccontextmanager
async def lifespan(_: FastAPI):
    await kit.init_orm(generate_schemas=True)
    try:
        yield
    finally:
        await kit.close_orm()


app.router.lifespan_context = lifespan
```

## Sending mail

```python
# Direct send
from vanty_mail import EmailMessage

await kit.mail_service.send(
    EmailMessage(
        to=["alice@example.com"],
        subject="Welcome",
        html="<h1>Hi Alice</h1>",
        text="Hi Alice",
    ),
)

# Template send (uses an EmailTemplate row)
await kit.mail_service.send_template(
    "welcome",
    to=["alice@example.com"],
    context={"name": "Alice"},
    organization_id=org_id,
)
```

## ESP switching cookbook

Pick a backend at runtime via the `backend=` kwarg, or switch the default in
`MailSettings.default_backend`. Each backend reads its own creds from
settings (or from the encrypted `ESPSetting` rows in the DB).

| Backend  | `default_backend` | Required settings                                           |
| -------- | ----------------- | ----------------------------------------------------------- |
| Resend   | `resend`          | `resend_api_key`, optional `resend_webhook_secret`          |
| SMTP     | `smtp`            | `smtp_host`, `smtp_port`, `smtp_username`, `smtp_password`  |
| Memory   | `memory`          | (test-only; records sent messages on the backend instance)  |
| Mailgun  | `mailgun`         | _stub — raises `NotImplementedError`_                       |
| SendGrid | `sendgrid`        | _stub — raises `NotImplementedError`_                       |
| Postmark | `postmark`        | _stub — raises `NotImplementedError`_                       |

```python
await kit.mail_service.send(message, backend="smtp")
```

The Mailgun, SendGrid, and Postmark backends are intentionally stubbed — they
follow the same interface and raise `NotImplementedError` with a clear hint
(e.g. `"set MAILGUN_API_KEY ..."`). Drop in your own implementation by
subclassing `EmailBackend` and registering with `register_backend("mailgun", ...)`.

## Webhooks

Mount once and provider events become normalized `vanty_core.events`:

```
POST /mail/webhooks/resend
POST /mail/webhooks/mailgun
...
```

Each provider's payload is parsed into the same `WebhookEvent` shape and the
matching domain event is published (`vanty_mail.mail.delivered`, `.opened`,
`.clicked`, `.bounced`, `.complained`). Subscribe with
`from vanty_core.events import on`.

## Encryption

ESP credentials and webhook secrets are encrypted at rest with Fernet from
the `cryptography` package. Set `MAIL_ENCRYPTION_KEY` (a urlsafe base64-encoded
32-byte key) in your environment. If unset, an **ephemeral** key is generated
in-process and a loud warning is logged — values written to the DB will be
unreadable on restart, so this mode is only useful for tests.

Generate a key with:

```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```

## Events

All published via `vanty_core.events.publish`:

- `vanty_mail.mail.sent`
- `vanty_mail.mail.delivered`
- `vanty_mail.mail.opened`
- `vanty_mail.mail.clicked`
- `vanty_mail.mail.bounced`
- `vanty_mail.mail.complained`
- `vanty_mail.mail.template_changed`
- `vanty_mail.mail.inbound_received`

## Publishing to GitHub

This package lives inside the Vanty monorepo but ships as a standalone repo.
Use the bundled Makefile target:

```bash
make publish-github REPO=git@github.com:advantch/vanty-mail.git
```

Requires [`git-filter-repo`](https://github.com/newren/git-filter-repo)
(`pip install git-filter-repo`). The target clones the monorepo to a temp
directory, filters history down to `vanty-mail/`, adds the new remote, and
pushes — your local monorepo checkout is never modified.

## Known limitations

- The Mailgun, SendGrid, and Postmark backends are stubs. They raise
  `NotImplementedError` with a clear hint — implement and register your own
  to enable.
- The default Tortoise SQLite driver is fine for tests; use Postgres in
  production for `OrganizationScopedModel` to behave correctly under load.
- The `MAIL_ENCRYPTION_KEY` ephemeral fallback emits a warning every process
  start; do not rely on it outside of tests.
