Skip to content

Search Postfix logs by recipient or domain

A user reports a delivery problem. They do not know the exact time. They only know the recipient’s domain, not the full address. Or they say “show me everything that went to this user.” You cannot grep for one queue ID or one log line. You search broadly, pipe the output to a text file, and then face the real obstacle: the matching lines belong to many different messages, all processed concurrently, so their cleanup, qmgr, smtp, and bounce lines are interleaved by timestamp and tied together only by queue ID. Reconstructing each message’s flow means manually grouping a wall of lines by queue ID.

This guide covers the practical grep approaches, why they hit a wall with domain and user searches, and how Postfix Insights solves the reassembly problem.

The basic grep search approaches and their limits

Section titled “The basic grep search approaches and their limits”

If you know the exact address, grep for it directly:

Terminal window
grep "to=<user@example.com>" /var/log/mail.log

This finds every attempt to deliver to that address. You will see one or more log lines per message, one line per delivery attempt. If the delivery succeeded, deferred, or bounced, the line includes the status.

For recent messages, tail the output:

Terminal window
grep "to=<user@example.com>" /var/log/mail.log | tail -20

This works when you know the full address and the rough time window. The output is still noisy if the user receives hundreds of messages, because each message produces multiple log lines (one from pickup, cleanup, qmgr, and smtp or bounce). All those lines are interleaved by timestamp and you must manually group them by queue ID.

If you only have the domain, not the full address, grep for any recipient at that domain:

Terminal window
grep "to=<[^>]*@example.com>" /var/log/mail.log

This regex matches the pattern to=<anything@example.com>, where [^>]* means “any characters except >”. It finds every message delivered to any address at example.com.

Note that Postfix logs every recipient as to=<addr>. There is no distinction between to, cc, and bcc recipients in the mail log. All recipients of a message appear as to= entries, one per log line. If a message was sent to multiple recipients, you will see multiple lines with the same queue ID and different recipient addresses.

The output is much larger now. You are no longer looking at messages to one user. You are seeing all recipients at that domain, possibly hundreds or thousands of delivery attempts, all printed in timestamp order. Interleaving is severe. A single message to alice@example.com and another to bob@example.com will have their lines scrambled together by arrival time and other concurrent processing.

Real mail servers rotate logs daily or more frequently. Log files accumulate as mail.log, mail.log.1, mail.log.2.gz, mail.log.3.gz, and so on. A delivery that spans days lives across multiple files.

Use zgrep to search compressed (gzipped) files. It handles both plain and gzipped input:

Terminal window
zgrep "to=<user@example.com>" /var/log/mail.log*

This searches mail.log, mail.log.1, mail.log.2.gz, mail.log.3.gz, and any other rotated files in the same directory. The zgrep command decompresses on the fly, so you do not need to manually decompress.

If you want to use a pipe instead:

Terminal window
zcat /var/log/mail.log* | grep "to=<user@example.com>"

The output will be in file order (mail.log, then mail.log.1, then mail.log.2.gz, etc.), not timestamp order. For broad searches, this is fine. For narrow time windows, you may get chronological gaps.

If you only have an approximate time (“it was this morning” or “sometime last week”), you cannot grep a precise time range without including many unrelated messages. You end up widening the search and getting more interleaved noise.

Postfix logs do not have a structured query language. You grep and filter by text patterns. If you want to search a specific hour, you must grep for lines with that hour in the timestamp and accept that you will catch multiple messages and all their side-effect lines.

Here is the hard part. Grep returns LINES, not messages. When you search by domain or user, you are matching hundreds or thousands of lines spread across many different messages, all processed concurrently by Postfix. Each message is identified by a queue ID (a unique alphanumeric string, typically 10-16 characters). The same queue ID appears on multiple log lines: once for submission (from the pickup daemon), once for cleanup, once or more for delivery attempts (from the smtp, lmtp, or bounce daemon), and possibly more.

With a broad search, you end up with a text dump where lines from different queue IDs are interleaved by timestamp. To reconstruct one message, you collect all lines with the same queue ID, in order. With dozens of messages, you are doing this by hand, out of a plain text file, scanning for queue IDs and grouping them manually.

Here is a synthetic example. You search for all recipients at acme.io:

Terminal window
grep "to=<[^>]*@acme.io>" /var/log/mail.log

Part of the output:

Jun 17 10:15:22 mail postfix/pickup[54321]: A1234567890ABC: uid=1000 from=<alice@example.com>
Jun 17 10:15:22 mail postfix/cleanup[54322]: A1234567890ABC: message-id=<alice@example.com>
Jun 17 10:15:23 mail postfix/smtp[54323]: B5678901234DEF: to=<bob@acme.io>, relay=mail.acme.io[192.0.2.1]:25, delay=0.45, status=sent
Jun 17 10:15:24 mail postfix/qmgr[54324]: A1234567890ABC: from=<alice@example.com>, size=1024, nrcpt=1 (queue active)
Jun 17 10:15:24 mail postfix/smtp[54325]: A1234567890ABC: to=<charlie@acme.io>, relay=mail.acme.io[192.0.2.1]:25, delay=0.46, status=bounced (550 5.1.1 user unknown)
Jun 17 10:15:25 mail postfix/bounce[54326]: B5678901234DEF: to=<bob@acme.io>, relay=none, dsn=2.0.0, status=sent

You have two messages: queue IDs A1234567890ABC (to charlie@acme.io, bounced) and B5678901234DEF (to bob@acme.io, sent). Their lines are scrambled by timestamp. To understand what happened to each message, you must manually group by queue ID, then read the lines in sequence.

For a few messages this is tedious but doable. For a domain with hundreds of recipients, it is not practical.

You could automate the grouping by writing a shell script that:

  1. Extracts all unique queue IDs from the search results.
  2. Re-greps the logs for each queue ID.
  3. Sorts the lines for each queue ID by timestamp.
  4. Outputs one message block per queue ID.

Here is a sketch of how you might extract unique queue IDs from a domain search:

Terminal window
# Search for all recipients at a domain and extract unique queue IDs
grep "to=<[^>]*@example.com>" /var/log/mail.log* | grep -oE "^[^ ]+ [^ ]+ [^ ]+ [^ ]+/[^ ]+\[[0-9]+\]: [^ ]+" | grep -oE "[A-Z0-9]{10,16}:" | sort -u

This isolates the queue ID from each matching line. You could then loop over each queue ID and re-grep the full logs, accumulating the lines for each message. This works, but it is extra work, does not scale to queries with hundreds of queue IDs, and loses per-recipient nuance. You see “message A bounced to recipient C,” but you do not see what happened to the other recipients of message A if they were sent to different domains. You also do not see the DSN code or delivery status at a glance; you have to parse each log line by hand.

For a one-off diagnostic this might be acceptable. For monitoring or repeated queries, it is not.

How Postfix Insights solves the reassembly problem

Section titled “How Postfix Insights solves the reassembly problem”

Postfix Insights automates the entire workflow. You search by recipient, domain, sender, subject, or queue ID, with an optional date range. The tool:

  1. Searches local or remote (SSHFS-mounted) log files and handles rotation and gzip compression transparently. The “which file is it in” problem disappears.
  2. Parses every matching line and extracts the queue ID, recipient, delivery status (sent, bounced, deferred, unknown), and DSN code.
  3. Groups all lines for each message by queue ID.
  4. Displays per-recipient delivery records, each showing the raw log line and a formatted summary with status and DSN.
  5. Provides filter chips to show only sent, bounced, deferred, or unknown messages, so you can spot patterns fast.

A search for all recipients at example.com returns a list of unique messages (by queue ID), each with a delivery status breakdown for each recipient. You no longer reassemble by hand. Open any message and the detail panel shows the correlated record: per-recipient status, the SMTP response, the queue ID, and the raw log.

DELIVERED Jun 14, 2026, 07:46:22 PM
SubjectOrder #4471 confirmation
Frombilling@acme.io
To
a.ortiz@globex.coDelivered
ops@initech.devDelivered
Delay0.71s
SMTP response SENT (250 2.0.0 OK 1781480782 9F2A1C0A2B - gsmtp)
Technical details
Queue ID9F2A1C0A2B
Raw log
Queue-ID: 9F2A1C0A2B
Date:     Jun 14, 2026, 07:46:22 PM
From:     billing@acme.io
To:       a.ortiz@globex.co (Delivered), ops@initech.dev (Delivered)
Subject:  Order #4471 confirmation
Status:   SENT
Delay:    0.71s
Details:  SENT (250 2.0.0 OK 1781480782 9F2A1C0A2B - gsmtp)
Copy Email this
The detail panel for one searched message: per-recipient status, the SMTP response, queue ID, and the raw correlated log. Representative sample data.

For understanding the distinction between deferred and bounced messages, and what each DSN code means, see Postfix log correlation. For a workflow on decoding why a specific message bounced, see How to find why an email bounced in Postfix.

Postfix Insights handles the parsing and correlation. You focus on the diagnosis.

Visit the Quick Start guide to set up the tool, connect your logs, and run your first search. The source code and issue tracker are at https://github.com/Xodus-CO/postfix-insights.