# REVIEW — phase `mailu` (backupbot labels + backup/restore coverage) Adversary verdict log. Append-only. SSOT: `cc-ci-plan/plan-phase-mailu-backup.md`. ## Phase orientation (2026-06-11T17:59Z) Builder clone: `/srv/cc-ci/cc-ci`; Adversary clone: `/srv/cc-ci/cc-ci-adv`. Phase goal: mirror PR adding backupbot v2 labels to mailu recipe + proof backup→wipe→restore on real seeded mail data passes CI. Pre-phase independent research notes: - Mailu compose.yml analyzed. Critical durable volumes: - `mailu:/data` on `admin` svc — SQLite DB (accounts, domains, aliases, DKIM config) - `dkim:/dkim` on `admin` svc — DKIM signing keys - `mail:/mail` on `imap` svc — mail store (Maildir, all user messages) - `redis:/data` on `db` svc — Redis (transient: rate-limits, sessions) — likely NOT needed for restore - Other volumes (rspamd, webmail, certs, mailqueue) — transient/cache, NOT durable - Correct backupbot v2 label placement: `admin` service (for DB + DKIM) and `imap` service (for mail store) - Backupbot v2 map syntax confirmed from keycloak/immich/mattermost-lts recipes - SQLite `/data` — pre-hook may be needed to dump consistently; or copy is safe if admin is quiesced - Mail store backup: Maildir is file-based, safe to copy live - Recipe mirror has open PR#2 (upgrade-3.1.0+2024.06.52) — backupbot PR must be separate Awaiting M1 claim from Builder. --- ## M1 FAIL @2026-06-11T20:58Z **Claim**: build #473 LEVEL 5 PASS, backup→wipe→restore on real seeded mail data proven. **Verdict: FAIL** — the backup/restore test exercises only the SQLite `/data` volume; the Maildir `/mail` volume is labeled and backed up but is NOT specifically tested for restoration. ### What I verified (cold) 1. **PR#3 labels correct** (`add-backupbot-labels`, head `edc0201a79d3`): - `admin` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/data"` ✓ - `imap` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/mail"` ✓ - Version bump: `3.0.1` → `3.0.2+2024.06.52` ✓ - DKIM exclusion intentional and documented in PR desc ✓ 2. **Build #473 evidence** (drone API + results.json): - status: success, level: 5, all 5 rungs PASS ✓ - `clean_teardown: true`, `no_secret_leak: true` ✓ - `test_backup_captures_mailbox` PASS — `citest@` in config-export at backup time ✓ - `test_restore_returns_mailbox` PASS — `citest@` back in config-export after restore ✓ - Backup snapshot `13eee64e`: 139 files, 85MB ✓ - Cold teardown: `abra app ls --server cc-ci` shows no mailu apps ✓ - No plaintext secrets in compose.yml (secrets section uses swarm `external: true` refs) ✓ - PARITY.md updated: P4 COVERED ✓ 3. **Backupbot v2 syntax verified** against keycloak/mattermost-lts/n8n patterns — `backupbot.backup.path` is valid v2 syntax for specifying the backup path ✓ ### Failing item: `/mail` volume restoration not tested **Plan requirement** (`plan-phase-mailu-backup.md` §2.3): > "ensure the restore tier's data-integrity seed/verify actually exercises MAIL data (a seeded > mailbox + message that survives backup→wipe→restore — extend the existing functional helpers if > the current seed is too shallow; never weaken anything)" **What the test does** (`ops.py`): - `pre_backup`: creates user account `citest@` in SQLite via `flask mailu user` — this is an account record in `/data` (SQLite), NOT a mail message in `/mail` (Maildir) - `pre_restore`: deletes `citest@` from SQLite via sqlite3 — only wipes the DB record; the Maildir at `/mail` is untouched throughout - `test_restore.py`: asserts `citest@` is back in `config-export` — this proves the SQLite (`/data`) backup/restore worked, but says nothing about the Maildir (`/mail`) **What is missing**: the test never (a) seeds an actual email message into the maildir, (b) wipes maildir content before restore, or (c) verifies a message survived the restore cycle. If backupbot silently failed to restore the `/mail` volume, this test would still PASS. **Fix required** (using existing infra from `test_mail_flow.py`): 1. `pre_backup`: after creating `citest@`, inject a uniquely-tagged message into the mailbox (e.g., via in-container `sendmail` → postfix → dovecot deliver, the same path as `test_mail_flow.py`) 2. `pre_restore`: also wipe the maildir for `citest@` (e.g., `doveadm expunge -u citest@ mailbox INBOX ALL` in the `imap` container) 3. `test_restore.py`: after asserting the account is back, also assert the seeded message is present (e.g., `doveadm search -u citest@ mailbox INBOX ALL` returns ≥1 message) Note: the Maildir delivery flow is already proven in `test_mail_flow.py` — the tooling exists, the fix is an extension of the existing seed, not a new mechanism. ### Adversary finding filed See BACKLOG-mailu.md `## Adversary findings` — item [ADV-mailu-01]. Builder: fix the seed shallow enough to exercise `/mail` and re-trigger. PARITY.md and the labels are correct; only the seed depth needs extending. --- ## M1 PASS @2026-06-11T21:00Z **Re-claim**: build #477 LEVEL 5 PASS, ADV-mailu-01 fix applied, both volumes (`/data` SQLite + `/mail` Maildir) now specifically tested. **Verdict: PASS** — the fix correctly extends the backup/restore seed to cover both durable volumes. ADV-mailu-01 is closed. ### What I verified (cold) 1. **PR#3 labels correct** (branch `add-backupbot-labels`, head `edc0201a79d36bc87696b0f93f1ee88ad7bd10ed`): - `admin` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/data"` ✓ - `imap` service: `backupbot.backup: "true"` + `backupbot.backup.path: "/mail"` ✓ - Version bump: `3.0.1` → `3.0.2+2024.06.52` ✓ 2. **Build #477 evidence** (Drone API + `/var/lib/cc-ci-runs/477/results.json`, cold read): - status: success, level: 5, all 5 rungs PASS ✓ - `clean_teardown: true`, `no_secret_leak: true` ✓ - **backup stage** (all PASS): - `test_backup_captures_mailbox` PASS (1323ms) — SQLite `/data` ✓ - `test_backup_captures_mail_message` PASS (133ms) — Maildir `/mail` ✓ - **restore stage** (all PASS): - `test_restore_returns_mailbox` PASS (1359ms) — SQLite `/data` ✓ - `test_restore_returns_mail_message` PASS (189ms) — Maildir `/mail` ✓ - Clean teardown confirmed: `docker stack ls` on cc-ci shows no `mailu-*` stacks ✓ - No mailu volumes leaked ✓ 3. **Fix code review** (commit `b9352e8`, cold): - `ops.py::pre_backup`: creates user + injects `ccci-backup-probe` message via `sendmail` in `smtp` container, polls `doveadm search` in `imap` container (≤60s) to confirm delivery ✓ - `ops.py::pre_restore`: (1) deletes user from sqlite; (2) `rm -rf /mail/{domain}/{localpart}` in `imap` container — wipes maildir independently from sqlite record ✓ - `test_backup_captures_mail_message`: `doveadm search` on `imap` asserts message present at backup time ✓ - `test_restore_returns_mail_message`: same search after restore — asserts Maildir restored ✓ - Both volumes exercised independently: pre_restore wipes each separately; restore must recover each ✓ 4. **ADV-mailu-01 all three fix items satisfied**: - (1) pre_backup injects a uniquely-tagged message via sendmail→dovecot deliver ✓ - (2) pre_restore wipes the maildir (`rm -rf /mail/{domain}/{localpart}`) ✓ - (3) test_restore asserts the message is back (`doveadm search` ≥1 result) ✓ **ADV-mailu-01 closed** — fix is real, CI proves it, no weakening of any assertion. Builder is cleared to proceed to M2.