Metadata-Version: 2.4
Name: vanty-mail
Version: 0.4.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/)

Vanty Mail is the transactional email package for the Vanty ecosystem. It gives
FastAPI applications one typed mail API, tenant-scoped persistence with Tortoise
ORM, Jinja2 templates, provider webhooks, and `vanty-core` domain events.

Supported delivery backends:

- Resend
- SMTP
- Mailgun
- SendGrid
- Postmark
- Memory, for tests and local development

## 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()
mail = mount_mail_router(app, settings=settings)
app.include_router(mail.admin_router, prefix="/admin/mail")


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


app.router.lifespan_context = lifespan
```

## Send Email

```python
from vanty_mail import EmailAttachment, EmailMessage

await mail.mail_service.send(
    EmailMessage(
        to=["alice@example.com"],
        subject="Welcome to Vanty",
        html="<h1>Hi Alice</h1>",
        text="Hi Alice",
        tags=["welcome"],
        metadata={"user_id": "usr_123"},
        attachments=[
            EmailAttachment(
                filename="receipt.txt",
                content=b"Thanks for your order",
                content_type="text/plain",
            )
        ],
    ),
)
```

Send with a specific provider for one call:

```python
await mail.mail_service.send(message, backend="postmark")
```

## Templates

`send_template` renders an active `EmailTemplate` row for the current
organization and sends the resulting message through any configured backend.

```python
await mail.mail_service.send_template(
    "welcome",
    to=["alice@example.com"],
    context={"name": "Alice"},
    organization_id=org_id,
)
```

Templates use Jinja2 with Vanty Mail's safe rendering filters. The rendered
subject, HTML body, and text body are persisted through the normal
`SentMessage` flow.

## Provider Configuration

Set `MailSettings.default_backend` to the provider you want by default. Each
provider can also be selected per message with `backend=`.

| Backend | Setting value | Required settings |
| --- | --- | --- |
| Resend | `resend` | `resend_api_key` |
| SMTP | `smtp` | `smtp_host`, `smtp_port`, and credentials when your SMTP server requires auth |
| Mailgun | `mailgun` | `mailgun_api_key`, `mailgun_domain` |
| SendGrid | `sendgrid` | `sendgrid_api_key` |
| Postmark | `postmark` | `postmark_server_token` |
| Memory | `memory` | No external credentials |

Optional provider settings:

| Setting | Purpose |
| --- | --- |
| `resend_api_url` | Override the Resend API base URL |
| `resend_webhook_secret` | Verify Resend Svix webhooks |
| `mailgun_api_url` | Use Mailgun's US or EU API base URL |
| `mailgun_webhook_signing_key` | Verify Mailgun webhook signatures |
| `sendgrid_api_url` | Use global or EU SendGrid API base URL |
| `sendgrid_webhook_public_key` | Verify SendGrid signed Event Webhooks |
| `postmark_api_url` | Override the Postmark API base URL |
| `postmark_message_stream` | Choose the Postmark message stream |
| `postmark_webhook_secret` | Verify a shared secret sent as a custom Postmark webhook header |

Environment variables use the `MAIL_` prefix. For example:

```bash
MAIL_DEFAULT_BACKEND=mailgun
MAIL_MAILGUN_API_KEY=key-...
MAIL_MAILGUN_DOMAIN=mg.example.com
MAIL_DEFAULT_FROM_EMAIL=hello@example.com
```

## Webhooks

Mount the mail router once and point each provider at its endpoint:

```text
POST /mail/webhooks/resend
POST /mail/webhooks/mailgun
POST /mail/webhooks/sendgrid
POST /mail/webhooks/postmark
```

Vanty Mail verifies provider signatures when the matching secret or public key
is configured, parses provider payloads, updates matching `SentMessage` rows,
and publishes normalized events:

- `vanty_mail.mail.delivered`
- `vanty_mail.mail.opened`
- `vanty_mail.mail.clicked`
- `vanty_mail.mail.bounced`
- `vanty_mail.mail.complained`

Subscribe with `vanty_core.events.on`:

```python
from vanty_core.events import on
from vanty_mail.events import MailBounced


@on(MailBounced)
async def handle_bounce(event: MailBounced) -> None:
    ...
```

For Postmark webhook verification, configure a custom webhook HTTP header named
`X-Vanty-Mail-Webhook-Secret` with the value stored in
`MAIL_POSTMARK_WEBHOOK_SECRET`.

## Encryption

ESP credentials and webhook secrets stored in `ESPSetting` rows are encrypted at
rest with Fernet. Set `MAIL_ENCRYPTION_KEY` to a urlsafe base64-encoded 32-byte
key before using database-backed provider settings.

Generate a key:

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

If the key is unset, Vanty Mail creates an in-process key and logs a warning.
That mode is useful for tests only because encrypted values cannot be read after
the process restarts.

## Data Model

- `EmailTemplate` stores tenant-scoped reusable subjects and bodies.
- `ESPSetting` stores encrypted provider credentials and defaults.
- `SentMessage` tracks outbound message status, provider IDs, timestamps, and bounce data.
- `InboundMessage` stores inbound email payloads for providers that forward inbound mail.

Use Postgres in production. SQLite is convenient for tests and small local
experiments, but production tenant-scoped workloads should use a database with
proper concurrency behavior.

## Development

```bash
uv sync
uv run pytest
uv run ruff check .
uv build
```

## Publishing to GitHub

This package lives inside the Vanty monorepo and can be published as a
standalone repository:

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

The target clones the monorepo to a temporary directory, filters history down to
`vanty-mail/`, adds the target remote, and pushes. Your local monorepo checkout
is not modified by the publish target.
