# Sending mail

All authenticated sends go through one pipeline: route + kind checks, suppression lookup, content gate, unsubscribe headers, then the outbound provider.

## Endpoints

| Method | Path | Notes |
| --- | --- | --- |
| `POST` | `/api/practices/{id}/send` | One message. **202** `{ message_id, id, status }` |
| `POST` | `/api/practices/{id}/send/batch` | Up to **500** messages |
| `POST` | `/api/practices/{id}/send/bulk` | One template, many recipients (`{{var}}`) |
| `POST` | `/api/practices/{id}/send/score` | Same gate as send, no delivery |
| `POST` | `/api/practices/{id}/send-test` | Quality / mailbox-simulator kinds |

Idempotency: send `Idempotency-Key` (or `X-Idempotency-Key`) on single send. Batch items may include `idempotency_key`. Replays return the original response.

## Body shape

Accepted aliases: `text` / `text_body`, `html` / `html_body`. `to` may be a string or an array.

```json
{
  "from": "Praxis <noreply@your-domain.example>",
  "to": ["patient@example.com"],
  "cc": [],
  "bcc": [],
  "reply_to": ["praxis@example.com"],
  "subject": "Ihr Termin",
  "text": "Plain text",
  "html": "<p>HTML</p>",
  "route": "outgoing",
  "message_kind": "termin",
  "tag": "appointment",
  "tags": ["appointment", "reminder"],
  "metadata": { "patient_id": "42" },
  "settings": { "track_opens": false, "track_clicks": false }
}
```

Limits: ≤50 recipients (to+cc+bcc). Addresses in more than one bucket are deduped (To wins over Cc over Bcc).

## Attachments

Base64 `attachments[]`. Caps: **5 files**, **10 MiB** decoded each, combined decoded ≤ ~30 MiB, HTTP body ≤ ~48 MiB.

Allowed: pdf, office documents, csv/tsv, ics, png/jpg/gif/webp. Archives and executables (`zip`, `rar`, `exe`, `js`, `html`, …) and suspicious double extensions are rejected. CSV/TSV always download as attachments.

Inline images: set `content_id` and reference `cid:your-id` in HTML. Bare ids are normalized.

`GET /api/meta` exposes `attachment_policy` so clients can enforce the same rules before upload.

## Calendar (Termin)

Include `calendar` (`summary`, `starts_at`, optional `ends_at` / `location`) to attach a `METHOD:REQUEST` `.ics`. Preview with `POST …/integrations/ics-preview`.

## Content reputation gate

Before send, Werkmail scores subject, body, HTML, and attachments. Optional scanner sidecar and hostname blocklists can add signals.

- Local score ≥ **70**, scanner reject, virus, or listed host → **hard block** (`content_blocked` in the JSON error). The message is stored as `failed` and webhooks `message.failed` + `message.content_blocked` fire.
- Softer issues return `content_warnings` without blocking.

`POST …/send/score` returns `score`, `grade`, `inbox_risk`, `would_block`, `reasons`, `warnings`, and recommendations — use it in compose UIs.

## Unsubscribe on every send

When the public origin is HTTPS, every message (test, transactional, marketing) gets:

- A subtle footer link to opt out of all mail from this sender
- `List-Unsubscribe` and `List-Unsubscribe-Post: List-Unsubscribe=One-Click`

Broadcast / newsletter also requires a complete Impressum (street, city, privacy URL) and injects a legal footer.

## Marketing eligibility

Broadcast requires:

- DKIM verified
- Warm-up day ≥ 3
- Complete Impressum
- Active newsletter consent (DOI) for list sends

Transactional routes reject promotional-looking content.

## Message-ID and platform signatures

Werkmail sets the RFC 5322 `Message-ID` to your **From domain** (raw MIME). Recipients who open “original message” should see `@your-domain`, not a platform host.

The outbound platform may still add **its own DKIM signature** in the raw source (`d=` on a platform domain). You cannot replace that signature. Inbox authentication uses **your** domain DKIM (`d=your-domain`) once those CNAMEs are verified. Custom MAIL FROM (`bounce.<domain>`) brands the envelope Return-Path the same way.

See [Raw source and branding](/raw-source-and-branding) for the full table of what we brand, what the platform always adds, and the SMTP-relay escape hatch.

## Safe mode

While safe mode is on, recipients must be the mailbox simulator, a placement tester, or an allowlisted address. `GET /api/meta` reports the current flag. Production tenants turn this off after go-live (`GET …/cutover` lists remaining checks).
