You submit your own contact form and nothing arrives. A customer says they never got their order confirmation. Someone clicks “Lost your password?” and sees “The email could not be sent. Possible reason: your host may have disabled the mail() function.” Or worse, WordPress says everything worked and the message simply vanishes. These are all the same problem wearing different clothes: WordPress handed a message to something, and that something either refused it, could not deliver it, or delivered it to a receiver that threw it away.
This guide is for anyone whose WordPress site has stopped sending, or never reliably sent, its own email. It explains how WordPress sends mail, why the default method fails so often, and how to replace it with authenticated SMTP. Spam placement after a DNS change is covered in FG-06, and decoding bounces in FG-43.
What is happening
Every email WordPress sends goes through one function: wp_mail(). Core uses it for password resets and admin notices, and almost every form plugin and WooCommerce use it too. Inside, wp_mail() builds the message with PHPMailer, a library bundled with WordPress in wp-includes/PHPMailer/.
By default PHPMailer runs in “mail” mode: it calls PHP’s mail(), which passes the message to the program in the sendmail_path setting, usually the server’s local mail agent (Exim or Postfix), which then delivers to the recipient’s server. Nothing in that chain logs in anywhere. The server simply claims “this is from you@example.com” and hopes the receiver believes it.
Today receivers check SPF, DKIM and DMARC. A web server sending as your Google Workspace or Microsoft 365 domain fails those checks unless you have specifically authorized it.
Two details make PHP mail() worse than it looks:
- The envelope sender. Email has two sender addresses. The one you see is the From header. The one servers use for SPF and bounces is the envelope sender (MAIL FROM, also called Return-Path). WordPress calls PHPMailer’s
setFrom() with automatic envelope sender turned off, so on many servers the envelope sender ends up as something like SITEUSER@SERVER-HOSTNAME. SPF then checks the server’s hostname, not your domain, and DMARC alignment fails.
- The default From address. If nothing overrides it, WordPress sends from
wordpress@ plus your site domain (with any leading www removed). If that mailbox does not exist, or your domain’s mail is hosted elsewhere and the web server is not in your SPF record, receivers have every reason to reject it.
wp_mail() returns true when PHPMailer successfully hands the message to the next hop. It does not mean the message was delivered. That is why “it says sent but nobody got it” is so common.
| Sending method |
Authenticates? |
Typical result today |
| PHP mail() via local MTA |
No |
Works only if the host has configured SPF, DKIM and a proper envelope sender for your domain |
| SMTP to your mailbox provider (Google Workspace, Microsoft 365, your host’s mail server) |
Yes, username and password or OAuth |
Reliable for low volume, aligned with your existing SPF and DKIM |
| Transactional provider (SMTP or API) |
Yes, API key |
Most reliable for stores and busy forms, with logs and bounce handling |
Before you start
You need a WordPress administrator login, SSH access for the command line steps, and access to your DNS provider and your mailbox provider’s admin console. Changing mail settings is low risk, but take a backup first and write down your current From address and form notification settings.
How to run commands. Server commands run in an SSH terminal from the site folder, the one that contains wp-config.php. The WordPress command line tool is called wp on most hosts and wp-cli on some managed platforms; if wp --info says “command not found”, try wp-cli --info. Network tests such as dig, openssl and nc run on your own computer: they are built in on macOS and Linux, and on Windows you can use WSL or the PowerShell equivalents shown below.
Step 1: Confirm what is actually failing
Before you change anything, find out which of three situations you are in: WordPress cannot hand the message off at all, it hands it off but delivery fails, or delivery succeeds and the message is filtered.
Run a test from the command line
This command runs on the server over SSH, from the site folder:
wp eval 'add_action( "wp_mail_failed", function( $e ) { print_r( $e->get_error_messages() ); } ); var_dump( wp_mail( "YOUR_ADDRESS@example.net", "WDS mail test", "Sent at " . gmdate( "c" ) ) );'
Replace YOUR_ADDRESS@example.net with a mailbox you can check, ideally one at a different provider from your domain (a personal Gmail or Outlook.com address works well). wp eval loads WordPress and runs the PHP you give it. The add_action part hooks wp_mail_failed, the action WordPress fires with a WP_Error when PHPMailer throws an exception, so the real error is printed instead of hidden. gmdate( "c" ) stamps the body with a UTC time so you can match it in logs.
- Good output:
bool(true) and the message arrives in the inbox within a minute or two.
- Bad output, hand-off failed:
bool(false) plus an array of errors such as Could not instantiate mail function. (PHP mail() is disabled or sendmail is missing), SMTP connect() failed (a plugin is set to SMTP but cannot reach the server) or SMTP Error: Could not authenticate. (wrong credentials or authentication disabled).
- Bad output, silent loss:
bool(true) but nothing arrives, not even in spam. The message left WordPress and died later. Go to the mail log check below, and read FG-43 when a bounce appears.
Check whether mail() is available
Also on the server:
wp eval 'echo ini_get( "sendmail_path" ), PHP_EOL, ( function_exists( "mail" ) ? "mail() exists" : "mail() missing" ), PHP_EOL, ini_get( "disable_functions" ), PHP_EOL;'
No placeholders. The output shows the sendmail program PHP calls (for example /usr/sbin/sendmail -t -i), whether mail() exists, and the functions disabled in php.ini. If mail is disabled or the sendmail path is empty, PHP mail cannot work here and you must use SMTP.
Look at the server mail log if you can
On a VPS, search the MTA log for your test recipient: /var/log/exim_mainlog for Exim, /var/log/mail.log or /var/log/maillog for Postfix. In Postfix, status=sent (250 ...) means the receiver accepted the message and status=bounced means it refused; in Exim, => is a delivery and ** is a failure. The text after the code says why. On shared hosting you usually cannot read these logs, so ask your host to look up the message by the UTC time in your test.
Step 2: Fix the From address everywhere
A very common cause of failing WordPress mail is a From address the sending system is not allowed to use, such as the visitor’s own address or a domain whose mail lives elsewhere.
From versus Reply-To in form plugins
Never put the visitor’s address in a notification’s From field. Your server may not send as someone@gmail.com, and Gmail’s DMARC policy tells receivers to treat it as forgery. Instead:
- Set From to a fixed address on your own domain, such as
website@example.com.
- Set Reply-To to the visitor’s email field, so pressing Reply in your mail app still answers the visitor.
In Contact Form 7 this lives on the form’s Mail tab. A correct setup looks like this (a file you edit, in the plugin’s form settings):
From: [your-name] <website@example.com>
Additional headers: Reply-To: [your-email]
[your-name] and [your-email] are Contact Form 7 mail tags; use whatever tag names your form actually defines. website@example.com stands for a real address on your domain. Contact Form 7 shows a configuration warning on the Mail tab if the From address does not belong to the site’s domain, which is a useful check. Gravity Forms, WPForms, Fluent Forms and Formidable all have separate From Email and Reply-To fields in each notification; apply the same rule.
WordPress and WooCommerce defaults
WooCommerce sets its own sender under WooCommerce, Settings, Emails, in the “Email sender options” section. Make sure that From address is on your domain and really exists. Also check Settings, General, Administration Email Address: if it points at an old freelancer’s address or a domain you no longer own, admin notices go nowhere useful.
Step 3: Send through authenticated SMTP
The durable fix is to stop using PHP mail() and make WordPress log in to a real mail server, exactly like a desktop mail app does. An SMTP plugin replaces PHPMailer’s “mail” mode with SMTP mode, using the phpmailer_init hook.
Choose where mail goes
- Your mailbox provider. If your domain’s mail is on Google Workspace, Microsoft 365 or your host’s mail service, sending through it means your existing SPF and DKIM already cover the mail. Good for brochure sites and low-volume forms.
- A transactional email provider. Services such as Postmark, Mailgun, SendGrid, Amazon SES, Brevo or SMTP2GO are built for application mail. They give you delivery logs, bounce handling and DKIM keys for your domain. Choose this for WooCommerce stores, membership sites and anything that sends more than a handful of messages a day.
Set up the plugin
WP Mail SMTP and FluentSMTP are the two widely used free options. The generic steps are the same in both:
- Install one SMTP plugin only; two fighting over
phpmailer_init cause confusing results.
- Set the From Email to an address on your domain and check the option to force it, so plugins cannot override it with a visitor’s address. Set the From Name.
- Pick the mailer. For a transactional provider, use its built-in integration with an API key where available. Otherwise choose generic SMTP.
- For generic SMTP, enter the host, port, encryption and credentials (see the port table below).
- Use the plugin’s test email screen, then repeat the
wp eval test from Step 1.
- Add the DNS records your provider asks for: usually an SPF include and one or more DKIM records. FG-06 covers how those records fit together.
Tip: WP Mail SMTP can read its password from a WPMS_SMTP_PASS constant in wp-config.php (with WPMS_ON set to true), which keeps the credential out of the database and out of database backups.
Ports and encryption
| Port |
Purpose |
Encryption setting |
| 587 |
Message submission by an authenticated client |
STARTTLS (labeled “TLS” in most plugins) |
| 465 |
Message submission over implicit TLS |
SSL/TLS from the first byte (labeled “SSL”) |
| 25 |
Server to server relay |
Do not use from WordPress; often blocked outbound |
| 2525 |
Unofficial alternative offered by many transactional providers |
Usually STARTTLS |
Mismatching port and encryption is a classic mistake: “SSL” on 587 or “TLS” on 465 hangs and then times out. Many cloud platforms and some hosts block outbound port 25 entirely, so a plugin set to port 25 fails with a connection timeout.
Test whether the port is reachable from the server
Run this on the server over SSH:
nc -vz -w 5 SMTP_HOST 587
openssl s_client -starttls smtp -connect SMTP_HOST:587 -crlf -quiet
SMTP_HOST is your provider’s SMTP server, for example smtp.gmail.com or smtp.office365.com. In the nc line, -v prints the result, -z checks the port without sending data and -w 5 gives up after five seconds. “succeeded” or “open” is good; “timed out” means a firewall blocks it, so try 465 or 2525, or ask your host. The openssl line performs STARTTLS like a mail client; -crlf sends SMTP line endings and -quiet trims certificate output. Type EHLO test and expect lines starting 250; type QUIT to leave. For port 465, drop -starttls smtp.
Step 4: Google Workspace and Microsoft 365 credentials
Both big mailbox providers have tightened password logins, which explains many “it worked for years” failures.
Google Workspace and Gmail
Google no longer accepts a normal account password over SMTP. “Less secure apps” access was removed for Workspace in 2025. You have two choices:
- OAuth. WP Mail SMTP’s Google mailer and FluentSMTP’s Gmail connection use OAuth, so there is no password to leak. Follow the plugin’s current walkthrough.
- App password. Turn on 2-Step Verification for the sending account, then create an app password in the Google Account security settings (the option is hidden until 2-Step Verification is on). Use
smtp.gmail.com, port 587, TLS, the full address as username and the 16-character app password.
Gmail rewrites the From header to the logged-in account unless the address is a verified “Send mail as” alias, so send as the address you log in with.
Microsoft 365
Microsoft 365 is the most common source of 535 5.7.139 Authentication unsuccessful errors. Three separate switches have to line up:
- SMTP AUTH at the organization level. In tenants created in recent years it is off by default. The error text then says
SmtpClientAuthentication is disabled for the Tenant.
- Authenticated SMTP on the mailbox. In the Microsoft 365 admin center, open Users, Active users, select the user, open the Mail tab, choose Manage email apps and check Authenticated SMTP.
- Basic authentication. If Security Defaults or a Conditional Access policy blocks legacy authentication, a username and password login fails even when SMTP AUTH is on. Microsoft has also announced the retirement of basic authentication for SMTP AUTH: under its revised timeline it stays available until the end of December 2026, after which Microsoft says it will be turned off by default for existing tenants (admins can re-enable it for now), with a final removal date to be announced later. Check Microsoft’s current Exchange Online announcements before relying on it.
The future-proof choice is OAuth: both WP Mail SMTP (Outlook mailer) and FluentSMTP support Microsoft OAuth. If you must use basic SMTP AUTH for now, a Microsoft 365 administrator can check and set it with Exchange Online PowerShell:
Get-TransportConfig | Format-List SmtpClientAuthenticationDisabled
Set-CASMailbox -Identity SENDER@example.com -SmtpClientAuthenticationDisabled $false
These run in PowerShell on an administrator’s computer after Connect-ExchangeOnline. SENDER@example.com is the mailbox WordPress logs in as. The first line shows the tenant-wide setting: True means SMTP AUTH is off for the organization. The second enables it for that one mailbox, which overrides the tenant default and is safer than turning it on for everyone. Then use smtp.office365.com, port 587, TLS.
Step 5: Log every message
An email log shows what was sent, to whom, when, and any error. FluentSMTP includes a log in its free version. WP Mail SMTP offers email logging in its paid tiers, and the free WP Mail Logging plugin works alongside any SMTP setup. Transactional providers also keep their own activity log, which shows what happened after the message left your site: delivered, bounced, deferred or marked as spam.
When a customer says “I never got my order email”, check the WordPress log (was it generated?), the provider log (was it delivered?) and their spam folder, in that order. WooCommerce can resend from the order screen’s Order actions box. Logs hold personal data, so set a retention period.
Step 6: Rule out plugin conflicts and broken hooks
If the Step 1 test works but one form does not, check that form’s notification: is it enabled, does its conditional logic ever match, and is the To field filled? Check your anti-spam or security plugin’s log for blocked submissions. Then look for old code changing mail. Run this on the server from the site folder:
grep -rln --include=*.php -E "wp_mail_from|phpmailer_init|pre_wp_mail" wp-content/themes wp-content/mu-plugins wp-content/plugins
No placeholders. -r searches recursively, -l lists matching files only, --include=*.php limits it to PHP and -E allows the alternatives. Matches inside your SMTP plugin are expected. Matches in a theme’s functions.php, a snippets plugin or an unfamiliar mu-plugin are suspects: a forgotten filter forcing a From address, or a pre_wp_mail filter that cancels sending.
Advanced: watching the SMTP conversation
When credentials look right but sending still fails, watch the SMTP conversation itself with swaks (available in Linux package managers and Homebrew). Run it on the server or your computer:
swaks --to YOUR_ADDRESS@example.net --from SENDER@example.com --server SMTP_HOST --port 587 --tls --auth LOGIN --auth-user SENDER@example.com --auth-password 'SMTP_PASSWORD'
SENDER@example.com is the account you authenticate as and send from, SMTP_HOST is your provider’s SMTP server, and SMTP_PASSWORD is the app password or SMTP credential (single quotes stop the shell interpreting special characters; leave --auth-password out and swaks will prompt for it instead, which keeps it out of your shell history). --tls requires STARTTLS; for port 465 use --tls-on-connect instead. --auth LOGIN picks the authentication method.
Lines starting <- are the server’s replies:
235 after AUTH: credentials are fine, so the problem is in WordPress settings.
535 after AUTH: login refused. Microsoft’s 5.7.139 text names the blocker (tenant, mailbox or basic authentication). Google’s 5.7.8 Username and Password not accepted usually means you used the real password, not an app password.
550 or 553 after MAIL FROM: logged in, but not allowed to send as that address. Send as the login address or add a “send as” permission.
250 after the body: accepted. Anything later is a delivery question, not a WordPress one.
Finally, view the raw source of a delivered test message and check Authentication-Results: you want spf=pass, dkim=pass with header.d= showing your domain, and dmarc=pass.
When to contact your host
Open a ticket when the port tests fail from the server, when mail() is disabled and you are unsure which SMTP route the host supports, or when the mail log shows rejections you cannot interpret. Include:
- The site domain and the exact From and To addresses of a failing message.
- The UTC time of your test send, from the
gmdate stamp in the body.
- The full output of the
wp eval test, including any wp_mail_failed errors.
- The SMTP host, port and encryption you are using, and the result of the
nc and openssl tests.
- Any bounce message, complete with headers.
- What changed recently: a migration, a new form plugin, a DNS move, a mail provider switch or a password reset on the sending mailbox.
For more checks, see the diagnostic tools. If order emails are failing on a busy store right now, start at the Emergency Room.