DEV Community

Cover image for WordPress form triage: missing mail, spam-folder messages, and bot submissions
AumCreate
AumCreate

Posted on

WordPress form triage: missing mail, spam-folder messages, and bot submissions

A visitor says the form did nothing. The form plugin shows no entry. The inbox is empty, but there is one message in the spam folder from a different address. The access log shows POST requests to the form endpoint from a handful of IPs. Those are separate failures. Changing the form settings before separating them usually removes the evidence you need.

Collect the basics before you touch settings: the exact submission time with timezone, the recipient address, the page URL, and whether the form plugin shows an entry. Ask the recipient to forward the message as an attachment if they can. Do not ask them to submit repeatedly.

What you see What it points at Where to look
No entry and no email mail path or form handler mail log, wp_mail test, PHP error log
Entry exists, email in spam mailbox filter or authentication message headers, SPF/DKIM/DMARC
Many entries and few real leads bot submissions access log, entry metadata
Email exists, entry missing storage or retention plugin entries, cron, database, backups

Check the mail path before you touch anti-spam

Run a mail test from the WordPress root:

cd /path/to/wordpress
wp eval 'var_dump( wp_mail( "you@example.com", "Mail test", "Test body" ) );'
Enter fullscreen mode Exit fullscreen mode

true means WordPress handed the message to the configured mail path. It does not mean the recipient's provider accepted it.

Log failures instead of guessing. Save this as wp-content/mu-plugins/mail-failures.php and create the directory if it does not exist:

<?php
add_action( 'wp_mail_failed', function ( $error ) {
    error_log( 'wp_mail_failed: ' . print_r( $error->get_error_messages(), true ) );
} );
Enter fullscreen mode Exit fullscreen mode

Then check the PHP error log. On a server with WP_DEBUG_LOG enabled, that is often wp-content/debug.log. If it is elsewhere, ask the host.

Mail logs tell you whether the server tried to send:

sudo tail -n 200 /var/log/mail.log
sudo grep -i 'recipient@example.com' /var/log/mail.log | tail -n 20
sudo journalctl -u postfix --since "30 minutes ago" | tail -n 100
Enter fullscreen mode Exit fullscreen mode

Paths depend on the server. On cPanel, /var/log/exim_mainlog is common. If the site sends through a remote SMTP service, the local mail log may show nothing; check that service's activity log.

Check the domain authentication records:

dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short TXT SELECTOR._domainkey.example.com
Enter fullscreen mode Exit fullscreen mode

SELECTOR is the DKIM selector from the DNS record your mail provider gave you. Do not guess it.

When a message arrives in spam, read the headers:

grep -i -E '^(Authentication-Results|Received-SPF|DKIM-Signature|From|Reply-To|Return-Path):' message.eml
grep -i -E '^(X-Spam-Status|X-Spam-Score|X-Spam-Report):' message.eml
Enter fullscreen mode Exit fullscreen mode

Look at whether the From domain is the domain you authenticated. A common failure is a form that uses the visitor's address as From. Use your own domain in From and put the visitor's address in Reply-To. That keeps SPF and DKIM aligned with the domain you control. Ask the recipient to mark the message not spam as a diagnostic, but do not treat that as the fix. Authentication and content are the parts you can change.

A spam-folder message and a bot POST need different evidence

A message in the spam folder has a mail filter decision attached to it. A bot submission is a POST request. They can happen on the same day and have different fixes.

Find the form requests in the access log:

sudo grep -E 'POST /(wp-json|wp-admin/admin-ajax[.]php)' /var/log/nginx/access.log | tail -n 20
sudo grep -E 'POST /(wp-json|wp-admin/admin-ajax[.]php)' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 20
Enter fullscreen mode Exit fullscreen mode

Adjust the path for Apache or a container. The first command shows the raw requests. The second groups them by IP. If the log format is different, group by the field that holds the client address.

Export a few recent entries from the form plugin and compare the submission time, referer, user agent, and message shape. Identical messages from unrelated IPs, a missing referer, and near-zero time-to-submit are useful signals. They are not proof.

Add controls in layers. A honeypot is a hidden field that should stay empty; validate it on the server, not with JavaScript only. A time trap rejects submissions that arrive faster than a person can fill the form. Rate limit per IP and per email address. Add a challenge only after those, and watch legitimate replies after you do.

Do not block an entire country or disable the REST API for all visitors to stop form spam. Both break normal site behavior.

Confirm whether the form stores entries at all

If the plugin has no entries screen or the entry list is empty, check its settings for an entry-storage option. Many forms are email-only. If this one is, the storage branch is not a bug; the missing submission is a mail or spam-filter problem.

List candidate tables:

wp db query "SHOW TABLES LIKE '%form%';"
wp db query "SHOW TABLES LIKE '%entry%';"
wp db query "SHOW TABLES LIKE '%submission%';"
Enter fullscreen mode Exit fullscreen mode

Inspect the table that looks like the form log:

wp db query "SHOW COLUMNS FROM wp_your_form_table;"
wp db query "SELECT * FROM wp_your_form_table ORDER BY 1 DESC LIMIT 5;"
Enter fullscreen mode Exit fullscreen mode

Replace the table name. If the primary key is not the first column, order by the date or ID column. Do not run SELECT * on a large table.

Check whether a cleanup job removed entries:

wp cron event list --fields=hook,next_run_relative,recurrence --format=table
Enter fullscreen mode Exit fullscreen mode

Look for a cleanup hook and check the form plugin's retention setting. If a retention job removed old entries, the mailbox may be the only copy.

Check uploaded files and database health:

wp db query "SELECT ID, post_date, post_title FROM wp_posts WHERE post_type='attachment' ORDER BY ID DESC LIMIT 20;"
wp db query "CHECK TABLE wp_posts;"
df -h
df -i
tail -n 100 wp-content/debug.log
Enter fullscreen mode Exit fullscreen mode

If the POST returned a success status but no row was written, look at the plugin's storage setting and the PHP error log. If the table exists and rows disappeared, check retention and backups before adding more logging.

If binary logging is on, ask the host for a copy of the relevant segment:

wp db query "SHOW BINARY LOGS;"
Enter fullscreen mode Exit fullscreen mode

Do not restore a binlog or a backup over the live database. Work on a copy.

If email exists and the entry does not, export the mailbox as the record. If neither exists, the access log can tell you whether the request reached WordPress; it cannot tell you what the form handler did with it. That is when you add temporary logging around the form handler, not more anti-spam.

If you need this wired into a custom form flow with storage and a CRM handoff, that is the kind of work I do at AumCreate: custom development.

Top comments (0)