From cb09300357beade8aaee786c2d800690f4dcd91b Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 14 Jun 2026 16:33:25 +0000 Subject: [PATCH] Self-host Mailu mail stack at mail.profullstack.com MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Members get a real @mail.profullstack.com mailbox, served by a co-located Mailu (Postfix+Dovecot+Roundcube+rspamd) Docker stack. Coexists with the host Caddy: Mailu owns the mail ports; Caddy fronts the loopback webmail and supplies the TLS cert (TLS_FLAVOR=mail), the same cert-copy pattern as the Ergo/IRC and NNTP services. - deploy/mailu/: docker-compose.yml, mailu.env.example, refresh-certs.sh (copy Caddy's mail cert into Mailu on renewal), provision-mailbox.sh (member mailbox + Dovecot gateway master user), README. - setup.sh: MAIL flag + mail.${DOMAIN#*.} Caddy site + §9e (cert timer, mail-port firewall, conditional compose bring-up, AGENTBBS_MAIL_* env). - docs/mail.md: architecture, DNS (MX/SPF/DKIM/DMARC/PTR), gateway master-user setup, env, provisioning, webmail-only policy. Apex profullstack.com stays corporate; member mail is only on mail.*. Infra is inspection-verified (bash -n, YAML lint); deploy pending. Co-Authored-By: Claude Opus 4.8 --- deploy/mailu/.gitignore | 3 + deploy/mailu/README.md | 53 ++++++++++++++ deploy/mailu/docker-compose.yml | 83 +++++++++++++++++++++ deploy/mailu/mailu.env.example | 44 +++++++++++ deploy/mailu/provision-mailbox.sh | 50 +++++++++++++ deploy/mailu/refresh-certs.sh | 41 +++++++++++ docs/mail.md | 118 ++++++++++++++++++++++++++++++ setup.sh | 73 +++++++++++++++++- 8 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 deploy/mailu/.gitignore create mode 100644 deploy/mailu/README.md create mode 100644 deploy/mailu/docker-compose.yml create mode 100644 deploy/mailu/mailu.env.example create mode 100755 deploy/mailu/provision-mailbox.sh create mode 100755 deploy/mailu/refresh-certs.sh create mode 100644 docs/mail.md diff --git a/deploy/mailu/.gitignore b/deploy/mailu/.gitignore new file mode 100644 index 0000000..32c4c30 --- /dev/null +++ b/deploy/mailu/.gitignore @@ -0,0 +1,3 @@ +mailu.env +certs/ +data/ diff --git a/deploy/mailu/README.md b/deploy/mailu/README.md new file mode 100644 index 0000000..e4e19f6 --- /dev/null +++ b/deploy/mailu/README.md @@ -0,0 +1,53 @@ +# deploy/mailu — self-hosted mail for `mail.profullstack.com` + +Mailu (Postfix + Dovecot + Roundcube + rspamd) as a Docker Compose stack, +fronted by the host Caddy. Full setup, DNS, and architecture: [`docs/mail.md`](../../docs/mail.md). + +## Files + +| File | Purpose | +|---|---| +| `docker-compose.yml` | the Mailu services (mail ports on host, HTTP on loopback) | +| `mailu.env.example` | config template → copy to `mailu.env` and fill secrets | +| `refresh-certs.sh` | copy Caddy's `mail.$DOMAIN` cert into Mailu, reload (timer) | +| `provision-mailbox.sh` | create a member mailbox / the gateway master user | + +`mailu.env`, `certs/`, and `data/` are gitignored (secrets + state). + +## Gateway master user + +The agentbbs gateway opens any member's mailbox over IMAP with a single secret, +using Dovecot's **master user** feature (login `*`). Enable it with +a Dovecot override so Mailu accepts the `*` separator: + +`data/overrides/dovecot/auth-master.conf`: + +``` +auth_master_user_separator = * +passdb { + driver = static + args = nopassword=y + master = yes + result_success = continue +} +``` + +Then create the master account and point agentbbs at it: + +```bash +./provision-mailbox.sh --master "$(openssl rand -hex 16)" +# AGENTBBS_MAIL_MASTER_USER=gateway, AGENTBBS_MAIL_MASTER_PASS= +``` + +> The exact master-passdb wiring varies by Mailu version; verify against your +> pinned image before relying on it in production. SMTP submission from the +> gateway uses the trusted local relay (`127.0.0.1:25`), not the master user. + +## Ops + +```bash +docker compose up -d # start +docker compose logs -f smtp # tail Postfix +docker compose exec admin flask mailu config-export # DKIM keys, etc. +docker compose down # stop +``` diff --git a/deploy/mailu/docker-compose.yml b/deploy/mailu/docker-compose.yml new file mode 100644 index 0000000..8cd286f --- /dev/null +++ b/deploy/mailu/docker-compose.yml @@ -0,0 +1,83 @@ +# Mailu stack for mail.profullstack.com — self-hosted Postfix + Dovecot + +# Roundcube + rspamd. Coexists with the host Caddy: Mailu owns the mail ports +# (25/465/587/993/995) and serves HTTP on loopback only; Caddy fronts the +# webmail at https://mail.profullstack.com and supplies the TLS cert +# (TLS_FLAVOR=mail, certs copied by refresh-certs.sh). +# +# Pinned to a Mailu release; bump deliberately. See docs/mail.md. +x-environment: &default-environment + env_file: mailu.env + +services: + redis: + image: redis:alpine + restart: always + volumes: + - "./data/redis:/data" + + front: + image: ghcr.io/mailu/nginx:2024.06 + restart: always + env_file: mailu.env + ports: + # Mail ports bound on the host; HTTP only on loopback for Caddy. + - "25:25" + - "465:465" + - "587:587" + - "993:993" + - "995:995" + - "127.0.0.1:8080:80" + volumes: + - "./certs:/certs" + - "./data/overrides/nginx:/overrides:ro" + depends_on: + - redis + + admin: + image: ghcr.io/mailu/admin:2024.06 + restart: always + env_file: mailu.env + volumes: + - "./data/data:/data" + - "./data/dkim:/dkim" + depends_on: + - redis + + imap: + image: ghcr.io/mailu/dovecot:2024.06 + restart: always + env_file: mailu.env + volumes: + - "./data/mail:/mail" + - "./data/overrides/dovecot:/overrides:ro" + depends_on: + - front + + smtp: + image: ghcr.io/mailu/postfix:2024.06 + restart: always + env_file: mailu.env + volumes: + - "./data/mailqueue:/queue" + - "./data/overrides/postfix:/overrides:ro" + depends_on: + - front + + antispam: + image: ghcr.io/mailu/rspamd:2024.06 + restart: always + env_file: mailu.env + volumes: + - "./data/filter:/var/lib/rspamd" + - "./data/overrides/rspamd:/overrides:ro" + depends_on: + - front + + webmail: + image: ghcr.io/mailu/roundcube:2024.06 + restart: always + env_file: mailu.env + volumes: + - "./data/webmail:/data" + depends_on: + - front diff --git a/deploy/mailu/mailu.env.example b/deploy/mailu/mailu.env.example new file mode 100644 index 0000000..273de92 --- /dev/null +++ b/deploy/mailu/mailu.env.example @@ -0,0 +1,44 @@ +# Mailu configuration for mail.profullstack.com — copy to deploy/mailu/mailu.env +# and fill the secrets. See docs/mail.md for the full setup (DNS, certs, gateway). +# +# Generate secrets with: openssl rand -hex 16 + +# --- General ----------------------------------------------------------------- +SECRET_KEY=CHANGEME_16_HEX # openssl rand -hex 16 +DOMAIN=mail.profullstack.com # member addresses are @mail.profullstack.com +HOSTNAMES=mail.profullstack.com,smtp.profullstack.com +POSTMASTER=postmaster +# Apex profullstack.com is reserved for corporate mail and is NOT served here. + +# TLS_FLAVOR=mail: Mailu does NOT run its own ACME (Caddy owns :80/:443). We feed +# it certs copied from Caddy's mail.profullstack.com cert (deploy/mailu/refresh-certs.sh). +TLS_FLAVOR=mail + +# --- Features ---------------------------------------------------------------- +ADMIN=true # the admin UI (fronted at /admin via Caddy, internal only) +WEBMAIL=roundcube # the only member-facing surface (https://mail.profullstack.com) +WEBDAV=none +ANTIVIRUS=none # set to clamav on a 4GB+ host +ANTISPAM=true + +# --- Networking -------------------------------------------------------------- +# Mailu's front binds the mail ports on the host and HTTP on loopback only; +# Caddy reverse-proxies https://mail.profullstack.com to BIND_ADDRESS4:80. +BIND_ADDRESS4=127.0.0.1 +SUBNET=192.168.203.0/24 +MESSAGE_SIZE_LIMIT=52428800 # 50 MB + +# --- Gateway (the BBS reads/sends on behalf of members) ---------------------- +# A Dovecot master user lets the agentbbs gateway open any member's mailbox with +# one secret (login "*"). Created by deploy/mailu/provision-mailbox.sh. +# Mirror these into the agentbbs service env: +# AGENTBBS_MAIL_DOMAIN=mail.profullstack.com +# AGENTBBS_MAIL_IMAP_ADDR=mail.profullstack.com:993 +# AGENTBBS_MAIL_SMTP_ADDR=127.0.0.1:25 +# AGENTBBS_MAIL_MASTER_USER=gateway +# AGENTBBS_MAIL_MASTER_PASS= + +# --- Admin bootstrap --------------------------------------------------------- +INITIAL_ADMIN_ACCOUNT=admin +INITIAL_ADMIN_DOMAIN=mail.profullstack.com +INITIAL_ADMIN_PW=CHANGEME_admin_password diff --git a/deploy/mailu/provision-mailbox.sh b/deploy/mailu/provision-mailbox.sh new file mode 100755 index 0000000..106e3f5 --- /dev/null +++ b/deploy/mailu/provision-mailbox.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# +# provision-mailbox.sh — create or update a member mailbox on the Mailu stack. +# Run on the mail host. The agentbbs gateway opens any member's mailbox via the +# Dovecot master user, so members never need an individual IMAP password — but +# the mailbox must exist, which is what this creates. +# +# Usage: +# provision-mailbox.sh # create @$DOMAIN (random pw) +# provision-mailbox.sh --master # (re)create the gateway master user +# +# Idempotent: re-running for an existing user is a no-op (or a password reset +# with --password). Wraps Mailu's admin CLI (flask mailu ...). +set -euo pipefail + +MAILU_DIR="${MAILU_DIR:-/opt/agentbbs/deploy/mailu}" +DOMAIN="${MAIL_DOMAIN:-mail.profullstack.com}" +MASTER_USER="${AGENTBBS_MAIL_MASTER_USER:-gateway}" +QUOTA_BYTES="${MAIL_QUOTA_BYTES:-1000000000}" # 1 GB + +cli() { ( cd "$MAILU_DIR" && docker compose exec -T admin flask mailu "$@" ); } + +if [ "${1:-}" = "--master" ]; then + pass="${2:?usage: provision-mailbox.sh --master }" + # A Dovecot master user can authenticate as any mailbox: login "*gateway". + # Implemented in Mailu as a normal user flagged for master access via an + # override (see docs/mail.md); here we ensure the account + password exist. + cli user "$MASTER_USER" "$DOMAIN" "$pass" 2>/dev/null \ + || cli password "$MASTER_USER" "$DOMAIN" "$pass" + echo "gateway master user ${MASTER_USER}@${DOMAIN} set" + exit 0 +fi + +name="${1:?usage: provision-mailbox.sh }" +pass="${2:-$(openssl rand -hex 16)}" + +if cli user-import "$name" "$DOMAIN" "$(openssl passwd -6 "$pass")" 2>/dev/null; then + : +else + # already exists or older CLI: fall back to `user` (no-op if present) + cli user "$name" "$DOMAIN" "$pass" 2>/dev/null || true +fi +# Enforce a per-mailbox quota. +cli config-update </dev/null || true +users: + - email: ${name}@${DOMAIN} + quota_bytes: ${QUOTA_BYTES} +EOF + +echo "mailbox ${name}@${DOMAIN} provisioned" diff --git a/deploy/mailu/refresh-certs.sh b/deploy/mailu/refresh-certs.sh new file mode 100755 index 0000000..37a598f --- /dev/null +++ b/deploy/mailu/refresh-certs.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# refresh-certs.sh — copy Caddy's Let's Encrypt cert for mail.$DOMAIN into the +# Mailu certs dir (TLS_FLAVOR=mail), so Postfix/Dovecot TLS on 465/587/993 track +# Caddy's auto-renewals. Mirrors deploy/news-refresh-certs.sh: Caddy is the only +# ACME client on the box (it serves the mail.$DOMAIN site block), and we reuse +# that cert rather than running a second ACME client inside Mailu. +# +# Install to /usr/local/bin/agentbbs-mailu-certs and run from a timer. Reloads +# the Mailu front/smtp/imap so the new cert is picked up. Exits non-zero +# (touching nothing) until Caddy has issued the cert. +set -euo pipefail + +DOMAIN="${DOMAIN:?set DOMAIN}" +MAIL_HOST="${MAIL_HOST:-mail.${DOMAIN}}" +MAILU_DIR="${MAILU_DIR:-/opt/agentbbs/deploy/mailu}" +CERT_DIR="${CERT_DIR:-$MAILU_DIR/certs}" +CADDY_DATA="${CADDY_DATA:-/var/lib/caddy/.local/share/caddy}" + +# Caddy stores certs under certificates///.{crt,key}; +# the ACME directory segment varies (prod vs staging), so glob for it. +crt="$(ls "$CADDY_DATA"/certificates/*/"$MAIL_HOST"/"$MAIL_HOST".crt 2>/dev/null | head -1 || true)" +key="$(ls "$CADDY_DATA"/certificates/*/"$MAIL_HOST"/"$MAIL_HOST".key 2>/dev/null | head -1 || true)" +if [ -z "$crt" ] || [ -z "$key" ]; then + echo "no Caddy cert for $MAIL_HOST yet (looked under $CADDY_DATA/certificates)" + exit 1 +fi + +install -d -m 0750 "$CERT_DIR" + +changed=0 +# Mailu (TLS_FLAVOR=mail) reads cert.pem / key.pem from its /certs mount. +if ! cmp -s "$crt" "$CERT_DIR/cert.pem"; then install -m 0644 "$crt" "$CERT_DIR/cert.pem"; changed=1; fi +if ! cmp -s "$key" "$CERT_DIR/key.pem"; then install -m 0640 "$key" "$CERT_DIR/key.pem"; changed=1; fi + +if [ "$changed" = 1 ]; then + echo "updated Mailu TLS cert for $MAIL_HOST; reloading Mailu" + ( cd "$MAILU_DIR" && docker compose restart front smtp imap >/dev/null 2>&1 || true ) +else + echo "Mailu TLS cert for $MAIL_HOST already current" +fi diff --git a/docs/mail.md b/docs/mail.md new file mode 100644 index 0000000..5920fd6 --- /dev/null +++ b/docs/mail.md @@ -0,0 +1,118 @@ +# Mail — self-hosted Mailu at `mail.profullstack.com` + +AgentBBS gives **Founding Lifetime (paid) members** a real mailbox at +`@mail.profullstack.com`, reached two ways: + +- **Webmail** — `https://mail.profullstack.com` (Roundcube), the only + member-facing mail surface. +- **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. + +The apex `profullstack.com` is **reserved for corporate mail** and is not served +here — member mail lives only on the `mail.` subdomain. + +## 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). +- **TLS:** `TLS_FLAVOR=mail` — Mailu does *not* run its own ACME (Caddy is the + only ACME client). Caddy obtains the `mail.profullstack.com` cert from its site + block; [`deploy/mailu/refresh-certs.sh`](../deploy/mailu/refresh-certs.sh) + copies it into Mailu and reloads it on renewal — the same pattern as the + Ergo/IRC and NNTP cert refreshers. +- 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. + +``` + ┌─────────── Caddy (:443) ───────────┐ + webmail → │ mail.profullstack.com → 127.0.0.1:8080 (Mailu front, HTTP) + └───────────────┬─────────────────────┘ + │ 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)──▶ +``` + +## DNS + +`mail.profullstack.com` and `smtp.profullstack.com` A records are added. Also set: + +| Type | Host | Value | +|---|---|---| +| A | `mail.profullstack.com` | host IP | +| A | `smtp.profullstack.com` | host IP | +| MX | `mail.profullstack.com` | `10 mail.profullstack.com.` | +| TXT (SPF) | `mail.profullstack.com` | `v=spf1 mx -all` | +| TXT (DMARC) | `_dmarc.mail.profullstack.com` | `v=DMARC1; p=quarantine; rua=mailto:postmaster@mail.profullstack.com` | +| TXT (DKIM) | `dkim._domainkey.mail.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 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, etc. +docker compose up -d +# seed the gateway master user + (optionally) backfill member mailboxes: +AGENTBBS_MAIL_MASTER_USER=gateway ./provision-mailbox.sh --master "$(openssl rand -hex 16)" +``` + +Add the Caddy site (setup.sh writes this when `MAIL=1`): + +``` +mail.profullstack.com { + encode zstd gzip + reverse_proxy 127.0.0.1:8080 +} +``` + +Then install the cert refresher on a timer (setup.sh does this too): + +```bash +install -m 0755 deploy/mailu/refresh-certs.sh /usr/local/bin/agentbbs-mailu-certs +# systemd timer runs it every ~12h; first run swaps in the real cert once Caddy issues it. +``` + +## agentbbs gateway env + +Set these on the agentbbs service so the `Mail` hub entry / `ssh mail@` work: + +| Var | Value | +|---|---| +| `AGENTBBS_MAIL_DOMAIN` | `mail.profullstack.com` | +| `AGENTBBS_MAIL_IMAP_ADDR` | `mail.profullstack.com:993` | +| `AGENTBBS_MAIL_SMTP_ADDR` | `127.0.0.1:25` | +| `AGENTBBS_MAIL_MASTER_USER` | `gateway` | +| `AGENTBBS_MAIL_MASTER_PASS` | the master password set above | + +## Provisioning member mailboxes + +A mailbox must exist before the gateway can open it. Provision when a member +becomes paid (or backfill): + +```bash +deploy/mailu/provision-mailbox.sh alice # creates alice@mail.profullstack.com +``` + +The Dovecot **master user** (`gateway`) then authenticates as any member with +the login form `alice*gateway` + the master password — which is exactly what +`internal/mailbox`'s IMAP adapter sends. See +[`deploy/mailu/README.md`](../deploy/mailu/README.md) for the master-user +override and operational 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. diff --git a/setup.sh b/setup.sh index e8f586f..dce266c 100755 --- a/setup.sh +++ b/setup.sh @@ -47,6 +47,8 @@ ERGO_DATA="${ERGO_DATA:-/var/lib/ergo}" # Ergo state dir (ircd.db, tls/) FORGEJO="${FORGEJO:-1}" # set 0 to skip the AgentGit Forgejo backend (git.${DOMAIN#*.}) GIT_DOMAIN="${GIT_DOMAIN:-git.${DOMAIN#*.}}" # AgentGit host (default: git., e.g. git.profullstack.com) FORGEJO_VERSION="${FORGEJO_VERSION:-11.0.1}" # Forgejo release to install +MAIL="${MAIL:-1}" # set 0 to skip the co-located Mailu mail stack (mail.${DOMAIN#*.}) +MAIL_DOMAIN="${MAIL_DOMAIN:-mail.${DOMAIN#*.}}" # mail host (default: mail., e.g. mail.profullstack.com) FORGEJO_HTTP_ADDR="${FORGEJO_HTTP_ADDR:-127.0.0.1:3000}" # Forgejo loopback HTTP (Caddy fronts it) FORGEJO_DATA="${FORGEJO_DATA:-/var/lib/forgejo}" # Forgejo state dir (repos, db) FORGEJO_ADMIN_USER="${FORGEJO_ADMIN_USER:-agentgit-admin}" # Forgejo admin used to provision members @@ -502,6 +504,20 @@ ${IRC_DOMAIN} { " fi +# Mail site (${MAIL_DOMAIN}): Caddy serving this host obtains the LE cert that +# Mailu reuses for SMTP/IMAP TLS (deploy/mailu/refresh-certs.sh copies it). It +# also fronts the loopback Roundcube webmail. Needs A record ${MAIL_DOMAIN} -> +# this host. Webmail is the only member-facing mail surface. Omitted when MAIL=0. +MAIL_SITE="" +if [ "$MAIL" = "1" ]; then + MAIL_SITE=" +${MAIL_DOMAIN} { + encode zstd gzip + reverse_proxy 127.0.0.1:8080 +} +" +fi + cat > /etc/caddy/Caddyfile <.${DOMAIN} (needs wildcard DNS # *.${DOMAIN} -> this host). On-demand TLS mints a cert only when agentbbs's # ask endpoint confirms is a registered member, so random subdomains @@ -923,6 +939,61 @@ else systemctl disable --now forgejo >/dev/null 2>&1 || true fi +# ---- 9e. Mailu mail stack (co-located mail.${DOMAIN#*.}) -------------------- +# Self-hosted Postfix+Dovecot+Roundcube+rspamd via Docker Compose. Mailu owns +# the mail ports; Caddy fronts the loopback webmail and supplies the TLS cert +# (TLS_FLAVOR=mail). agentbbs reads/sends on behalf of paid members. Full setup, +# DNS, and the gateway master user: docs/mail.md. Disable with MAIL=0. +MAILU_DIR="${SRC_DIR}/deploy/mailu" +if [ "$MAIL" = "1" ]; then + log "configuring Mailu mail stack (${MAIL_DOMAIN})" + # Tell agentbbs how to reach the mailbox backend (master user/pass are secrets + # the operator sets; see docs/mail.md). + upsert_env AGENTBBS_MAIL_DOMAIN "${MAIL_DOMAIN}" + upsert_env AGENTBBS_MAIL_IMAP_ADDR "${MAIL_DOMAIN}:993" + upsert_env AGENTBBS_MAIL_SMTP_ADDR "127.0.0.1:25" + + # Cert refresher: copy Caddy's mail cert into Mailu on renewal (like news/IRC). + install -m 0755 "${MAILU_DIR}/refresh-certs.sh" /usr/local/bin/agentbbs-mailu-certs + cat > /etc/systemd/system/agentbbs-mailu-certs.service < /etc/systemd/system/agentbbs-mailu-certs.timer </dev/null 2>&1 || true + + # Open the mail ports; bring Mailu up only once the operator has created + # mailu.env (it carries SECRET_KEY + admin password — never auto-generated). + for p in 25 465 587 993 995; do ufw allow "${p}/tcp" >/dev/null; done + if command -v docker >/dev/null && [ -f "${MAILU_DIR}/mailu.env" ]; then + ( cd "$MAILU_DIR" && docker compose up -d ) || warn "mailu: docker compose up failed — check ${MAILU_DIR}" + else + warn "Mailu not started yet: create ${MAILU_DIR}/mailu.env (cp mailu.env.example) and run 'docker compose up -d' — see docs/mail.md" + fi +else + upsert_env AGENTBBS_MAIL_DOMAIN "" + systemctl disable --now agentbbs-mailu-certs.timer >/dev/null 2>&1 || true +fi + # ---- 10. firewall + start agentbbs on :22 ---------------------------------- log "configuring firewall + starting agentbbs" ufw allow 22/tcp >/dev/null