Is my hacked WordPress site sending spam?

Maybe, and your email log can’t rule it out. The log plugin and your SMTP provider’s dashboard only see the mail WordPress sends. A spam script dropped onto a hacked site doesn’t have to go through WordPress at all: it can hand its messages straight to the mail software on the server. That mail never reaches the log or the provider, so both look clean while the server’s IP collects complaints. To find out, look where the mail actually goes: your host’s mail logs, and a PHP setting that records every script that sends.

Why the log and the provider dashboard can miss it

Log plugins and mailer plugins both sit inside wp_mail(), the one function WordPress uses to send. A script that skips that function skips them both.

In the current core source, wp_mail() fires its hooks in order: the wp_mail filter; pre_wp_mail, which lets a plugin take over; phpmailer_init, where mailer plugins point PHPMailer (the library that builds the message) at their SMTP server; then wp_mail_succeeded or wp_mail_failed. Log plugins attach to those hooks: WP Mail Logging 1.17.1 and Check & Log Email 2.0.16 both record from the wp_mail filter and wp_mail_failed. Mailer plugins reroute inside the same function: WP Mail SMTP 4.10.3 at phpmailer_init (its email log is Pro-only), FluentSMTP 2.4.1 by replacing wp_mail() with a copy that fires the same hooks.

PHP also has its own mail() function, which passes a message to the server’s mail software (sendmail, Exim or Postfix) with no WordPress code involved. Sucuri’s signature for a PHP spambot shows the pattern: a loop that takes a recipient list from a web request and calls mail() once per address. A script can also skip mail() and open its own connection to a remote mail server. Either way, the log has nothing to record and the provider never sees it.

In the reverse case, malware that calls wp_mail() goes out through your configured mailer and appears in the log and in the provider’s activity. If that is what you find, rotate the mail credentials and read the provider’s logs.

Where the evidence is

  • Bounces and replies to messages you never sent: delivery failures, out-of-office replies, angry answers.
  • Blocklists. Look up the server’s IP address and your domain on Spamhaus’s IP and Domain Reputation Checker. A shared-hosting IP can be listed for another customer’s spam.
  • Reports on your domain. If the spam claims to come from your domain and the domain publishes a DMARC (Domain-based Message Authentication, Reporting, and Conformance) is a DNS TXT record that tells receivers what to do with mail from your domain that fails SPF or DKIM, and where to send reports about those failures. Read full reference → record with a rua= address, aggregate reports list the IP addresses that reporting mail services saw sending as you, with volume and SPF (Sender Policy Framework) is a DNS TXT record that lists the servers allowed to send mail using your domain. Receivers compare the sending IP to that list and use the result as one input to their accept/reject decision. Read full reference → and DKIM (DomainKeys Identified Mail) is a cryptographic signature attached to each outgoing message, verifiable against a public key published in DNS. It lets receivers confirm the message was not altered in transit and originated from a server the domain authorised to sign on its behalf. Read full reference → results. Your web server’s IP sending mail you can’t account for is evidence (parsing the reports). Spam under someone else’s address never appears there.
  • The host. An abuse notice or a hit sending limit can be the first sign. On cPanel, Email > Track Delivery lists deliveries and failures from the account: the 250 most recent by default, kept as long as the host configures. With root access (a VPS or dedicated server), or by asking the host, the Exim log can show the directory a sending script ran in, as a cwd= field. Exim writes it when it logs its command-line arguments, which Exim doesn’t do by default; GoDaddy’s cPanel servers do, so check the host has it on. GoDaddy’s help page gives this command to rank those directories by count:
grep cwd /var/log/exim_mainlog | grep -v /var/spool | awk -F"cwd=" '{print $2}' | awk '{print $1}' | sort | uniq -c | sort -rn

A folder under wp-content/uploads near the top of that list is where to look: uploads holds media, not code that sends mail.

PHP’s mail log names the script

Two php.ini settings record who calls mail(). mail.log names a file that logs every call with the full path of the calling script, the line number, the To address and the headers. mail.add_x_header adds an X-PHP-Originating-Script header (user ID and filename) to each message. Both can be set per directory: in .user.ini when PHP runs as CGI/FastCGI (PHP-FPM included), in .htaccess when it runs as an Apache module. Check with the host which applies. The log file has to be writable by the user PHP runs as. A running script can’t turn either setting off with ini_set(), but a script that can write files can drop its own .user.ini or .htaccess beside itself, so where the host allows, set mail.log in the main php.ini or the PHP-FPM pool config instead.

The log records only from the moment it is switched on, so it catches a sender that is still active; last week’s spam is in the Exim log and Track Delivery.

On 22 October 2026 we tested it on PHP 8.3.6 with the PHPMailer 7.1.1 bundled with the WordPress 7.2 beta, loaded on its own (not in a WordPress install), sendmail stubbed so nothing was sent. PHPMailer’s mail() path logged PHPMailer.php as the caller; a bare mail() call logged the test script:

[22-Oct-2026 07:11:34 UTC] mail() on [/tmp/mail-test/direct-mail.php:3]: To: [email protected] -- Headers: From: [email protected] -- Subject: Test subject

With an SMTP or API mailer, WordPress never calls mail(), so every line is suspect. With no mailer plugin, WordPress’s own sends log as wp-includes/PHPMailer/PHPMailer.php; any other caller is the one to chase.

Spam that starts again after a clean-up

Spam that resumes after a clean-up means the clean-up missed something, such as a backdoor, a scheduled job that fetches the script again, or a malicious plugin uploaded a second time. The script path in the mail log, or the cwd= directory in Exim’s, points at the file that was missed; hand it to whoever does the clean-up, starting from WordPress.org’s hacked-site FAQ.

Stopping it while the site is cleaned

Two host-level controls narrow the paths around WordPress. Neither closes them completely.

Disable mail(). Adding mail to disable_functions stops the direct path. It must be set in php.ini or the PHP-FPM pool config, not per directory, so on shared hosting it’s a request to the host; the PHP manual also warns it can be circumvented. Only do it if all legitimate mail goes through an SMTP or API mailer. Anything that ends at mail() breaks: WordPress with no mailer plugin, a mailer left on its PHP default (WP Mail SMTP’s “Default (none)”), or a plugin that calls mail() itself.

On PHP 8 that breakage is a fatal error, not a failed send. Since PHP 8.0 a disabled function counts as nonexistent; in our test setup, a PHPMailer send on that path ended in an uncaught “Call to undefined function mail()” error. Core’s wp_mail() catches only PHPMailer’s own exception type, so the error ends the request (unless the calling code catches it) and wp_mail_failed never fires. WP Mail Logging, which saves its entry at the wp_mail filter and adds errors from wp_mail_failed, shows the message with no error.

Restrict outbound SMTP from the web user. On cPanel servers, WHM’s SMTP Restrictions lets only the mail server, Mailman and root connect out on ports 25, 465 and 587. Per cPanel’s support article, anyone else’s connection on those ports is redirected to the server’s own Exim, not refused, so a script that talks SMTP itself lands in the local mail log and under the host’s sending limits. Other ports are untouched: an SMTP client on 2525, plugin or malware, connects out as before. A mailer plugin on 587 is redirected too, and probably fails against your own server’s certificate or login (we haven’t tested that). It’s a root setting, so on shared hosting it’s the host’s switch.

With mail() disabled, SMTP and API mailers keep working. With both controls on, HTTPS API mailers keep working, and SMTP works only on ports outside 25, 465 and 587.

Neither control stops malware that calls wp_mail() through your own mailer. That case runs on your provider credentials: revoke them now, and issue new ones once the site is clean.