
One Report Correlating Mail Service Errors and Delivery Failures
Before you run this
This is a read-only reporting script. It queries the systemd journal and prints a single, time-ordered report that interleaves two things: systemd service-level errors for your mail stack (Postfix and Dovecot logging at priority err or worse) and Postfix delivery failures (bounces, deferrals, and NOQUEUE rejects). The point is to see, in one place, whether a run of deferred deliveries at 02:15 lines up with Dovecot restarting at 02:14.
It does not change, delete, or send anything. It creates a temporary directory under /tmp, removes it on exit, and writes its output to your terminal. There is nothing to undo.
Privileges: reading another unit's journal entries requires either root (sudo) or membership in the systemd-journal group. Run it as a user who can read the full journal, or prefix with sudo. If you only ever see your own session's logs, that's the permission issue.
Test first: read the script before you run it, and run it first with a short, quiet window (SINCE="10 minutes ago") on one host so you can eyeball the output before you trust it or schedule it. Because it is read-only, there is no production risk beyond reading logs — but confirm the unit names and syslog identifiers it uses actually match your system (see Confirm your identifiers below) or it will quietly report nothing.
What I'm assuming
- Debian 12 or Ubuntu 22.04, bash 5.
- Postfix and Dovecot, with their logs going to the systemd journal (the default on these releases).
- Postfix daemons log under identifiers like
postfix/smtp,postfix/smtpd,postfix/lmtp,postfix/local.
If your box still sends mail logs to /var/log/mail.log through rsyslog and not to the journal, this approach won't find them — check that first (below). The unit names also matter: on Debian the real Postfix instance is postfix@-.service, not postfix.service (which is a wrapper). Dovecot is dovecot.service.
Confirm your identifiers
Two one-liners, run before anything else, so you're not guessing:
# What are the real unit names on this host?
systemctl list-units --all '*postfix*' '*dovecot*'
# Are Postfix delivery lines actually in the journal under this identifier?
journalctl -t postfix/smtp -n 5 --no-pager
If the second command prints recent Postfix lines, you're good. If it prints nothing but mail is flowing, your logs aren't in the journal under that identifier — stop and sort that out first. Adjust the SERVICE_UNITS and DELIVERY_IDS arrays in the script to whatever these commands tell you is real on your system.
The script
#!/usr/bin/env bash
#
# mail-error-report.sh
# Build one time-ordered report that interleaves systemd service errors for the
# mail stack with Postfix delivery failures, read from the systemd journal.
# Read-only. Nothing is changed.
#
set -euo pipefail
# --- Settings you may edit ---------------------------------------------------
SINCE="${SINCE:-today}" # any journalctl --since value, e.g. "2 hours ago"
UNTIL="${UNTIL:-now}" # any journalctl --until value
# Confirm these against your host (see the guide): on Debian the live Postfix
# instance is postfix@-.service, NOT postfix.service.
SERVICE_UNITS=(postfix@-.service dovecot.service)
# Postfix daemon syslog identifiers that carry delivery outcomes.
DELIVERY_IDS=(postfix/smtp postfix/lmtp postfix/local postfix/error postfix/bounce)
# -----------------------------------------------------------------------------
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT # always clean up the temp dir
# 1. Service-level errors: anything these units logged at priority err or worse.
# This deliberately excludes routine delivery lines, which log at info/notice.
unit_args=()
for u in "${SERVICE_UNITS[@]}"; do unit_args+=(-u "$u"); done
journalctl "${unit_args[@]}" -p err --since "$SINCE" --until "$UNTIL" \
-o short-iso --no-pager \
| sed 's/^/[SERVICE] /' > "$work/service.txt" || true
# 2. Delivery failures: Postfix lines showing a failed/deferred/rejected outcome.
# Multiple -t flags are OR'd together by journalctl.
id_args=()
for i in "${DELIVERY_IDS[@]}"; do id_args+=(-t "$i"); done
journalctl "${id_args[@]}" --since "$SINCE" --until "$UNTIL" \
-o short-iso --no-pager \
| grep -E 'status=(bounced|deferred)|NOQUEUE: reject' \
| sed 's/^/[DELIVERY] /' > "$work/delivery.txt" || true
# 3. Merge into one timeline. short-iso puts a sortable timestamp in field 2
# (field 1 is now our [SERVICE]/[DELIVERY] tag), so sort on field 2 to
# interleave the two streams chronologically.
echo "Mail error report — ${SINCE} .. ${UNTIL} — $(hostname)"
echo "============================================================"
sort -k2,2 "$work/service.txt" "$work/delivery.txt"
svc=$(wc -l < "$work/service.txt")
dlv=$(wc -l < "$work/delivery.txt")
echo "------------------------------------------------------------"
echo "Service errors: ${svc} Delivery failures: ${dlv}"
Save it, make it executable, and run it:
chmod +x mail-error-report.sh
sudo ./mail-error-report.sh
# Or for a specific window:
sudo SINCE="6 hours ago" UNTIL="now" ./mail-error-report.sh
Why it's built this way
- The two queries are deliberately different. Service errors come from
-u <unit> -p err, which catches daemon failures, restarts, and fatals but skips routine per-message logging. Delivery failures are matched by syslog identifier and then filtered by outcome, because a bounce or deferral is logged atinfo/noticepriority — a priority filter would hide exactly what you're looking for. sort -k2,2interleaves the two streams by their ISO timestamp so the timeline reads top-to-bottom in time. This is a lexical sort, which is correct as long as all the entries share one UTC offset. If your window spans a daylight-saving change the ordering can slip by an hour at the boundary — run the journal in UTC (journalctlhonours the host's time settings) if that matters to you.|| trueon thejournalctl/greppipelines keepsset -efrom aborting the script when a window legitimately contains zero matches.
Scheduling it (optional)
The mainstream way to run this on a schedule is a systemd timer calling the script with a fixed window, or a plain cron entry, piping the output to mail for a daily digest. I'm not walking through either here — both are standard — but if you go the cron route, redirect to a file you can inspect rather than relying on cron's own mail.
Verify it worked
Because it only reads, "working" means the counts and lines match reality. Cross-check each section against the journal by hand:
# Should roughly match the "Service errors" count for the same window:
journalctl -u postfix@-.service -u dovecot.service -p err \
--since today --no-pager | wc -l
# Should roughly match the "Delivery failures" count:
journalctl -t postfix/smtp -t postfix/lmtp -t postfix/local \
-t postfix/error -t postfix/bounce --since today --no-pager \
| grep -Ec 'status=(bounced|deferred)|NOQUEUE: reject'
If the report shows zero delivery failures but you know mail has been bouncing, the usual cause is a mismatched identifier — re-run the journalctl -t postfix/smtp -n 5 check and add whatever identifier your bounces actually appear under to DELIVERY_IDS.
For the exact meaning of journalctl flags (-u, -t, -p, -o short-iso, --since), see the journalctl man page, which is the authoritative reference. For how Postfix logs delivery status (status=bounced / status=deferred) and NOQUEUE: reject, see the Postfix documentation (postconf/basic logging sections on postfix.org).
Undo
There is nothing to undo — no files are written outside the auto-removed temp directory, and no configuration or state is changed. If you scheduled it with cron or a systemd timer, removing that one entry or .timer/.service pair is the full reversal.
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.
Related guides
Email a Daily Postfix Delivery Summary with Bash
This guide sets up a small bash script that reads yesterday's Postfix log, runs it through pflogsumm to build a delivery summary (messages received/delivered/deferred/ bounced, top senders, deferral reasons), and emails that summary to you once a day from cron.
Automatically Restart a Hung Service on a Schedule, with Logging
Some services don't crash cleanly. The process stays alive, systemctl still calls it active , but it has stopped answering — a wedged worker pool, a deadlocked thread, a leaked connection table. systemd's own Restart=on-failure handles a process that exits , but it won't help you with one that's technically running and doing nothing. This guide sets up a periodic health check that catches that case, restarts the unit when it's genuinely stuck, and logs every decision so you can prove what happened at 3 a.m.
Bulk-Export Postfix Virtual Maps to an Encrypted Backup
This guide builds a small bash script that reads your Postfix virtual-table configuration, copies the source files behind each file-based map ( hash: , lmdb: , btree: , cdb: , texthash: ) plus main.cf and master.cf , bundles them into a tarball, and encrypts that tarball with GnuPG so you can store it off-box safely. The purpose is a routine, restorable snapshot of your mail routing and alias data.
Automate Cleanup of Stalled Messages in the Postfix Queue
Postfix is good at retrying mail on its own, but over time a busy server collects two kinds of junk in its spool: messages that have been deferred for days and will never deliver, and partial or misplaced files left behind by a crash or an abrupt restart. This guide sets up a safe, scheduled cleanup for both — using Postfix's own tools, not manual file surgery.




