=== Quillrush Newsletter Studio for Sendy ===
Contributors: gunjanjaswal
Tags: sendy, sendy-ses, amazon-ses, newsletter, email-marketing
Requires at least: 5.8
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.6.3
Donate link: https://ko-fi.com/gunjanjaswal
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

Newsletter studio for the Sendy + Amazon SES stack. Visual builder, two formats, scheduling, multi-list, auto-fetched lists.

== Description ==

📧 **Quillrush Newsletter Studio for Sendy** turns your WordPress dashboard into a full-blown newsletter creation studio for the **Sendy + Amazon SES** stack. Drag your latest posts into a beautiful responsive HTML email, pick the Sendy lists/segments to ship to, and send via your self-hosted Sendy installation, which forwards every message through Amazon SES. No HTML coding. No external SaaS. No paid tier.

> ⚠️ **Requires a self-hosted Sendy install backed by Amazon SES.** This plugin is the WordPress front-end — it does not replace Sendy or SES. If you don't have Sendy + SES set up yet, see [sendy.co](https://sendy.co/) and [aws.amazon.com/ses](https://aws.amazon.com/ses/).

= ✨ Headline features =

* 🎨 **Visual newsletter builder** — drag posts in, see the rendered email update live.
* 📨 **Two newsletter formats per campaign** — *The Roundup* (subscriber-facing) and *The Insider Brief* (editorial pitch for media & partners).
* 📋 **Auto-fetched Sendy lists & segments** — pulled live via the Sendy API, shown as checkboxes with active subscriber counts. Cached 10 minutes; one-click refresh.
* 🧠 **Remembers your last selection** — the lists you sent to last time are pre-checked next time.
* 🔍 **Infinite-scroll post search** — AJAX-loads posts in batches of 10 as you scroll.
* 🖼️ **Smart hero image** — uses your uploaded banner if present, otherwise falls back to the first post's featured image.
* 📱 **Mobile-first responsive layout** — 2 columns on desktop, single column on mobile (< 600px), auto-height cards.
* 📅 **Three send modes** — Save as Draft in Sendy, Send Immediately, or Schedule for a future date/time.
* 🕐 **Timezone-aware scheduling** — uses your WordPress timezone; shows current server time + zone next to the picker.
* ♻️ **Auto-recovery for overdue campaigns** — if WP-Cron didn't fire on time, overdue scheduled campaigns are auto-sent on next admin page-load.
* 🛠️ **Retry on failure** — failed sends show the exact Sendy error + a one-click Retry Send button.
* ⚙️ **Optional cron trigger** — after each send, optionally hit `scheduled.php?i=BRAND_ID` on your Sendy host so queued campaigns process without a server cron.
* ✍️ **Custom footer block** — extra text/HTML above the footer in a highlighted box. Supports anchor tags + auto `nl2br`.
* 🌐 **Social footer icons** — Instagram, LinkedIn, X (Twitter), YouTube.
* 🗃️ **Campaign history** — every campaign stored as a `qrnss_campaign` custom-post-type entry with Status / Scheduled For / Error columns.
* 🔒 **WordPress-native security** — nonces on every action, `manage_options` capability checks, `wp_safe_redirect()` for all redirects.

= 📨 The two newsletter formats =

🗞️ **The Roundup** — visual hero + 2-column story grid for your subscribers. Uses the "Custom Footer Text" highlighted box.

✉️ **The Insider Brief** — personal greeting, lead paragraph, centered hero with featured image, "What Else We're Seeing" 2-column grid, "Why this matters" callout, "For Media & Collaborations" CTA block, and a centered About Us block. Built for media pitches & partner updates. All copy editable from *Settings → The Insider Brief — Template Texts*.

Pick the format per-campaign on the Create Newsletter page (Design Settings → Newsletter Format). Header and dark footer (logo, social, copyright, unsubscribe) are shared across both formats.

= 📋 Full settings reference =

🔌 **Sendy Connection Settings**

* **Sendy Installation URL** — base URL of your Sendy install (e.g. `https://sendy.yourdomain.com/`).
* **API Key** — from Sendy → Settings → Your API Key.
* **Brand ID (Optional)** — from Sendy → Settings → Your Brand → ID. Required by some Sendy versions and used for auto-fetching lists.
* **Default From Name** — pre-filled into every new campaign.
* **Default From Email** — pre-filled into every new campaign.
* **Auto-Trigger Cron** — checkbox. After sending, hit `<sendy-url>/scheduled.php?i=BRAND_ID` so queued campaigns process without a system cron.
* **Show Article Excerpt** — checkbox. Insert a 20-word excerpt between the post title and the "Read More" button.

🎨 **Footer & Social Settings**

* **Footer Logo URL** — logo shown in the dark footer band.
* **Copyright Text** — footer copyright line. `{year}` is replaced with the current year.
* **Custom Footer Text** — textarea, HTML allowed. Shown in a highlighted box above the footer in *The Roundup*. Newlines → `<br>` automatically.
* **"Read More Articles" Link** — optional link below the post grid.
* **Instagram URL** / **LinkedIn URL** / **X (Twitter) URL** / **YouTube URL** — social icons in footer.

✉️ **The Insider Brief — Template Texts** *(used only by The Insider Brief format)*

* **Greeting** — e.g. `Hi [First Name],`
* **Intro Paragraph** — lead paragraph above the hero story (HTML allowed).
* **Hero Section Label** — small label above the hero (e.g. `Hero Story`).
* **Grid Section Heading** — e.g. `🔍 What Else We're Seeing`
* **"Why This Matters" Heading + Body**
* **Collaboration Heading + Body** — e.g. `📩 For Media & Collaborations` + CTA bullets and contact info (HTML allowed).
* **About Us Heading + Body** — centered About block above the footer.

= 📅 Send modes =

* 💾 **Save as Draft in Sendy** — POSTs to `create-campaign.php` with `send_campaign=0`. Campaign appears in your Sendy dashboard as a draft. WP logs status: `draft`.
* 🚀 **Send Immediately** — POSTs to `create-campaign.php` with `send_campaign=1`. Sendy queues + dispatches via SES. WP logs status: `sent`. If *Auto-Trigger Cron* is on, also hits `scheduled.php?i=BRAND_ID`.
* ⏰ **Schedule** — stored as a `qrnss_campaign` post with status `scheduled`. WordPress registers a one-off `wp_schedule_single_event` for the chosen datetime. When the event fires, the plugin sends via Sendy. Datetime picker is timezone-aware and uses your WP timezone setting.

= 🛟 Failure handling =

Failed sends never silently disappear:

* ❌ Red admin notice at the top of the Campaigns screen.
* 📋 Exact Sendy error in the **Error** column.
* 🔁 One-click **Retry Send** button (CSRF-nonced).
* ♻️ Auto-recovery — overdue scheduled campaigns automatically send on next admin page-load.

= 🗄️ Campaign history (CPT) =

Every campaign is stored as a `qrnss_campaign` custom-post-type entry. Admin columns:

* **Title** — subject line.
* **Status** — `draft` (grey), `scheduled` (yellow), `sent` (green), `failed` (red).
* **Scheduled For** — datetime if scheduled.
* **Error** — Sendy error message if the send failed.

= 🔌 Sendy API endpoints used =

All requests go straight to **your own self-hosted Sendy installation** at the URL you set in Settings. The plugin never talks to any third-party SaaS — Sendy itself fans out to Amazon SES from your host.

* `api/lists/get-lists.php` — fetch lists for the Create Newsletter screen (cached 10 min). Sends: `api_key`, `brand_id`.
* `api/subscribers/active-subscriber-count.php` — per-list subscriber counts. Sends: `api_key`, `list_id`.
* `api/campaigns/create.php` — Save as Draft, Send Immediately, scheduled fire. Sends: `api_key`, `from_name`, `from_email`, `reply_to`, `title`, `subject`, `html_text`, `plain_text`, `list_ids`, `brand_id`, `send_campaign`.
* `scheduled.php` (your Sendy host) — only when *Auto-Trigger Cron* is ticked. Sends: `i=BRAND_ID` query param.

= 🔒 Requirements =

* WordPress 5.8 or higher (tested up to 7.0).
* PHP 7.4 or higher.
* A reasonably recent self-hosted Sendy installation.
* Amazon SES configured inside Sendy (out of scope for this plugin).

== Installation ==

1. Upload the plugin files to `/wp-content/plugins/quillrush-newsletter-studio-for-sendy`, or install via Plugins → Add New.
2. Activate the plugin through the Plugins screen.
3. Open **Quillrush Newsletter** in the admin sidebar.
4. Fill in **Sendy Installation URL**, **API Key**, and (recommended) **Brand ID**. Hit **Save Settings**.
5. Go to **Quillrush Newsletter → Create Newsletter** — your Sendy lists appear as checkboxes with subscriber counts. Build away!

= 🆙 Migrating from a predecessor install =

If you previously ran an earlier release line under a different option/CPT/meta prefix (e.g. `sssb_*` or `pvnss_*`), the plugin auto-migrates everything on first load:

* All matching options → `qrnss_*` (including dynamic list-cache transients)
* Custom-post-type rows → `qrnss_campaign`
* Every legacy post-meta key → `_qrnss_*`
* Any pending legacy `*_send_scheduled_campaign` cron event → `qrnss_send_scheduled_campaign`
* Rewrite rules flushed

Each legacy prefix is guarded by a one-shot `qrnss_migrated_from_<prefix>` flag — runs at most once per install. Existing campaigns, drafts, scheduled sends, list-cache, and settings come across without manual intervention.

Steps:

1. Deactivate the previous plugin in WP Admin.
2. Delete it (your data lives in `wp_options` / `wp_posts` / `wp_postmeta`, safe).
3. Install + Activate Quillrush Newsletter Studio for Sendy.
4. Migration fires automatically on first load. Existing campaigns appear under the new top-level menu.

== Frequently Asked Questions ==

= Why didn't my scheduled campaign send on time? =

WP-Cron only runs on page-loads. The plugin auto-recovers overdue campaigns on next admin page-load (you'll see a green success notice). For deterministic timing on production sites, replace WP-Cron with a system cron:

`define('DISABLE_WP_CRON', true);` in `wp-config.php`, then add a server cron:

`* * * * * wget -q -O - https://yourdomain.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1`

= My campaign is stuck on "Preparing to send..." in Sendy. =

Sendy's own cron isn't running. Either:

* Tick **Auto-Trigger Cron** in plugin settings, OR
* Add a server cron on the Sendy host:

`*/5 * * * * php /path/to/sendy/scheduled.php > /dev/null 2>&1`

= My lists don't appear in Create Newsletter. =

Check **Sendy Installation URL**, **API Key**, and **Brand ID** in Settings. The plugin fetches lists via `api/lists/get-lists.php` which requires the Brand ID on most Sendy versions. Use the **Refresh lists from Sendy** link on the Create Newsletter page to bust the 10-minute cache.

= A campaign failed. How do I retry? =

Open **Quillrush Newsletter → Campaigns** in the admin sidebar. Failed campaigns show the exact Sendy error and a **Retry Send** button. The retry clears the previous error before re-sending.

= Can I customise the editorial copy without touching code? =

Yes. Every line of "The Insider Brief" (greeting, intro, hero label, grid heading, "Why this matters", collaboration block, About Us) lives under **Settings → The Insider Brief — Template Texts**. HTML allowed where appropriate.

= Does it work with WordPress 7.0 / PHP 7.4+? =

Yes. Tested up to WordPress 7.0. PHP 7.4 minimum.

= Where does the plugin send data? =

Only to **your own self-hosted Sendy installation** at the URL you set in Settings. No third-party SaaS. Sendy itself handles the SES handoff from your host.

== Screenshots ==

1. Newsletter Builder — search for posts, drag them into the layout, watch the live preview.
2. Settings → Sendy Connection — Installation URL, API Key, Brand ID, defaults.
3. Settings → The Insider Brief Texts — every editorial line customisable.
4. Campaigns CPT — status colours at a glance (draft / scheduled / sent / failed).
5. Responsive email preview — 2-column desktop → single-column mobile.

== Changelog ==

= 1.6.3 =
* Dropped the jQuery UI datepicker. The scheduling field has been a normal date and time input for a while now, but the plugin was still loading the datepicker library and carrying the old code that drove it, none of which ever ran. One less script on the newsletter screen.

= 1.6.2 =
* Security: every `$_POST['campaign']` field is now sanitized individually before use (subject, from_name, from_email, plain_text, list_id, send_type, schedule_date). The `html_text` field is run through a dedicated email-safe `wp_kses` allowlist (tables, inline styles, images, anchors, headings, lists — but no `<script>`, `<iframe>`, `<form>`, or `on*` event attributes) before it is stored to `post_content` or sent to the Sendy API. New filter `qrnss_email_kses_allowed_html` lets third parties extend the allowlist.
* Required-field validation: subject, from_email, and html_text must be non-empty; send_type must be one of `draft` / `send` / `schedule` or it falls back to `draft`.
* Removed remote-loaded social icons. The footer in the rendered newsletter and the admin preview no longer fetch Instagram/LinkedIn/X/YouTube logos from flaticon.com / wikimedia.org / freepik.com — they now use self-contained styled letter-buttons (`IG`, `in`, `X`, `YT`) so the plugin has zero remote-image dependencies and the newsletter renders identically offline / in restricted email clients.

= 1.6.1 =
* Fix: removed the plugin's own injected "View details" row-meta link to prevent a duplicate entry, since WordPress auto-injects "View details" for wp.org-hosted plugins. Row meta is now `View details | Plugin Support | Contact Developer`.

= 1.6.0 =
* **WordPress 7.0 tested** and audited; PHP minimum bumped to 7.4.
* Added `Requires at least`, `Tested up to`, and `Requires PHP` headers to the main plugin file.
* Added one-time auto-migration on first load (priority 1) for predecessor installs: sweeps every legacy option, custom-post-type row, post-meta key, and pending scheduled cron event to the current `qrnss_*` namespace. Per-prefix guard flag so each migration path runs at most once.
* Added **Support on Ko-fi** action link next to Deactivate on the Plugins screen.
* Added **Plugin Support** (wordpress.org/support/plugin/quillrush-newsletter-studio-for-sendy) and **Contact Developer** (mailto:hello@gunjanjaswal.me) row-meta entries.
* Added "Support the developer" card on the Settings screen with branded Ko-fi button, Plugin Support Forum link, and Contact Developer mailto.
* Donate link set to Ko-fi (https://ko-fi.com/gunjanjaswal).

= 1.5.2 =
* Fix: Infinite scroll in the "Add Posts" panel now actually triggers. The scroll handler was using event delegation which doesn't work for scroll events; it's now bound directly to the results container.

= 1.5.1 =
* Improvement: "About Us" body in The Insider Brief footer block is now centered (heading was already centered).
* Cleanup: Removed all "Buy Me A Coffee" links and the Support card from the settings page, newsletter builder, plugin row meta, and readme files.

= 1.5.0 =
* Feature: "Add Posts" panel now supports infinite scroll. Older posts load automatically as you scroll the results list (10 at a time).
* Internal: `qrnss_search_posts` AJAX endpoint now accepts a `page` parameter and returns `{ posts, page, has_more }`.

= 1.4.x =
* Feature: Auto-fetched Sendy lists with active subscriber counts. Cached 10 minutes; one-click refresh.
* Feature: Plugin remembers the lists used in your last send and pre-checks them next time.
* Feature: Editorial format ("The Insider Brief") added alongside The Roundup. All copy editable from settings.

= 1.3.x =
* Feature: Scheduled sending with timezone-aware datetime picker.
* Feature: Failed-campaign retry and auto-recovery for overdue campaigns.
* Fix: Schedule datetime picker with proper z-index for calendar visibility.
* Fix: Reply-to email correctly passed for all campaign types.
* Fix: Resolved "cURL error 28: SSL connection timeout".
* Security: All inputs properly sanitized and validated.

= 1.0.0 =
* Initial release.

== Upgrade Notice ==

= 1.6.3 =
Removes the unused jQuery UI datepicker from the newsletter screen. No change to how scheduling works.

= 1.6.2 =
Security hardening: per-field POST sanitization + dedicated email-HTML kses allowlist for newsletter content. Remote-loaded social icons replaced with self-contained styled buttons.

= 1.6.1 =
Fixes duplicate "View details" entry on the Plugins screen.

= 1.6.0 =
WordPress 7.0 tested; PHP 7.4 minimum. Adds one-time auto-migration from predecessor installs (settings, campaigns, post-meta, list-cache, and pending scheduled sends), Ko-fi support link, Plugin Support + Contact Developer row meta, and a Support card on the Settings screen.
