diff --git a/JOURNAL-mailu.md b/JOURNAL-mailu.md new file mode 100644 index 0000000..019a483 --- /dev/null +++ b/JOURNAL-mailu.md @@ -0,0 +1,81 @@ +# JOURNAL — phase mailu + +Design rationale, dead-ends, investigation notes. Not for Adversary pre-verdict reading. + +--- + +## 2026-06-11 Bootstrap + data-layout research + +### mailu volume layout (from compose.yml analysis) + +Services and their durable volumes: +- `admin` service: mounts `mailu` vol → `/data` (sqlite DB: users, mailboxes, domains, settings) +- `imap` (dovecot) service: mounts `mail` vol → `/mail` (Maildir message storage) +- `admin` service also mounts `dkim` vol → `/dkim` (DKIM private keys) +- `antispam` service: mounts `rspamd` vol → `/var/lib/rspamd` (antispam training data — ephemeral) +- `db` (redis) service: mounts `redis` vol → `/data` (session cache — ephemeral) +- `webmail` service: mounts `webmail` vol → `/data` (roundcube prefs — ephemeral) +- `smtp` service: mounts `mailqueue` vol → `/queue` (postfix queue — ephemeral) +- `app` (nginx) + `certdumper`: mount `certs` vol (TLS cert dumps — regenerable) + +### Backup decision: admin/data + imap/mail + +For genuine backup/restore coverage: +- **`admin:/data`** = sqlite DB → primary source of truth for mailboxes/users. If this is lost, + all accounts are gone. Must backup. +- **`imap:/mail`** = Maildir storage → the actual messages. Loss = all mail gone. Must backup. +- `dkim:/dkim` = DKIM keys. In production, loss = need re-keying + DNS update. BUT: for CI testing, + we don't have DNS-side DKIM records anyway, so DKIM regeneration is harmless. NOT labeled for + CI simplicity (can add in a follow-up if operator wants DKIM key recovery tested). +- Other volumes: ephemeral / regenerable. Not labeled. + +### Backupbot v2 syntax decision + +From studying n8n and discourse examples: +- v2 uses `backupbot.backup: "true"` + `backupbot.backup.path: ""` +- v1 used `backupbot.volumes.=true/false` (immich pattern — do NOT use for new work) +- mailu has no Postgres (uses SQLite), so no pg_dump hook needed +- For `admin`: `backupbot.backup.path: "/data"` (whole sqlite DB dir) +- For `imap`: `backupbot.backup.path: "/mail"` (whole Maildir) + +### mailu compose.yml structure note + +mailu uses `deploy.labels` (list form with `- "key=value"` strings) for the app service's traefik labels. The backupbot labels need to go on the services that own the data: +- `admin` service uses `labels:` directly (not `deploy.labels`) — no traefik label there +- `imap` service similarly uses `labels:` directly + +Wait, actually checking the compose.yml — there's no `labels:` on `admin` or `imap` at all. +The `app` (nginx) service has `deploy.labels` for traefik. For backupbot, the labels need to be +on the DEPLOYED service (under `deploy.labels` or top-level `labels`). In Docker Swarm, backupbot +uses service labels (which are deploy-time labels). So we need `deploy.labels` on admin + imap. + +The `app` service already uses `deploy.labels` (list form) for traefik. For admin + imap we need +to add `deploy:` → `labels:` sections. + +### Version bump + +Current version: `3.0.1+2024.06.52` (on `app` service `deploy.labels` → `coop-cloud.${STACK_NAME}.version`) +New version: `3.1.0+2024.06.52` (minor version bump for backupbot feature addition) + +### CI test design + +**ops.py hooks** (consistent with n8n pattern): +- `pre_backup(ctx)`: create a test mailbox `citest@` via `flask mailu user citest ''` in the admin container +- `pre_restore(ctx)`: delete the mailbox via `flask mailu user delete citest@` (or equivalent) to simulate data loss + +**test_backup.py**: assert `citest@` is in `config-export` at backup time + +**test_restore.py**: assert `citest@` is back in `config-export` after restore + +The `_mailu.py` helpers already provide: +- `flask_mailu(domain, cmd)` → runs flask mailu CLI in admin container +- `config_export(domain)` → parses config-export JSON +- `user_emails(cfg)` → list of email addresses from config + +### Delete-user CLI for pre_restore + +Need to confirm the delete command. From mailu docs, the admin CLI: +- Create: `flask mailu user ''` +- Delete: `flask mailu user delete ` (where email = local@domain) +- Or: `flask mailu user delete @` +Need to verify the exact syntax. Will use `flask mailu user delete citest@` and add error handling. diff --git a/STATUS-mailu.md b/STATUS-mailu.md new file mode 100644 index 0000000..f7d808e --- /dev/null +++ b/STATUS-mailu.md @@ -0,0 +1,49 @@ +# STATUS — phase mailu (backupbot labels for mailu recipe) + +**Phase plan:** `/srv/cc-ci/cc-ci-plan/plan-phase-mailu-backup.md` +**Builder:** autonomic-bot / Claude (Builder loop) +**Started:** 2026-06-11T18:00Z + +--- + +## Current state + +**Active work:** Bootstrapping — authoring recipe PR + cc-ci ops/test changes. + +**Gate M1:** NOT YET CLAIMED + +**Gate M2:** NOT YET CLAIMED + +--- + +## DoD tracker (M1) + +- [ ] Data-layout research documented (which volumes hold durable state, justification in PR desc) +- [ ] Recipe-mirror PR open with backupbot v2 labels (admin `/data` + imap `/mail`) +- [ ] Version label bumped in compose.yml +- [ ] cc-ci: `tests/mailu/ops.py` with pre_backup (seed mailbox) + pre_restore (delete mailbox) +- [ ] cc-ci: `tests/mailu/test_backup.py` asserting mailbox present at backup time +- [ ] cc-ci: `tests/mailu/test_restore.py` asserting mailbox restored after restore +- [ ] cc-ci: `tests/mailu/PARITY.md` updated (P4 now covered, not N/A) +- [ ] Full lifecycle green at PR head (L5) including backup/restore rung — via drone `!testme` +- [ ] Before/after level recorded (was: L4 intentional skip → now: L5 earned) + +## DoD tracker (M2) + +- [ ] Fresh Adversary cold pass (independent re-trigger at PR head) +- [ ] Levels reconciled +- [ ] DEFERRED entry closed +- [ ] STATUS-mailu.md operator summary +- [ ] REVIEW-mailu.md shows PASS for M1 + M2 (within 24h) + +--- + +## Blocked items + +(none) + +--- + +## DONE + +Not yet. Written here only when all DoD items have Adversary PASS in REVIEW-mailu.md.