The Server That Sends Spam: PHP Mailing Scripts, Mail Queues and Domain Reputation

How to recognize a server that is sending spam, find the PHP script responsible, flush Postfix and Exim queues, and recover your domain’s reputation.

A server with dozens of red envelopes flying out of it, next to a mailq panel with an almost full queue and a yellow warning sign

The first sign usually doesn’t come from the website, but from email. Quotes sent to customers land in the spam folder, a few recipients reply with an error message talking about “reputation”, the hosting provider writes that the account has been suspended for mass mailing. The site seems to work normally, yet thousands of emails that nobody in the company wrote are leaving that server.

A spam script is invisible in the browser: you see it in the mail logs, in the server queue and, too late, in the domain’s reputation.

A compromised WordPress site is a valuable tool for anyone sending spam or phishing: it has an IP address with a good history, a real domain and, often, DNS records that make the messages credible. In this article we look at how to recognize the problem, how to trace the script that is sending, how to handle the Postfix and Exim queues, what to do when SMTP credentials are being abused, and how to recover the reputation of the domain and the IP.

Symptoms of a server that sends spam

Some signs are obvious, others you only notice if you know where to look:

  • the server’s mail queue holds hundreds or thousands of messages, many in deferred status because recipients rejected them;
  • the sender’s mailbox receives many delivery failure messages (bounces) for emails never sent by real people;
  • legitimate emails, including WooCommerce transactional ones, end up in spam or are rejected with codes that mention blocklists or poor reputation;
  • server load grows without a matching increase in web traffic, with many PHP or sendmail processes active;
  • the provider reports abuse, restricts port 25 or suspends the account.

Before thinking about malware, rule out the most mundane explanation: an unprotected contact form, used by a bot to send messages through the “send me a copy” feature. The result is similar, but the fix is different (a CAPTCHA or removing that feature). If instead the messages have subjects and contents the site could never generate, such as fake bank notices, drugs or cryptocurrencies, we are almost certainly dealing with a script uploaded by an attacker.

Where the mail comes from: three routes

A malicious PHP script can send mail in three ways, and each leaves traces in different places. The first is PHP’s mail() function, which passes the message to the system’s sendmail program (provided by Postfix or Exim), from which it enters the local queue: it is the simplest route and the one that leaves the most traces. The second is an authenticated SMTP connection, to the server itself or to an external server, using stolen credentials: often those of a mailbox on the domain, read from an SMTP plugin or guessed. The third is a direct connection to port 25 of the recipient servers, which bypasses the local mail server entirely and is visible only in network connections.

The most common spam kits are PHP scripts of a few hundred lines, often obfuscated and hidden in plugin folders or in uploads, which receive the recipient list and the text from outside, perhaps through a POST request, and send them at a controlled pace. Others bundle a copy of PHPMailer and use SMTP; still others are simple webshells with a built-in sending function.

Diagram of the three routes a compromised PHP script uses to send spam. First route: mail() function, sendmail program, Postfix or Exim queue, recipient server; next to it, the traces to look for: PHP mail.log, X-PHP-Originating-Script header, pickup line with uid in the mail log. Second route: authenticated SMTP connection with stolen credentials, traced by the sasl_username field in the log. Third route: direct connection to port 25 of the recipients, visible only with ss or with outbound firewall rules.
The three routes of outgoing mail and where each leaves its traces.

Finding the script that is sending

For the mail() route PHP offers two directives made for this. mail.add_x_header adds the X-PHP-Originating-Script header to every message, with the UID and the name of the file that called mail(). mail.log writes every call to mail() to a file, with the script path, the line number and the recipients. Both are disabled by default, as the PHP documentation states, and it is best to enable them at the PHP-FPM pool level, where the site cannot change them:

; in the site's PHP-FPM pool, e.g. /etc/php/8.3/fpm/pool.d/sito1.conf
php_admin_flag[mail.add_x_header] = on
php_admin_value[mail.log] = /home/sito1/logs/php-mail.log

If the queue already holds messages, the header lets you trace the script after the fact, provided it was active when they were generated. With Postfix you read a queued message with postcat; with Exim, with exim -Mvh:

# Postfix: headers of a queued message
postcat -h -q 4F2A91C0B3 | grep -i -E '^(from|to|subject|x-php-originating-script):'

# Exim: headers of a queued message
exim -Mvh 1tXyZa-0001Ab-Cd | grep -i x-php-originating-script

Even without that header, the mail logs say a lot. Postfix records messages entered through sendmail with a line from the pickup process that reports the system user’s UID: knowing which user sent narrows the search to a single site or account. For locally generated messages, Exim on cPanel servers records the script’s working directory (cwd=), which often points directly to the folder where the malicious file lives.

# Postfix: messages entered through sendmail, grouped by UID
grep 'postfix/pickup' /var/log/mail.log | grep -o 'uid=[0-9]*' | sort | uniq -c | sort -rn
getent passwd 1004    # which user the most active UID maps to

# Exim (cPanel): directories from which the most messages were sent
grep 'cwd=' /var/log/exim_mainlog | grep -v '/var/spool' \
  | grep -o 'cwd=[^ ]*' | sort | uniq -c | sort -rn | head

# In the access logs, the PHP scripts that receive many POST requests
awk '$6 ~ /POST/ {print $7}' /var/log/nginx/access.log | cut -d'?' -f1 \
  | sort | uniq -c | sort -rn | head -20

The last command cross-references mail with web traffic: a spam script is almost always called from outside, and an unknown PHP file receiving thousands of POST requests from a handful of IP addresses is a strong candidate. On very large logs, or when requests are spread across many sources, this correlation is the kind of work where log analysis with local AI models helps us. Finally, searching the files for typical functions helps find any other scripts that are still dormant:

grep -rl --include='*.php' -E 'mail\s*\(|fsockopen|PHPMailer|smtp_(host|pass)' \
  /home/sito1/public_html/wp-content/uploads /home/sito1/public_html/wp-content/plugins \
  | xargs -r ls -la

Inspecting and flushing the mail queue

Once you have found and stopped the script, the queue is still full of messages that the server will keep trying to deliver for days, worsening its reputation with every attempt. It has to be emptied, but carefully: legitimate emails may be mixed in, such as order confirmations or replies to customers. Before deleting, put the queue on hold and count the messages by sender.

# ── Postfix ──
postqueue -p | tail -n 1                 # summary: how many messages and how many KB
postsuper -h ALL                         # puts the whole queue on hold
postqueue -p | awk '/^[0-9A-F]/ {print $7}' | sort | uniq -c | sort -rn | head

# Delete only the messages from a given sender
postqueue -p | awk 'BEGIN {RS = ""} /mittente-abusato@dominio\.it/ {print $1}' \
  | tr -d '*!' | postsuper -d -

postsuper -H ALL                         # releases what remains

# ── Exim ──
exim -bpc                                # number of messages in the queue
exim -bp | exiqsumm | sort -rn | head    # summary by recipient domain
exiqgrep -i -f 'mittente-abusato@dominio\.it' | xargs -r exim -Mrm

The postsuper -d ALL command, which deletes everything, is a last resort, to be used only after checking that there are no messages worth saving. And before deleting, keep a few sample messages with full headers: you will need them to work out where the spam came from and, later, for blocklist removal requests.

When SMTP credentials are being abused

If in the logs the spam messages don’t go through pickup but come from authenticated connections, the problem isn’t just a script on the site: someone has the password of a mailbox. They may have read it from the settings of an SMTP plugin saved in the database, from an exposed wp-config.php, or obtained it with brute-force attacks against the mail server. In Postfix the authenticated username appears in the sasl_username field; in Exim, on cPanel servers, in the A=dovecot_login: or A=dovecot_plain: field.

# Postfix: messages sent per authenticated user and per source IP
grep 'sasl_username=' /var/log/mail.log | grep -o 'sasl_username=[^ ,]*' | sort | uniq -c | sort -rn
grep 'sasl_username=info@dominio.it' /var/log/mail.log | grep -o 'client=[^,]*' | sort | uniq -c | sort -rn | head

# Exim: authenticated logins per mailbox
grep -o 'A=dovecot_[a-z]*:[^ ]*' /var/log/exim_mainlog | sort | uniq -c | sort -rn | head

A mailbox used from dozens of IP addresses in different countries within a few hours is compromised. The response is to change the password immediately, close open sessions, check the mailbox’s forwarding rules (attackers often add one to receive a copy of the mail) and verify where that password was stored: if it was in a plugin’s settings, the site’s database must also be considered compromised, and you should look for any hidden admin users.

SPF, DKIM and DMARC: what they protect and what they don’t

The three email authentication mechanisms tell receiving servers which messages truly belong to the domain. SPF lists in DNS the IP addresses authorized to send for the domain; DKIM signs each message with a private key whose public part is published in DNS; DMARC tells the recipient what to do with messages that fail SPF or DKIM aligned with the sender’s domain, and where to send reports. Since February 2024 Google requires anyone sending more than 5,000 messages a day to Gmail accounts to have all three, as stated in the sender guidelines.

# Check the published records
dig +short TXT dominio.it | grep spf1
dig +short TXT selettore._domainkey.dominio.it
dig +short TXT _dmarc.dominio.it

# Example records
dominio.it.         TXT "v=spf1 ip4:203.0.113.10 include:_spf.provider.example -all"
_dmarc.dominio.it.  TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@dominio.it; adkim=s; aspf=s"

The point to understand is that these records protect the domain from anyone sending from other servers while pretending to be it, not from a script running on the authorized server. If the malware uses mail() on the server listed in the SPF, or a mailbox with valid credentials, its messages will pass the checks, will be DKIM-signed and will directly damage the domain’s reputation. SPF, DKIM and DMARC are therefore essential, but they don’t replace cleanup. Aggregate DMARC reports, on the other hand, are an excellent diagnostic tool: they show which IPs send messages on behalf of the domain and with what outcome, and often reveal abuse before the blocklists do.

Diagram of the receiving server's checks. A message on behalf of dominio.it goes through three DNS checks: SPF checks the sending IP, DKIM checks the signature, DMARC checks alignment and applies the policy. At the top, an external server spoofing the sender is rejected with a red cross. At the bottom, a spam script on the authorized server passes all three checks with green ticks, and a yellow label warns that the reputation being hit is the domain's.
SPF, DKIM and DMARC stop anyone impersonating the domain from outside, not the script sending from the authorized server.

Blocklists and removal requests

After a spam episode, the server’s IP and sometimes the domain end up on one or more blocklists, lists that mail servers query in real time to decide whether to accept a message. The most widely used are Spamhaus, SpamCop and Barracuda; each has its own site with a lookup tool and a removal procedure. You can also run the check from the command line, by reversing the octets of the IP:

# IP 203.0.113.10 → 10.113.0.203
dig +short 10.113.0.203.zen.spamhaus.org
dig +short 10.113.0.203.bl.spamcop.net
dig +short 10.113.0.203.b.barracudacentral.org
# no answer = not listed; 127.0.0.x = listed (the code indicates the list)

One caveat: Spamhaus does not answer correctly to queries coming from public DNS resolvers such as Google’s or Cloudflare’s, and returns error codes in the 127.255.255.x range that do not mean “listed”. For a reliable result, run the query from your own resolver or through the web tool.

Only request removal after you have eliminated the cause. The procedures ask you to confirm the problem is fixed, and a delisted IP that starts sending spam again is relisted, sometimes with longer removal times. For the big mail providers there are dedicated tools: Google Postmaster Tools for reputation with Gmail, Microsoft SNDS for Outlook and Hotmail. If the site’s domain has also ended up on safe-browsing lists, the procedure is the one described in the article on removing a site from the Google blacklist.

Limiting the damage next time

No site is immune to vulnerabilities, but you can make sure a compromise doesn’t turn into thousands of emails. The most effective measures work at the server level, where the site cannot change them:

  • Sending limits per user or per domain. cPanel lets you set a maximum number of emails per hour per domain; with Postfix you use a policy service to limit messages per sender, with Exim the ratelimit rules in the ACLs. A site that normally sends twenty emails a day has no reason to be able to send a thousand in an hour.
  • Outbound port 25 blocked for site users and allowed only to the mail server, so a script cannot contact recipient servers directly.
  • mail.add_x_header and mail.log always on, to know in seconds which file sent what.
  • Transactional mail on a dedicated SMTP service, with its own limited credentials, separate from staff mailboxes; and mail() disabled in the PHP pool if the site doesn’t use it.
  • Queue monitoring, with an alert when it exceeds a threshold: it is often the earliest sign of a compromise.
# Block outbound port 25 for the site's user (iptables, owner module)
iptables -A OUTPUT -p tcp --dport 25 -m owner --uid-owner sito1 -j REJECT

# Periodic Postfix queue check: alert above 200 messages
n=$(postqueue -j | wc -l); [ "$n" -gt 200 ] && echo "Coda di posta: $n messaggi" | mail -s "Allarme coda $(hostname)" admin@dominio.it
Timeline of the response to a spam episode in seven phases: stop the sending by putting the queue on hold, find the abused script or mailbox, remove the malware and the persistence, clean the queue while saving legitimate messages, change the credentials, verify SPF, DKIM and DMARC, request removal from blocklists. A blue note under the first phases reminds you to keep some messages with full headers; a red note under the last one warns that removal should be requested only after the cause has been eliminated.
The order of the response: first stop the sending, request blocklist removal last.

A server that sends spam is almost always the symptom of a broader compromise: the sending script got in through a vulnerability, and alongside it there are usually backdoors, scheduled tasks or stolen credentials. In our engagements we treat mail as part of the system analysis: mail and access logs, queues, PHP pools and mail server configuration, in addition to the WordPress files and database. The guaranteed cleanup in 6, 12 or 24 hours per installation includes finding the script and its entry points; if your domain has started landing in spam for no apparent reason, write to us with a few sample headers.

THE NEXT STEP

Is your site
infected?

Every hour counts: the malware keeps hitting visitors and customers, and Google, your host and the blacklists won’t wait. Choose a plan, pay online and share your access: the cleanup starts right away, at a fixed price and with guaranteed times.

Request a cleanup

Takes you to the plans: cleanup within 6, 12 or 24 hours of receiving access.

MANAGED SERVER S.R.L.info@malwarecleanup.com +39 02 5656 9681

Monday – Friday · 9:30am – 7:30pm Italian time (CET)

Prefer an online form?
Contact us at managedserver.it ↗