Com TLS_MODE=acme o certificado e emitido por este servico, nao vigiado em disco, portanto nao ha watcher para estar saudavel. O painel reportava false e mostrava "indisponivel" em permanencia, ao lado de um certificado perfeitamente valido. Passa a ser null, e a pagina nao mostra veredito nenhum. O README ganha uma seccao sobre os dois modos de certificado. O modo traefik falha de duas maneiras silenciosas -- sem router para o PRIMARY_MAIL_HOST o nome nunca chega ao acme.json, e o acme.json fica em 600 do root, ilegivel para o utilizador nao-root do contentor. Nos dois casos o HTTPS continua a funcionar porque quem o serve e o proxy, enquanto as portas de correio caem no autoassinado e sao recusadas por qualquer cliente que verifique. Foi exactamente o que aconteceu nesta instalacao. Actualizado tambem o que ja nao esta por acabar: a resposta de ausencia, a assinatura e as regras Sieve sao consumidas, e a reconciliacao de DNS conduz o estado em vez de o relatar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
221 lines
11 KiB
Markdown
221 lines
11 KiB
Markdown
# LambdaMail
|
|
|
|
A self-hosted mail server with an integrated webmail and admin console, built to
|
|
run on [Coolify](https://coolify.io) from a single `docker-compose.yaml`.
|
|
|
|
SMTP, IMAP, POP3 and ManageSieve, with SPF, DKIM, DMARC, ARC, MTA-STS and
|
|
TLS-RPT, DNS automation through Cloudflare, and spam and virus filtering. No
|
|
paid tiers and no artificial limits on domains or mailboxes.
|
|
|
|
> **Status: pre-release.** The mail path, the security posture and the web
|
|
> interfaces work end to end, and there is an automated test suite behind them.
|
|
> See [What is not finished](#what-is-not-finished) before putting real mail
|
|
> through it.
|
|
|
|
---
|
|
|
|
## What it does
|
|
|
|
| Area | |
|
|
| :--- | :--- |
|
|
| **Protocols** | SMTP MX (25), Submission (587 STARTTLS, 465 implicit TLS), IMAP (143/993) with UID, flags, IDLE and CONDSTORE, POP3 (110/995), ManageSieve (4190) |
|
|
| **Authentication** | SPF, DKIM (Ed25519 + RSA dual signature), DMARC, ARC sealing, DANE/TLSA and MTA-STS on delivery, TLS-RPT ingestion |
|
|
| **Filtering** | Rspamd scoring with greylisting, RBLs and rate limiting; ClamAV; Sieve |
|
|
| **Delivery** | Durable outbound queue in Postgres with the retry ladder and RFC 3464 bounces; smarthost fallback for hosts with port 25 blocked |
|
|
| **DNS** | Cloudflare reconciliation of the expected records for every active domain, idempotent, with per-record verification against public resolvers |
|
|
| **Filing** | Sieve rules built in the UI, custom folders, and an RFC 5230 vacation responder that replies in thread and once per sender per week |
|
|
| **Web** | Webmail at `/user/*` and an admin console at `/admin/*`, isolated by session audience, with TOTP two-factor, recovery codes, app passwords, and English, Portuguese and Spanish |
|
|
| **Console** | Server-side paging and filtering, user editing (role, quota, language, password, second factor), per-user aliases, and a diagnostics page at `/admin/tests` |
|
|
|
|
## Architecture
|
|
|
|
Two runtimes, split by what each is good at:
|
|
|
|
- **`protocols`** (Go) — every protocol listener, the delivery worker, TLS
|
|
certificate handling, DNS reconciliation, and the webmail's mail API. It owns
|
|
folder, UID, flag and blob semantics.
|
|
- **`auth`** (TypeScript) — logins, sessions, two-factor, account management and
|
|
the admin console API.
|
|
- **`web`** (Next.js) — both web surfaces, and the proxy that gives the browser a
|
|
single origin.
|
|
- **`storage`** (TypeScript) — health only today.
|
|
|
|
Postgres is the source of truth for everything, including the outbound queue.
|
|
Redis is a cache and dispatch index; losing it loses no mail.
|
|
|
|
```
|
|
┌──────────┐
|
|
browser ─────────▶│ web │──┬─▶ auth (sessions, accounts, admin)
|
|
└──────────┘ └─▶ protocols (mail read/send, DKIM, TLS)
|
|
│
|
|
internet ──25/587/465/143/993──────────┘
|
|
│
|
|
Postgres ─┴─ Redis ─ Rspamd ─ ClamAV
|
|
```
|
|
|
|
## Deploying
|
|
|
|
For a real VPS, follow **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)**. It covers the
|
|
three things no code can do for you — reverse DNS, the DNSSEC delegation and
|
|
getting port 25 unblocked — and the order the rest has to happen in.
|
|
|
|
The short version:
|
|
|
|
```bash
|
|
cp .env.production.example .env # then replace every CHANGE
|
|
make preflight # checks port 25, PTR, RBLs, token scope
|
|
docker compose up -d
|
|
make create-admin EMAIL=you@example.com PASSWORD='...'
|
|
```
|
|
|
|
`make create-admin` is not optional: a fresh install has no mailbox, and every
|
|
route into the console needs one.
|
|
|
|
### Ports
|
|
|
|
Everything below is TCP. "Inbound" is what the host's firewall and the
|
|
provider's security group must allow; "outbound" is what the host must be able
|
|
to reach.
|
|
|
|
| Port | Direction | Who connects | Required? | Notes |
|
|
| ---: | :--- | :--- | :--- | :--- |
|
|
| 25 | in | Any sending MTA | **Yes** to receive mail | Must be open to the world. Rate-limited, not authenticated — that is how SMTP works. |
|
|
| 25 | **out** | This host → remote MTAs | **Yes** to send mail | The one most providers block. Without it, set a relay (`RELAY_HOST`); see [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md). |
|
|
| 80 | in | Let's Encrypt, browsers | **Yes** | ACME HTTP-01 challenge, and the redirect to HTTPS. Closing it means no certificate. |
|
|
| 443 | in | Browsers | **Yes** | Webmail, admin console, MTA-STS policy, autodiscover. |
|
|
| 587 | in | Mail clients | Recommended | Submission with STARTTLS. The port clients should use. |
|
|
| 465 | in | Mail clients | Optional | Submission with implicit TLS, for clients that insist. |
|
|
| 143 | in | Mail clients | Optional | IMAP with STARTTLS. Prefer 993. |
|
|
| 993 | in | Mail clients | Recommended | IMAPS. |
|
|
| 110 / 995 | in | Mail clients | Optional | POP3 / POP3S. Leave closed unless something needs POP3. |
|
|
| 4190 | in | Mail clients | Optional | ManageSieve, for editing filters from a desktop client. |
|
|
| 53 | out | This host → resolver | **Yes** | MX, SPF, DKIM, DANE and MTA-STS lookups. UDP **and** TCP — DNSSEC answers overflow UDP. |
|
|
| 443 | out | This host → APIs | If used | Cloudflare DNS automation, MTA-STS policy fetch, ClamAV signature updates. |
|
|
|
|
Ports 110, 995 and 4190 are published by the compose file but are worth closing
|
|
at the firewall unless a client actually needs them — every open port is one
|
|
more thing to keep patched.
|
|
|
|
### What the host has to allow
|
|
|
|
- **Reverse DNS (PTR)** for the public IPv4 must resolve to `PRIMARY_MAIL_HOST`,
|
|
and that name must resolve back to the same address. Set by whoever owns the
|
|
IP block — your provider's panel, never Cloudflare. Gmail and Outlook
|
|
spam-folder or refuse mail without it. If the host has a public IPv6 and
|
|
publishes an AAAA record, it needs a PTR too: senders that connect over IPv6
|
|
are judged on the IPv6 reverse.
|
|
- **Outbound port 25 unblocked.** Ask the provider; a timeout rather than a
|
|
refusal when connecting to a remote MX is the signature of a block.
|
|
- **Docker** with the compose plugin, and a user in the `docker` group or root.
|
|
- **Ports below 1024** are bound by Docker's proxy on the host, so the daemon
|
|
needs the privilege — the default on a normal install. The containers
|
|
themselves run unprivileged.
|
|
- **The proxy's certificate directory**, under `TLS_MODE=traefik` only — see
|
|
[Certificates](#certificates) below, and prefer `TLS_MODE=acme` if you can.
|
|
- **Disk** for the mail spool (`protocols_spool`) and Postgres. Mail is stored
|
|
once and reference-counted, but plan for growth.
|
|
- **A firewall that does not rate-limit port 25 into uselessness.** Some
|
|
providers apply aggressive SYN limits that look like intermittent delivery
|
|
failures.
|
|
|
|
## Running locally
|
|
|
|
```bash
|
|
cp .env.example .env
|
|
make gen-dev-cert # self-signed certificate for the listeners
|
|
make up # builds from source via the build overlay
|
|
make seed # a postmaster@example.test fixture
|
|
```
|
|
|
|
The webmail is on `http://localhost:3000`. The seeded password is
|
|
`dev-password-only` — it is a fixture, not a credential.
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
make test # Go suite, auth and webmail
|
|
make lint # go vet, tsc, and the language lint
|
|
```
|
|
|
|
The Go and auth suites include integration tests that need Postgres. They skip
|
|
without `TEST_DATABASE_URL` and run against a real database in CI, which is
|
|
where the security regressions are enforced — wrong password, wrong TOTP code,
|
|
session revocation, surface isolation, cross-mailbox reads.
|
|
|
|
## Configuration
|
|
|
|
Every variable is documented inline in
|
|
[`.env.production.example`](.env.production.example). The ones without a safe
|
|
default:
|
|
|
|
| Variable | |
|
|
| :--- | :--- |
|
|
| `JWT_SECRET` | Signs sessions. At least 32 characters; the service refuses to start below that rather than fall back to a constant. |
|
|
| `LAMBDAMAIL_MASTER_KEY` | Encrypts DKIM keys, TOTP secrets and provider tokens. **Back it up separately from the database** — losing it makes every stored secret unrecoverable. |
|
|
| `POSTGRES_PASSWORD`, `REDIS_PASSWORD` | No defaults; compose fails without them. |
|
|
| `MAIL_DOMAIN`, `PRIMARY_MAIL_HOST`, `PUBLIC_IPV4` | Identity. `PRIMARY_MAIL_HOST` must match the PTR of `PUBLIC_IPV4`. |
|
|
|
|
### Certificates
|
|
|
|
Two modes, and the choice matters more than it looks.
|
|
|
|
**`TLS_MODE=acme` (recommended).** LambdaMail obtains its own certificate for
|
|
`PRIMARY_MAIL_HOST`, `mta-sts.<domain>` and `autoconfig.<domain>` over DNS-01,
|
|
using the Cloudflare token already configured for DNS. It needs
|
|
`LAMBDAMAIL_MASTER_KEY` to seal the account and certificate keys, and
|
|
`ACME_EMAIL`. It is the only mode DANE can run in, because it is the only one
|
|
where the key for the next certificate exists before that certificate does.
|
|
|
|
**`TLS_MODE=traefik`.** The mail listeners read the certificate the reverse
|
|
proxy already obtained, from `COOLIFY_PROXY_DIR`, mounted read-only. Two things
|
|
have to be true, and neither is obvious when they are not:
|
|
|
|
- the proxy must have a router for `PRIMARY_MAIL_HOST`, or that name never
|
|
appears in `acme.json` and no certificate exists to read;
|
|
- `acme.json` must be readable by the container's non-root user. Left at mode
|
|
`600` owned by root — which is how the proxy writes it — it cannot be.
|
|
|
|
Either failure is silent in the same way: HTTPS keeps working, because the
|
|
proxy is serving it, while the mail listeners fall back to a **self-signed
|
|
certificate** and every client that verifies refuses SMTP, IMAP and POP3. It
|
|
reads as a certificate problem and is usually a permissions problem.
|
|
|
|
`/admin/security` names the issuer and says outright when the certificate is
|
|
self-signed, which is the fastest way to tell the two apart.
|
|
|
|
## Operating
|
|
|
|
| | |
|
|
| :--- | :--- |
|
|
| `make preflight` | Port 25 egress, PTR and FCrDNS, RBL listings, Cloudflare token scope, certificate presence |
|
|
| `make create-admin EMAIL=… PASSWORD='…'` | First administrator on a fresh install |
|
|
| `make reset-password EMAIL=… PASSWORD='…'` | Recovers a locked-out account and signs out its sessions |
|
|
| `docker compose logs -f protocols` | Delivery, TLS and DNS reconciliation |
|
|
| `GET /health` on `protocols` | `200` healthy, `200` with `X-LambdaMail-Health: degraded` when a certificate needs attention, `503` when a dependency is down |
|
|
|
|
[docs/OPERATIONS.md](docs/OPERATIONS.md) covers backups, certificate rotation,
|
|
what the alerts mean and how to read a delivery failure.
|
|
|
|
## What is not finished
|
|
|
|
Stated plainly, because a mail server that half works loses mail:
|
|
|
|
- **Read-only console panels.** The guided domain onboarding checklist reports
|
|
on state rather than driving it.
|
|
- **Not implemented.** JMAP, BIMI, CalDAV/CardDAV, multi-node. Backups are
|
|
documented but not automated.
|
|
- **DANE** is off unless `TLS_MODE=acme`, and deliberately so: under a
|
|
Traefik-managed certificate the key changes at every renewal and the published
|
|
TLSA record stops matching, which permanently rejects mail at every validating
|
|
MTA.
|
|
|
|
## Contributing
|
|
|
|
- All code, comments and identifiers are in English. `make lint` enforces it.
|
|
- Tests come with the change that needs them; the suites above must stay green.
|
|
- Commits reference their issue: `#N: what changed and why`.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|