Shore Up
A filing clerk pulling one folder out of a sealed archive box, opening it to confirm every page is still inside, then sliding it back — repeated on a calendar schedule.
LinuxMail

Automate Mail Backup Verification With a Periodic Restore Test

Ketan Aagja8 min read
No ratings yet

A backup you have never restored is a hope, not a backup. The cheapest way to turn hope into evidence is to restore something small on a schedule and check it landed. Here I restore one sample Maildir from the latest snapshot into a throwaway directory, confirm it looks like a real mailbox, log the result, and clean up. Run it weekly and you will know your mail backups are restorable before the day you need them.

Before you run this

What it does: the script restores a single user's Maildir from your most recent restic snapshot into a temporary directory, checks the restored folder contains the expected cur/new structure and at least one message file, writes the result to a log, then deletes the temporary directory. It is a read-and-restore-to-scratch test — it never touches, moves, or overwrites your live mailboxes.

Privileges: it needs to read your restic repository and its password file, both of which are normally root-owned, and it reads mail paths that live under /var/vmail with restrictive permissions. So run it as root (that is why the systemd unit below runs it as root). It writes only to a mktemp scratch directory and to a log file.

Test it first: read the script, then run it by hand once (/usr/local/bin/backup-verify.sh) and watch the output before you ever put it on a timer. Point SAMPLE_PATH at a small, low-value test mailbox the first time, not your busiest user.

Destructive edges: the only thing this script deletes is its own temporary restore directory, and that path comes from mktemp — it is never a path you typed. Do not "improve" the cleanup line to point at a fixed directory; that is how people rm -rf something they meant to keep. Restoring does not alter production mail, so there is nothing to roll back on the mail server itself.

What I'm assuming

  • Debian 12, systemd, restic installed from apt (apt install restic).
  • Virtual mailboxes stored as Maildir under /var/vmail/<domain>/<user>/Maildir, the common Dovecot layout.
  • A restic repository that already exists and is being backed up on a schedule by something else. This guide verifies that backup; it does not create it.
  • The restic repo password is stored in a root-readable file (RESTIC_PASSWORD_FILE), which is the standard non-interactive setup.

On RHEL/Alma the only differences are the package manager (dnf install restic, sometimes via EPEL) and possibly your mail path. Everything else is identical.

The script

Save this as /usr/local/bin/backup-verify.sh and chmod 750 it. Edit the four values in the configuration block — they are the obvious placeholders.

#!/usr/bin/env bash
set -euo pipefail

# ---- Configuration: edit these four lines ----
export RESTIC_REPOSITORY="/srv/backup/restic-mail"        # your restic repo location
export RESTIC_PASSWORD_FILE="/root/.restic-mail-pass"     # file holding the repo password
SAMPLE_PATH="/var/vmail/example.com/testuser/Maildir"     # a Maildir to restore-test
LOG="/var/log/backup-verify.log"
# ----------------------------------------------

timestamp() { date '+%Y-%m-%d %H:%M:%S'; }
log() { echo "$(timestamp) $*" | tee -a "$LOG"; }

# Scratch target comes from mktemp, never a hard-coded path.
RESTORE_DIR="$(mktemp -d /tmp/backup-verify.XXXXXX)"
cleanup() { rm -rf "$RESTORE_DIR"; }   # only ever removes the mktemp dir
trap cleanup EXIT

log "Starting restore test of $SAMPLE_PATH from repo $RESTIC_REPOSITORY"

# Restore ONLY the sample Maildir from the newest snapshot into scratch.
# restic recreates the full absolute path under --target.
restic restore latest \
  --target "$RESTORE_DIR" \
  --include "$SAMPLE_PATH"

# Where restic placed the restored copy:
RESTORED_PATH="${RESTORE_DIR}${SAMPLE_PATH}"

# A real Maildir has a 'cur' directory; if it's missing, the restore is wrong.
if [ ! -d "$RESTORED_PATH/cur" ]; then
  log "FAIL: expected Maildir 'cur' directory not found at $RESTORED_PATH"
  exit 1
fi

# Count restored message files. Zero files means an empty/failed restore.
COUNT=$(find "$RESTORED_PATH" -type f | wc -l)
if [ "$COUNT" -eq 0 ]; then
  log "FAIL: restored Maildir contains no files"
  exit 1
fi

log "OK: restored $COUNT files for $SAMPLE_PATH from latest snapshot"

A few notes on the non-obvious parts:

  • set -euo pipefail makes the script stop on the first error, so a failed restic restore aborts instead of silently reporting success.
  • restic restore latest selects the most recent snapshot; --include limits the restore to just the one Maildir instead of the whole repo. I'm restoring by path prefix, which is the mainstream way to pull one folder out of a snapshot.
  • restic recreates the full source path under --target, so the restored copy lives at ${RESTORE_DIR}${SAMPLE_PATH}. That is why the two variables are concatenated.
  • The exit codes matter: exit 1 on failure lets systemd (below) mark the run failed so you can alert on it.

If you want a stronger check, you can also compare a single known-static message by checksum against the live copy — but new mail arriving between backup and test makes whole-mailbox file counts unreliable, so I keep the check to "structure exists and is non-empty" and leave content comparison as an exercise you add deliberately.

A complementary check, mentioned not walked through

restic check verifies the repository's internal integrity (that blobs and indexes are consistent). It is worth running periodically too, but it does not prove a file will actually restore — only an actual restore does that. Treat the two as separate jobs; this guide is the restore test.

Put it on a schedule

I use a systemd timer rather than cron so failures land in the journal and are easy to alert on. Create the service unit at /etc/systemd/system/backup-verify.service:

[Unit]
Description=Verify mail backup by restoring a sample Maildir

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-verify.sh

And the timer at /etc/systemd/system/backup-verify.timer:

[Unit]
Description=Weekly mail backup restore test

[Timer]
OnCalendar=Sun 03:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true runs the job after boot if the machine was off at the scheduled time. Enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now backup-verify.timer

The equivalent cron approach is a single root crontab line calling the script; the timer just gives cleaner logging and failure handling. To be alerted on failure, add an OnFailure= handler to the service or have your monitoring watch for the string FAIL in the log — check the systemd documentation for the exact OnFailure unit syntax before wiring that up.

Verify it worked

Run it once by hand and read the output and the log:

sudo /usr/local/bin/backup-verify.sh
sudo tail -n 5 /var/log/backup-verify.log

You want a line ending OK: restored N files .... Then confirm the timer is scheduled:

systemctl list-timers backup-verify.timer

After the first scheduled run, check the journal for that unit:

journalctl -u backup-verify.service --since "yesterday"

A successful run exits 0 and leaves nothing behind in /tmp (the trap cleanup removes the scratch directory even on failure).

Undo it

Nothing was changed on the mail server, so there is no data to reverse. To stop and remove the automation:

sudo systemctl disable --now backup-verify.timer
sudo rm /etc/systemd/system/backup-verify.timer \
        /etc/systemd/system/backup-verify.service
sudo systemctl daemon-reload
sudo rm /usr/local/bin/backup-verify.sh          # optional

That returns the box to exactly where it started, minus a log file you can keep or delete. Your backups are untouched — now you just have a weekly record proving they restore.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Weekly Mail Store Integrity Checks with Checksums

This sets up a weekly job that walks your Maildir tree, computes a SHA-256 for every message file, and compares this week's checksums against last week's to catch silent corruption (bit rot, a bad disk, a botched restore) in files that should never have changed once written.

10 min read

Schedule Automatic ZFS Snapshots and Pruning with Sanoid

This guide installs Sanoid and configures it to take ZFS snapshots on a schedule (hourly, daily, monthly) and to automatically destroy snapshots older than the retention you set. Snapshots are your cheap, instant, on-box rollback points; the pruning half is what stops them from filling the pool forever.

9 min read

Scheduled Database and File Backups to S3-Compatible Storage with rclone

This sets up an unattended nightly job that dumps your MySQL/MariaDB databases and tars up a couple of directories, uploads both to an S3-compatible bucket with rclone , and then deletes backups in that bucket older than a retention window. Its purpose is a hands-off off-site copy that prunes itself so the bucket doesn't grow forever.

8 min read

Automate iptables/nftables Backup and Restore

This guide sets up two things: a small script that dumps your host's live packet-filter ruleset to a timestamped, versioned file, and a scheduled systemd timer to run it. It also shows the standard restore path. The backup part is read-only and safe. The restore part is not — reloading a ruleset replaces your firewall's entire running state in one transaction, and a bad ruleset can drop your SSH session and cut the host off the network instantly.

9 min read