# 1 Mailshot Use and comply with `requirements/ui_design.md`. ## 1.0 Environment Baseline * All writable mailshot tables and mailshot asset binaries are stored in the database named by `.env` variable `MAILSHOTS_REMOTE_MYSQL_DB`. * This includes `mailshots`, `mailshot_queries`, `mailshot_attachments`, `mailshot_last_run`, and any mailshot asset tables created for PDF/image assets. * In this document, references in the form `MAILSHOTS_REMOTE_MYSQL_DB.` are normative. * Recipient/source data reads used to resolve mailshot recipients and DSL source fields must use `.env` variable `MEMBERS_REMOTE_MYSQL_DB`. * SMTP/IMAP credentials are sourced from WordPress-side per-user mailshot credential storage. * If user credentials are missing, send/run actions must be blocked and the UI must direct users to the WordPress mailshot credentials page. ## 1.1 Purpose Provide a Mailshot feature to: * manage mailshot definitions * preview recipient data and template rendering * test mailshots safely * execute full mailshot runs ## 1.2 Data Model and Dependencies * Use `MAILSHOTS_REMOTE_MYSQL_DB.mailshots` as the source of mailshot definitions. * Use `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries` as the source of recipient/data-source queries. * Send mail using per-user SMTP/IMAP credentials from WordPress-side mailshot credential storage. * If user credentials are missing, block send/run and direct user to the WordPress mailshot credentials page. * Copy successfully sent messages to configured IMAP `Sent` folder. ### 1.2.1 Schema Reference: `MAILSHOTS_REMOTE_MYSQL_DB.mailshots` | Column | Type | Null | Key | Notes | | -------------------------- | ---------------- | ---- | --- | ----------------------------------------------------------------------------------- | | `id` | `int(11)` | NO | PK | Auto-increment | | `Purpose` | `varchar(100)` | YES | | Human-readable mailshot purpose | | `DataSource` | `varchar(100)` | NO | | Data-source name matching `mailshot_queries.name` | | `CC` | `varchar(100)` | YES | | Optional CC list | | `BCC` | `varchar(100)` | YES | | Optional BCC list | | `Subject` | `longtext` | NO | | Message subject template | | `Message` | `longtext` | NO | | Message body template | | `PDFAttachment` | `longtext` | YES | | Optional PDF attachment html template | | `AttachmentNames` | `longtext` | YES | | JSON array of named entries from `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_attachments` | | `PDFFilenameDerivedFrom` | `varchar(128)` | YES | | Optional data-source field used as base filename for generated PDFs | | `ReplyTo` | `varchar(100)` | YES | | Optional reply-to address | | `RecipientEmailField` | `varchar(255)` | YES | | Optional selected data-source field used as recipient email destination | ### 1.2.2 Schema Reference: `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries` | Column | Type | Null | Key | Notes | | ------------- | -------------- | ---- | ------ | -------------------------------------------------------- | | `ID` | `int(11)` | NO | PK | Auto-increment | | `name` | `varchar(100)` | NO | UNIQUE | Data-source name used by `mailshots.DataSource` | | `dsl_text` | `longtext` | YES | | DSL sentence (source of truth) | | `dsl_version` | `varchar(16)` | NO | | DSL version marker; defaults to `v1` | | `sql` | `longtext` | NO | | Legacy column retained; compile target not user-authored | ## 1.3 Navigation and Pages Under `Mailshot`, provide child pages: * `Mailshots` * `Assets` * `Mailshot Recipients Preview` * `Mailshot Test` * `Run Mailshot` * `Download PDF` Under `Mailshot > Assets`, provide: * `Mailshot Data Source > Data Sources` * `PDF Assets` * `Attachments` ## 1.3.1 Attachments Data Model Use `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_attachments` as reusable uploaded attachments with: * `id` * `name` (unique) * `file_name` * `mime_type` * `file_bytes` * `created_at` * `updated_at` Persist selected named attachments on each mailshot in `MAILSHOTS_REMOTE_MYSQL_DB.mailshots.AttachmentNames` (JSON array of attachment names). ## 1.4 Mailshots Page (`MAILSHOTS_REMOTE_MYSQL_DB.mailshots` CRUD) ### 1.4.1 Data Pane * Display rows from `MAILSHOTS_REMOTE_MYSQL_DB.mailshots`. * In the list/table view, hide internal `id` and display user-facing columns (`Purpose`, `DataSource`, `Subject`). * Provide `Sort by` and direction controls for the list/table view. * Sortable list columns are `Purpose`, `DataSource`, and `Subject`; the `Actions` column is not sortable. * Default list sort is `Purpose` ascending. * Preserve selected sort when editing, saving, duplicating, or deleting from the mailshot list. * Provide editing for all mutable fields. * Track whether the edit form has unsaved changes and warn before closing/quitting an edit with unsaved changes. * Use a separate pane for editing the fields.  Note that the Message field is multiline html-formatted and should be edited using an html edit control.  The design of the page should maximise room for this field * Provide a separate html edit control for `PDFAttachment` (optional template) using the same html editing features as `Message`. * Message and PDF Attachment html editors should open in overlay panes to maximise editing room. * Provide `PDF Filename Derived From` dropdown populated from available data-source fields for the selected `DataSource`. * Persist `PDF Filename Derived From` as `MAILSHOTS_REMOTE_MYSQL_DB.mailshots.PDFFilenameDerivedFrom`. * Provide `Recipient Email Field` dropdown populated from available data-source fields for the selected `DataSource`. * Persist `Recipient Email Field` as `MAILSHOTS_REMOTE_MYSQL_DB.mailshots.RecipientEmailField`. * If `RecipientEmailField` is blank, show an inline warning that the mailshot cannot be used to send a mailshot; PDF-only workflows may leave it blank. * The Data Pane also include a table of mailshots,  width limited to the page width,  with truncation of fields. * Keep editing responsive; do not trigger per-keystroke save/reload. * Data-source pulldowns must be sorted by data-source `name` (ascending). * Show available data-source fields in the editor and allow one-click token insertion into `Subject`, `Message`, and `PDFAttachment`. * Token insertion must generate Twig tokens using `{{ ... }}` syntax (not `m_...` syntax). * Field insert token format default is `{{ field_name }}` where `field_name` is a canonical safe field token derived from the selected source field. * `Subject`, `Message`, and `PDFAttachment` are always rendered through Twig at render/send time. * In editor pane, provide `Attachments` dropdown (initially blank) sourced from `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_attachments.name`. * Provide `Add selected attachment` button to append selected item into mailshot attachment list. * Allow removing previously selected attachments from the mailshot attachment list. ### 1.4.2 Action Pane * Provide `Save` button. * Provide `Quit`/close action for leaving the edit pane; if unsaved changes exist, require confirmation before closing without saving. * Provide `New` button to add a new `MAILSHOTS_REMOTE_MYSQL_DB.mailshots` row. * Provide per-row `Delete` button. * On per-row `Delete`, require confirmation before delete is applied. * Provide per-row `Duplicate` button in the list/table. * `Duplicate` must copy all mailshot definition fields into a new row and assign a unique purpose: * first choice: `Copy of ` * subsequent collisions: `Copy of (2)`, `Copy of (3)`, and so on * blank original purpose fallback: `Copy of Mailshot #` * resulting purpose must fit the `Purpose varchar(100)` column. * After successful duplicate, redirect to the edit modal for the newly-created copy. ### 1.4.3 Validation * Require `DataSource` to match an existing `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries.name`. * Prevent save when `Subject` is blank. * Allow `Message` to be blank for PDF-only workflows. * If `RecipientEmailField` is non-blank, it must be one of the available fields for the selected `DataSource`. * Display validation errors in the information pane. ## 1.5 Template Engine (Twig) * Use Twig as the template engine for mailshot rendering. * All template tokens and expressions use Twig syntax. * Template context is the selected recipient row (plus approved helper functions/filters). * Twig uses strict variables. Undefined variables or invalid expressions must fail rendering with a clear error. * Default escaping policy is: * Twig autoescape is `html` for `Subject`, `Message`, and `PDFAttachment`. * Rendered `Subject` is HTML-entity-decoded before use as an email subject. * Template authors may use Twig filters such as `raw` where unescaped HTML output is required. * Control-flow tags (`{% if %}`, `{% for %}`, `{% set %}`) are supported by Twig in all template fields. * Template context must include both original query field keys (for example `pages.Page`) and canonical safe token keys (for example `pages_page`). * Boolean-like fields whose field name starts with `is`, `has`, or `can`, or whose field name is `selected`, `active`, `enabled`, `disabled`, or `lapsed`, should render as `Yes`/`No` when their values are recognizably boolean-ish. ## Mailshot Data Sources Page This is defined in `requirements/mailshot_data_source.md`. ## 1.6 Mailshot Recipients Preview Page ### 1.6.1 Selection and Query Execution * Provide control to select a data source from `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries`. * On preview request, execute the selected data-source query and display resulting recipient rows. ### 1.6.2 Preview Table * Display columns from a deterministic field contract derived from all DSL-cited sources, plus any additional source-native columns present in preview output. * The deterministic field contract must include all allowed fields for each cited source (built-in or custom). * Provide row count and sample-limit indicator in the information pane. * Provide per-row checkbox selection in the recipients table. * Provide a header-level checkbox to select/deselect all currently visible rows. * Display count of currently selected rows in the information pane/status line. * Persist row selection state for the current browser session only (no database storage). * Session-persisted selection must survive leaving and re-entering the page in the same browser session. * Provide generalized recipient sorting controls (`Sort by` and sort direction) and apply them to the displayed preview rows. * If query returns zero rows, display zero-result message (no error state). * Provide `Download spreadsheet` action. * On `Download spreadsheet`, generate `recipients.xlsx` from rows currently shown in the data pane and download it. * In `recipients.xlsx`, format the header row in bold. * In `recipients.xlsx`, auto-size each column width to fit header/data content. ### 1.6.3 Safety Rules * Preview does not send email. * Preview does not persist changes to recipient-source tables. ## 1.7 Mailshot Test Page ### 1.7.1 Test Setup * Provide control to select a mailshot. * Provide control to select one preview/test recipient row from the resolved query output. * Recipient-row selector must also provide an `All recipients` option. * Provide `Test email address` input for explicit destination override, defaulting to configured `.env` value `MAILSHOT_TEST_TO_DEFAULT` when set. * If `MAILSHOT_TEST_TO_DEFAULT` is unset/blank, initialize `Test email address` as blank. * After successful render-only test, provide an `Open Render Preview` action. * Render preview opens in an overlay/modal and displays rendered `Message` and rendered `PDFAttachment` side by side in sandboxed iframe previews. * Render preview includes rendered `Subject` as modal context. ### 1.7.2 Test Mode: Render Only (No Send) * Provide action `Render Test (No Send)`. * Perform full Twig template rendering for selected row. * Display rendered `Subject`, `Message`, and `PDFAttachment` through the render preview modal. * Render-only UI must not store the full rendered html payload in WordPress options; large rendered payloads should be recomputed after redirect or passed by another size-safe mechanism. * Do not send SMTP mail. * Do not write IMAP `Sent` copy. ### 1.7.3 Test Mode: Send Test Email * Provide action `Send Test Email`. * Require non-blank `Test email address`. * When `Test email address` is blank, block send and show validation error in the information pane. * Use selected test row only for template rendering context. * Send exactly one email to `Test email address` (not to original recipient address). * When `All recipients` is selected, send one rendered test email per recipient row, all to `Test email address`. * Confirmation prompt text must be context-aware: * one-row mode: `Send one test email to the entered address?` * all-recipient mode: `Send emails to the entered address?` * `Render Test (No Send)` must require a specific recipient row (not `All recipients`). * Copy sent test email to IMAP `Sent`. * Display success/failure status and timestamp. * Send-test UI must keep the user on the Mailshot Test page and display same-pane feedback. If the server returns non-JSON or an HTTP error, show an inline error rather than navigating to a WordPress critical-error page. * Apply the configured Download PDF memory limit, when valid, before render/send test actions that may render PDF attachments. ## 1.8 Run Mailshot Page ### 1.8.1 Pre-Run Validation * Require selected mailshot. * Require resolved recipient query. * Block run when recipient query returns zero recipients. * Display validation failures and do not start run when validation fails. ### 1.8.2 Execution Semantics * On `Run Mailshot`, execute recipient query and iterate recipients. * For each recipient row: * perform Twig template rendering * send one email to the recipient destination email * copy successful sends to IMAP `Sent` * Continue processing remaining recipients after per-recipient failures. * Provide action `Run Mailshot to Selected Rows` to run only rows selected in `Review Recipients`. * `Run Mailshot to Selected Rows` must use session-persisted selected recipient keys from `Review Recipients` (same browser session, no database storage). * If no selected rows are available for the selected mailshot data source, block action with explicit validation message. * Run page must include hint text: `Select rows for mailshot in Review Recipients page.` with a link to the Review Recipients admin page. ### 1.8.3 Run Results * Display live or final counters: * attempted * sent * failed * skipped (if applicable) * Provide downloadable or copyable run log in the UI. * Include per-recipient failure reason where available. ### 1.8.4 Last Run Retry Table * Persist the outcome of the most recent run only in `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_last_run`. * Clear `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_last_run` when starting a new `Run Mailshot` execution. * Each row in `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_last_run` must store: * `mailshot_id` * `recipient_key` (stable non-email identifier) * `recipient_key_field` (source field used to derive key) * `recipient_email_last` (most recent resolved destination email) * `status` * `error_message` and/or `warning_message` * `attempt_count` * `last_attempt_at` * Run Mailshot page must display the latest rows for the selected mailshot from `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_last_run`. * Run Mailshot page must provide: * `Retry Failed Sends` button to retry all failed rows from the selected mailshot. * per-row `Retry` button to retry one failed row by recipient key. * Retry must resolve recipient data from the current data source and current template values at retry time (not snapshot payload from original run). * Email address must not be used as the retry key. ## 1.9 Download PDF Page ### 1.9.1 Setup and Validation * Provide control to select a mailshot. * Require selected mailshot. * Require resolved recipient query. * Block generation when recipient query returns zero recipients. * Block generation when selected mailshot has blank `PDFAttachment` template. * Merged PDF generation requires Ghostscript (`gs`) and PHP `exec()` availability. * ZIP generation uses PHP `ZipArchive` when available; if unavailable, it requires a `zip` CLI binary and PHP `exec()` availability. ### 1.9.2 Output Options * Provide action to generate and download individual recipient PDFs as a single `.zip`. * Provide action to generate and download a single merged PDF containing all recipient PDFs. * Use html-to-pdf conversion of rendered `PDFAttachment` content per recipient row. * Downloaded merged PDF filename must be based on the selected mailshot `Purpose`, sanitized to safe filename characters, with suffix `_merged.pdf`. * Downloaded ZIP filename must be based on the selected mailshot `Purpose`, sanitized to safe filename characters, with suffix `_pdfs.zip`. * If `Purpose` is blank or cannot be loaded, use fallback base name `mailshot_`. * PDF rendering must use Dompdf with default font `DejaVu Sans` and font subsetting enabled. * Before Dompdf rendering, normalize contact-symbol characters that commonly fail in PDF fonts: * `📞` and `☎` render as a `☎` symbol pinned to `DejaVu Sans` * `📧` and `✉` render as a `✉` symbol pinned to `DejaVu Sans` * Template authors should still prefer PDF-safe symbols (`☎`, `✉`) over color emoji symbols. ### 1.9.3 Results and Logging * Display generation success/failure status in the UI. * Generate downloadable files directly (no per-row log table required on this page). ## 1.10 Mailshot Queries Page Provide a CRUD page for the `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries` table. ### 1.10.1 DSL Built-in Sources Built-in DSL sources include: * `contacts` * `accounts` * `renewals` `renewals` is a first-class source and must be joinable to: * `accounts` via `renewals.account_id = accounts.ID` * `contacts` through accounts (`renewals.account_id = contacts.Accountid`) ### 1.10.2 DSL Renewal and Membership Filters DSL filters include: * `selected-renewal` * `pending-renewal` * `fen1-contact` * `primary-contact` * `member-or-affiliate-or-parish-council` `selected-renewal` uses saved field values (`renewals.selected = true`). `pending-renewal` filters rows to `renewals.status = 'pending'`. ## 2.0 No-Effect and Error Handling * If `Save` is clicked with no changes, no effect. * If `Quit`/close is clicked with no changes, close without confirmation. * If a selected mailshot references a missing `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_queries` row, block preview/test/run and display error. * If SMTP send fails for one recipient, record error and continue with next recipient. * If IMAP copy fails after successful SMTP send, mark warning for that recipient. * For any DSL sentence, all fields from all cited sources must be available for token insertion and template rendering; missing values may render empty, but fields must not disappear from mailshot field availability. * Twig template syntax errors must not crash the page or run; show clear error identifying mailshot and template field (`Subject`, `Message`, or `PDFAttachment`). * Default behavior on template syntax error is fail-fast for that recipient (skip send for recipient, continue run for others) and record error. * Retry actions must update `MAILSHOTS_REMOTE_MYSQL_DB.mailshot_last_run` rows in-place (incrementing `attempt_count` and updating status/messages/timestamp). ### 2.0.1 IMAP Sent-Copy Contract * Use per-user configured IMAP sent-folder path from WordPress mailshot credentials. * IMAP folder names must be encoded/decoded using IMAP UTF-7 rules. * For each successful SMTP send, attempt a single IMAP append for sent-copy persistence. * IMAP append failure must not change SMTP send outcome; classify as `warning`, not `failed`. * IMAP append retries are not automatic in run loop; retry occurs only via explicit operator retry action. * Sent-copy operations must be idempotent per attempt id; duplicate append for the same attempt id is not allowed. ## 2.1 PDF Assets ### 2.1.1 Purpose Provide reusable image assets that can be inserted into `PDFAttachment` html templates using Twig syntax and resolved at PDF render time. ### 2.1.2 Navigation and Page Under `Mailshot`, provide page `PDF Assets`. ### 2.1.3 Data Model Provide CRUD storage for PDF assets with at least: * `id` (primary key) * `name` (unique asset identifier used in token marker) * `file_name` (original upload filename) * `mime_type` * `file_bytes` (or storage reference) * `width_mm` * `height_mm` * `justification` (`left`, `right`, `in-place`) * `created_at` * `updated_at` ### 2.1.4 PDF Assets Page Behavior * List assets in a table. * Support `Create`, `Read`, `Update`, `Delete`. * Create/update requires image bytes, provided either by file upload or by direct base64 entry. * Edit dimensions in millimetres. * Edit justification (`left`, `right`, `in-place`). * Validate name, filename-derived or explicit MIME type, positive dimensions, allowed justification, and valid base64 image bytes. ### 2.1.5 Mailshots PDF Editor Integration * In the `PDF Attachment` editor overlay, add an insert-asset action with dropdown of available assets. * On insert, place Twig token syntax at cursor position. * Default insert format is `{{ pdf_asset('asset_name') }}`. * Asset names used in inserted tokens must be escaped safely as Twig string literals; the saved asset name itself is not renamed during insertion. ### 2.1.6 Rendering Behavior * On rendering `PDFAttachment` html (Mailshot Test preview/send, Run Mailshot, Download PDF), resolve each inserted PDF asset Twig token to rendered image html for that asset. * Render image using configured dimensions (`width_mm`, `height_mm`) and configured justification. * If template references a missing asset, rendering must fail with a clear error naming the missing asset. * If `pdf_asset()` is unavailable, the asset has no readable bytes, dimensions are invalid, or justification is invalid, rendering must fail with a clear error. * PNG assets require either GD PNG support or ImageMagick `convert` for runtimes where Dompdf cannot consume the PNG bytes directly; otherwise rendering must fail with a clear error. ### 2.1.7 PDF Filename Derivation * For each rendered recipient pdf (download and email attachment), if `PDFFilenameDerivedFrom` is set, use row value from that field as base filename. * Sanitize filename to safe filename characters. * If `PDFFilenameDerivedFrom` is blank or field value resolves blank, use fallback naming strategy. * Ensure filename uniqueness within a generated batch (append numeric suffix as needed). ## 2.2 PDF Assets Decisions * Asset binaries are stored in DB table (`LONGBLOB`) for portability and backup simplicity. * Supported filename-derived MIME types are `jpg/jpeg`, `png`, `gif`, `webp`, `svg`, `bmp`, `tif`, and `tiff`. * Scope/ownership: global asset library, editor-capable users (WordPress `edit_pages`) may CRUD. * Asset naming: unique case-insensitive `name`; renaming does not auto-migrate existing asset insertions in templates and should warn user. * Marker syntax is removed. PDF asset insertion uses Twig syntax. * Helper name is `pdf_asset(name)` as a global Twig helper, not a filter. * Dimension/aspect behavior: fixed width and height in millimetres are emitted as inline image style. * Justification semantics: `in-place` renders inline at marker location; `left` renders a block-aligned element on its own line; `right` floats the image right so surrounding text can fill the space to its left. * Missing asset token behavior: fail rendering with a clear error. * Delete behavior: delete the asset row by id; current implementation does not block deletion for in-use assets. ## 2.3 Mailshot Attachments ### 2.3.1 Page and Layout Under `Mailshot > Assets`, provide page `Attachments`. * Data pane: table of attachments sorted by `name`. * CRUD pane (right): edit `name` and uploaded file. * Provide actions: `Save`, `New`, `Delete`. ### 2.3.2 CRUD and Validation * `name` is required and unique. * Create/update requires file bytes, provided either by file upload or by direct base64 entry. * Supported filename-derived MIME types are `txt`, `csv`, `pdf`, `doc`, `docx`, `odt`, `rtf`, `html`/`htm`, `json`, `xml`, `jpg`/`jpeg`, `png`, `gif`, `webp`, `svg`, and `zip`. * Delete behavior: delete the attachment row by id; current implementation does not block deletion for in-use attachments. ### 2.3.3 Send/Run Behavior * On `Mailshot Test > Send Test Email`, include all selected named attachments. * On `Run Mailshot`, include all selected named attachments for each recipient email. * If a selected attachment name is missing, continue send/run and record warning.