Repository navigation
feat: simple SMTP — drop Pro, Meow Mailer parity (#17) - #18
Merged
Merged
Conversation
- Manager::connection_config() centralizes provider-specific config derivation (Postmark stream from the connection's purpose); both the failover chain and the Test endpoint connect through it, so a Marketing connection now tests on the broadcast stream instead of silently testing transactional. - readme rewritten the simple way (Meow Mailer register): title is just "Mailyard", first-person description, tags smtp/email log/mailer/ email/deliverability, changelog in dated Add:/Fix: format with the 1.0.0 feature list restored. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_011WAxszR5QVMYFxWnj3XRT8
Mailyard is now a standalone SMTP plugin: no add-on shell, no campaign routing. - Admin: remove the shell module registry and the dashboard, settings, data and AI extension filters; App.jsx routes the five pages directly. The WP submenu mirrors the sidebar one-to-one. - PHP: remove the mailyard_admin_submenus filter, the mailyard_admin_enqueue action and MAILYARD_SHELL_VERSION. - Routing: connection purpose and the X-Mailyard-Purpose header are gone (mailyard_purpose filter removed); Postmark sends on the connection's own stream, default outbound. Marketing-only connections are switched off once on upgrade so they don't start carrying transactional mail. - Copy: Pro mentions removed from readme, abilities and settings. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
wp_mail() arguments are now parsed once (Message::from_wp_mail) into the shape every driver sends: - Cc/Bcc/Reply-To come from the headers. pre_wp_mail never passes a `cc` key, so they were silently dropped for every provider. - Custom headers and caller-chosen attachment filenames pass through. - One message goes to all recipients, as wp_mail() does, instead of a copy per address. Override gains Offline mode (log, never send) and Background sending (log as pending, send on shutdown after fastcgi_finish_request, then settle the same row). Send test and Resend force a synchronous send and report which provider delivered, including a backup takeover. Also fixed along the way: - Manager clones each driver per connection; two connections on one provider used to share credentials during failover. - SES MIME parts are base64 (UTF-8, long minified lines). - SMTP encryption "none" disables PHPMailer's auto-STARTTLS. - PHP Mail is a real connection type: the UI tile used an id the REST API rejected. SMTP now extends it rather than duplicating the wp_mail() plumbing. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Importer reads WP Mail SMTP, Easy WP SMTP, FluentSMTP and Post SMTP
setups server-side and appends them as Mailyard connections. Secrets
never pass through the browser.
- WP Mail SMTP / Easy WP SMTP: wp-config constants when {PREFIX}ON is
set; the SMTP password is opened with their sodium secretbox key. A
password that won't open comes over empty, never as ciphertext.
- FluentSMTP: every connection, default first and fallback second,
per-sender mappings become from_match routing; AES-256-CTR secrets
are decrypted and checked against the salt suffix.
- Post SMTP: base64-encoded secrets decoded.
- A source is offered only when it maps to a registered provider and
its required credentials are readable. Imports go live only when
Mailyard has no enabled connection; otherwise they arrive switched
off. Imported sources are remembered and not offered again.
Connection storage and the primary-to-settings sync moved from
REST_API into Options so the importer shares them.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
… Maileroo
Six API drivers on the same contract as Resend: recipients from
Recipients::split(), Cc/Bcc, Reply-To, custom headers, in-memory
attachments, and a plain-sentence error pulled from each API's JSON.
- Mailgun: multipart form built by hand (repeated to/cc/bcc, h:
headers, raw attachment parts), US/EU region, bare "Forbidden" on a
bad key handled.
- SendGrid: to/cc/bcc deduplicated (the API rejects repeats).
- SMTP2GO: Reply-To travels in custom_headers; a 200 with failures is
a failure.
- Maileroo: Bearer sending key, { address, display_name } shape, subject
trimmed to 255, and a 200 with success:false is a failure.
- MailerSend and Maileroo skip custom headers: MailerSend gates the
field to paid plans and rejects the email otherwise; Maileroo v2
documents no field for them.
Registered in Plugin, Manager, Options (all API providers, so no local
MX check), the provider picker and icons. Errors::humanize learns the
new unverified-sender, sandbox and auth phrasings. Readme lists the
providers and their external-service endpoints and legal links.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- WP_Emails: off switches for the notifications core sends by itself (new-user admin/welcome, password-reset admin notice, password and email change, new comment, moderation, core/plugin/theme auto-update reports). Password reset emails are deliberately not switchable. - Return path: Settings > Delivery sets PHPMailer's envelope sender for SMTP, PHP Mail and WordPress's own sends; mailyard_return_path filters it per message. - Plugin Check: annotate the SES API host (a newer Plugin Check reads amazonaws.com as offloaded assets) and the deliberate use of core's wp_mail_content_type filter. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Alerts (Settings > Alerts):
- Failure alert by email, sent through WordPress's own mailer via
Override::unrouted() so it doesn't travel through the provider that
just failed, and isn't logged (no alert loops).
- "Backup took over" alert on the new mailyard_send_rescued action, so
a broken primary doesn't hide behind "sent" rows.
- At most one of each per hour, claimed with a transient before
sending.
- Webhook: Slack {text}, Discord {content}, Teams (Power Automate) an
Adaptive Card, anything else a plain JSON object. Non-blocking for
real alerts; the test waits, reports, and uses the URL as typed.
Individual emails are never posted.
- Weekly summary on a weekly cron (cleared on deactivate or switch-off):
counts plus top errors, routed like ordinary mail, skipped on a
quiet week.
Logger gains count_failed_since() and top_errors(). ToggleRow moves to
a shared component.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Logger::query filters by provider; export_csv() writes the filtered log (newest first, up to 10k rows, no bodies), with cells that start with = + - @ prefixed so a spreadsheet won't run them as formulas. - GET logs/export returns the CSV; the browser saves it as a file. - Email log page: provider select, search with a short debounce, 25 per page with Previous/Next, Export CSV, and Resend on any non-pending row. - Log retention setting: 7 / 30 (default) / 90 days, or 0 = forever. - Dashboard: "Why emails failed · 7 days" from Logger::top_errors(), humanized through Errors. - MCP list-logs accepts the pending and offline statuses. The page now fetches through get() directly; src/hooks/useLogs.js is no longer imported. It's left in place for now (see PR notes). Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Constants: any provider field can be set as
MAILYARD_{PROVIDER}_{FIELD} (MAILYARD_SMTP_PASSWORD,
MAILYARD_POSTMARK_API_KEY…). Manager::connection_config() applies
them over stored values; GET connections reports them as `locked`.
- Encryption (Settings > Security): Crypto seals secret config fields
with sodium secretbox, keyed from MAILYARD_ENCRYPTION_KEY or the
site's salts. It happens at the one storage choke point:
Options::save_connections() seals, Options::connections() opens.
Manager now reads through Options::connections() too. Toggling
re-saves every connection immediately. A secret that won't open
(keys rotated) reads as '' and is flagged; the dashboard shows a
"Locked" banner and Security explains the fix.
- Backup (Settings > Data & danger): export settings + connections
(credentials included) as JSON; import restores through the same
settings whitelist and rebuilds connections field by field. "Empty
log" clears the log alone, with a confirm step.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Checks::run() feeds a list of setup problems to the dashboard — things that quietly break or weaken delivery even when sends "succeed": - wp_mail() defined outside wp-includes/pluggable.php (another plugin took over; Mailyard's filters never run). Names the owner. - Default From address on a different domain than the site (subdomains either way are fine). - Contact Form 7 / WPForms / Gravity Forms set to send "from" the visitor's address. CF7 only counts a tag in the address part, so the stock "[_site_title] <wordpress@…>" sender isn't flagged. Separate from Conflicts (other SMTP plugins). Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
A network admin, on the main site (Settings > Network), can share its connections with every site. Sharing the sender (From email/name, Return path) and the delivery options (background, logging, retention) is each optional. Logs always stay per site. - Options::settings() lays the shared groups over the subsite's own; Options::connections() reads the main site's list. Both go through shared_source(), which is 0 outside multisite, on the main site, or with sharing off. - Subsites can't edit what's shared: connection writes, the importer and settings import return managed_by_network (403); save_settings skips shared keys; test results aren't written back. - The Delivery screen on a subsite says it's managed by the main site and greys out the shared groups. Verified in a WordPress Playground multisite (14 checks); the single-site suites still pass. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
No password stored. The admin creates an OAuth app, pastes its
credentials, saves, and clicks Connect account.
- OAuth (class-oauth.php): authorize links with a state binding a
nonce to the connection; the admin-post callback (thin wrapper around
complete()) trades the code for tokens and keeps them on that
connection; access_token() refreshes within 60s of expiry and saves
back, including Microsoft's rotated refresh tokens. The admin lands on
admin.php?page=mailyard&mailyard_oauth=ok|error#/connections, since
the hash router owns #/...
- Gmail: MIME via WordPress's PHPMailer (Mime::build), sent as
base64url `raw` to users.messages.send.
- Microsoft 365 (delegated) and app-only: Graph JSON, not MIME. Graph
then sends as the mailbox; a MIME From it doesn't own fails with
ErrorSendAsDenied. Only X- headers pass, since Graph rejects others.
App-only uses client credentials with a secret or a certificate-signed
JWT (RS256, x5t thumbprint), the token cached in a transient keyed to
the credential, and adds Entra hints on 401/403/404.
- Zoho Mail: data-center hosts; account id and verified addresses
learned at sign-in; attachments uploaded first; an unverified Reply-To
is dropped and an unowned From is sent as the account's address
rather than failing.
- REST: tokens never reach the browser (stripped, `connected` flag
instead); saves keep stored tokens unless client id, tenant or data
center changed; POST connections/{id}/authorize; PEM keeps its line
breaks.
- Importer: WP Mail SMTP / Easy WP SMTP gmail, outlook and zoho,
FluentSMTP gmail and outlook, and Post SMTP gmail_api bring app
credentials only (tokens are bound to the other plugin's redirect
URI), so those imports arrive disabled, flagged to connect.
- UI: setup steps, copyable redirect URI, Save & connect account,
Account card with Reconnect, "Not connected" in the list, and a
toast on return.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ts save loop
- Connection editor: fields set in wp-config.php (MAILYARD_{PROVIDER}_
{FIELD}) render disabled with the constant's name and aren't
required; the page gets the provider => locked-keys map from
Settings::locked_fields().
- Multisite: Options::save_connections() on a subsite under shared
settings writes to the main site's option (with its encryption
setting), so an OAuth token refreshed mid-send updates the shared
connection instead of forking a stale local copy.
- Alerts settings: auto-save fires on user edits only (change counter,
as in Delivery). Keying the save on the form object re-saved after
every hydration. Verified in a Playground: one toggle, one POST.
- Merge of the OAuth work with wp-config constants: connection_config()
applies constants and passes _connection_id; the public connection
shape carries `connected` and `locked`.
Refs #17
Co-Authored-By: Claude Opus 5.5 <[email protected]>
"What it does" now covers the log's search/export/retention, alerts, WordPress email switches, background and offline modes, multisite, credential storage and the importer. Updated the migration and log retention answers. Added an External services entry for the chat webhook: what is posted (site, counts, error text — never contents or recipients) and only when a URL is set. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
ifahimreza
marked this pull request as ready for review
October 5, 2026 23:49
Ran the plugin's suites against WP 7.1.2 this cycle; user approved the bump (2026-10-06). Clears the only Plugin Check error. Refs #17 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This was referenced Oct 6, 2026
ifahimreza
added a commit
that referenced
this pull request
Oct 6, 2026
Refs #17 Co-authored-by: Claude Opus 5.5 <[email protected]>
This was referenced Oct 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #17
What
Mailyard becomes a clean, standalone SMTP plugin at feature parity with Meow Mailer, keeping what Mailyard already does better (multi-provider failover, sender routing, bounce webhooks, deliverability checker, MCP delivery tools). Built in steps; see the checklist in #17.
Landed
Message) — Cc/Bcc/Reply-To from headers (were silently dropped for every provider), custom headers + attachment filenames pass through, one message to all recipients. Offline mode, Background sending (pending → settled afterfastcgi_finish_request), PHP Mail as a real connection (zero-setup backup), Send test names the delivering provider and any backup takeover.mailyard_return_pathfilter).MAILYARD_{PROVIDER}_{FIELD}), optional at-rest encryption (sodium; "Locked" state if salts rotate), settings export/import, empty log.connectedflag).All 11 checklist items of #17 are in.
Why
User decision (2026-10-06): no Pro add-on, SMTP only, simple and modern like Meow Mailer.
How
Message(wp_mail parsing),Importer,Alerts,WP_Emails,Crypto,Checks; six ESP drivers.Options::connections()/save_connections()(encryption choke point);Managerclones a driver per connection (fixes two same-provider connections sharing credentials during failover).Plugin::retire_marketing_purpose()(idempotent): Marketing-only connections are disabled once,purposekeys stripped.release: 1.0.2commit (684f536). Changelog is written against 1.1.0; theVersion:header is not bumped (needs your OK).src/hooks/useLogs.jsis no longer imported (the log page fetches directly); left in place pending removal.Breaking
Public hooks removed (only Mailyard Pro consumed them):
mailyard_admin_submenus,mailyard_admin_enqueue,mailyard_purpose,MAILYARD_SHELL_VERSION.wp.hooks):mailyard.shell.modules,.dashboardWidgets,.settingsSections,.dataPanels,.aiSections.X-Mailyard-Purposeheader ignored. An email to several To addresses is now one message (as core wp_mail sends it), not one per address.New public hooks:
mailyard_send_rescuedaction,mailyard_return_pathfilter.Testing
npm run buildcompiles (2 pre-existing size warnings);php -lcleanTested up to: 7.0 < 7.1(WP 7.1 shipped; fails on main too) — bump needs your OKScreenshots
Retake the
.wordpress.orgscreenshots after merge (6 captions in readme).🤖 Generated with Claude Code