The fp-runtime image deliberately ships no MTA, so PHP’sDocumentation Index
Fetch the complete documentation index at: https://docs.frankenpress.com/llms.txt
Use this file to discover all available pages before exploring further.
mail() fails
silently and wp_mail() returns true while no email actually
delivers. Every FrankenPress site lands in this silent-fail state by
default. This page is the opt-in fix.
The chart’s smtp.* values + the
SMTPMailer mu-plugin component
wire wp_mail() through any SMTP provider — Postmark, SendGrid,
Mailgun, AWS SES, or an in-cluster relay. Same surface, swap the host.
How it works
- Chart: emits 5
FP_SMTP_*env vars from the configmap (host, port, encryption, optional from-email/name) plus 2secretKeyRefenv entries (username, password) into the Deployment, the wpcron CronJob, and the install Job. - Mu-plugin: reads those env vars, hooks
phpmailer_init, sets the global PHPMailer’s transport. - Site Health: when
FP_SMTP_HOSTis set, the mu-plugin’s SiteHealth component adds a TCP-reachability test on the wp-admin → Site Health screen.
Before you start: site image must include SMTPMailer
The chart’ssmtp.* values are wiring — the actual SMTP send happens
inside the WP runtime, in the
SMTPMailer mu-plugin component.
Flipping smtp.enabled=true against a site image that doesn’t include
fp-mu-plugin >=0.5.0 is a no-op: the env vars get injected, no filter
gets registered, wp_mail() keeps falling through to broken mail().
| If your site image is built from… | Action |
|---|---|
fp-site-template (default) | Use v0.2.4+. Earlier tags don’t include SMTPMailer. |
A fork of fp-site-template | Bump eightoeight/fp-mu-plugin in your composer.json to ^0.5.0, run composer update, rebuild the site image, then point the chart at the new tag. |
| A custom site image | Same — composer-install eightoeight/fp-mu-plugin: ^0.5.0 somewhere on the autoloader path. |
Quick start (manual Secret)
For a single-namespace test or a kind-cluster demo:Production: ExternalSecret + ESO
Real deployments don’t keep credentials inkubectl --from-literal — they
pull from a secret manager (AWS Secrets Manager, GCP Secret Manager,
HashiCorp Vault, 1Password Connect, etc.) via External Secrets
Operator. The chart doesn’t care how
smtp.auth.existingSecret got into the namespace, so any of those work.
The ESO + AWS Secrets Manager recipe (with the API token stored at
my/asm/path under field smtp_token) looks like:
Provider recipes
Defaultsport: 587 and encryption: tls (STARTTLS) work for every
modern SMTP provider — most rows below only need to set host. The
auth.existingSecret always points at a Secret you create with
username and password keys.
Postmark
username and password are both your Server API
token (Postmark uses the same value for both). Set the Sender
Signature for fromEmail in the Postmark dashboard before sending,
or messages will reject.
SendGrid
username is the literal string apikey; password is
your SendGrid API key. Verify the Sender Authentication for
fromEmail in the SendGrid dashboard.
Mailgun
username is the SMTP username from your Mailgun domain
settings (typically postmaster@yourdomain.com); password is the
SMTP password generated for that user.
AWS SES
username and password are the SMTP credentials
generated from an IAM user with ses:SendRawEmail (use IAM → Security
credentials → “Create SMTP credentials” — these are not your access
key id / secret access key). Verify the fromEmail identity (or its
domain) in SES.
In-cluster relay (mailpit / mailhog)
For development environments where you want to capture rather than send mail:auth.existingSecret is empty, no FP_SMTP_USERNAME / FP_SMTP_PASSWORD
env is injected, and the mu-plugin sets SMTPAuth = false.
Failure modes
| State | Behaviour |
|---|---|
smtp.enabled=false (default) | No SMTP env injected. wp_mail() falls through to mail() → silent fail. The install Job suppresses the password-change-notification email on the syncAdminCredentials path so you don’t see noisy sendmail: not found lines in Job logs. |
smtp.enabled=true, server reachable, auth + identity valid | Provider accepts the message. wp_mail() returns true. SiteHealth test reports good. true is not the same as “delivered” — see verification below. |
smtp.enabled=true, server unreachable | wp_mail() returns false. The failure is logged via error_log (visible in pod stderr / your log shipper). SiteHealth test reports critical with errno + message. No retry. |
smtp.enabled=true, auth invalid | Same as unreachable — log + false return. Auth failures only surface during the SMTP handshake, not on the SiteHealth TCP-reachability check. |
smtp.enabled=true, sender identity unverified (Postmark Sender Signature, SES verified identity, etc.) | TCP reachability passes, auth passes, the provider rejects with a 5xx during MAIL FROM. wp_mail() returns false and the SMTP response is logged. SiteHealth test stays green because TCP-reachability doesn’t probe the handshake. Most common first-time-setup failure — verify the fromEmail identity in the provider dashboard before flipping smtp.enabled=true. |
Site image lacks fp-mu-plugin >=0.5.0 | SMTP env vars get injected by the chart, but no phpmailer_init filter is registered inside WP, so wp_mail() keeps falling through to mail() and fails silently. SiteHealth’s SMTP-reachability test isn’t registered either — there’s no signal that anything’s wrong. See Before you start. |
What’s not in the platform
- No queueing / retry / async delivery. Request-time send only. If you need transactional reliability for high volumes, install a queue plugin (Action Scheduler, etc.) or have the provider handle retries.
- No transactional email templates. Site authors handle in WP itself or via plugin.
- No DKIM / SPF / DMARC setup. Operator-side DNS concern, owned by whoever sets up the provider account. Without these your provider may quietly send-but-spam-folder.
- No provider-specific API mode. SMTP only — universal across all
providers. Sites that need a provider’s API mode can install the
provider’s official WordPress plugin alongside; it’ll override
SMTPMailer’s config (last-writer wins on
phpmailer_init).
Verify it’s working
After deploying withsmtp.enabled=true:
Operations
Token rotation
When you rotate the API token in your provider’s dashboard:- Update the value at the source (your secret manager).
-
ESO refreshes the K8s Secret on its
refreshInterval(1 hour by default). The Secret in the namespace will pick up the new value transparently. -
Running pods don’t notice — env vars were injected at pod start.
Roll the Deployment so new pods read the rotated value:
Auto-rolling on Secret change (Reloader)
If you rotate often (or just don’t want a manualkubectl rollout restart in your runbook), install
Stakater Reloader cluster-wide.
It watches Secret and ConfigMap objects and triggers a rolling
update on associated workloads when their content changes — closing
the gap between “ESO refreshed the Secret” and “running pods see the
new value”.
Install via the Helm chart on Artifact Hub:
commonAnnotations, not podAnnotations — Reloader watches the
Deployment object’s metadata.annotations, not the pod-template’s
spec.template.metadata.annotations:
commonAnnotations is applied to every resource the chart renders
(Service, ConfigMap, Secret, Job, CronJob, ServiceMonitor, etc.), not
only the Deployment. The Reloader annotations on non-workload resources
are harmless — Reloader only acts on Deployments, StatefulSets, and
DaemonSets — but they will be visible if you kubectl describe other
objects.Reloader is platform-wide infrastructure, not SMTP-specific. Once
installed it solves the same “manual rollout-restart” problem for
every Secret the chart references — the database password, the
WP keys+salts, S3 credentials, and the SMTP token. Worth installing
once for the whole platform if any of those rotate on a schedule.
Sender-identity verification
Most providers require you to prove ownership of thefromEmail
address (or its domain) before they’ll accept sends. Skipping this is
the single most common first-deploy gotcha — the SMTP handshake
succeeds, auth succeeds, but the provider rejects MAIL FROM with a
5xx and wp_mail() returns false.
| Provider | Where to verify |
|---|---|
| Postmark | Dashboard → Servers → (your Server) → Sender Signatures (per-address) or Domains (whole domain — recommended) |
| SendGrid | Settings → Sender Authentication → Verify a Single Sender / Authenticate Your Domain |
| Mailgun | Sending → Domains → (your domain) → DNS records |
| AWS SES | Identities → Create identity (Domain or Email address) |
fromEmail values
forever. Plus DKIM signing only works at the domain level.
Related
fp-mu-plugin→ SMTPMailer — the component referencefp-chartsvalues — thesmtp.*keys- Operations → Configuration — full env-var reference