Independent cold pass: Adversary posted !testme on PR#3 (comment #14363); build #483 reached LEVEL 5 (install/upgrade/backup_restore/functional/lint all pass); both Maildir tests pass again (test_backup_captures_mail_message + test_restore_returns_mail_message); clean_teardown+no_secret_leak true; DEFERRED closed; levels reconciled; PARITY.md dual-volume; operator summary complete. Phase mailu DONE. Builder cleared for ## DONE in STATUS-mailu.md.
10 KiB
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:/dataonadminsvc — SQLite DB (accounts, domains, aliases, DKIM config)dkim:/dkimonadminsvc — DKIM signing keysmail:/mailonimapsvc — mail store (Maildir, all user messages)redis:/dataondbsvc — 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:
adminservice (for DB + DKIM) andimapservice (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)
-
PR#3 labels correct (
add-backupbot-labels, headedc0201a79d3):adminservice:backupbot.backup: "true"+backupbot.backup.path: "/data"✓imapservice: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 ✓
-
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_mailboxPASS —citest@<domain>in config-export at backup time ✓test_restore_returns_mailboxPASS —citest@<domain>back in config-export after restore ✓- Backup snapshot
13eee64e: 139 files, 85MB ✓ - Cold teardown:
abra app ls --server cc-cishows no mailu apps ✓ - No plaintext secrets in compose.yml (secrets section uses swarm
external: truerefs) ✓ - PARITY.md updated: P4 COVERED ✓
-
Backupbot v2 syntax verified against keycloak/mattermost-lts/n8n patterns —
backupbot.backup.pathis 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 accountcitest@<domain>in SQLite viaflask mailu user— this is an account record in/data(SQLite), NOT a mail message in/mail(Maildir)pre_restore: deletescitest@<domain>from SQLite via sqlite3 — only wipes the DB record; the Maildir at/mailis untouched throughouttest_restore.py: assertscitest@<domain>is back inconfig-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):
pre_backup: after creatingcitest@<domain>, inject a uniquely-tagged message into the mailbox (e.g., via in-containersendmail→ postfix → dovecot deliver, the same path astest_mail_flow.py)pre_restore: also wipe the maildir forcitest@<domain>(e.g.,doveadm expunge -u citest@<domain> mailbox INBOX ALLin theimapcontainer)test_restore.py: after asserting the account is back, also assert the seeded message is present (e.g.,doveadm search -u citest@<domain> mailbox INBOX ALLreturns ≥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)
-
PR#3 labels correct (branch
add-backupbot-labels, headedc0201a79d36bc87696b0f93f1ee88ad7bd10ed):adminservice:backupbot.backup: "true"+backupbot.backup.path: "/data"✓imapservice:backupbot.backup: "true"+backupbot.backup.path: "/mail"✓- Version bump:
3.0.1→3.0.2+2024.06.52✓
-
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_mailboxPASS (1323ms) — SQLite/data✓test_backup_captures_mail_messagePASS (133ms) — Maildir/mail✓
- restore stage (all PASS):
test_restore_returns_mailboxPASS (1359ms) — SQLite/data✓test_restore_returns_mail_messagePASS (189ms) — Maildir/mail✓
- Clean teardown confirmed:
docker stack lson cc-ci shows nomailu-*stacks ✓ - No mailu volumes leaked ✓
-
Fix code review (commit
b9352e8, cold):ops.py::pre_backup: creates user + injectsccci-backup-probemessage viasendmailinsmtpcontainer, pollsdoveadm searchinimapcontainer (≤60s) to confirm delivery ✓ops.py::pre_restore: (1) deletes user from sqlite; (2)rm -rf /mail/{domain}/{localpart}inimapcontainer — wipes maildir independently from sqlite record ✓test_backup_captures_mail_message:doveadm searchonimapasserts 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 ✓
-
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.
M2 PASS @2026-06-11T21:15Z
Claim: DEFERRED closed; levels reconciled; PARITY.md updated; operator summary written; fresh Adversary re-trigger via independent !testme on PR#3.
Verdict: PASS — all M2 DoD items verified independently. Phase mailu is DONE.
What I verified (cold)
-
PR#3 still open, unmerged (Gitea API cold check):
- state: open, head sha:
edc0201a79d36bc87696b0f93f1ee88ad7bd10ed, merged: False ✓
- state: open, head sha:
-
DEFERRED.md mailu entry closed:
- Entry
2026-05-29 — mailu: no backup configmarked[x] CLOSED @2026-06-11with PR#3 + build #477 pointers; re-entry checkbox also ticked ✓
- Entry
-
PARITY.md updated with dual-volume evidence (
tests/mailu/PARITY.md):- P4 section now states "earned via recipe-mirror PR#3" ✓
- Documents both
/data(SQLite) and/mail(Maildir) seeded + wiped + verified restored ✓ ops.py,test_backup.py,test_restore.pyeach described correctly ✓- Before/after level:
backup_capable=False → L4-skip→backup_capable=True → L5-earned✓
-
Levels reconciliation independently verified:
runner/harness/generic.py::backup_capable()scanscompose*.ymlforbackupbot.backup.*true✓- Main branch: no backupbot labels →
backup_capable=False→ backup rung = intentional skip → L4 ✓ - PR#3 head: admin+imap labels present →
backup_capable=True→ backup rung earned → L5 ✓
-
Operator summary in STATUS-mailu.md: complete, accurate, actionable — specifies PR#3 URL, head SHA, what the PR adds, what CI proved, what operator must do (merge PR#3) ✓
-
Fresh independent re-trigger (Adversary posted
!testmeon PR#3 at 2026-06-11T21:04:39Z, comment #14363):- Drone build #483: LEVEL 5 SUCCESS, recipe=mailu, PR=3, ref=
edc0201a79d3 - All 5 rungs PASS: install / upgrade / backup+restore / functional / lint ✓
- Backup stage:
test_backup_captures_mailboxPASS (1377ms) +test_backup_captures_mail_messagePASS (149ms) ✓ - Restore stage:
test_restore_returns_mailboxPASS (1402ms) +test_restore_returns_mail_messagePASS (168ms) ✓ clean_teardown: true,no_secret_leak: true✓- No mailu stacks or volumes on host post-run (
docker stack ls+docker volume lsconfirm) ✓ - Result is reproducible: two independent builds (#477, #483) both LEVEL 5 at the same PR head ✓
- Drone build #483: LEVEL 5 SUCCESS, recipe=mailu, PR=3, ref=
Phase DoD satisfied
All items from plan-phase-mailu-backup.md §5:
- Mirror PR open with evidence-justified backupbot v2 labels ✓ (PR#3)
- backup→wipe→restore proven on real seeded mail data at PR head incl. drone path ✓ (builds #477 + #483)
- mailu's backup rung earned (not skipped) with levels reconciled ✓
- DEFERRED closed ✓
- M1 + M2 fresh Adversary PASSes ✓ (this entry + M1 PASS above)
- PR unmerged for the operator ✓
Phase mailu is complete. Builder is cleared to write ## DONE to STATUS-mailu.md.