Andrew Mercer
on this page

Day-to-day Postfix operation is mostly inspecting the mail queue, occasionally manipulating it, and reading logs.

Viewing the queue

postqueue -p          # same as the traditional `mailq`
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
05AC71CC67      987 Tue Mar  9 02:47:59  [email protected]
       (host mx.example.org[203.0.113.5] said: 450 4.7.1 <mx.example.org>:
        Helo command rejected: Host not found (in reply to RCPT TO command))
                                         [email protected]

A1B2C3D4E5*    2310 Tue Mar  9 02:50:12  [email protected]
                                         [email protected]

B9C8D7E6F5!    1204 Tue Mar  9 01:10:03  [email protected]
                                         [email protected]
-- 4 Kbytes in 3 Requests.

How to read it:

  • The queue ID is the handle for everything else you do with that message.
  • A * after the ID means it's in the active queue (being delivered right now). A ! means it's on hold. No suffix means deferred.
  • The indented parenthesized text is the last delivery attempt's response. Here, a 450 4.7.1 temporary failure because the receiving server couldn't resolve this server's HELO name. That's fixed by making myhostname a real FQDN with matching DNS on your side, not by retrying harder.
postqueue -j | jq .                       # JSON, one object per message (Postfix 3.1+)
qshape deferred                           # which destinations are backing up, by age (postfix-perl-scripts on Debian)
postqueue -p | tail -1                    # just the totals
find /var/spool/postfix/deferred -type f | wc -l

Inspecting a message

postcat -vq 05AC71CC67     # full queue file: envelope, headers, body (-h headers only, -b body only)

Use this to see the actual envelope sender and recipients, which may differ from the headers (see Email Architecture).

Retrying

postqueue -f               # flush: attempt delivery of everything deferred now
postqueue -i 05AC71CC67    # retry just this one message
postqueue -s domain.tld    # retry everything for one destination (needs fast_flush_domains)

After fixing a cause (DNS, relay credentials, a firewall), flush rather than waiting for the backoff timer. Postfix retries with growing intervals between minimal_backoff_time and maximal_backoff_time.

Holding, releasing, requeueing and deleting

postsuper -h 05AC71CC67     # hold: keep it, don't try to deliver
postsuper -H 05AC71CC67     # release from hold
postsuper -r 05AC71CC67     # requeue: re-run it through cleanup/rewriting (after changing maps or relayhost)
postsuper -d 05AC71CC67     # delete one message
postsuper: 05AC71CC67: removed
postsuper: Deleted: 1 message

Each command also accepts ALL and a queue name: postsuper -h ALL deferred, postsuper -r ALL, postsuper -d ALL deferred.

postsuper -d ALL deletes every message in every queue, irreversibly. Reach for it only when you're certain the whole queue is junk, for example after a compromised account or a script loop filled it. Usually you want to delete selectively (below) or -h ALL first while you investigate.

Bulk operations by sender or recipient

postsuper -d - reads queue IDs from stdin. Combine it with the JSON output to delete exactly what you mean:

# Delete everything FROM a given sender
postqueue -j | jq -r 'select(.sender == "[email protected]") | .queue_id' | postsuper -d -

# Delete everything TO a domain that no longer exists
postqueue -j | jq -r 'select(any(.recipients[]; .address | endswith("@dead-domain.example"))) | .queue_id' | postsuper -d -

# Hold everything older than 2 days for review
postqueue -j | jq -r 'select(.arrival_time < (now - 172800)) | .queue_id' | postsuper -h -

Aliases

After editing /etc/aliases, rebuild the compiled database:

postalias /etc/aliases     # Postfix-native
newaliases                 # Sendmail-compatible spelling of the same thing

For any other map (virtual, transport, generic, sasl_passwd, tls_policy), use postmap <file>, then postfix reload if the map is used by a long-running daemon.

Useful one-liners

postconf -n                               # the config you actually have, minus defaults
postfix check                             # config and permission sanity check
postfix reload                            # apply changes gracefully
postmap -q [email protected] hash:/etc/postfix/virtual    # what would this lookup return?
sendmail -bv [email protected]              # how would this address be delivered? (report mailed to you)
postfix flush                             # alias for postqueue -f
postfix status

Auto-reply and other pipe services

An address can be routed through a transport_maps entry to a pipe(8) service defined in master.cf, which hands each message to a script. Virtual Domains, Mailing Lists and Virtual Mailboxes has a complete, loop-safe auto-reply example.

Logs

Postfix logs through syslog by default, under the mail facility, with program names like postfix/smtpd, postfix/qmgr and postfix/smtp:

Setup Where to look
RHEL / Rocky / Alma (rsyslog) /var/log/maillog
Debian / Ubuntu with rsyslog /var/log/mail.log (and mail.err)
Systems with only journald (recent Debian minimal installs, containers) journalctl -u postfix or journalctl -t postfix/smtp, etc.
maillog_file configured That file. Postfix writes it itself via postlogd (3.4+)

Postfix 3.4+ can log without syslog, which is very handy in containers (log to stdout) and on journald-only hosts:

maillog_file = /var/log/postfix.log       # or /dev/stdout in a container
# master.cf: must be present (it is on current installs)
postlog   unix-dgram n  -       n       -       1       postlogd

To rotate it, either run postfix logrotate from cron or a timer (it renames the file using maillog_file_rotate_suffix and reopens it), or use logrotate with copytruncate.

Live-tailing while testing:

tail -F /var/log/maillog                  # -F keeps following across log rotation; -f doesn't
journalctl -f -t postfix/smtpd -t postfix/smtp -t postfix/qmgr -t postfix/cleanup

Follow one message end to end by its queue ID:

grep 05AC71CC67 /var/log/maillog
journalctl --since today | grep 05AC71CC67

pflogsumm (package pflogsumm) produces a daily summary report of volume, rejects, deferrals and top senders and recipients from these logs. It's worth emailing to yourself from cron.

See Troubleshooting for systematically scanning the logs for problems.