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”Search by full recipient address
Section titled “Search by full recipient address”If you know the exact address, grep for it directly:
grep "to=<user@example.com>" /var/log/mail.logThis 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:
grep "to=<user@example.com>" /var/log/mail.log | tail -20This 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.
Search by domain only
Section titled “Search by domain only”If you only have the domain, not the full address, grep for any recipient at that domain:
grep "to=<[^>]*@example.com>" /var/log/mail.logThis 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.
Searching across rotated and gzipped logs
Section titled “Searching across rotated and gzipped logs”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:
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:
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.
Fuzzy time windows
Section titled “Fuzzy time windows”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.
The core obstacle: lines, not messages
Section titled “The core obstacle: lines, not messages”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:
grep "to=<[^>]*@acme.io>" /var/log/mail.logPart 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=sentJun 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=sentYou 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.
The manual approach: extract and re-grep
Section titled “The manual approach: extract and re-grep”You could automate the grouping by writing a shell script that:
- Extracts all unique queue IDs from the search results.
- Re-greps the logs for each queue ID.
- Sorts the lines for each queue ID by timestamp.
- Outputs one message block per queue ID.
Here is a sketch of how you might extract unique queue IDs from a domain search:
# Search for all recipients at a domain and extract unique queue IDsgrep "to=<[^>]*@example.com>" /var/log/mail.log* | grep -oE "^[^ ]+ [^ ]+ [^ ]+ [^ ]+/[^ ]+\[[0-9]+\]: [^ ]+" | grep -oE "[A-Z0-9]{10,16}:" | sort -uThis 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:
- Searches local or remote (SSHFS-mounted) log files and handles rotation and gzip compression transparently. The “which file is it in” problem disappears.
- Parses every matching line and extracts the queue ID, recipient, delivery status (sent, bounced, deferred, unknown), and DSN code.
- Groups all lines for each message by queue ID.
- Displays per-recipient delivery records, each showing the raw log line and a formatted summary with status and DSN.
- 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.
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)
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.
Get started
Section titled “Get started”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.
References
Section titled “References”- Postfix documentation. https://www.postfix.org/documentation.html
- RFC 3463: SMTP Enhanced Status Codes. https://www.rfc-editor.org/rfc/rfc3463
- RFC 5321: Simple Mail Transfer Protocol. https://www.rfc-editor.org/rfc/rfc5321