postsuper -d ALL only provides a few minutes of peace before new messages flood in again. The real problem is not the mail queue itself, but a compromised web application or malicious script running locally and submitting mail through the local Postfix daemon.
On shared hosting servers and multi-tenant virtual private servers, identifying the exact script responsible for outbound spam comes down to choosing between three diagnostic methods. Depending on whether the queue is still actively filling, whether your web applications execute as separate system users, and how much performance overhead your server can handle, each approach offers a different balance of speed and depth.
The Three Ways to Trace Local Mail Injection
mail() function, which hands the message off to the local /usr/sbin/sendmail binary. Postfix's sendmail wrapper then writes the message into the local maildrop or incoming queue.
Because this handoff happens entirely inside the operating system, remote authentication logs (/var/log/maillog or /var/log/mail.log) rarely show SASL login events. To find the source file, you must inspect the transaction using one of three methods: direct envelope inspection of messages already sitting in the Postfix queue, PHP-level execution logging, or a custom wrapper script placed around the system sendmail binary.
Approach 1: Postfix Queue and Header Inspection (postcat)
mailq and running postcat -q <QueueID>, you read the raw spool file, including the internal Postfix envelope records, client attributes, and RFC 822 headers.
- When to choose this: The spamming is actively occurring, the queue holds unpurged messages, and you cannot afford to restart PHP or modify server configuration files.
- What it tells you: Postfix envelope records record the Unix user ID (UID) of the process that called the sendmail binary. If your web server runs via PHP-FPM pools where each website has its own Linux user (such as
user_client1), this UID immediately isolates the compromised account. - Where it falls short: If the entire server runs PHP under a shared web server user (such as
www-data,nobody, orapache), the envelope will only show that shared UID. Furthermore, if you or an automated cleanup script already purged the queue,postcathas nothing left to read.
Approach 2: PHP Native Mail Auditing (mail.add_x_header and mail.log)
mail.add_x_header = On forces PHP to inject an X-PHP-Originating-Script header into every generated message, showing the UID and the exact PHP file path (such as 1001:upload_handler.php). Complementing this with mail.log = /var/log/php_mail.log records the timestamp, filename, and line number of every mail() call without waiting for messages to be inspected in the mail queue.
- When to choose this: You manage the PHP configuration, all or most websites run via PHP, and you want persistent auditing that survives queue purges and captures spam before external hosts reject it.
- What it tells you: The exact script path on disk and the line number where
mail()was invoked, regardless of whether all sites share a single system user. - Where it falls short: It only catches scripts that use the PHP
mail()function. If an attacker uploads a compiled binary, runs a Perl/Python script, or uses an application that connects directly tolocalhost:25via sockets, PHP's internal logging will never see it. Modifyingphp.inior pool files also requires a reload of PHP-FPM or Apache.
Approach 3: Sendmail Wrapper Interception
/usr/sbin/sendmail binary is renamed (for instance, to sendmail.real), and a small shell or Perl script is placed at /usr/sbin/sendmail. Whenever any program on the server tries to send mail, the wrapper logs the current working directory (pwd), the caller's process tree, the date, and the standard input, before piping the message along to the real binary.
- When to choose this: You have an elusive compromise where attackers bypass PHP logging, multiple runtimes (Node.js, Python, Perl) are installed, or your PHP stack is locked down and cannot be easily modified.
- What it tells you: The absolute working directory of any process invoking the system mailer, catching every language and non-PHP CGI script.
- Where it falls short: It introduces process overhead for every outbound message. If a spam loop sends 500 emails per minute, spawning shell wrappers can cause CPU spikes. In addition, package managers (like
aptordnf) can overwrite your custom wrapper during routine Postfix updates, breaking logging or delivery.
Comparing the Diagnostic Approaches
| Criteria | Queue Inspection (postcat) | PHP Native Auditing | Sendmail Wrapper |
|---|---|---|---|
| Setup Required | None (instant) | Low (edit php.ini & reload) | Moderate (rename binary, write script) |
| Runtime Overhead | Zero | Negligible | Low to Moderate (spawns extra subshell) |
| Shared-User Resolution | Poor (shows UID only) | High (shows exact script file) | High (shows current directory & process) |
| Non-PHP Mail Visibility | High (captures any sendmail call) | None | High (captures all local calls) |
| Durability After Queue Flush | None | High (persists in dedicated log) | High (persists in dedicated log) |
Practical Execution: Extracting Origin Data
BASH
# Find the top sender addresses and queue IDs currently waiting
mailq | head -n 20
# Inspect the envelope and headers of a specific queue entry (example ID: 4Tz9sK1Q3rz8wB)
postcat -q 4Tz9sK1Q3rz8wB | grep -E -i '^(named_attribute: log_ident|X-PHP-Originating-Script|From:|User-Agent:)'postcat, look closely for two fields:
named_attribute: log_ident: On many Postfix setups, this records the system user who executed the local sendmail command. If you seelog_ident=siteuser4, you immediately know which document root contains the compromised software.X-PHP-Originating-Script: If PHP mail headers were previously enabled on the system, this line will immediately state the script name (for example,1004:class-smtp.php(326) : eval()'d code).
apache or www-data user with no script header, queue inspection has given you all the information it can. At that point, you should switch immediately to PHP auditing by appending mail.log = /var/log/php_mail.log and mail.add_x_header = On to your global php.ini or target PHP-FPM pool configuration, followed by a graceful service reload.
Verifying Access Logs After Finding the Script
.php file in /wp-content/uploads/ or an injected snippet inside a theme template), do not stop at simply deleting the file. Web shells and spam injection scripts are usually the symptom of an unpatched plugin, weak credentials, or an outdated CMS, not the initial entry point.
Take the path of the identified file and cross-reference its creation timestamp with your web server's access logs. Look for incoming POST requests occurring at the same time the spam queue began swelling. Keep in mind that a malicious request does not necessarily trigger an error; as detailed in Why HTTP 200 in Access Logs Does Not Mean an Exploit Succeeded, attackers often leverage vulnerabilities that return clean HTTP 200 responses while executing payload deliveries in the background.
Matching the outbound queue surge with incoming HTTP requests confirms whether the script was uploaded through a web vulnerability or executed via an authorized shell session.
Closing the Loop on Outbound Queue Spikes
postcat on existing queue items to establish whether the mail was submitted via an authenticated SMTP account or a local process UID. If the server runs a multi-tenant environment under a single web server user, rely on PHP native mail logging directives rather than guessing which site is at fault. Once the file path is confirmed, remove the offending script, patch the underlying web application, and only then purge the backlog of deferred spam messages to allow legitimate mail to clear.