=== Emerge Campaigns ===
Contributors: redigit
Tags: email-campaigns, newsletter, email-marketing, segments, subscribe
Requires at least: 6.0
Tested up to: 7.0
Requires PHP: 8.1
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Self-hosted email campaigns for WordPress. Authored in the block editor, sent through your own mailbox, tracked in your own database.

== Description ==

Emerge Campaigns turns WordPress into a self-hosted email-marketing tool. Build contact lists from your existing WordPress audience, group them into static or dynamic segments, author HTML emails in the block editor, schedule one-off or recurring campaigns, and track opens, clicks, and unsubscribes — all without leaving wp-admin.

Sends dispatch through WordPress's standard `wp_mail()` function, so the plugin works with whatever mail transport your site is already configured for — host SMTP, a transactional service plugin, or PHP's default mail.

= Key features =

* **Contacts from your existing WordPress audience** — daily background sync pulls in WordPress users, comment authors, WooCommerce customers, Easy Digital Downloads customers, Jetpack contact form submissions, Jetpack Subscriptions, MailPoet subscribers, Newsletter Plugin subscribers, Contact Form 7 (via Flamingo) submissions, and Gravity Forms entries. Each source is opt-in from the settings screen. Manual add is supported from the Contacts screen.
* **Static and dynamic segments** — static segments are manually-curated lists. Dynamic segments are rule-based (status, source, subscribed-at) and re-evaluated at send time, so membership stays current as new contacts arrive.
* **Block-editor email templates** — author emails using Gutenberg, including Query Loop blocks to pull recent posts into newsletters. Mustache-style merge tags (`{{first_name}}`, `{{email}}`, `{{site_name}}`, `{{unsubscribe_url}}`, `{{view_in_browser_url}}`) substituted at send time. Starter newsletter and receipt block patterns included in a dedicated email category.
* **Broadcasts and Newsletters** — two dedicated surfaces over the same engine. *Broadcasts* are one-time sends: draft → scheduled → sending → sent / paused / cancelled, with schedule-for-later, pause/resume in-flight, and duplicate-as-draft. *Newsletters* are recurring series: pick a daily / weekly / monthly cadence and a send time, and a background sweeper spawns each issue automatically, picking up template and segment changes on the parent.
* **Newsletter issue log & smart resend** — every newsletter keeps a read-only Issues log (the original send plus each recurring issue, numbered to match the subject line), with pause/stop controls for an issue that's mid-send. An opt-in "send only if content changed" gate re-renders the template each run and skips the send when nothing meaningful changed — nonces, per-render ids, and insignificant whitespace are normalized out so only genuine content changes trigger a new issue.
* **Background batched sending** — Action Scheduler chained batches with a configurable per-minute throttle (default 2/min ≈ 120/hour) to stay within mail-provider rate limits. Action Scheduler is bundled — no separate install required.
* **Resilient failure handling** — syntactically invalid email addresses are skipped per-recipient rather than failing the whole campaign. When the underlying mail transport breaks (N consecutive `wp_mail()` failures, configurable in Settings → Sending), the campaign auto-pauses and resets failed sends back to pending so they aren't lost.
* **Retry failed sends** — one-click button on the campaign edit screen re-queues transport failures once delivery is healthy again. Legitimate skips (unsubscribed, missing contact, invalid address) are not re-queued.
* **Emerge Subscribe — public-facing signup form** — accept new subscribers in three interchangeable ways: a `[emerge_campaigns_subscribe]` shortcode, an "Emerge Subscribe" block in the block editor, or an "Emerge Subscribe" classic widget. Same configuration shape across all three (heading, description, button label, optional first/last name fields, optional consent text). Submissions are tagged with the `emerge_subscribe` source and recorded with explicit consent so they're identifiable in segment rules.
* **Progressive-enhancement AJAX submit** — the subscribe form posts to a REST endpoint via vanilla JavaScript and renders the result inline with no page reload. Falls back cleanly to a classic POST + redirect path when JavaScript is disabled or fails to load. Same nonce, same validation, only the transport differs.
* **Spam controls on the subscribe form** — nonce verification, hidden honeypot field, and a per-IP rate limit (default 5/hour, filterable).
* **Test sends and preview** — send a test to any address from the campaign edit screen with full merge-tag substitution. Preview templates in the browser before assigning them to a campaign.
* **Per-recipient tokens** — every send gets a unique token used for unsubscribe, view-in-browser, open-pixel, and click-tracking URLs. No shared secrets, no guessable URLs.
* **RFC 8058 one-click unsubscribe** — `List-Unsubscribe-Post` headers so Gmail and Apple Mail show a native unsubscribe button.
* **Built-in analytics** — open pixel, click redirect, and unsubscribe events recorded locally in your own database. Per-campaign stats (sent, opens, unique opens, clicks, unique clicks, unsubscribes, rates) and per-contact send history visible inside wp-admin.
* **Privacy-conscious tracking** — IP addresses are stored as a salted SHA-256 hash, never in the clear. Open pixel is opt-out via the wrapper filter.
* **Bounce & complaint handling** — when the companion Emerge Mail plugin is active, Emerge Campaigns consumes its `emerge_mail_bounce_received` events and reflects them onto contacts: hard bounces and complaints are suppressed (dropped from the mailable audience and hidden from the default Contacts view), soft bounces are marked but kept mailable. Optional, guarded cleanup on a hard bounce can delete a linked WordPress user (reassigning their content) and reassign the address's anonymous comments to a fallback user.
* **Authenticated subscribe webhook** — besides the public form, an API-key-protected REST endpoint (`POST /wp-json/emerge-campaigns/v1/webhook/subscribe`, `x-api-key` header) lets external systems add contacts programmatically. Generate or revoke the key from Settings; webhook signups are tagged with the `webhook` source.

= How it works =

When you activate the plugin, it creates its own custom database tables for contacts, segments, campaigns, sends, and events. A daily background job populates your contacts from whichever sources you've enabled under Settings.

You author email templates in the block editor. When you build a campaign, you pick a template and a segment; on send, the segment is snapshotted into per-recipient send records (one per recipient, idempotent so re-runs never duplicate). A background worker picks up batches of pending sends, renders the template with each recipient's merge context, dispatches via `wp_mail()`, and records the result.

Recurring campaigns are driven by a small sweep job: at each interval (daily / weekly / monthly), the sweeper duplicates the parent campaign as a fresh child and queues it for send. The parent stays editable, so changes to the template or segment propagate to the next occurrence.

Public signups via the subscribe shortcode / block / widget are inserted directly into the contacts table with explicit consent and the source `emerge_subscribe`, so they show up in the Contacts list table and can be targeted by segment rules.

Tracking is opt-out via the wrapper filter. By default, every email includes a 1×1 pixel that hits `?emerge_open=<token>` when loaded, and rewritten link targets that hit `?emerge_click=<token>&url=<encoded>` before redirecting to the real destination.

= What this is not =

* Not a SaaS — nothing is hosted off your site. All data, including tracking events, lives in your database.
* Not an SMTP plugin — Emerge Campaigns hands sends to WordPress's `wp_mail()` and lets your existing mail configuration take it from there.
* Not a transactional-only tool — `wp_mail()` calls from other plugins (password resets, order receipts) are not intercepted. This plugin is for outbound campaigns you compose and send yourself.

== Installation ==

1. Upload the `emerge-campaigns` folder to `/wp-content/plugins/`, or install via the Plugins screen.
2. Activate the plugin from the Plugins menu.
3. Action Scheduler is bundled — no separate install is needed for reliable background sending.
4. Visit **Emerge Campaigns → Settings** and pick which contact sources to sync from. Then visit **Emerge Campaigns → Contacts** — the first sync runs an hour after activation; click **Sync now** to populate immediately.
5. Author your first template under **Emerge Campaigns → Templates** and build a campaign under **Emerge Campaigns → Campaigns**.
6. To accept public signups, drop the `[emerge_campaigns_subscribe]` shortcode on a page, add the "Emerge Subscribe" block in the editor, or add the "Emerge Subscribe" widget to a sidebar.

== Frequently Asked Questions ==

= How do sends actually get delivered? =

Emerge Campaigns dispatches every send via WordPress's `wp_mail()` function — the same function any other WordPress plugin uses to send email. Whatever transport your site is already configured for (host SMTP, a transactional-email plugin, or PHP's default mail) handles the actual delivery. Deliverability depends on that configuration.

= Where do contacts come from? =

A daily background sync pulls from whichever sources you enable in Settings: WordPress users, comment authors, WooCommerce customers, Easy Digital Downloads customers, Jetpack contact form submissions, Jetpack Subscriptions, MailPoet subscribers, Newsletter Plugin subscribers, Contact Form 7 submissions (via Flamingo), and Gravity Forms entries. You can also add contacts manually from the Contacts screen, and accept self-signups via the Emerge Subscribe form (shortcode, block, or widget).

= What's the difference between static and dynamic segments? =

A **static segment** is a manually-curated list — pick the contacts you want, save the segment, those contacts are members until you remove them. A **dynamic segment** is a rule definition (e.g. "source = woocommerce AND subscribed_at >= 2026-01-01") evaluated at send time, so membership stays current as new contacts arrive.

= What's the difference between a Broadcast and a Newsletter? =

A **Broadcast** is a one-time send — compose it, pick an audience, and send now or schedule it for later. A **Newsletter** is a recurring series: you choose a daily / weekly / monthly cadence and a time of day, and a background sweep spawns each issue automatically. Newsletters have their own menu and a read-only Issues log showing every issue that's gone out. Both are built on the same engine; they're just separated so the one-off and ongoing workflows stay uncluttered.

= Can I send recurring newsletters? =

Yes — that's what the Newsletter surface is for. Set a daily, weekly, or monthly cadence with a send time; a background sweep job spawns the next issue as a fresh child campaign at each interval. The parent stays editable, so changes to the template or segment propagate forward. By default a new issue is only spawned when the rendered content has actually changed since the last one (an opt-out "send only if content changed" checkbox) — ideal for newsletters that pull dynamic content like recent posts. You can pause or stop the series at any time.

= What happens when an address bounces or reports spam? =

If the companion Emerge Mail plugin is delivering your mail, it reports bounces and complaints back to Emerge Campaigns. A hard bounce or spam complaint suppresses the contact — it's dropped from the mailable audience and hidden from the default Contacts view — while a soft bounce is marked but kept mailable. On a hard bounce you can optionally (off by default) delete a linked WordPress user, reassigning their content, and reassign the bounced address's anonymous comments to a fallback user. Without Emerge Mail, this is simply inactive.

= Can I use Query Loop blocks in email templates? =

Yes. Templates render through the same `the_content` filter pipeline as front-end posts, so Query Loop and any other block that works in posts works in emails. This is especially useful for newsletter templates that pull in recent posts dynamically.

= How do I add a subscribe form to my site? =

Three ways, all interchangeable: drop the `[emerge_campaigns_subscribe]` shortcode anywhere on a post or page; add the "Emerge Subscribe" block in the block editor; or add the "Emerge Subscribe" widget to a sidebar or block-based widget area. All three render the same form, accept the same options (heading, description, button label, optional first/last name fields, optional consent line), and submit to the same endpoint. Submissions land in the Contacts table with source `emerge_subscribe`.

= Does the subscribe form work without JavaScript? =

Yes. With JavaScript enabled, submissions go through a small vanilla-JS module that posts to a REST endpoint and renders the result inline with no page reload. With JavaScript disabled or blocked, the same form falls back to a classic POST + redirect that surfaces the same result inline on the next render. Same nonce, same validation, only the transport differs.

= How does unsubscribe work? =

Every send has a unique token. The token is used to build a per-recipient unsubscribe URL placed in both the email body (`{{unsubscribe_url}}`) and the `List-Unsubscribe` header. Mail clients that support RFC 8058 (Gmail, Apple Mail) show a native one-click unsubscribe button that POSTs to the URL without a browser round-trip.

= How are opens and clicks tracked? =

Opens are tracked via an invisible 1×1 pixel placed at the bottom of every email. Clicks are tracked by rewriting `<a href>` targets to a redirect URL on your site, which records the click and 302-redirects to the real destination. Tracking can be disabled site-wide by replacing the wrapper template via the `emerge_campaigns_email_wrapper` filter.

= How is recipient privacy protected? =

IP addresses are never stored in the clear. When a recipient opens an email or clicks a link, their IP is salted (with `wp_salt('auth')`) and SHA-256 hashed before it's recorded with the event. The hash is stable per-site (so we can count unique recipients) but the raw IP can't be recovered. User agents are stored verbatim for delivery diagnostics, capped at 255 characters.

= What happens if a send fails? =

The send row is marked `failed` with the error stored alongside it. Failed sends show on the campaign edit screen with recipient email and error reason — up to the 50 most recent. The campaign keeps sending; one failure doesn't pause the whole batch.

Syntactically invalid email addresses and per-recipient bounces are recorded as terminal failures (`invalid_email`, `recipient_bounced`) and skipped — they don't pause the campaign. But if N consecutive `wp_mail()` calls return false from the same transport (default 10, configurable in Settings → Sending, set to 0 to disable), the campaign auto-pauses and the burst of failures is reset to pending so nothing is lost. A **Retry failed sends** button on the campaign edit screen lets you re-queue them once delivery is healthy.

= Can I throttle sends to stay within mail-provider rate limits? =

Yes. Set **Maximum sends per minute** on **Emerge Campaigns → Settings**. The send worker clamps each batch to the remaining budget and defers to the start of the next minute when exhausted. Default is 2 (≈ 120/hour); set to 0 for unlimited. Smaller per-minute batches keep each Action Scheduler dispatch short and resilient to provider latency spikes.

= Is `wp_mail` modified or replaced by this plugin? =

No. Campaigns are dispatched via `wp_mail()` like any other plugin's email, so transactional `wp_mail()` calls from elsewhere in WordPress are unaffected.

= How do I remove all data on uninstall? =

Deleting the plugin from the Plugins screen runs `uninstall.php`, which drops all six custom tables, deletes plugin options, and clears all scheduled jobs.

== External services ==

Emerge Campaigns does not contact any external service. All sending, tracking, and storage stays on your WordPress site.

Sends are dispatched via WordPress's `wp_mail()` function. Whatever delivery mechanism your site is configured for — PHP's default mail, host SMTP, or another plugin that hooks `wp_mail` — handles the actual transport. Emerge Campaigns does not call any third-party API directly.

== Screenshots ==

1. Contacts list table with status/source filters and a per-row edit screen showing segments and send history.
2. Segment edit screen — static contact picker and dynamic rule builder.
3. Template edit screen — block editor with Subject and Preheader meta box plus Send-test and Preview controls.
4. Broadcasts list table (one-time sends) with progress column and row actions.
5. Newsletters list table (recurring series) with cadence, plus the read-only Issues log for a series.
6. Campaign edit screen — main form, cadence + send-time controls, test-send block, performance stats, and recent failures with retry.
7. Settings screen with contact source toggles, per-minute send cap, dead-end auto-pause threshold, and bounce handling.
8. Emerge Subscribe form rendered on the front end via shortcode, block, or widget.

== Changelog ==

= 1.0.0 =
First public release.

* Contacts table with daily background sync from WordPress users, comment authors, WooCommerce customers, Easy Digital Downloads customers, Jetpack contact form submissions, Jetpack Subscriptions, MailPoet subscribers, Newsletter Plugin subscribers, Contact Form 7 (Flamingo) submissions, and Gravity Forms entries. Each source toggleable from settings; manual add supported.
* Static segments (manual contact picker) and dynamic segments (rule builder over status / source / subscribed_at), re-evaluated at send time.
* Email templates authored as custom posts in the block editor, with merge tags, starter block patterns (newsletter + receipt), test-send, and preview.
* Broadcasts — one-time sends with the full draft / scheduled / sending / sent / paused / cancelled lifecycle, schedule-for-later, pause/resume in-flight, and duplicate-as-draft.
* Newsletters — recurring series on a daily / weekly / monthly cadence with a send time, spawned automatically by a background sweep. Read-only per-series Issues log with pause/stop controls for an in-flight issue, and an opt-in "send only if content changed" gate that uses normalized-render hashing so nonces, per-render ids, and whitespace never trigger spurious issues.
* Background batched send worker via bundled Action Scheduler, with a settings-configured per-minute throttle (default 2/min ≈ 120/hour).
* Per-recipient skip codes (`invalid_email`, `recipient_bounced`) so per-contact data problems don't pause healthy campaigns.
* Dead-end auto-pause: when N consecutive transport failures occur (default 10, configurable), the campaign auto-pauses and the burst of failures resets back to pending. One-click "Retry failed sends" re-queues transport failures once delivery is healthy.
* Bounce & complaint handling: consumes the companion Emerge Mail plugin's `emerge_mail_bounce_received` events — hard bounces and complaints suppress the contact, soft bounces are marked but kept mailable — with optional, guarded hard-bounce cleanup (delete a linked WordPress user with content reassignment; reassign the address's anonymous comments to a fallback user).
* Per-recipient send tokens for unsubscribe, view-in-browser, open-pixel, and click-tracking URLs. RFC 8058 `List-Unsubscribe` / `List-Unsubscribe-Post` headers for native one-click unsubscribe in Gmail and Apple Mail.
* Open-pixel + click-redirect tracking recorded in your own database; IPs stored only as salted SHA-256 hashes. Per-campaign stats (sent, opens, unique opens, clicks, unique clicks, unsubscribes, rates) and per-contact send history on the admin screens.
* Emerge Subscribe: public signup form available three ways — `[emerge_campaigns_subscribe]` shortcode, "Emerge Subscribe" block, and classic widget — all sharing identical output and configuration, with progressive-enhancement AJAX submit and a no-JS POST + redirect fallback. Spam controls: nonce verification, hidden honeypot, and a per-IP rate limit (default 5/hour, filterable).
* Authenticated subscribe webhook: API-key-protected REST endpoint (`POST /wp-json/emerge-campaigns/v1/webhook/subscribe`, `x-api-key` header) for adding contacts programmatically; webhook signups tagged with the `webhook` source.
* Extension points for integrators: `emerge_campaigns_subscribed`, `emerge_campaigns_bounce_recorded`, `emerge_campaigns_dead_end_paused`, and `emerge_campaigns_email_wrapper`.

== Upgrade Notice ==

= 1.0.0 =
First public release on the WordPress plugin repository.
