FluentSMTP
Source |
Public — |
Purpose |
Gives |
Special infra |
None. No writable path, no PVC. |
Why it is needed at all
A stock php:8.3-apache image has no MTA. sendmail_path defaults to /usr/sbin/sendmail,
that binary does not exist, and so every wp_mail() call fails — returning false and
raising nothing an operator would ever see.
That is worth stating plainly because the failure is invisible. Everything that depends on it silently does nothing:
-
WooCommerce customer and admin order mail
-
password resets and new-user notifications
-
Forminator form notifications
-
UpdraftPlus backup reports — including on a site whose chart sets
updraft_email, where the address is configured and the report has never once been delivered
Installing an SMTP plugin is what supplies the transport. Nothing else in the image does.
Why this plugin
The transport is a per-site choice — whichever mailer plugin is installed supplies it — so the selection is worth recording rather than rediscovering.
FluentSMTP is free with no paid tier, and its Amazon SES and Microsoft 365 OAuth connections
are included. Easy WP SMTP, used on other sites, gates both of those behind its Pro upgrade.
Neither is needed for the authenticated-SMTP connector below, but a site that later needs SES —
the likely route if mail ever has to originate from a customer’s own domain, see
SMTP Configuration — would otherwise need a licence per
site. It also supports the wp_config credential store this page relies on.
Three delivery routes
Which route a site uses is a deployment and customer question, not a property of the platform.
All three are selected with mail.provider and all three keep their credentials out of the
database the same way.
mail.provider |
Use it for | Credentials |
|---|---|---|
|
Any authenticated SMTP server. The generic route — a customer-hosted mail server, a third-party relay, or a Microsoft 365 tenant reached over SMTP AUTH (which is what our own sites do today, because it is simpler to operate than Graph). |
|
|
Microsoft 365 via the Graph API. For our own corporate tenants, and the route to take if a tenant disables SMTP AUTH — which Microsoft has been progressively doing. |
|
|
Amazon SES. For volume, and — the more important case — when mail must originate from a customer’s own domain. See the warning below. |
|
|
A customer’s domain cannot be sent from our tenant’s connector
SPF is evaluated against the domain in the From address, not against the relay. Mail sent as
No amount of configuration on our side fixes that. It needs a sending identity for the
customer’s domain, with records published in their DNS, which is what the |
outlook is the one route that is not fully declarativeGraph is OAuth, so beyond the client id and secret there is a consent step: it is granted once in wp-admin, and the resulting refresh token is stored in the database — the same shape as the UpdraftPlus licence connect. It survives pod restarts, because the database is the mutable layer, but not a database rebuild.
|
The split: connection declared, credentials not
The connection is chart configuration; the credentials are not.
| Where it lives | Why | |
|---|---|---|
Host, port, encryption, sender, logging |
|
Non-secret, and it is what changes between environments. Declared means it is reviewable and reproducible. |
The credential pair, whichever route is in use |
Constants defined in |
See below — there are two independent reasons, and either alone would justify it. |
The connection opts into the constants by setting key_store: wp_config. Every provider
implements it, each in its own handler — Smtp/Handler.php::setSettings(),
AmazonSes/Handler.php::filterConnectionVars(), Outlook/Handler.php::withResolvedKeys(). The
SMTP one reads:
public function setSettings($settings)
{
if (Arr::get($settings, 'key_store') == 'wp_config') {
$settings['username'] = defined('FLUENTMAIL_SMTP_USERNAME') ? FLUENTMAIL_SMTP_USERNAME : '';
$settings['password'] = defined('FLUENTMAIL_SMTP_PASSWORD') ? FLUENTMAIL_SMTP_PASSWORD : '';
}
...
Reason one: nothing for the harvest to promote
A password stored in wp_options is a password sitting in the table the
harvest reads. It would be caught by the secret filter and
withheld — but "caught by a filter" is a weaker guarantee than "never written". With
key_store: wp_config the option holds two empty strings and there is nothing to catch.
Reason two: DB-stored credentials are coupled to the WordPress salts
FluentSMTP encrypts DB-stored secrets using the site’s AUTH_KEY salts (use_encrypt). Rotate
the salts — which any credible incident response would do — and the stored SMTP password
becomes undecryptable. The plugin has a dedicated admin notice for this exact situation:
FluentSMTP Plugin may not work properly. Looks like your Authentication unique keys and salts are changed.
Constants have no such coupling. Salts can be rotated without breaking mail.
Configuration shape
The option is fluentmail-settings. Its structure comes from the plugin source
(app/Models/Settings.php), not from documentation:
connections[<key>].provider_settings the connection itself
mappings[<sender email>] which connection sends for that address
misc.default_connection fallback for every other address
|
The plugin’s own UI keys connections by The consequence is worth knowing: editing the connection in wp-admin creates a second, md5-keyed entry rather than updating this one. In the immutable posture the next deploy reverts that; in the mutable posture the harvest surfaces it. Either way, change the chart, not the admin screen. |
Per-environment
| Environment | provider |
Connection |
|---|---|---|
Production |
|
The Microsoft 365 connector the EMS services already use —
|
Dev |
|
GreenMail in the |
Local |
|
A Mailpit container in the compose stack, filling the same role as GreenMail with a built-in UI on port 8025. |
A customer’s own domain |
|
Not currently deployed. The route to reach for when it is needed — see the SPF warning above. Requires a verified sending identity in SES for that domain and the account out of the sandbox. |
|
Exchange rejects a sender the tenant does not own
It applies to |
Containment on a dev site
FLUENTMAIL_SIMULATE_EMAILS makes the plugin accept every message and deliver none.
wp_mail() still returns true, so nothing downstream changes behaviour.
Use it on any site that must not be able to reach a real recipient. It matters most immediately after a production content refresh, when the dev database holds real customer addresses and production’s cron schedule.
Pointing dev at GreenMail is the better default, because a sink that captures mail is strictly more useful than one that discards it — you can see what WooCommerce would have sent. Simulation is the belt to that brace, for when there is no sink to point at.
Verifying
wp eval 'var_dump(wp_mail("[email protected]", "subject", "body"));'
false means the transport is not working. If it returns true and nothing arrives, check
that the sender address is mapped:
wp eval '$s = get_option("fluentmail-settings");
echo $s["misc"]["default_connection"], "\n";
print_r($s["mappings"]);'
And that the constants actually reached PHP — an unset environment variable is the common cause, and the plugin reports it as a validation error against the username field rather than as a missing-configuration error:
wp eval 'echo defined("FLUENTMAIL_SMTP_USERNAME") ? "set\n" : "MISSING\n";'