# Limristem eMail API Reference

Base URL:
- public: `https://<mail-host>/`
- internal: `http://127.0.0.1:8080/`

OpenAPI/Swagger is **off by default**. Enable with `LIMRISTEM_MAIL_ENABLE_API_DOCS=yes` (`/docs`, `/redoc`, `/openapi.json`).

---

## Authentication

### Without admin MFA

- HTTP Basic over HTTPS
- username: `LIMRISTEM_MAIL_API_ADMIN_USER`
- password verified against `LIMRISTEM_MAIL_API_ADMIN_PASS_HASH` (plaintext only if explicitly configured)
- brute-force protection via Redis (`LIMRISTEM_MAIL_API_AUTH_*`)

### With admin MFA enabled

1. `POST /auth/login` with username/password  
2. If MFA is required: complete `POST /auth/mfa/verify` (and optionally `POST /auth/mfa/sms` first)  
3. Use `Authorization: Bearer <access_token>` on subsequent API calls  

When MFA is enabled, **HTTP Basic alone is rejected** unless  
`LIMRISTEM_MAIL_MFA_ALLOW_BASIC_WITHOUT_TOKEN=yes` (emergency only).

Example:

```bash
# 1) login
curl -sS -X POST https://mail.example.com/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"…"}'

# → either { "access_token": "…", "token_type": "bearer", … }
# → or    { "mfa_required": true, "challenge_id": "…", "methods": ["totp","sms"] }

# 2) if SMS method: request OTP
curl -sS -X POST https://mail.example.com/auth/mfa/sms \
  -H 'Content-Type: application/json' \
  -d '{"challenge_id":"…"}'

# 3) verify TOTP or SMS code
curl -sS -X POST https://mail.example.com/auth/mfa/verify \
  -H 'Content-Type: application/json' \
  -d '{"challenge_id":"…","method":"totp","code":"123456"}'

TOKEN=…
curl -sS -H "Authorization: Bearer $TOKEN" https://mail.example.com/domains
```

---

## Auth / MFA

| Method | Path | Description |
|--------|------|-------------|
| POST | `/auth/login` | Password step → token or MFA challenge |
| POST | `/auth/mfa/sms` | Send SMS OTP for a challenge |
| POST | `/auth/mfa/verify` | Verify TOTP or SMS → access token |
| POST | `/auth/logout` | Revoke Bearer token |
| GET | `/auth/mfa` | Admin MFA status |
| POST | `/auth/mfa/totp/enroll` | Start TOTP enrollment (secret + otpauth URI) |
| POST | `/auth/mfa/totp/confirm` | Confirm TOTP with a 6-digit code |
| DELETE | `/auth/mfa/totp` | Disable TOTP |
| PATCH | `/auth/mfa/sms` | Enable/configure SMS phone + provider (`crisaleo` \| `sms2sms`) |
| GET | `/auth/mfa/providers` | List SMS provider configs (secrets masked) |
| PUT | `/auth/mfa/providers` | Create/update provider credentials |

### SMS providers (Crilide-style)

Headers: `X-Client-Id`, `X-Client-Secret`, `X-Api-Key`

| Code | Default base | Default send path |
|------|--------------|-------------------|
| `crisaleo` | `https://api.crisaleo.eu` | `/api/v1/modules/sms/send` |
| `sms2sms` | `https://api.sms2sms.eu` | `/api/v1/modules/sms2sms/send` (+ `route`: `internal` \| `global`) |

Credentials can also be set via env (`LIMRISTEM_MAIL_SMS_*`); secrets in DB are encrypted at rest.

### Mail protocols (SMTP / IMAP / POP)

Interactive OTP is **not** supported mid-session by standard clients. For mailbox hardening:

| Method | Path | Description |
|--------|------|-------------|
| GET | `/accounts/{id}/app-passwords` | List app passwords |
| POST | `/accounts/{id}/app-passwords` | Create one (`{ "label": "Thunderbird" }`) → one-time plaintext |
| DELETE | `/accounts/{id}/app-passwords/{password_id}` | Revoke |
| PATCH | `/accounts/{id}` | Set `require_app_password` / `mfa_hint` |

Dovecot uses `scripts/dovecot-checkpassword.sh` to accept the primary argon2id hash **and** active app passwords. If `require_app_password=true`, only app passwords work.

---

## Mailbox import (remote IMAP → local)

Import every folder and message (including Sent) from a remote IMAP server into a local Limristem account. Jobs are queued and run by a background worker; progress includes percent, bytes transferred/remaining, speed, and ETA.

| Method | Path | Description |
|--------|------|-------------|
| POST | `/mailbox-import/test` | Test source IMAP credentials (no job created) |
| POST | `/mailbox-import` | Queue an import job |
| GET | `/mailbox-import` | List jobs (`?status=&limit=`) |
| GET | `/mailbox-import/{id}` | Job status + live progress |
| POST | `/mailbox-import/{id}/cancel` | Cancel pending/running job |
| DELETE | `/mailbox-import/{id}` | Delete a finished job |

Create body:

```json
{
  "destination_account_id": 1,
  "source_host": "imap.old-provider.com",
  "source_port": 993,
  "source_protocol": "imaps",
  "source_username": "user@old-provider.com",
  "source_password": "…",
  "source_auth_mode": "login"
}
```

`source_protocol`: `imaps` (SSL), `starttls`, or `imap` (plain).  
`source_auth_mode`: `login`, `plain`, or `cram-md5`.  
Source passwords are encrypted at rest. Panel UI: `/panel/import` (tabs **New import** / **Jobs**).

CLI:

```bash
limristem-mail import test --host imap.example.com --user u --password p --protocol imaps
limristem-mail import mailbox --dest local@example.com --host imap.example.com --user u --password p --wait
limristem-mail import list --json
limristem-mail import status 1 --json
limristem-mail import cancel 1
```

---

## Domains

| Method | Path | Description |
|--------|------|-------------|
| GET | `/domains` | List domains |
| POST | `/domains` | Create domain |
| GET | `/domains/{id}` | Details |
| PATCH | `/domains/{id}` | `is_active`, `max_users`, `dmarc_policy`, DKIM rotation policy fields |
| DELETE | `/domains/{id}` | Delete domain (+ mailboxes on disk) |
| POST | `/domains/{id}/dkim` | Generate / rotate DKIM (overlap window for previous selector) |
| POST | `/domains/{id}/dkim/retire-previous` | Drop previous selector early |
| POST | `/domains/dkim/maintenance` | Run auto-rotate / expire previous keys |
| GET | `/domains/{id}/dns` | Suggested DNS records |
| POST | `/domains/{id}/dns/provider` | Configure DNS provider (`cloudflare` / `none`) |
| POST | `/domains/{id}/dns/sync-dkim` | Publish DKIM TXT via provider |
| POST | `/domains/{id}/dns/sync-all` | Publish full mail DNS plan when supported |

Create example:

```json
{
  "name": "example.com",
  "is_active": true,
  "max_users": 50,
  "dmarc_policy": "quarantine",
  "generate_dkim": true,
  "dkim_selector": "default",
  "dkim_auto_rotate": false,
  "dkim_rotation_interval_days": 90,
  "dkim_overlap_days": 14
}
```

DNS hints include MX, SPF (no self-recursive include), DMARC, DKIM, PTR reminder, and optional MTA-STS / TLS-RPT for the primary domain.

Cloudflare provider:

```json
{
  "dns_provider": "cloudflare",
  "dns_zone_id": "0123456789abcdef0123456789abcdef",
  "dns_api_token": "cloudflare-zone-token",
  "dns_sync_enabled": true
}
```

Tokens are write-only: responses expose `dns_has_api_token`, never the secret.

---

## Accounts

| Method | Path | Description |
|--------|------|-------------|
| GET | `/accounts?domain=example.com` | List |
| GET | `/accounts/{id}` | Details |
| POST | `/accounts` | Create |
| PATCH | `/accounts/{id}` | Password, quota, active, send limits, app-password flags |
| DELETE | `/accounts/{id}` | Delete + remove Maildir |
| POST | `/accounts/send-limits/sync` | Rebuild Redis/maps for all mailboxes |

Create example:

```json
{
  "domain": "example.com",
  "local_part": "user",
  "password": "StrongMailboxPass123",
  "quota_mb": 2048,
  "is_active": true,
  "send_rate_burst": 0,
  "send_rate_per_minute": 0,
  "send_rate_per_hour": 0,
  "send_rate_per_day": 0,
  "send_rate_per_week": 0,
  "send_rate_per_month": 0
}
```

Notes:
- `quota_mb` feeds Dovecot quota enforcement.
- `max_users` on the domain is enforced for active accounts.
- Per-mailbox send rates of `0` inherit global service limits; non-zero values are enforced by Rspamd Lua on authenticated SMTP (soft-reject).
- Response includes both configured and `effective_send_rate_*` values.
- `require_app_password` / `mfa_hint` control mail-client MFA posture (see above).

---

## Aliases

- `GET /aliases`
- `POST /aliases`
- `PATCH /aliases/{id}`
- `DELETE /aliases/{id}`

```json
{
  "domain": "example.com",
  "source_local": "info",
  "destination": "user@example.com",
  "is_active": true
}
```

---

## Redirects

- `GET /redirects`
- `POST /redirects`
- `PATCH /redirects/{id}`
- `DELETE /redirects/{id}`

```json
{
  "domain": "example.com",
  "source_local": "sales",
  "target_email": "team@other.example",
  "is_active": true
}
```

Notes: self-redirects are rejected; external forwarding works better with optional PostSRSd (SRS).

---

## Reputation

- `GET /reputation`
- `GET /reputation/lookup?entity_type=ip&entity_value=1.2.3.4`
- `POST /reputation` — upsert on `(entity_type, entity_value)`
- `PATCH /reputation/{id}`

`entity_type`: `ip` | `domain` | `email` | `asn` | `hash`

---

## Operations (deliverability & self-checks)

| Method | Path | Description |
|--------|------|-------------|
| GET | `/operations/overview` | Summary of ops helpers |
| GET/POST | `/operations/deliverability` | DNS/TLS/MTA-STS style checks (script-backed) |
| GET | `/operations/e2e` | TCP/TLS probes (SMTP/IMAP/HTTPS; no mailbox password) |
| GET | `/operations/ip-reputation` | Outbound IP DNSBL self-check |
| GET | `/operations/ip-reputation/history` | Stored check rows |
| GET/PUT | `/operations/rbl` | Optional inbound Rspamd DNSBL toggle |

Inbound DNSBL is **off by default** (false-positive risk). Prefer dedicated resolvers when enabling.

---

## Statistics (privacy-safe traffic)

Only **mailbox + size + direction + time** are stored (no subjects, bodies, or remote IPs).

| Method | Path | Description |
|--------|------|-------------|
| POST | `/stats/collect` | Ingest from Postfix journal |
| GET | `/stats/traffic` | Recent events (filters: direction, mailbox) |
| GET | `/stats/summary` | Aggregates |
| GET | `/stats/daily` | Day series |
| GET | `/stats/mailboxes` | Per-mailbox breakdown |
| GET | `/stats/chart.svg` | Simple SVG chart for dashboards |

Collector: `scripts/collect-mail-stats.sh` + `limristem-mail-stats.timer`.  
Retention: `LIMRISTEM_MAIL_STATS_RETENTION_DAYS` (default 400).

---

## Topology (multi-node)

See also `docs/TOPOLOGY.md`.

| Method | Path | Description |
|--------|------|-------------|
| GET | `/topology` | Current mode + local config |
| POST | `/topology/wizard` | Apply mode wizard (`standalone` \| `routed` \| `cluster`) |
| GET | `/topology/servers` | List peer servers |
| POST | `/topology/servers` | Register peer |
| DELETE | `/topology/servers/{id}` | Remove peer |
| GET | `/internode/v1/hello` | Internode health (HMAC/Bearer) |
| POST | `/internode/v1/register` | Peer registration |
| POST | `/internode/v1/sync` | Apply sync payload (routed/synced DB) |

Internode traffic requires `LIMRISTEM_MAIL_INTERNODE_TOKEN` (min 32 chars) over HTTPS only.

---

## Health

- `GET /health` — public readiness (DB + Redis)
- `GET /health/details` — authenticated details (hostname, paths)

---

## Admin

### Queue

- `GET /admin/queue`
- `POST /admin/queue/{queue_id}/{hold|release|requeue}`
- `DELETE /admin/queue/{queue_id}`

### Bans (fail2ban)

- `GET /admin/bans`
- `POST /admin/bans`
- `DELETE /admin/bans?ip=203.0.113.5`

### Limits

- `GET /admin/limits`
- `PATCH /admin/limits` — auth lockouts, message size, SMTP rates, global send quotas, milter default, greylist, etc.

### Firewall (nftables)

- `GET /admin/firewall`
- `PATCH /admin/firewall` — `firewall-enabled`, `firewall-rules-json`, legacy port keys

### Backups

- `GET /admin/backups` — config + local runs
- `PATCH /admin/backups`
- `POST /admin/backups/run`
- `GET|POST /admin/backups/schedules`, `DELETE /admin/backups/schedules/{id}`
- `GET|POST /admin/backups/storages`, `DELETE|POST …/test`

### SSL

- `GET /admin/ssl`
- `POST /admin/ssl/selfsigned`
- `POST /admin/ssl/letsencrypt`
- `POST /admin/ssl/manual`

### Settings / updates

| Method | Path | Description |
|--------|------|-------------|
| GET | `/admin/settings` | Settings snapshot (`update_channel`, `update_check_url`, versions, …) |
| PATCH | `/admin/settings` | `server_name`, `panel_title`, `auto_updates_enabled`, **`update_channel`** |
| POST | `/admin/settings/favicon` | Upload panel favicon |
| POST | `/admin/settings/check-update` | Query selected channel’s `version.json` |
| POST | `/admin/settings/apply-update` | Download + install from selected channel |

#### Update channels

| Channel | Default `version.json` | Publish script |
|---------|------------------------|----------------|
| **stable** | `https://get.limristem.eu/mail/version.json` | `./push-install-package.sh` |
| **nightly** | `https://nightly.limristem.eu/mail/version.json` | `./push-install-package-nightly.sh` |

Env:
- `LIMRISTEM_MAIL_UPDATE_CHANNEL=stable|nightly`
- `LIMRISTEM_MAIL_UPDATE_CHECK_URL_STABLE=…`
- `LIMRISTEM_MAIL_UPDATE_CHECK_URL_NIGHTLY=…`

Nightly layout on the CDN host (`…/public/mail/`):
- only `version.json` at the `mail/` level  
- archive, checksum and unpacked tree under `mail/<version>/`  
- published metadata includes `channel: "nightly"`, `download_url`, `sha256_url`, `package_dir`

`PATCH` example:

```json
{
  "update_channel": "nightly",
  "auto_updates_enabled": false
}
```

---

## Web panel

When `LIMRISTEM_MAIL_ENABLE_WEB_PANEL=yes`:

- Login: `GET|POST /panel/login`, MFA: `POST /panel/login/mfa`, `POST /panel/login/mfa/sms`
- Dashboard, domains, traffic, statistics, queue, backups, security (bans + firewall + **admin MFA**), settings (general/SSL/limits/API + **update channel**), operations, topology, SSL, API credentials

Panel sessions are Redis-backed with CSRF, `Secure` + `HttpOnly` cookies.

---

## Operational notes

- Schema is applied by `scripts/install.sh` / update (`database/schema.sql`), not on API import.
- Privileged ops (queue, bans, SSL, backups, firewall, limits, stats collect, topology) use root helpers via constrained sudoers; the API unit must allow those escalations.
- DKIM keys under `LIMRISTEM_MAIL_DKIM_KEYS_DIR`; DNS tokens encrypted (`config/.dns-token-key`).
- Redis cache TTL: `LIMRISTEM_MAIL_CACHE_TTL_SECONDS`.
- Postfix milter default: `tempfail` if Rspamd is down.
- Trusted proxies: `LIMRISTEM_MAIL_TRUSTED_PROXIES` for `X-Forwarded-For` client IP.

---

## Example curl (Basic, no MFA)

```bash
curl -u admin:'your-bootstrap-password' \
  -H 'Content-Type: application/json' \
  https://mail.example.com/domains \
  -d '{"name":"example.com","generate_dkim":true}'

curl -u admin:'your-bootstrap-password' \
  -H 'Content-Type: application/json' \
  https://mail.example.com/accounts \
  -d '{"domain":"example.com","local_part":"alice","password":"StrongMailboxPass123"}'

curl -u admin:'your-bootstrap-password' \
  https://mail.example.com/health/details

curl -u admin:'your-bootstrap-password' \
  -H 'Content-Type: application/json' \
  https://mail.example.com/domains/1/dns/provider \
  -d '{"dns_provider":"cloudflare","dns_zone_id":"…","dns_api_token":"…","dns_sync_enabled":true}'

curl -u admin:'your-bootstrap-password' -X POST \
  https://mail.example.com/domains/1/dns/sync-dkim

curl -u admin:'your-bootstrap-password' \
  -H 'Content-Type: application/json' \
  -X PATCH https://mail.example.com/admin/settings \
  -d '{"update_channel":"nightly"}'
```
