mm3-hk-export/README.md
veg f4ed22ceab Drop the /var/tmp handoff hardening for a plain copy
The SHA-256 double-hash and TOCTOU defenses (O_NOFOLLOW/O_NONBLOCK, S_ISREG, fsync, in-process durable mkdir) guarded against a hostile same-uid process racing the /var/tmp handoff — a threat that does not match this deployment (a small trusted collective, no hostile local users), and the cost showed up as a 10-15s silent gap after the progress bar (two full passes over the archive).

Replace the ~60-line python3 publish block with plain shell: mkdir -p, mktemp in the destination dir, cp, atomic mv -f, rm the source. The two-stage handoff and atomic rename stay (the web user cannot write into a root-owned destination, and the rename keeps $out from being partial). The exporter no longer hashes either. README updated to match.
2026-05-31 14:51:50 +00:00

2.8 KiB

mm3-hk-export

Export a private HyperKitty (Mailman 3) list archive to mbox from the command line — the CLI equivalent of the web UI's Download button.

Why

HyperKitty ships no hyperkitty_export command; the only built-in export is the web Download view. This drives the same Email.as_bytes() path through the mailman-web wrapper, so the output is byte-for-byte identical — no web login, no DB reconstruction. It has to be mailman-web, not mailman-wrapper: the archive lives in the HyperKitty DB, not core.

Requirements

Run as root — mailman-web drops to the web user to read the archive, then the script publishes the result as root. Uses mailman-wrapper to report a list's archive_policy when there's nothing to export.

Usage

mm3-hk-export [-z|--gzip] <list@domain> [output]

output defaults to ./<localpart>.mbox (.mbox.gz with -z). -z gzips the stream to match the web .mbox.gz byte-for-byte (deterministic header, mtime=0).

mm3-hk-export announce@lists.example.com /root/mm3/announce.mbox
mm3-hk-export --gzip announce@lists.example.com /root/mm3/announce.mbox.gz

Exit codes

Never writes a partial or empty file. The exit code reports whether a backup was produced:

Exit Meaning
0 archive written (wrote N messages to …)
3 nothing to archive — empty list, or no archive at all (reports archive_policy: never vs nothing-yet)
2 usage / bad address
1 error (e.g. mailman-web failed)

3 is distinct from 0 so a caller can tell "backed up" from "nothing to back up" and refuse to destroy an uncaptured list.

Gating a removal on the backup

The script only exports. To remove a list after a verified backup, gate it on the exit code yourself:

mm3-hk-export "$addr" "/root/mm3/$list.mbox" \
    || { echo "no backup; keeping $addr" >&2; exit 1; }
mailman-wrapper members "$addr" > "/root/mm3/$list.members"
mailman-wrapper remove "$addr"

Export before remove, never remove on a non-zero exit. (Core's remove leaves the HyperKitty archive untouched anyway — separate DB — but a failed export must still halt the destroy.)

Caveats

  • Reconstruction, not the original bytes. HyperKitty rebuilds each message from parsed fields (From, To, Subject, Date, Message-ID, In-Reply-To, body, attachments); Cc, References, List-*, and X-* are lost. Inherent to HyperKitty — the web download is identical.
  • Large lists. Streamed via a DB iterator, so no memory blow-up like the web export.
  • Re-import. Valid mbox; re-importable with hyperkitty_import.
  • Handoff. The web user builds the archive in /var/tmp (it can't write into a root-owned destination); root copies it out and renames into place, so $out is never left partial.