Skip to content

Repository files navigation

A rolling-window mailbox backup/restore tool for Zimbra Collaboration Suite, written in Python 3 with a SQLite-tracked backup catalog.

Purpose. mslzmbackup is built for one specific job: keeping a rolling retention window (e.g. 30 days) of mailbox backups so that a user who accidentally deletes mail can get it back — not long-term archival. Every design choice (chain-aware rotation, per-account granularity, safety checks before deleting anything) follows from that goal.

Why this exists

This project started as a from-scratch Python rewrite after running into a class of bugs (unexpanded shell parameter substitutions, fragile quoting on long account lists) in a bash-based backup tool used in production. Rather than keep patching shell script around those issues, this reimplements the same transport mechanism in a language better suited to structured data and correctness: Python 3 for the logic, SQLite for tracking what was backed up and when.

Credit where it's due: the idea of using Zimbra's own zmmailbox getRestURL/postRestURL REST endpoints (fmt=tgz) as the backup/restore transport — rather than something lower-level — was inspired by zmbackup by Lucas Costa Beyeler. mslzmbackup is an independent implementation (no code shared with that project), and it diverges from it on the two things that matter most for restore reliability: zmbackup tracks each full and incremental as an independent session with no link between an incremental and the full it belongs to (the operator has to know and replay the right sequence by hand), and its rotation deletes sessions by flat age, full or incremental alike, with no awareness of which full a still-needed incremental depends on. mslzmbackup computes the full→incremental chain from timestamps instead of relying on the operator, and a full is only rotated once every account that depended on it has a newer successor — see Key design decision below for the full reasoning. If you need distribution list / alias / signature / domain backup with a proven track record on legacy Zimbra versions, zmbackup is worth a look too.

Key design decision: chain-aware retention

Full and incremental backups are not tracked as isolated sessions. Instead, which incrementals depend on which full is computed from timestamps (see schema.sql, view account_full_chain) rather than stored — full/incremental chains are always non-overlapping time ranges, so "which full does this incremental belong to" is derivable, not something that needs to be kept in sync.

Retention operates per account, per object type, not per session. A full backup is only deletable once the full that superseded it is itself older than the retention window, for every single account that depended on it — never based on its own age, and never the most recent full (which has no successor yet). If even one account never got a successful subsequent full, that backup is kept and the account is flagged for attention instead of silently losing its recovery point. This directly guards against a real failure mode found in the wild: a backup session can report success at the session level while silently producing a 0-byte backup for a specific account (e.g. due to a misconfigured public service hostname) — a purely date-based rotation would delete the only good copy without anyone noticing.

Installation

No build step — just Python 3 (stdlib, including sqlite3) and the Zimbra binaries already present on any Zimbra host (zmmailbox, zmprov, ldapsearch). No venv needed (stdlib only).

sudo ./install.sh
# edit /etc/mslzmbackup/mslzmbackup.conf: LDAPPASS / RETENTION_DAYS for your environment

install.sh installs application files (the Python package, schema.sql, the entrypoint) under LIBDIR (default /usr/local/lib/mslzmbackup) and creates an empty data directory under BACKUPDIR (default /opt/zimbra/mslzmbackup) — backup sessions and the mslzmbackup.sqlite3 catalog live there, never application code. Override either with the MSLZMBACKUP_LIBDIR / MSLZMBACKUP_BACKUPDIR env vars before running the script. It also symlinks the entrypoint into /usr/local/bin/mslzmbackup.

Heads up on JVM heap. zmmailbox/zmprov (the Zimbra client tools mslzmbackup shells out to) run under their own JVM, sized independently from mailboxd's. getRestURL/postRestURL buffer the whole mailbox export in memory before writing it to disk, so backing up an account of non-trivial size can throw OutOfMemoryError if that client-side heap is too small. It's controlled by the zimbra_zmjava_options key in localconfig.xml (-Xmx...) — check it with zmlocalconfig zimbra_zmjava_options and raise it if needed, e.g.:

zmlocalconfig -e zimbra_zmjava_options='-Xmx1024m -Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3 -Djava.net.preferIPv4Stack=true'

(keep the existing -D flags from your current value, just raise -Xmx). Some installs default this quite low (256m); 1024m comfortably handles mailboxes in the hundreds-of-MB range.

Usage

# backup
mslzmbackup backup full user@domain.com,other@domain.com
mslzmbackup backup full /path/to/account-list.txt          # one per line
mslzmbackup backup incremental --domain example.com         # auto-enumerate a domain
mslzmbackup backup incremental /path/to/account-list.txt

# distribution lists / aliases / signatures / the domain's own LDAP entry
mslzmbackup backup-dl --domain example.com
mslzmbackup backup-alias aliases.txt
mslzmbackup backup-sig user@domain.com
mslzmbackup backup-domain --domain example.com

# list tracked sessions
mslzmbackup list

# restore a session (all accounts, or a subset)
mslzmbackup restore full-20260824193850
mslzmbackup restore full-20260824193850 user@domain.com

# restore-on-account: restore into a different destination account
mslzmbackup restore-ro full-20260824193850 user@domain.com sandbox@domain.com

# restore the full chain (full + every incremental since) for one account,
# optionally stopping at a specific point instead of going all the way to now
mslzmbackup restore-chain user@domain.com
mslzmbackup restore-chain user@domain.com --up-to inc-20260901003000

# import a session produced by another zmbackup-style tool, already
# transferred locally
mslzmbackup import-legacy /path/to/full-20260824193850 --hostname mailstore-1

# plan (dry-run) or apply rotation
mslzmbackup rotate
mslzmbackup rotate --apply

# wipe every backup and empty the catalog (double confirmation) - not
# retention, ignores the window entirely
mslzmbackup reset-all

Rotation is a permanent delete, never triggered automatically by backup — you decide when it runs. rotate --apply deletes immediately with no prompt, meant for a cron job that already trusts the retention window and the per-account chain safety check (see below).

reset-all is a different, much blunter operation: it deletes every tracked session unconditionally — full, incremental, today's or thirty days old, it doesn't check the retention window or the chain safety logic at all — and empties the catalog. There's no flag to skip the prompt: it always asks for two separate interactive confirmations (type yes, then re-type DELETE ALL) before touching anything, since there's no scenario where you'd want this running unattended.

Data model

See schema.sql for the full SQLite schema and the reasoning behind each table/view in comments. Two reference tables (hosts, sessions), one fact table (account_backups, one row per account × object type × session), and one view (account_full_chain) that computes the retention-relevant chain relationships on the fly.

What it does not do (yet)

  • It does not create/provision accounts, distribution lists, or aliases — it assumes they already exist on the target. It restores mailbox content only.
  • No built-in scheduler — call it from cron like you would any other tool.
  • Email notifications are opt-in (ENABLE_EMAIL_NOTIFY) and go through the local sendmail binary — no external SMTP dependency.

License

MIT — see LICENSE. Copyright (c) 2026 Mirio Salvini / msl-tech.

No warranty. This software is provided "as is", without any guarantee of correctness or fitness for any purpose. Backups are only as good as your testing of them — verify restores actually work in your environment before relying on this for anything you can't afford to lose. The author accepts no liability for data loss or any other damage arising from the use of this software (see LICENSE for the full disclaimer).

About

Chain-aware, rolling-window mailbox backup/restore for Zimbra Collaboration Suite (Python 3 + SQLite)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages