Guide · Outbound spam on cPanel

Server sending spam: find the account and script that is sending it

The first sign is rarely the spam itself. It is a bounce storm, a queue of thousands of messages, a host abuse ticket, or the server’s IP on Spamhaus while every site looks normal. Until you know whether the sender is a stolen mailbox password, a PHP script planted in one account, or a legitimate form being abused, every fix is a guess. This guide shows how to find the sender with read-only Exim commands and the WHM and cPanel mail tools, how to stop it without destroying the evidence, and when to request delisting.

Symptom
Large mail queue, bounces, abuse reports, or the server IP on a blacklist
Usual sources
A stolen mailbox password, a planted PHP mailer, or an abused form
First step
Identify the sender from the Exim logs and queue before deleting anything
Risk
Every account on the shared IP loses delivery, then the host suspends

What is happening

Why is my server sending spam?

On a cPanel server, almost all outbound mail passes through Exim, and Exim records who handed it each message — so unlike most compromises, outbound spam leaves a detailed log. The mail was either submitted over SMTP by a client logged in as a mailbox, or handed to Exim locally by a process running as a cPanel account, usually PHP. Telling those two paths apart is most of the diagnosis.

Our WHM/cPanel response guide lists “outbound spam or a blacklisted IP” among the symptoms that point above the site: the server’s address is on a block list whether or not any site looks infected. A shared IP is shared reputation. On a server carrying hundreds of accounts — the WHM server in our incident report held 261 — one account’s spam is every account’s delivery problem.

Three sources

Compromised mailbox, PHP script or contact form: how to tell them apart

Each source leaves a different signature in /var/log/exim_mainlog and in the message headers. Identify which one you have before changing anything, because the fix for each is different.

Three paths by which mail leaves a cPanel server — an authenticated mailbox, a PHP script calling mail(), and a WordPress form using wp_mail — each with the Exim log or header signature that identifies it, plus malware connecting straight to port 25 without an Exim log line.
Figure 1 · Three ways spam leaves a cPanel server

Signatures as they typically appear in exim_mainlog and message headers on cPanel; what is logged depends on the Exim configuration and cPanel version.

A compromised mailbox password

Someone has a real mailbox’s password and sends through the server’s authenticated SMTP, often from many IP addresses. The log shows A=dovecot_login: or A=dovecot_plain: and the mailbox name. No website is involved; cleaning WordPress changes nothing.

A compromised PHP script

A mailer uploaded into an account — often in wp-content/uploads, a plugin directory, or a file named to look like core — calls PHP mail() or the local SMTP port. The log shows the account’s local user and, for mail submitted through sendmail, a cwd= path inside the account. This is a site compromise.

An abused contact form or wp_mail

Nothing is hacked in the strict sense: a form, registration page, or “send a copy to yourself” option lets bots choose the recipient or insert their text. The mail goes out through WordPress’s own wp_mail, so the access log is what gives it away.

Rarer: relay misconfiguration or direct SMTP

Exim on cPanel is not an open relay by default, but relay and trusted-host settings can be widened by mistake. Malware can also skip Exim and connect straight to remote port 25 — the Exim logs look clean while the IP keeps getting listed.

SourceWhat exim_mainlog showsWhat the headers showWhere to confirm
Compromised mailboxA=dovecot_login:user@domain, P=esmtpsa, many different H= addresses-auth_id with the mailbox name; no X-PHP-Originating-ScriptDovecot logins in /var/log/maillog; the mailbox’s password age
PHP scriptU=cpaneluser P=local, and a cwd= path inside the accountX-PHP-Originating-Script naming an unfamiliar fileThe file on disk and the access log for requests to it
Abused form or wp_mailcwd= of the document root or wp-admin, the site’s own userX-PHP-Originating-Script naming PHPMailer.phpPOST requests to the form, admin-ajax.php or registration page
Direct SMTP bypassNothing, or far less than the volume reportedNot applicable — the messages never touched EximOutbound port 25 connections in ss -tnp, owned by a hosting user

Read-only checks

Exim commands to find what is sending spam

Run these as root over SSH. Every command below only reads the queue or the log; none of them deletes, freezes, or delivers anything. Paths are the cPanel defaults — verify them against your cPanel version, and use zgrep on rotated exim_mainlog files for older days.

  1. 01

    Count the queue

    exim -bpc prints the number of messages waiting. Thousands, or a number that climbs while you watch, means something is sending now.

  2. 02

    See where the queue is going and who it is from

    exim -bp | exiqsumm summarises the queue by destination domain. For the sender side, exim -bp | awk '/^ *[0-9]+[mhd] /{print $4}' | sort | uniq -c | sort -rn | head lists the envelope senders with the most queued messages.

  3. 03

    List the message IDs for one sender

    exiqgrep -i -f 'user@example\.com' prints only the IDs of queued messages from that sender (the argument is a regular expression). exiqgrep -c -f with the same pattern gives the count.

  4. 04

    Read the headers of a sample message

    exim -Mvh <message-id> prints the spool header file: the local user and uid that submitted it, an -auth_id line naming the mailbox for authenticated SMTP, and headers such as X-PHP-Originating-Script and cPanel’s X-Get-Message-Sender-Via. exim -Mvb <message-id> shows the body.

  5. 05

    Find the directories scripts sent from

    grep -o 'cwd=[^ ]*' /var/log/exim_mainlog | sort | uniq -c | sort -rn | head -20 counts the working directory recorded for each message handed to Exim’s sendmail binary. Ignore cwd=/var/spool/exim (Exim’s own queue runs); a directory inside one account sending thousands of messages is your lead.

  6. 06

    Find the mailboxes that authenticated to send

    grep -o 'A=dovecot_[a-z]*:[^ ]*' /var/log/exim_mainlog | sort | uniq -c | sort -rn | head -20 counts messages per authenticated mailbox. For a suspect mailbox, grep -F 'A=dovecot_login:user@example.com' /var/log/exim_mainlog | grep -oE '\[[0-9a-f.:]+\]' | sort | uniq -c | sort -rn shows the client IPs it logged in from.

Without a shell

Find the spamming account in WHM and cPanel

Without SSH, the same questions can be answered from the interface. Menu names and locations change between cPanel versions — verify these against yours.

ToolWhereWhat it answers
View Mail Statistics SummaryWHM › EmailWhich domains and users sent the most mail over a period — the fastest way to spot the account that is out of line.
Mail Queue ManagerWHM › EmailWhat is queued now, from which sender, to whom. You can open a message to read its headers and body before acting on it.
Mail Delivery ReportsWHM › EmailServer-wide delivery history, searchable by sender, recipient, and result.
Track DeliverycPanel › EmailThe same history, limited to the account’s own domains — the tool for an account owner without WHM.

The script

Find which PHP script is sending mail: X-PHP-Originating-Script

When PHP’s mail.add_x_header setting is on, every message sent through mail() carries a header of the form X-PHP-Originating-Script: 1005:mailer.php — the uid of the account and the file name (not the full path) of the PHP file that called mail(). getent passwd 1005 maps the uid to an account. If the setting is off, it can be enabled per PHP version in WHM › MultiPHP INI Editor, for mail sent after the change.

An unfamiliar name, or a file in wp-content/uploads, is a planted mailer. PHPMailer.php means the message went through WordPress’s wp_mail(), driven by a form, a plugin, or injected code. The cwd= path in the Exim log is the working directory of the request — usually the directory of the requested script, not the file that sent the mail. Together they narrow the search to one account and one entry point.

Then go to the access log for the domain — on cPanel under /etc/apache2/logs/domlogs/ or /usr/local/apache/domlogs/, depending on version — and look at requests that line up with the spam timestamps: grep 'POST' <domain-log> | awk '{print $7}' | sort | uniq -c | sort -rn | head shows which URLs are receiving the most POST requests. A mailer called from many IPs, or a form endpoint hit thousands of times, confirms the source.

WordPress

wp_mail spam: contact forms, registrations and hacked WordPress sites

When the header says PHPMailer.php, the question becomes what made WordPress send.

Registration and account emails

Open registration, or WooCommerce account creation, sends a welcome email to any address a bot submits — with spam text from the name field riding along.

Forms that copy the sender

A “send me a copy” option, or a submitted field used in the headers, lets a bot choose the recipient. The form works as configured; the configuration is the problem.

Injected code calling wp_mail()

A compromised plugin, theme file, or must-use plugin can send through WordPress’s own mail function, so the header looks legitimate and the evidence comes from a malware review of the site.

Stolen SMTP plugin credentials

SMTP plugins store the mailbox password in the database. If the site was compromised, that password may be used from elsewhere — which looks like a compromised mailbox, not a script.

Containment

How to stop outbound spam without destroying the evidence

The order matters. Stop new mail leaving first, preserve what is queued, close the source, and only then clear the queue.

  1. 01

    Preserve samples and logs

    Save exim -Mvh and exim -Mvb output for several messages from each suspect sender to files outside the queue, and copy /var/log/exim_mainlog, /var/log/maillog, and the account’s access logs. It is the record you will need for the host, the blacklist, and the cleanup.

  2. 02

    Suspend or hold outgoing email for the account

    WHM can suspend or hold outgoing email for a whole cPanel account; the API calls are whmapi1 suspend_outgoing_email user=USERNAME and whmapi1 hold_outgoing_email user=USERNAME, and where the option sits in the interface varies by version. A cPanel user can do the same for one mailbox under Email Accounts › Manage. Hold keeps new messages in the queue; suspend stops them being sent. Verify both against your cPanel version before relying on them.

  3. 03

    Freeze what is already queued

    exim -Mf <message-id> freezes a message so Exim stops trying to deliver it, without deleting it. exiqgrep -i -f with the sender pattern, piped to xargs exim -Mf, freezes one sender’s messages in bulk.

  4. 04

    Close the source

    For a mailbox, change its password and check for forwarders and filters the attacker added. For a script, keep a copy, remove it, and investigate the account as compromised. For a form, add a CAPTCHA or rate limit, remove any copy-to-sender option, and close unused registration.

  5. 05

    Only then clear the queue

    With samples preserved and the source closed, remove that sender’s spam — exiqgrep -i -f with the sender pattern, piped to xargs exim -Mrm — or use Mail Queue Manager. Leave legitimate mail alone.

  6. 06

    Block the paths that bypass Exim

    WHM › Security Center › SMTP Restrictions (or CSF’s SMTP_BLOCK) stops hosting users connecting to remote mail servers directly. A per-domain hourly email limit in WHM › Tweak Settings caps the next incident.

Blacklists

Server IP blacklisted: Spamhaus, SpamCop and Barracuda delisting

Check the server’s outbound IP on each list’s own lookup page, not only on aggregator sites, and note the reason and date. Spamhaus runs several lists with different removal processes, and its lookup page explains which applies. SpamCop listings are driven by recent reports and are designed to expire on their own once the reports stop. Barracuda has its own removal request form. Large mailbox providers keep their own reputation systems and postmaster pages. SORBS, which older guides mention, has shut down.

Request removal once, after the source is closed and the queue is clear, and say briefly what was found and what was changed. Each list sets its own review time; none of them can be hurried, and a relisting after a premature request is harder to undo than the original listing.

Afterwards

What to rotate after a server has been sending spam

  • The password of every mailbox that authenticated to send spam, then every other mailbox on the same domain — mail passwords are commonly reused.
  • SMTP credentials stored in WordPress plugins and in any application config that sends mail through the server.
  • The cPanel account password, FTP and SFTP accounts, and API tokens for any account where a script was found.
  • Database passwords and WordPress salts in wp-config.php for a compromised site, since the attacker could read the file.
  • SSH keys in ~/.ssh/authorized_keys for the affected account, and root access if the spam came from a process outside any account.
  • Forwarders, filters, and autoresponders the attacker may have added to keep reading or sending mail.

Common questions

Common questions about a server sending spam

How do I find which account is sending spam on my cPanel server?

Confirm the queue is abnormal with exim -bpc, then count senders in /var/log/exim_mainlog. A=dovecot_login: entries point to a mailbox; cwd= paths and U= local users point to a script. In WHM, View Mail Statistics Summary shows the same picture without a shell.

How do I find which PHP script is sending mail?

Read a queued message’s headers with exim -Mvh. With mail.add_x_header on, X-PHP-Originating-Script gives the account uid and the file that called mail(). The cwd= path gives the directory, and the access log shows which requests triggered it.

My WordPress site is sending spam. Does that mean it is hacked?

Not always. A contact form or registration page abused by bots sends spam without any file being changed. An unfamiliar file in the X-PHP-Originating-Script header, a mailer in wp-content/uploads, or mail sent when no form was being submitted does mean the site is compromised, and it needs a full cleanup rather than a deleted file.

Should I just delete the mail queue?

Not first. Preserve a few messages’ headers and bodies and copy the logs, suspend or hold outgoing mail for the account, and close the source. Then clear the spam. Deleting first removes the evidence and does nothing about whatever is still sending.

How do I get my server IP removed from Spamhaus?

Close the source, clear the queue, and confirm no more spam is leaving. Then use the Spamhaus lookup page for the IP, which explains the listing and the removal route. Review times are set by the list.

The Exim logs look normal but the IP keeps getting listed. What now?

Look for mail that bypasses Exim: a process owned by a hosting user holding outbound connections to port 25, visible with ss -tnp 'dport = :25'. Check that SMTP Restrictions or CSF’s SMTP_BLOCK is enabled. Also confirm which IP is listed — on servers with several addresses, outbound mail may not use the IP you are checking.

Can one hacked account get the whole server blacklisted?

Yes. Outbound mail from every account usually leaves from the same IP, so one mailbox or mailer damages delivery for all of them — which is why outbound spam is a server symptom, not a site one.

Choose the response path

The containment plan should match the size of the incident.

One WordPress site sending spam

Remove the mailer and close the way it got in.

Best when the spam traces to one site — a planted script, injected code calling wp_mail(), or stolen SMTP plugin credentials — and the rest of the server is clean.