Two further hardening findings:
- Destroy-on-success was unsafe: the script exited 0 for empty/no-archive
lists too, so the README's &&-gated loop could remove a list that
produced no backup artifact. Give the no-file outcomes a distinct exit
code 3 ("nothing to archive" — not an error). The destroy loop now
removes only on exit 0 (archive written) + a successful member dump,
and leaves exit-3 lists intact unless ALLOW_NO_ARCHIVE=1. Safe by
default: an unexpected "no archive" can no longer authorise a delete.
- Publish wasn't crash-durable: the handoff fsynced file contents but not
the parent directory, so the rename's directory entry could be lost to
a crash after success was reported and the list removed. fsync the
destination directory after os.replace(), before returning success.
Verified with stubbed mailman-web: NONE/EMPTY -> exit 3 (no file),
FILE -> exit 0 (0600, content intact, source temp unlinked), bad
address -> exit 2. sh -n + shellcheck clean, embedded python compiles.
Address three hardening findings:
- Gate the README backup-and-destroy loop so `mailman-wrapper remove`
only runs after both the export and member dump succeed; a failed
backup now leaves the list intact instead of falling through to an
irreversible delete. Empty/no-archive lists still pass (exit 0 by
design), so only real failures block removal.
- Replace the curl/REST archive_policy lookup with `mailman-wrapper
shell`, reading archive_policy straight from core's DB via
getUtility(IListManager). Eliminates REST credential handling
entirely (no secret in argv / /proc), drops the curl dependency, and
removes the @->. list-id conversion.
- Make the final publish a safe privilege handoff: reject non-regular /
symlinked temp files, copy the web-user mkstemp output into a
root-owned temp file on the destination filesystem, chmod 600, then
atomically rename into place. Fixes inherited web-user ownership and
the cross-filesystem mv that could leave a partial or clobbered
backup.
Scriptable equivalent of HyperKitty's web Download button for
backup-and-destroy loops. Runs the export through Debian's mailman-web
wrapper (HyperKitty lives in the web DB, separate from core), reproducing
the web view's Email.as_bytes() output.
- Handles the three archive states without writing partial/empty files:
populated (writes mbox), row-but-empty, and no row at all.
- For a missing row, best-effort core REST lookup distinguishes
archive_policy=never from 'nothing archived yet'.
- -z/--gzip matches the web .mbox.gz (deterministic header, mtime=0).
- Privilege-safe: web user writes a 0600 mkstemp file, root moves it.
- Address validated before interpolation into the embedded Python.