Mailgun Watch self-updates
Custom WordPress plugin that handles sending emails via Mailgun and tracks failures and unopened messages and logs it in the backend
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/truemarket/mailgun-watch/archive/refs/heads/main.zipShips its own WordPress updater (Plugin Update Checker), so new versions show up under Dashboard → Updates.
Sends every outgoing WordPress email directly through the Mailgun HTTP API — no WP Mail SMTP or other SMTP plugin required — logs every send, reconciles the real delivery outcome via Mailgun webhooks, flags failures, and alerts by email + SMS (Twilio). SMS is the fallback that still works when the site can't send any email at all — recipients just need a phone number, no app or account required. (Slack support still exists in the code but is currently disabled and hidden from Settings; see Alerting logic.)
Formerly "Mailgun Email Monitor". The rename is cosmetic only — internally the plugin still uses its original
wpelprefix (class names, options, DB table, hooks, REST route, file names) for backward compatibility; see the note at the top ofmailgun-email-monitor.php.
Install
- Copy the
mailgun-email-monitorfolder intowp-content/plugins/. - Activate Mailgun Watch in Plugins. Activation creates the log table and a daily prune cron.
- Deactivate WP Mail SMTP (or any other SMTP plugin) — this plugin replaces it as the mail transport. Leaving one active alongside this plugin is harmless (this plugin's
pre_wp_mailhook wins), but there's no reason to keep it. - Add the shared credentials to
wp-config.php— see Shared credentials (wp-config.php). They're the same on every site, so they aren't in Settings. - Go to Mailgun Watch → Settings (two tabs: Mailgun Sending and Alerting & Logging) and fill in:
- Mailgun API key — create a separate key for each site (Mailgun → Settings → API Keys) so one site's key can be revoked without affecting the others. Use a full API key, not a domain sending key.
- Mailgun sending domain (US region only) — a dedicated one for this site
- Default from name/email — must be on a domain verified in Mailgun, or sends are rejected
- Alert email recipient
- Alert phone numbers — comma-separated numbers that should receive SMS alerts. Recipients don't need an app, an account, or to subscribe to anything.
- In the Mailgun dashboard, go to Send → Webhooks → Add webhook → Domain-level (not Account-level), pick this site's domain, and point it at the endpoint shown on the Settings page:
https://YOURSITE/wp-json/wpel/v1/mailgun-webhookSubscribe it to at least:
accepted,delivered,permanent_fail(addtemporary_failif you want soft-failure alerts too). - Click Send test email on the Settings page to confirm the whole pipeline — API send, logging, and webhook reconciliation — works end to end.
What gets logged
Every send becomes a row with a status that moves through
pending → sent (accepted) → delivered, or lands on failed / temp-fail / complained.
View and filter them under Mailgun Watch → Log.
How it works
This plugin is the Mailgun transport — it doesn't sit alongside another SMTP plugin, it replaces one. Three layers reconcile into one log:
- Pre-send capture (
wp_mailfilter): records the send the instant it happens, asstatus = pending. - Send (
pre_wp_mailfilter,includes/class-wpel-mailer.php): posts the message straight to Mailgun's HTTP API and short-circuitswp_mail()with the real result — WordPress's default PHPMailer/SMTP transport is never invoked. The row from step 1 is immediately updated with Mailgun's assigned message-id, or markedfailed(and alerted) on an API-level send failure. - Webhook reconcile: the source of truth for the final outcome (delivered vs bounced) — the one thing step 2 can't see, since it only knows the API handoff succeeded. Matches by message-id, falls back to recipient+subject for the rare case where step 2 didn't fire (e.g. Mailgun sending was switched off at send time), and can create a row on its own as a last resort.
Sending behavior
- Force from address (on by default): always sends using the configured default from name/email, regardless of what a plugin/theme sets via
wp_mail()'s headers. Turn this off if you specifically want per-email From addresses honored — but they must all be on Mailgun-verified domains, or those sends will fail. - Send via Mailgun API toggle (on by default, including on installs that activated before this option existed): an emergency off-switch. When disabled,
wp_mail()falls through to WordPress's default transport (usually PHPmail(), which most hosts don't deliver reliably) — logging and alerting still work viawp_mail_failed. - HTML vs. plain text, Cc/Bcc, Reply-To, and file attachments (via hand-built
multipart/form-data, since WordPress's HTTP API has no upload helper) are all forwarded to the Mailgun API.
Alerting logic
- Every failure → SMS via Twilio to every configured number (always) + alert email (best effort, itself sent through Mailgun).
- A burst of failures with no successful sends in the window → a distinct "POSSIBLE TOTAL EMAIL OUTAGE" text (throttled to one per window). This is the case where the alert email itself can't get out, which is exactly what SMS covers.
- Slack support (
notify_slack()inincludes/class-wpel-monitor.php) is still in the code but every call site is currently commented out in favor of Twilio SMS, and the Settings field is hidden (not removed — seerender_settings_page()). Uncomment the calls (inhandle_failure(),notify_outage_alarm(), andnotify_unopened()) and un-hide the field to run Slack alongside SMS again — the saved webhook URL, if any, is untouched.
Thresholds, retention, and whether to store message bodies are all on the Settings page.
Shared credentials (wp-config.php)
Credentials that are the same on every site live only in wp-config.php and aren't shown in Settings. Copy the same block into each site's wp-config.php:
define( 'WPEL_MAILGUN_SIGNING_KEY', 'account-level HTTP webhook signing key' );
define( 'WPEL_SLACK_WEBHOOK', 'https://hooks.slack.com/services/...' ); // unused while Slack alerting is disabled
// SMS alerts:
define( 'WPEL_TWILIO_ACCOUNT_SID', 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); // real Account SID — always goes in the API URL
define( 'WPEL_TWILIO_SID', 'ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' ); // Basic Auth user: the Account SID above, OR an API Key SID (starts with SK)
define( 'WPEL_TWILIO_AUTH_TOKEN', 'your Twilio auth token or API Key secret' ); // pairs with whichever WPEL_TWILIO_SID you used
define( 'WPEL_TWILIO_FROM_NUMBER', '+15551234567' ); // E.164, must include the leading +
WPEL_TWILIO_ACCOUNT_SID must always be the real Account SID from the Twilio Console dashboard (starts with AC) — the API path requires it regardless of which credential pair you authenticate with. WPEL_TWILIO_SID + WPEL_TWILIO_AUTH_TOKEN is whatever you use for Basic Auth: either that same Account SID plus the master Auth Token, or (recommended, since it's independently revocable) an API Key SID (SK...) plus its Secret from Console → Account → API keys & tokens.
The Twilio constants are only needed on sites that have alert phone numbers set. The setup checklist on the Settings page lists any constant that's missing.
Sites set up with an older version of this plugin may still have these values saved in Settings. Those saved values are used as a fallback until the constant is defined; the constant always wins once it's there.
The sending domain, From address, and alert email/phone numbers stay in Settings because they're different on each site (each site sends from its own dedicated domain — see the checklist note on why domains can't be shared).
Notes / limitations
- Webhook calls are rejected unless the Mailgun signature verifies, so the signing key is required.
wp_mail_succeeded/wp_mail_faileddon't fire natively for Mailgun-sent mail (short-circuitingpre_wp_mailskips the rest of core'swp_mail()), so third-party plugins that hook those directly won't see Mailgun sends. Monitor still catches failures independently viaWPEL_Mailer::mark_failed_and_alert().blocking => falseis used for the Twilio API call so a slow response never stalls a page load.- On a Twilio trial account, each "to" number must be verified in the Twilio Console before it can receive SMS; upgrade to a paid account to text arbitrary numbers. For any real US SMS volume, Twilio may also require A2P 10DLC brand/campaign registration for the sending number to avoid carrier filtering — low-volume ops alerts to a handful of numbers typically work unregistered, but this is worth checking if messages start getting silently dropped.
Updates
This plugin ships with the Plugin Update Checker library and checks the private TrueMarket/mailgun-watch GitHub repo for new tagged releases, surfacing them on the Plugins screen like any wordpress.org plugin.
Since the repo is private, each site needs a read-only GitHub token to check for updates:
- Create a fine-grained personal access token at GitHub → Settings → Developer settings → Personal access tokens, scoped to only the
mailgun-watchrepo with Contents: Read-only permission. - Add it to
wp-config.php:define( 'WPEL_GITHUB_TOKEN', 'github_pat_...' );
Releasing a new version
To ship an update, paste this to Claude Code (fill in the changelog notes), or follow it by hand:
Release a new version of this plugin. Bump the Version header and
WPEL_VERSION in mailgun-email-monitor.php together (patch bump unless
I say otherwise). Add a new entry at the top of the == Changelog ==
section in readme.txt containing a concise list of all changes
Show me the list of changes before commiting
Commit everything, tag the commit vX.Y.Z to match, and push both main
and the tag to origin.
The changelog notes matter, not just as documentation — they're what WordPress shows in the "View version X.X details" popup on the Plugins screen, since the update checker reads them straight out of readme.txt's == Changelog == section for whichever tag it's looking at.
That's: bump Version: in the file header and WPEL_VERSION together, add the changelog entry to readme.txt, commit, then tag and push — e.g. git tag v2.3.0 && git push origin main && git push origin v2.3.0.