mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-10-01 19:43:49 +00:00
Registration has been dead since 2026-09-13. `ssh join@bbs.profullstack.com` creates the account, then fails at the confirmation step with "couldn't email the code" and disconnects, so nobody can finish signing up. Cause: Caddy owns ACME for mail.profullstack.com and renewed on 2026-08-14 (valid to Nov 12), but Mailu went on serving the certificate it loaded at container start (Jun 15 -> Sep 13). When that lapsed, the STARTTLS handshake from internal/mail started failing verification and every transactional send died with it -- confirmation codes, signup notifications, credential mail. Reproduced against production; 25/465/993 all still present the expired cert while :443 serves the renewed one. Three things let a single stale certificate take registration down: - setup.sh installed the refresher and enabled its *timer*, but never ran it. `systemctl enable --now <timer>` starts the timer, not the service, so a redeploy left a stale cert in place (and did nothing at all if the timer was never scheduled). The news and IRC sections already run theirs at provision time; the Mailu section now does too, which is what repairs the live host. - refresh-certs.sh only compared files, so a copy whose reload silently failed left a fresh cert on disk and an expiring one on the wire -- invisible. It now reads back what the relay actually serves, forces a reload when that disagrees with /certs, refuses to copy a source cert that is itself expired, and no longer swallows the `docker compose restart` failure. It restarts `front` alone, the only container that mounts ./certs. - internal/mail verified the relay's certificate even on loopback, where there is nothing to intercept. It now skips verification for a loopback relay (the reasoning docs/mail.md already applies to the plaintext Dovecot hand-off) and gains AGENTBBS_SMTP_SERVERNAME, mirroring AGENTBBS_MAIL_SMTP_SERVERNAME, so the documented 127.0.0.1:25 config can verify against the mail host instead of an IP literal. A non-loopback relay is still verified. Errors are wrapped with the address and the failing stage so the next failure is one journal line to diagnose rather than nine days of silence. Tests cover the envelope, the unreachable-relay message, and both halves of the TLS decision: a loopback relay with an expired cert delivers, a non-loopback one with the same cert is refused. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
201 lines
9.5 KiB
Markdown
201 lines
9.5 KiB
Markdown
# Mail — self-hosted Mailu
|
|
|
|
AgentBBS gives **every verified member** (free and paid alike) a real mailbox at
|
|
`<name>@bbs.profullstack.com`, reached two ways:
|
|
|
|
- **Webmail** — `https://mail.profullstack.com` (Roundcube).
|
|
- **AgentMail** — the in-BBS client (`internal/mailbox`): the `Mail` hub entry
|
|
or `ssh mail@bbs.profullstack.com` (a TUI for humans, a JSON bot mode for
|
|
agents). It connects to this stack.
|
|
|
|
Two distinct names are involved — don't conflate them:
|
|
|
|
| | value | role |
|
|
|---|---|---|
|
|
| **Address domain** | `bbs.profullstack.com` | the `@`-part of member addresses (`AGENTBBS_MAIL_ADDR_DOMAIN`) |
|
|
| **Mail server host** | `mail.profullstack.com` | where IMAP/SMTP/webmail actually run (`AGENTBBS_MAIL_DOMAIN`) |
|
|
|
|
The apex `profullstack.com` is **reserved for corporate mail** and is not served
|
|
here.
|
|
|
|
## Architecture
|
|
|
|
The host already runs **Caddy** (owns `:80`/`:443`) and the **agentbbs** process.
|
|
Mailu (Postfix + Dovecot + Roundcube + rspamd) runs as a Docker Compose stack:
|
|
|
|
- Mailu owns the **mail ports** on the host: `25, 465, 587, 993, 995`.
|
|
- Mailu's HTTP front is bound to **loopback** (`127.0.0.1:8080`); **Caddy**
|
|
reverse-proxies `https://mail.profullstack.com` to it (webmail + admin + API).
|
|
- **TLS:** `TLS_FLAVOR=mail` — Mailu does *not* run its own ACME (Caddy is the
|
|
only ACME client). Caddy obtains the `mail.profullstack.com` cert; the cert
|
|
refresher copies it into Mailu and reloads on renewal.
|
|
- The **agentbbs gateway** reads/sends on behalf of members: IMAP via a Dovecot
|
|
**master user** (one secret opens any mailbox), SMTP via the co-located relay
|
|
on `127.0.0.1:25`. Members therefore never manage an IMAP/SMTP password.
|
|
- **Provisioning** is automatic: when a member verifies their email at `join@`
|
|
(or opens `Mail`), agentbbs ensures `<name>@bbs.profullstack.com` exists via
|
|
Mailu's **admin REST API** (`internal/mailu`, token = `API_TOKEN`). The manual
|
|
`deploy/mailu/provision-mailbox.sh` is only for the gateway master user and
|
|
backfills.
|
|
|
|
```
|
|
┌─────────── Caddy (:443) ───────────┐
|
|
webmail → │ mail.profullstack.com → 127.0.0.1:8080 (Mailu front: webmail/admin/API)
|
|
└───────────────┬─────────────────────┘
|
|
│ copies LE cert (refresh-certs.sh)
|
|
clients → Mailu front (:25 :465 :587 :993 :995) ──→ Postfix / Dovecot / rspamd
|
|
▲
|
|
agentbbs ──IMAP 993 (master user)──┘ ──SMTP 127.0.0.1:25 (local relay)──▶
|
|
agentbbs ──admin API (token) http://127.0.0.1:8080/api/v1──▶ (auto-provision)
|
|
```
|
|
|
|
## DNS
|
|
|
|
Mail is delivered to the **address domain** (`bbs.profullstack.com`), so its MX
|
|
must point at the **server host** (`mail.profullstack.com`):
|
|
|
|
| Type | Host | Value |
|
|
|---|---|---|
|
|
| A | `mail.profullstack.com` | host IP |
|
|
| MX | `bbs.profullstack.com` | `10 mail.profullstack.com.` |
|
|
| TXT (SPF) | `bbs.profullstack.com` | `v=spf1 mx -all` |
|
|
| TXT (DMARC) | `_dmarc.bbs.profullstack.com` | `v=DMARC1; p=quarantine; rua=mailto:postmaster@bbs.profullstack.com` |
|
|
| TXT (DKIM) | `dkim._domainkey.bbs.profullstack.com` | from `flask mailu config-export` after first boot |
|
|
| PTR | host IP | `mail.profullstack.com` (set at your VPS provider) |
|
|
|
|
> **Port 25 / deliverability:** many cloud providers (incl. DigitalOcean) block
|
|
> outbound `:25` by default — request an unblock, set the PTR/rDNS, and warm the
|
|
> IP, or relay outbound through a smarthost. Inbound MX and the gateway's local
|
|
> submission work regardless.
|
|
|
|
## Install
|
|
|
|
```bash
|
|
cd /opt/agentbbs/deploy/mailu
|
|
cp mailu.env.example mailu.env # fill SECRET_KEY, INITIAL_ADMIN_PW, API_TOKEN, DOMAIN=bbs.profullstack.com, HOSTNAMES=mail.profullstack.com
|
|
docker compose up -d
|
|
# add the address domain + the gateway master user:
|
|
docker compose exec admin flask mailu domain bbs.profullstack.com
|
|
AGENTBBS_MAIL_MASTER_USER=gateway ./provision-mailbox.sh --master "$(openssl rand -hex 16)"
|
|
```
|
|
|
|
setup.sh writes the Caddy `mail.profullstack.com` site and the cert-refresh
|
|
timer when `MAIL=1`, and brings the stack up once `mailu.env` exists.
|
|
|
|
## agentbbs gateway env
|
|
|
|
Set these on the agentbbs service (setup.sh §9e upserts the non-secret ones):
|
|
|
|
| Var | Value |
|
|
|---|---|
|
|
| `AGENTBBS_MAIL_ADDR_DOMAIN` | `bbs.profullstack.com` |
|
|
| `AGENTBBS_MAIL_DOMAIN` | `mail.profullstack.com` |
|
|
| `AGENTBBS_MAIL_IMAP_ADDR` | `127.0.0.1:14143` (Dovecot direct, loopback) |
|
|
| `AGENTBBS_MAIL_IMAP_PLAINTEXT` | `1` (the loopback path is plaintext) |
|
|
| `AGENTBBS_MAIL_SMTP_ADDR` | `127.0.0.1:25` |
|
|
| `AGENTBBS_MAIL_ADMIN_URL` | `http://127.0.0.1:8080` |
|
|
| `AGENTBBS_MAIL_API_TOKEN` | the Mailu `API_TOKEN` (secret) |
|
|
| `AGENTBBS_MAIL_MASTER_USER` | `gateway` |
|
|
| `AGENTBBS_MAIL_MASTER_PASS` | the master password set above (secret) |
|
|
| `AGENTBBS_WEBMAIL_URL` | `https://mail.profullstack.com` (default = mail host) |
|
|
|
|
Without `AGENTBBS_MAIL_API_TOKEN` auto-provisioning is skipped (the address is
|
|
still shown); without `AGENTBBS_MAIL_MASTER_PASS` the gateway can't open
|
|
mailboxes.
|
|
|
|
> **`AGENTBBS_MAIL_SMTP_ADDR` is `127.0.0.1:25` on purpose** — it's the gateway's
|
|
> *loopback* hand-off into the on-box Mailu/Postfix MTA, not a remote submission
|
|
> client, so `25` is correct (465/587 are for authenticated remote clients).
|
|
> This is a separate config from `AGENTBBS_SMTP_*` (the transactional sender for
|
|
> confirmation codes and `notify-creds`, which defaults to STARTTLS `:587`). See
|
|
> [`credentials.md`](credentials.md#two-smtp-paths-and-why-one-is-25).
|
|
|
|
### Why the gateway talks to Dovecot directly (plaintext loopback)
|
|
|
|
Mailu's **front** (nginx mail proxy) pre-authenticates every IMAP/SMTP login
|
|
against Mailu's user DB before proxying to Dovecot — and it rejects the Dovecot
|
|
master-user login form `<addr>*gateway`. So the gateway must reach **Dovecot
|
|
directly**, bypassing the front. The `imap` container has no TLS cert (only the
|
|
front does), so the bypass is plaintext over loopback — safe because the
|
|
connection (and the master password) never leave the host. Wiring:
|
|
|
|
- Publish Dovecot's IMAP on loopback (docker-compose.override.yml):
|
|
`imap.ports: ["127.0.0.1:14143:143"]`.
|
|
- The Dovecot master user is defined in `data/overrides/dovecot/dovecot.conf`
|
|
(Mailu includes exactly that filename — *not* `*.conf`):
|
|
|
|
```
|
|
auth_master_user_separator = *
|
|
passdb { driver = passwd-file; master = yes; args = /overrides/master-users }
|
|
```
|
|
|
|
with `data/overrides/dovecot/master-users` holding `gateway:{SHA512-CRYPT}$6$…`
|
|
(the hash of `AGENTBBS_MAIL_MASTER_PASS`). The file must be **world-readable
|
|
(644)** — Dovecot reads it as a non-root user, and 640 root:root yields a
|
|
`temp_fail`. Do **not** add `result_success = continue` (that would also
|
|
require the target user's own password); the target mailbox comes from userdb.
|
|
- Point the gateway at it: `AGENTBBS_MAIL_IMAP_ADDR=127.0.0.1:14143` +
|
|
`AGENTBBS_MAIL_IMAP_PLAINTEXT=1`.
|
|
|
|
## Sending mail from the BBS (verify codes + notifications)
|
|
|
|
The join@ verification code and signup notifications use `internal/mail` (the
|
|
`AGENTBBS_SMTP_*` knobs), separate from the per-member mailbox client. Point
|
|
them at the local Mailu relay so codes actually send:
|
|
|
|
```
|
|
AGENTBBS_SMTP_HOST=127.0.0.1
|
|
AGENTBBS_SMTP_PORT=25
|
|
AGENTBBS_SMTP_FROM=bbs@bbs.profullstack.com
|
|
AGENTBBS_SMTP_SERVERNAME=mail.profullstack.com # set by setup.sh
|
|
# user/pass omitted: the co-located relay accepts local submission unauthenticated
|
|
```
|
|
|
|
`AGENTBBS_SMTP_SERVERNAME` is the name STARTTLS certificates are verified
|
|
against when it differs from the dialled host — the relay answers on
|
|
`127.0.0.1` but presents a cert for the mail host. It mirrors
|
|
`AGENTBBS_MAIL_SMTP_SERVERNAME` on the mailbox gateway and `setup.sh` sets it
|
|
whenever the Mailu stack is enabled.
|
|
|
|
On a **loopback** relay the sender skips certificate verification outright. The
|
|
connection never leaves the host, so there is nothing to intercept — and tying
|
|
`join@` registration to an on-box cert being both name-matched and unexpired is
|
|
precisely what broke signups for nine days in September 2026 (see below).
|
|
|
|
### When confirmation codes stop sending
|
|
|
|
`join@` reporting *"couldn't email the code"* means `internal/mail` could not
|
|
hand the message to the relay. The error is in the journal
|
|
(`journalctl -u agentbbs -g "send code"`), and it now names the address and the
|
|
failing stage. The usual cause is the mail host's TLS cert: Caddy owns ACME for
|
|
`mail.$DOMAIN` and `deploy/mailu/refresh-certs.sh` copies it into Mailu, but
|
|
Mailu keeps serving whatever it loaded at container start. Check what is
|
|
actually on the wire rather than what is on disk:
|
|
|
|
```bash
|
|
printf 'QUIT\r\n' | openssl s_client -quiet -starttls smtp \
|
|
-connect 127.0.0.1:25 -servername mail.$DOMAIN 2>&1 | grep -i notAfter
|
|
sudo /usr/local/bin/agentbbs-mailu-certs # copies + reloads; loud on failure
|
|
```
|
|
|
|
## Provisioning member mailboxes
|
|
|
|
Provisioning is automatic at `join@` verification. To create or backfill by hand:
|
|
|
|
```bash
|
|
deploy/mailu/provision-mailbox.sh alice # creates alice@bbs.profullstack.com
|
|
```
|
|
|
|
(Set `MAIL_DOMAIN=bbs.profullstack.com` for the script, since the address domain
|
|
differs from the server host.)
|
|
|
|
The Dovecot **master user** (`gateway`) authenticates as any member with the
|
|
login form `alice*gateway` + the master password — exactly what
|
|
`internal/mailbox`'s IMAP adapter sends. See
|
|
[`deploy/mailu/README.md`](../deploy/mailu/README.md) for details.
|
|
|
|
## Webmail only for members
|
|
|
|
Members are pointed at `https://mail.profullstack.com` (Roundcube) and the BBS
|
|
`Mail` client — they are not given the Mailu admin UI or alias management. Admin
|
|
is operator-only.
|