agentbbs/docs/mail.md
Anthony Ettinger 006235ce92
Feat/mail all members (#55)
* feat(mail): give every verified member a free @bbs.profullstack.com mailbox

Email was built but paid-only (Founding Lifetime gate) and never wired to a
running backend. Make it a free benefit of membership and split the address
domain from the mail-server host.

- internal/mailu: Mailu admin-API client; EnsureUser idempotently provisions a
  mailbox via the loopback admin REST API (token = mailu.env API_TOKEN).
- main.go: auto-provision <name>@<mailDomain> at join@ verification and on first
  Mail open; un-gate the Mail hub entry + mail@ (membership/email-verified, not
  Premium); address domain (AGENTBBS_MAIL_ADDR_DOMAIN, default the BBS host) is
  now distinct from the mail server host (AGENTBBS_MAIL_DOMAIN) and the webmail
  URL. Drop the forwardemail alias path (Mailu now owns delivery for everyone).
- mailbox: gate on membership (a registered handle) instead of Paid;
  ErrNotPaid -> ErrNotMember.
- join@ copy: list email under free membership; premium now pitches custom
  domains + Tor only.
- setup.sh / docs/mail.md / deploy/mailu: address-domain vs server-host split,
  Mailu API token, MX for the address domain, local-relay SMTP for verify codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(mailu): pin Docker network subnet to match SUBNET; ignore runtime state

The base compose declares no network, so Docker assigns the default bridge an
arbitrary subnet that won't match mailu.env SUBNET — breaking Mailu's internal
service auth/relay. Add a docker-compose.override.yml.example that pins the
default network to 192.168.203.0/24, and gitignore the live override + Mailu
runtime state (mailu.env, certs/, data/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(mail): plaintext loopback IMAP so the gateway bypasses Mailu's front

Mailu's front (nginx mail proxy) pre-authenticates against Mailu's user DB before
proxying to Dovecot, which rejects the Dovecot master-user login <addr>*gateway.
The gateway must reach Dovecot directly. The imap container has no TLS cert (only
the front does), so the bypass is plaintext over loopback — the master password
never leaves the host.

- mailbox: IMAPConfig.Plaintext dials with DialInsecure (loopback only).
- main.go: mailClientFor sets Plaintext from AGENTBBS_MAIL_IMAP_PLAINTEXT.
- override.example: add the unbound resolver (admin needs DNSSEC), webmail image
  fix (2024.06 uses mailu/webmail), and publish Dovecot 143 on 127.0.0.1:14143.
- docs/mail.md: document the front-bypass, the dovecot.conf master passdb (Mailu
  includes that exact filename), and the 644 master-users perms (640 = temp_fail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* deploy(mailu): wire gateway IMAP to the loopback Dovecot path in setup.sh

setup.sh §9e set AGENTBBS_MAIL_IMAP_ADDR to the front's :993, which the front's
auth proxy rejects for the master-user login (and would clobber the working
loopback wiring on every self-update). Point it at 127.0.0.1:14143 +
AGENTBBS_MAIL_IMAP_PLAINTEXT=1 instead, matching the override + docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(mail): give free members a webmail password at join@

The gateway opens mailboxes via the Dovecot master user (no member password),
but webmail (Roundcube) needs the member to have a password. join@ now sets a
fresh, readable webmail password via the Mailu API and shows it with the webmail
URL + login, so free members can use webmail at mail.profullstack.com.

- mailu: SetPassword (PATCH /user/<email> raw_password) + test.
- main.go: setWebmailPassword + readablePassword; join@ displays url/login/password.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 03:38:31 -07:00

7.6 KiB

Mail — self-hosted Mailu

AgentBBS gives every verified member (free and paid alike) a real mailbox at <name>@bbs.profullstack.com, reached two ways:

  • Webmailhttps://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

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.

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
# user/pass omitted: the co-located relay accepts local submission unauthenticated

Provisioning member mailboxes

Provisioning is automatic at join@ verification. To create or backfill by hand:

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 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.