Route all WordPress email through the Cloudflare Email Sending API and keep a searchable log of every message. Configured entirely through wp-config.php constants — there is no settings screen.
- Tiny surface. No admin settings; the only screen is the log viewer (Tools → Cloudflare Email), built with WordPress DataViews.
- Drop-in. Hooks
wp_mail()without redefining it — every plugin/theme that sends mail is routed automatically, with Cc/Bcc/Reply-To/custom headers/inline images preserved. - Logged. Every send (and every failure) is recorded; view, resend, or delete from the log.
- Self-updating. Pulls new versions straight from GitHub Releases.
- WordPress 6.6+ and PHP 8.4+.
- A Cloudflare account with Email Sending enabled for your sending domain (see Cloudflare's setup guide;
npx wrangler email sending enable yourdomain.com). - A Cloudflare API token with the Email Sending permission.
Deliverability records (SPF/DKIM/DMARC) and domain onboarding are managed in Cloudflare — this plugin only sends.
Add to wp-config.php:
// Required
define('CLOUDFLARE_EMAIL_ACCOUNT_ID', 'your-cloudflare-account-id');
define('CLOUDFLARE_EMAIL_API_TOKEN', 'token-with-email-sending-permission');
// Recommended — the From domain must be onboarded to Cloudflare Email Sending.
// (WordPress otherwise defaults to wordpress@<site-domain>, which is usually unverified.)
define('CLOUDFLARE_EMAIL_FROM', 'no-reply@your-verified-domain.com');
define('CLOUDFLARE_EMAIL_FROM_NAME', 'Your Site');
// Optional
define('CLOUDFLARE_EMAIL_LOG', false); // stop logging *successful* sends
// (failures are always logged); default true
define('CLOUDFLARE_EMAIL_LOG_RETENTION_DAYS', 30); // auto-prune window; 0 = keep foreverWhen the required constants are absent the plugin does nothing and WordPress mail continues through its normal transport.
If Cloudflare rejects a message (e.g. an unverified sender, or a custom header it disallows) the send is logged as failed and wp_mail() returns false — there is no silent fallback. To adjust the outgoing payload, filter it:
add_filter('cloudflare_email_payload', function (array $payload) {
unset($payload['headers']['X-Some-Header']);
return $payload;
});With WP-CLI:
wp cloudflare-email verify # check the API token
wp cloudflare-email send-test you@example.comThen open Tools → Cloudflare Email to see the log entry.
The log viewer is a TypeScript DataViews app bundled with rolldown; the dev shell and release build are driven by Nix (with direnv):
direnv allow # or: nix develop
composer install # PHP deps (plugin-update-checker)
npm install # JS deps (rolldown, typescript, oxlint, @wordpress/dataviews)
npm run dev # watch-build the DataViews app (rolldown --watch)
npm run build # production bundle -> build/
npm run check # oxfmt + oxlint + type-aware oxlint + tsc (the CI gate)npm run build emits build/index.js (IIFE), build/index.asset.php (the WordPress
dependency manifest), and build/index.css (the DataViews stylesheet).
@wordpress/dataviews and its non-core deps are bundled; every other @wordpress/*
package resolves to its core wp.* global at runtime.
Linting/formatting use the OXC toolchain — oxlint (all categories
at error, plus a type-aware pass via oxlint-tsgolint) and oxfmt. npm run check
runs the full gate; npm run lint:fix / npm run format apply autofixes. The same
gate runs in CI on every push and pull request.
Build the distributable, self-contained zip (bundles vendor/ and the compiled build/):
nix build .#zip -L # -> result/cloudflare-email.zipCommits to main follow Conventional Commits. Release Please maintains a release PR; merging it bumps the version in composer.json, package.json, and the plugin header, updates CHANGELOG.md, tags vX.Y.Z, and the release workflow builds cloudflare-email.zip with Nix and attaches it to the GitHub Release. Client sites self-update from that asset via plugin-update-checker.
MIT © Avunu LLC