Bali, Indonesia
Insights

Finding a Compromised Mailbox in the Postfix Queue After Moving From cPanel Exim

October 24, 2026
0:00
Your Postfix server's queue suddenly holds thousands of messages, most of them look like spam, and your fingers type exim -bp out of habit. On a plain Postfix box that command does nothing, exigrep is gone too, and there's no WHM screen to tell you which account is sending. The question you're really asking hasn't changed, though: which mailbox logged in and handed all this mail to the server? Postfix records that as clearly as Exim did. It just puts it in a different place. This is a common moment for admins who moved off cPanel. The Exim tools did a lot of correlation for you, and Postfix spreads the same job across a few small commands. Once you see how they fit together, the hunt usually takes minutes.
Start by looking at the queue itself. postqueue -p prints the same thing as mailq: one block per message with the queue ID, size, arrival time, envelope sender, and recipients. A * after the queue ID means the message is in the active queue right now. A ! means it's on hold. Everything else is waiting in the deferred queue for another delivery attempt. The last line gives the total size and message count.
BASH
# Total count and size (last line of the listing)
postqueue -p | tail -n 1

# Rank envelope senders in the queue
postqueue -p | awk '/^[0-9A-Za-z]+[*!]? / {print $7}' | sort | uniq -c | sort -rn | head

# Sender domains in the deferred queue, grouped by age (qshape may need an extra package)
qshape -s deferred | head
The sender ranking is a quick first look, but don't treat it as the answer. The envelope sender is just whatever the client claimed in MAIL FROM, and spammers using a stolen login often set it to something random or to another address on your domain. What it does tell you is whether this is one flood or a mix. If the queue is so full that Postfix starts reporting write errors, check the inode situation first, as covered in How to Check If a Stuck Mail Queue Ran Your Server Out of Inodes. Pick two or three queue IDs from the top of the flood and keep them handy. They're the thread you'll pull for the rest of the investigation.
Most of the confusion comes from looking for one tool that does what exigrep did. In Postfix, the queue ID is the join key. Every daemon that touches a message logs a line starting with that ID, so a plain grep on the mail log rebuilds the whole story.
On cPanel EximOn PostfixWhat changes
exim -bppostqueue -p or mailqSimilar listing, different column layout
exim -Mvh ID / exim -Mvb IDpostcat -q IDShows envelope, headers, and body of the queue file together
exigrep ID /var/log/exim_mainloggrep ID on the mail logNo grouping tool. The queue ID links the lines
exiqsummqshapeGroups by domain and age instead of a flat summary
A=dovecot_login:user in exim_mainlogsasl_username= on the smtpd lineSame fact, logged once when the message is accepted
exim -Mrm IDpostsuper -d IDPermanent deletion. Hold first if unsure
The last two rows are the important ones. On cPanel you probably found the culprit by grepping A=dovecot_login out of exim_mainlog. On Postfix the equivalent is sasl_username. SASL is the authentication layer Postfix uses for SMTP AUTH, usually backed by Dovecot.
Before going to the logs, look at one of your sample messages with postcat -q ID | less. You're checking that it really is spam and not a legitimate newsletter that hit a rate limit. You'll also see the envelope sender and recipients, the headers the client sent, and the Received header Postfix added when it accepted the message, which includes the client's IP. Don't expect the authenticated username in the headers. Postfix only adds an (Authenticated sender: ...) note to the Received header if smtpd_sasl_authenticated_header is enabled, and that's off by default because it exposes login names to every recipient. Run postconf smtpd_sasl_authenticated_header to see your setting. Whatever it says, the mail log is the reliable source.
When a client authenticates and submits a message, the smtpd process logs a line with the new queue ID, the client address, and the SASL details. Messages from local programs that called the sendmail binary go through a different path and show up as pickup lines instead. The two look like this:
TEXT
Illustrative output format:
postfix/submission/smtpd[2101]: 3F1A2B4C5D: client=unknown[203.0.113.50], sasl_method=LOGIN, [email protected]
postfix/cleanup[2105]: 3F1A2B4C5D: message-id=<[email protected]>
postfix/qmgr[987]: 3F1A2B4C5D: from=<[email protected]>, size=2048, nrcpt=50 (queue active)
postfix/pickup[1200]: 9E8D7C6B5A: uid=33 from=<www-data>
Notice that the first message's envelope sender ([email protected]) differs from the login ([email protected]). That gap is exactly why the queue listing alone misleads you. Grep your sample IDs, then rank every authenticated sender in the log:
BASH
# Log path depends on distro: /var/log/mail.log (Debian/Ubuntu) or /var/log/maillog (RHEL family)
grep '3F1A2B4C5D' /var/log/mail.log

# Rank authenticated users by number of accepted messages
grep -o 'sasl_username=[^ ,]*' /var/log/mail.log | sort | uniq -c | sort -rn | head

# Which client IPs logged in as the suspect user
grep '[email protected]' /var/log/mail.log | grep -o 'client=[^,]*' | sort | uniq -c | sort -rn | head
If your server logs only to journald, pipe journalctl for the Postfix unit into the same grep commands. The unit name varies by distro, so check systemctl list-units | grep postfix first. A compromised mailbox usually stands out right away: one username far ahead of the rest, often logging in from many unrelated IPs. That spread is typical when stolen credentials are handed around a botnet, and it's also a hint that blocking a single IP won't hold.
If none of your sample queue IDs has a sasl_username line and they all trace back to pickup, no mailbox was compromised. A local process submitted the mail, and the uid tells you which system user ran it. On web servers that's often the PHP user, which points to a vulnerable script or plugin. Changing mailbox passwords won't help here. You need to find and remove the script. A queue full of messages with from=<> is a different problem. The empty sender means bounces, so your server is generating non-delivery reports, often in response to inbound spam sent to addresses that don't exist. It looks like an outbound flood in mailq, but it's a recipient validation issue. Finally, ignore the From: header in the message body. It's set by the client and easy to forge. The sasl_username field is what Postfix verified against a real password, so it's the one to trust.
Containment changes live mail flow and destroys data if you rush it. Do it in this order:
  1. Reset the mailbox password wherever your SASL backend reads it (Dovecot's user database, LDAP, or SQL, depending on the setup). Then ask the owner to check their devices, since a reused or phished password will just get stolen again.
  2. Put the spam on hold instead of deleting it: feed the matching queue IDs to postsuper -h -, which reads IDs from standard input. Held mail stops retrying but stays on disk, so you can still inspect it and release anything legitimate with postsuper -H.
  3. Once you've reviewed the held messages, delete them with postsuper -d -. Deletion is permanent and can't be undone. Double-check your ID list before running it, and avoid postsuper -d ALL unless you really mean to drop every message on the server.
  4. Don't flush the queue to clear the backlog. Pushing spam out faster only damages your IP reputation further. Should You Run postqueue -f or Let Postfix Retry Deferred Mail on Its Own? covers why.
To verify, follow the log and watch for that username. New sasl_username lines for it should stop, or turn into authentication failures in the Dovecot or smtpd logs. Clients that were already logged in may keep their session until they disconnect. If you still see accepted mail from the account after the reset, block the client IPs at the firewall while their connections drain. Re-run the queue count from the first section a little later. It should be falling, and new arrivals should no longer come from the same login. The habit to carry over from cPanel is the reasoning, not the commands. Find the queue ID, follow it in the log to the smtpd line, and trust sasl_username over anything the message claims about itself. Keep the ranking one-liner somewhere easy to reach. The next time the queue spikes, it answers the key question before you've opened a single message.