Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion STATUS.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# STATUS

**Tier:** build
**Board:** [PlugPress HQ](https://github.com/orgs/plugpressco/projects/3)

## Last session (2026-10-06) — simple SMTP: Pro removed, Meow Mailer parity (#17, PR #18)
## Last session (2026-10-09) — Settings → SMTP + declutter (#20, PR #21 — awaiting your merge)

- **User ask:** remove the top-level menu, move the page to Settings → SMTP, clean up and declutter the admin UI. Filed as #20; PR #21 is ready, **CI green, NOT merged** (two hard stops below).
- **Menu:** `add_options_page()` at `options-general.php?page=mailyard`, labelled "SMTP" (page title "Mailyard SMTP"); screen id `settings_page_mailyard` (`Settings::SCREEN`). The top-level menu, its 5 submenu entries, the menu icon and the submenu click interceptor are gone. `Settings::url( $route )` is now the one way to link into the app. Old `admin.php?page=mailyard…` URLs 302 there with query args kept, and the `#/…` fragment survives.
- **UI:** the app sidebar became the design system's **topbar** shell (Overview · Connections · Email log · Deliverability · Settings · Help, no icons). Settings' grouped rail became one header + `Tabs` row, with a `SectionIntro` line per section. Dashboard is labelled **Overview**; "Data & danger" is now "Data".
- **Notices:** the send-failure notice shows on the WP Dashboard + Plugins screens only, with one dismiss path (the non-persistent × is gone).
- **CSS gotcha fixed:** `#mailyard-admin * { border-color }` had id specificity and silently overrode every authored `border-*`/`divide-*` colour plus library component borders. It's now `:where(#mailyard-admin) *`, so authored colours (e.g. the blue selected preset chip) show for the first time.
- **QA:** WP Playground via `npx @wp-playground/cli` (needs **node@22**: `PATH=/opt/homebrew/opt/node@22/bin:$PATH`; Node 26 has no fs-ext prebuild) with a scratchpad auto-login mu-plugin. All views + tabs, the legacy redirect, the OAuth-style return, the action link, notice screens + Dismiss, 600/380px widths. The local Studio site (`plugpess.wp.local`) wasn't running. Note: on that site use `studio wp …`, not bare `wp`.
- **Found + filed:** #22, the OAuth error reason never reaches the toast (core's canonical-URL script strips the `message` query arg). This was already happening before #20.

## Earlier session (2026-10-06) — simple SMTP: Pro removed, Meow Mailer parity (#17, PR #18)

- **Direction change (user):** Mailyard is a standalone, simple SMTP plugin — no Mailyard Pro, no marketing layer — at feature parity with [Meow Mailer](https://wordpress.org/plugins/meow-mailer/), keeping Mailyard's own edges (failover chain, sender routing, bounce webhooks, deliverability checker, MCP tools). Scope choice "option b": drop Pro hooks + Marketing purpose, keep everything SMTP.
- **All 11 checklist items of #17 landed via PR #18, squash-merged to `main` as `c0d9d22` with `[skip actions]`, so WordPress.org was NOT synced and still shows 1.0.1: Pro shell/filters + Marketing purpose removed; send pipeline rewritten (`Message` parser — **Cc/Bcc in headers were silently dropped for every provider**, now fixed; one message to all recipients); Offline mode; Background sending; PHP Mail as a real connection; log filters/paging/CSV/retention/top errors; alerts (email unrouted, Slack/Discord/Teams/JSON webhook, weekly summary); WordPress email switches; Return path; importer (WP Mail SMTP/Easy WP SMTP/FluentSMTP/Post SMTP incl. their encryption); wp-config constants + sodium encryption + settings backup; 6 API providers + 4 OAuth (Gmail, M365, M365 app-only, Zoho); multisite shared settings; dashboard setup checks.
Expand Down Expand Up @@ -78,6 +88,9 @@
- Regenerated `languages/mailyard.pot`; earlier full audit: 0 blockers (hardening backlog: mask connection secrets on read, webhook signature verification, React i18n — file as board issues).

## Next up
- **Merge PR #21 (needs your OK):** it touches `readme.txt`, so a plain merge to `main` fires `assets.yml` and syncs the readme (unreleased 1.1.0 changelog) to WordPress.org right away. Squash with `[skip actions]` in the title, as #18 did, unless you want it live. It's also ~520 changed lines. After merge: remove the `in-progress` label from #20 and move the card to Done.
- **#23:** add `'default' => array()` to the optional-input ability schemas. Today a plain Abilities REST call with no input 400s; MCP/Saddle are unaffected. Verified 2026-10-09: on `main` + Saddle 1.5.1 all 4 read-only tools register as `saddle/mailyard-*` and run, and live plugpress.io answers through Saddle MCP.
- **#22:** rename the OAuth `message` return arg (e.g. `mailyard_oauth_message`) in `class-oauth.php` + `Connections.jsx`.
- **Release 1.1.0 when ready:** set `Version:` + `MAILYARD_VERSION` + `Stable tag` to 1.1.0 (changelog already written; header still says 1.0.2), merge, then tag `v1.1.0`. That deploys the code and syncs the readme to WordPress.org. Do the live-send QA below first.
- **Delete Pro branches on origin (blocked for me):** `git push origin --delete feat/merge-pro feat/freemius-parent feat/smtp-ux-pass feat/universal-shell`.
- With reads unblocked: delete the now-unused `src/hooks/useLogs.js`; have `Data_Deleter`/`uninstall.php` also clear `mailyard_network` (site option), the `mailyard_alerted_*` transients and the `mailyard_weekly_summary` cron; consider folding `Conflicts` into the new `Checks`.
Expand Down
26 changes: 13 additions & 13 deletions docs/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Amazon's Simple Email Service. Very low cost at volume ($0.10 per 1,000 emails).
1. In AWS Console → SES → Verified identities, verify your sending domain.
2. Create an IAM user with `ses:SendRawEmail` permission.
3. Generate an access key and secret for that user.
4. In Mailyard → Connections → Add → Amazon SES, paste the key, secret, and region.
4. In Settings → SMTP → Connections → Add → Amazon SES, paste the key, secret, and region.

**Required fields:** Access Key ID, Secret Access Key, AWS Region (e.g. `us-east-1`).

Expand All @@ -48,7 +48,7 @@ Premium transactional email. Known for the fastest delivery and best inbox place
**Setup:**
1. In Postmark → Sender Signatures, add and verify your sending domain.
2. Create a Server and copy its Server API Token.
3. In Mailyard → Connections → Add → Postmark, paste the token.
3. In Settings → SMTP → Connections → Add → Postmark, paste the token.

**Required fields:** Server API Token.

Expand All @@ -65,7 +65,7 @@ Modern email API with a generous free tier (3,000 emails/month free). Simple RES
**Setup:**
1. In Resend → Domains, add and verify your sending domain.
2. In Resend → API Keys, create a key.
3. In Mailyard → Connections → Add → Resend, paste the key.
3. In Settings → SMTP → Connections → Add → Resend, paste the key.

**Required fields:** API Key.

Expand All @@ -78,7 +78,7 @@ Free tier includes 300 emails per day with no monthly cap on contacts.
**Setup:**
1. In Brevo → Senders & Domains, authenticate your domain (adds DKIM and SPF records).
2. In Brevo → SMTP & API → API Keys, create a key.
3. In Mailyard → Connections → Add → Brevo, paste the key.
3. In Settings → SMTP → Connections → Add → Brevo, paste the key.

**Required fields:** API Key.

Expand All @@ -90,7 +90,7 @@ Use any SMTP server: Gmail app passwords, Microsoft 365, Mailgun SMTP, your own

**Setup:**
1. Gather your SMTP host, port, encryption type, and credentials from your provider.
2. In Mailyard → Connections → Add → Custom SMTP, fill in the fields.
2. In Settings → SMTP → Connections → Add → Custom SMTP, fill in the fields.

**Required fields:** SMTP Host, Port (default 587).

Expand All @@ -113,7 +113,7 @@ Uses WordPress's own `wp_mail()` without modification — whatever your hosting

### Priority order

Connections are attempted in priority order (1 = primary, 2 = first backup, etc.). Drag and drop in Mailyard → Connections to reorder them.
Connections are attempted in priority order (1 = primary, 2 = first backup, etc.). Drag and drop in Settings → SMTP → Connections to reorder them.

The **primary** connection handles every email under normal conditions. If it returns an error, Mailyard immediately tries the next enabled connection — still inside the same `wp_mail()` call.

Expand Down Expand Up @@ -144,7 +144,7 @@ The routing engine filters connections by purpose before selecting the chain.

## Email logging

When logging is enabled (Settings → Email logging), every outgoing email is recorded to a custom database table (`wp_mailyard_logs`).
When logging is enabled (Settings → SMTP → Settings → Delivery → Email logging), every outgoing email is recorded to a custom database table (`wp_mailyard_logs`).

### What is logged

Expand All @@ -161,7 +161,7 @@ When logging is enabled (Settings → Email logging), every outgoing email is re

### Viewing logs

Go to Mailyard → Logs. You can:
Go to Settings → SMTP → Email log. You can:
- Filter by status (All / Sent / Failed)
- Search by recipient address or subject
- Page through results (20 per page)
Expand All @@ -170,13 +170,13 @@ Go to Mailyard → Logs. You can:

Logs older than 30 days are deleted automatically via a daily WP-Cron job (`mailyard_daily_cleanup`). This keeps the database from growing unbounded.

To disable logging entirely, go to Settings → Email logging and toggle it off. Existing logs are kept until they age out or you uninstall the plugin.
To disable logging entirely, go to Settings → SMTP → Settings → Delivery and switch Email logging off. Existing logs are kept until they age out or you uninstall the plugin.

---

## Deliverability checker

Mailyard → Deliverability scans your sending domain's DNS records and scores your authentication setup from 0–100.
Settings → SMTP → Deliverability scans your sending domain's DNS records and scores your authentication setup from 0–100.

### What it checks

Expand Down Expand Up @@ -249,7 +249,7 @@ Click **Send test** to send a real email through your live routing chain. Leave

## Connect AI (Abilities API / MCP)

Mailyard registers its delivery tools on the **WordPress Abilities API** (WordPress 7.0+), so an AI assistant can diagnose email problems for you. Control everything in **Settings → Connect AI**.
Mailyard registers its delivery tools on the **WordPress Abilities API** (WordPress 7.0+), so an AI assistant can diagnose email problems for you. Control everything in **Settings → SMTP → Settings → Connect AI**.

### The tools

Expand All @@ -265,14 +265,14 @@ Every tool requires the `manage_options` capability. Credentials (`config`), and

### Permissions

**Settings → Connect AI** has a master switch ("AI access") and a switch per tool. Turning the master off unregisters every Mailyard ability immediately. The four read-only tools are on by default; `mailyard/send-test-email` is OFF by default because it delivers a real email — turn it on deliberately.
**Settings → SMTP → Settings → Connect AI** has a master switch ("AI access") and a switch per tool. Turning the master off unregisters every Mailyard ability immediately. The four read-only tools are on by default; `mailyard/send-test-email` is OFF by default because it delivers a real email — turn it on deliberately.

### Connecting a client

Mailyard exposes abilities but ships **no MCP server** — install a bridge, e.g. the free [WordPress MCP Adapter](https://github.com/wordpress/mcp-adapter). Then:

1. Create an Application Password (**Users → Profile → Application Passwords**).
2. Copy the endpoint from Settings → Connect AI (the "How to connect" guide has copy buttons).
2. Copy the endpoint from Settings → SMTP → Settings → Connect AI (the "How to connect" guide has copy buttons).
3. Add it to your client, e.g. Claude Code:

```bash
Expand Down
7 changes: 4 additions & 3 deletions includes/class-alerts.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function test( string $channel, string $target = '' ) {
*/
public static function webhook_payload( string $url, string $event, string $text, array $extra = array() ): array {
$host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
$logs = admin_url( 'admin.php?page=mailyard#/logs' );
$logs = Settings::url( 'logs' );

if ( preg_match( '/(^|\.)slack\.com$/', $host ) ) {
return array( 'text' => $text . "\n" . $logs );
Expand Down Expand Up @@ -183,7 +183,7 @@ private function claim( string $kind ): bool {

private function notify( string $subject, array $lines, string $event, array $extra ): void {
if ( ! empty( Options::settings()['alert_email'] ) ) {
$this->send_unrouted( $this->recipient(), $subject, implode( "\n\n", $lines ) . "\n\n" . __( 'The email log:', 'mailyard' ) . ' ' . admin_url( 'admin.php?page=mailyard#/logs' ) . $this->footer() );
$this->send_unrouted( $this->recipient(), $subject, implode( "\n\n", $lines ) . "\n\n" . __( 'The email log:', 'mailyard' ) . ' ' . Settings::url( 'logs' ) . $this->footer() );
}
$this->post_webhook( $event, $subject . ' — ' . implode( ' ', array_slice( $lines, 1 ) ), $extra );
}
Expand Down Expand Up @@ -232,6 +232,7 @@ private function site_name(): string {
}

private function footer(): string {
return "\n\n— " . __( 'Sent by Mailyard. Change alerts in Mailyard → Settings → Alerts.', 'mailyard' );
/* translators: %s: URL of Mailyard's alert settings. */
return "\n\n— " . sprintf( __( 'Sent by Mailyard. Change alerts: %s', 'mailyard' ), Settings::url( 'settings/alerts' ) );
}
}
2 changes: 1 addition & 1 deletion includes/class-conflicts.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function maybe_show_notice(): void {
}

$screen = get_current_screen();
if ( $screen && ! in_array( $screen->id, array( 'toplevel_page_mailyard', 'plugins' ), true ) ) {
if ( $screen && ! in_array( $screen->id, array( Settings::SCREEN, 'plugins' ), true ) ) {
return;
}

Expand Down
16 changes: 9 additions & 7 deletions includes/class-failure-notice.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
defined( 'ABSPATH' ) || exit;

/**
* Surfaces email send failures as a dismissible admin notice with a link to the
* log. Records the latest failure (fed by the `mailyard_send_failed` action) and
* Surfaces email send failures as a dismissible admin notice (Dashboard and
* Plugins screens) with a link to the log. Records the latest failure (fed by the `mailyard_send_failed` action) and
* auto-clears it when a later send succeeds or the connection config changes, so
* a fixed setup dismisses the warning on its own — it isn't nagware.
*/
Expand Down Expand Up @@ -82,9 +82,10 @@ public function render(): void {
return;
}

// The log is already on the Mailyard admin page — don't double up there.
// Only where an admin lands — the Dashboard and Plugins screens — not
// on every admin page. Mailyard's own page shows it in its overview.
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( $screen && false !== strpos( (string) $screen->id, 'page_mailyard' ) ) {
if ( ! $screen || ! in_array( $screen->id, array( 'dashboard', 'plugins' ), true ) ) {
return;
}

Expand All @@ -98,7 +99,7 @@ public function render(): void {
$title = isset( $failure['title'] ) ? $failure['title'] : __( 'Email could not be sent', 'mailyard' );
$guidance = isset( $failure['guidance'] ) ? $failure['guidance'] : '';

$log_url = admin_url( 'admin.php?page=mailyard#/logs?status=failed' );
$log_url = Settings::url( 'logs?status=failed' );
$dismiss_url = wp_nonce_url(
add_query_arg( 'mailyard_dismiss_failure', '1' ),
'mailyard_dismiss_failure'
Expand All @@ -109,11 +110,12 @@ public function render(): void {
? sprintf( _n( 'Mailyard: %d email failed to send', 'Mailyard: %d emails failed to send recently', $count, 'mailyard' ), $count )
: __( 'Mailyard: an email failed to send', 'mailyard' );

// One dismiss path: the link (per user, until the next failure). No
// `is-dismissible` × — it only hides the notice until the next page load.
printf(
'<div class="notice notice-warning is-dismissible"><p><strong>%s</strong> — %s %s</p><p><a href="%s" class="button button-secondary">%s</a> <a href="%s" style="margin-left:8px">%s</a></p></div>',
'<div class="notice notice-warning"><p><strong>%s</strong> — %s</p><p><a href="%s" class="button button-secondary">%s</a> <a href="%s" style="margin-left:8px">%s</a></p></div>',
esc_html( $heading . ( $title ? ': ' . $title : '' ) ),
esc_html( $guidance ),
'',
esc_url( $log_url ),
esc_html__( 'View email log', 'mailyard' ),
esc_url( $dismiss_url ),
Expand Down
2 changes: 1 addition & 1 deletion includes/class-oauth.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static function return_url( $result ): string {
$args = is_wp_error( $result )
? array( 'mailyard_oauth' => 'error', 'message' => $result->get_error_message() )
: array( 'mailyard_oauth' => 'ok' );
return admin_url( 'admin.php?page=mailyard&' . http_build_query( $args, '', '&', PHP_QUERY_RFC3986 ) . '#/connections' );
return add_query_arg( rawurlencode_deep( $args ), admin_url( 'options-general.php?page=' . Settings::PAGE ) ) . '#/connections';
}

/**
Expand Down
2 changes: 1 addition & 1 deletion includes/class-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function run_cleanup(): void {
}

public function plugin_action_links( array $links ): array {
$url = admin_url( 'admin.php?page=mailyard' );
$url = Settings::url();
$label = __( 'Settings', 'mailyard' );
array_unshift( $links, '<a href="' . esc_url( $url ) . '">' . $label . '</a>' );
return $links;
Expand Down
Loading
Loading