mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Self-host Mailu mail stack at mail.profullstack.com
Members get a real <name>@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 <noreply@anthropic.com>
This commit is contained in:
parent
49db5e103d
commit
cb09300357
8 changed files with 464 additions and 1 deletions
3
deploy/mailu/.gitignore
vendored
Normal file
3
deploy/mailu/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mailu.env
|
||||
certs/
|
||||
data/
|
||||
53
deploy/mailu/README.md
Normal file
53
deploy/mailu/README.md
Normal file
|
|
@ -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 `<name>*<master>`). 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=<that secret>
|
||||
```
|
||||
|
||||
> 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
|
||||
```
|
||||
83
deploy/mailu/docker-compose.yml
Normal file
83
deploy/mailu/docker-compose.yml
Normal file
|
|
@ -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
|
||||
44
deploy/mailu/mailu.env.example
Normal file
44
deploy/mailu/mailu.env.example
Normal file
|
|
@ -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 <name>@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 "<name>*<master>"). 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=<the master password you set>
|
||||
|
||||
# --- Admin bootstrap ---------------------------------------------------------
|
||||
INITIAL_ADMIN_ACCOUNT=admin
|
||||
INITIAL_ADMIN_DOMAIN=mail.profullstack.com
|
||||
INITIAL_ADMIN_PW=CHANGEME_admin_password
|
||||
50
deploy/mailu/provision-mailbox.sh
Executable file
50
deploy/mailu/provision-mailbox.sh
Executable file
|
|
@ -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 <name> # create <name>@$DOMAIN (random pw)
|
||||
# provision-mailbox.sh --master <pass> # (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 <password>}"
|
||||
# A Dovecot master user can authenticate as any mailbox: login "<name>*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 <name>}"
|
||||
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 <<EOF 2>/dev/null || true
|
||||
users:
|
||||
- email: ${name}@${DOMAIN}
|
||||
quota_bytes: ${QUOTA_BYTES}
|
||||
EOF
|
||||
|
||||
echo "mailbox ${name}@${DOMAIN} provisioned"
|
||||
41
deploy/mailu/refresh-certs.sh
Executable file
41
deploy/mailu/refresh-certs.sh
Executable file
|
|
@ -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/<acme-dir>/<host>/<host>.{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
|
||||
Loading…
Add table
Add a link
Reference in a new issue