FluentSMTP

Source

Public — wpackagist-plugin/fluent-smtp, pinned in composer.lock

Purpose

Gives wp_mail() a working transport, and routes it through a declared SMTP connection

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

smtp

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).

FLUENTMAIL_SMTP_USERNAME
FLUENTMAIL_SMTP_PASSWORD

outlook

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.

FLUENTMAIL_OUTLOOK_CLIENT_ID
FLUENTMAIL_OUTLOOK_CLIENT_SECRET

ses

Amazon SES. For volume, and — the more important case — when mail must originate from a customer’s own domain. See the warning below.

FLUENTMAIL_AWS_ACCESS_KEY_ID
FLUENTMAIL_AWS_SECRET_ACCESS_KEY

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 @customer-domain.co.za through the myriadevents.co.za connector fails SPF at the recipient, because that connector is not authorised in the customer’s DNS — and Microsoft 365 refuses it from the other side anyway, rejecting any sender the tenant does not own.

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 ses route exists for. Each new customer domain is its own piece of work. See SMTP Configuration.

outlook is the one route that is not fully declarative

Graph 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.

smtp and ses have no such step. Both are fully described by the chart plus two secret values, which is why smtp remains the default even against a 365 tenant.

The split: connection declared, credentials not

The connection is chart configuration; the credentials are not.

Where it lives Why

Host, port, encryption, sender, logging

mail.* in the chart, rendered into the fluentmail-settings option by the bootstrap

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 config/application.php from environment variables, which come from the Secret — the pair named in the routes table

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 md5(sender_email). The chart uses the literal primary instead, because Helm has no md5 function and the key is only ever a lookup handle — mappings and misc.default_connection both point at it.

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

smtp

The Microsoft 365 connector the EMS services already use — myriadevents-co-za.mail.protection.outlook.com:25, authenticated, with opportunistic STARTTLS (encryption: none + auto_tls: yes, which is FluentSMTP’s spelling of starttls.enable). FLUENTMAIL_SMTP_PASSWORD must be in the Secret before the site is switched to a chart version that enables mail.

Dev

smtp

GreenMail in the event-dev namespace — greenmail.event-dev.svc.cluster.local:25, no auth, no TLS. It accepts everything and relays nothing, and what the site sent is readable in SnappyMail. Note SnappyMail’s per-domain config governs which sender domains you can actually open, so pick a dev sender in a domain it knows.

Local

smtp

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

ses

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

force_from_email: yes pins the envelope sender to the configured address rather than letting WooCommerce set its own per-message. Without it, mail sent as a customer’s address is refused by the connector — which surfaces as intermittent delivery failure that correlates with nothing obvious.

It applies to smtp and ses. The outlook route has no such setting because Graph always sends as the authenticated mailbox regardless.

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";'