Helping Organisations get organised!

Changelog

  • 3.2.0: Emails to one person now go out straight after the page has been sent, instead of waiting for the queue, which on some sites took over an hour. Every email is still written to the queue table and queued in Action Scheduler first; the send happens once the server has closed the connection to the visitor, so no page is slowed, and if it does not happen the queue sends it as before, with the existing claim stopping any double send. An email addressed to one recipient (To, Cc and Bcc together) is sent this way by default, as is a single-mode compose; anything to several people, and every broadcast, stays staged in the queue. A sender can force either behaviour with an X-LH-Email-Delivery: immediate or staged header, which is not passed on, and the lh_email_send_immediately filter can override the choice. At most five emails are sent after any one response (lh_email_max_sends_after_response); any more are left to the queue. On servers that cannot close the connection early, nothing is sent after the response and the queue handles everything.
  • 3.1.1: copy-imap-attachment now declares an output schema describing what it returns: the source reference to pass to lh-mcp-file-abilities, and the details of the attachment or sandbox file it copied to. This completes LH Email’s MCP declarations; every ability in the plugin now declares its annotations, input schema and output schema. No behaviour changes.
  • 3.1.0: LH Email’s MCP abilities now each live in their own class under includes/abilities/, as copy-imap-attachment already did. The thirteen abilities that were registered in one 1,000-line trait (create-draft, create-reply-draft, update-draft, get-compose, list-composes, search-messages, list-imap-connections, list-imap-folders, list-imap-messages, search-imap-messages, get-imap-message, get-imap-headers, get-imap-quota) have moved into one class each, extending a shared LH_Email_Ability_Base, and the trait now only registers the category and loads each class. Their behaviour, names, inputs and permissions are unchanged. Every one of them now declares readonly, destructive and idempotent annotations, so MCP clients can tell reads from writes: the eleven readers are read-only, create-draft and create-reply-draft are non-destructive writes, and update-draft is marked destructive because a supplied field replaces the stored value. Each also declares an output schema describing what it returns, and an input schema that rejects unknown parameters. list-imap-messages’ description now says that only date sorting spans the whole folder, and each description states its required capability.
  • 3.0.2: The email log’s checkbox column reads each row as an object, as the other columns already do, since WP_List_Table allows a row to be an array or an object. No visible change: rows are always objects in practice. This clears the last static-analysis finding in LH Email’s own code.
  • 3.0.1: Coding-standards and small correctness fixes across LH Email, with no change to what it sends or stores. In the email log, a recipient linked to a user account now shows as their name followed by their address in angle brackets; it was double-escaped before, so the address appeared as literal HTML entities. The sort order parameter is now unslashed and sanitised, the list’s pagination figures are passed as integers, and a zero per-page screen option can no longer divide by zero. The inbox message view escapes its header fields where they are printed rather than earlier. WP-Cron is spawned at shutdown through a small void wrapper, spawn_cron_on_shutdown(), after a queued, retried or dispatched email, instead of hooking spawn_cron() directly as an action. An unused debug array that copied all request data on the email log screen is removed, as is a duplicate write_log() in the sender class; logging goes through LH_Email_plugin::write_log() as elsewhere. Translator comments are added or moved onto every translatable string with a placeholder. The connection slug is read and sanitised in one step. Two checks that could never fail are removed from attachment lookup, update-draft only calls wp_update_post() when a subject or body was actually supplied, clean_html() drops an unreachable branch, parse_url() is replaced by wp_parse_url(), and the activation and deactivation hooks are registered with the two arguments WordPress accepts.
  • 3.0.0: LH Email no longer reads attachments: it now only copies them out of the mailbox, and reading any file, from an email or anywhere else, is done by LH MCP File Abilities. This is a breaking change to the MCP interface. Three abilities are removed: get-imap-attachment (the bytes as base64), get-imap-attachment-as-resource (the same bytes as an MCP resource block) and get-imap-attachment-text (text extraction from .docx, plain text, PDF and spreadsheets). Their replacements are lh-mcp-file-abilities get-file-as-resource and get-file-text, which also add describe-file, get-file-rows, search-file and get-file-image, the last of which renders scanned documents and photos that no text extractor can read. save-imap-attachment is renamed copy-imap-attachment, and its visibility input is replaced by destination: private (the default, as before) and media (formerly visibility public) file the attachment into the media library as before, and the new sandbox destination writes it to the caller’s own LH MCP Sandbox directory for reading without keeping it, needing LH MCP Sandbox 1.4.0 or later. Every destination now returns a source reference that LH MCP File Abilities accepts directly, so an attachment is read in two calls: copy-imap-attachment, then for example get-file-text with the returned source. The attachment’s provenance is now returned as origin rather than source, and its title no longer carries WordPress’s Private: prefix. Attachments saved by earlier versions are still recognised, so a repeat copy returns them rather than a duplicate. get-imap-message’s description now points to copy-imap-attachment. The attachment and spreadsheet extraction classes are deleted, and the PDF and spreadsheet libraries (smalot/pdfparser, prinsfrank/pdfparser, phpoffice/phpspreadsheet) are removed from Composer, leaving webklex/php-imap as the only dependency. Sending, queuing, the email log and IMAP reading are unchanged.
  • 2.13.2: lh-email no longer loads its Composer dependencies on every request. LH_Email_IMAP and LH_Email_Attachment_Extract each required vendor/autoload.php as soon as their files were parsed, so every request on every site in the network loaded the 500 KB Composer classmap and ran its eager files autoloads: the Symfony polyfills and the Illuminate global helper functions such as collect() and data_get() that webklex/php-imap depends on. That cost OPcache memory and per-request work, and defined those global functions for every other plugin, where a different vendored Illuminate version could have collided with them. The autoloader is now loaded on first use only: LH_Email_IMAP::is_available() loads it before the IMAP client is created, and LH_Email_Attachment_Extract loads it before PDF text extraction. Nothing else in the network calls those helpers or the PHP 8.5 polyfilled functions, which was checked before this change. The IMAP abilities, the Inbox screen, attachment text extraction and save-imap-attachment work as before. This release also records a change made on 20 September 2026 without a version: the on-disk readme.txt, readme.md and CHANGELOG.md were removed after the reconstruction check showed that their content is covered by the portfolio entry and its changelog entries, since both readmes are now compiled from the portfolio when the plugin is pushed or zipped.
  • 2.13.1: Shows the sending status introduced in 2.13.0 everywhere statuses are listed. The email log list table has a Sending filter view and shows Sending as the status label, the single email view shows Sending as its status, and the search-messages MCP ability accepts lh_email_sending as a status filter (it was previously rejected by the schema and ignored by the query). A record normally only holds this status for the few seconds while it is being sent; one that stays in it longer was interrupted mid-send and is marked failed on the next hourly wakeup.
  • 2.13.0: Fixes duplicate sending of queued emails. Recipients of a broadcast could receive the same message two or three times, because nothing stopped more than one queue runner from sending the same record: each runner checked that a record was still queued, then spent several seconds in wp_mail(), and only then marked it sent, so any runner that read the record in that window sent it again. It was made worse on a network: the messages table is network-wide, but the hourly wakeup cron runs on every site and each one drained the whole network’s queue, so several sites’ wakeups and the per-record async actions could be working through the same broadcast at once. Each record is now claimed before it is sent, with a single conditional database update that moves it from queued to a new sending status; only the runner that makes that change sends the message, and any other runner skips it. Each site’s wakeup now only processes its own site’s records. A record left in sending for more than 30 minutes (only possible if the process died mid-send, so it may or may not have been delivered) is marked failed with an explanatory error on the next wakeup rather than being retried automatically, so it can be reviewed and retried by hand from the email log without risking another duplicate.
  • 2.12.0: Adds the save-imap-attachment MCP ability, which files one IMAP attachment into the current site’s media library server-side and returns the attachment ID, so registration forms, invoices and signed documents can be kept rather than just read, without the file ever passing through the MCP response. Attachments are private by default: the file is written straight into the LH Private Uploads directory above the webroot (never into public uploads first) and is served only through its access-checked proxy URL; visibility public must be passed explicitly, and a private save is refused if LH Private Uploads is not active on the site. The file type is checked the same way the normal uploader checks it, so anything WordPress would refuse is refused. The source (connection, folder, UID, part, Message-ID, sender, date, subject) is stored in attachment meta, and a repeat call for the same message and part returns the existing attachment instead of saving a second copy. Optional title and parent post. Requires upload_files and edit_posts. The ability lives in its own class under the new includes/abilities directory, the first lh-email ability to follow the one-file-per-ability layout, and the get-imap-attachment description now points to it.
  • 2.11.1: Spreadsheet extraction improvements found by testing on a real team registration form. Highlighted cells are now marked: a cell with a solid, patterned or gradient fill other than white gets [highlighted #RRGGBB] after its value (or just [highlighted] when the colour cannot be read), since forms ask for a choice such as the playing day or grade to be marked by highlighting it, and a fill is not part of the value. Cells that only look blank no longer come through as values: non-breaking and other Unicode spaces are treated as ordinary spaces, runs of spaces collapse to one, and a cell holding nothing but date separators (an unfilled date field such as ” / / “) counts as empty. Also passes IOFactory::class to ReflectionClass rather than a plain string, which fixes a static-analysis finding.
  • 2.11.0: Adds spreadsheet support to get-imap-attachment-text: .xlsx, .xlsm, .xls and .ods attachments are now read server-side using phpoffice/phpspreadsheet (a new Composer dependency), through a new LH_Email_Spreadsheet_Extract class that plugs into the existing extractor filters. Each sheet becomes a section headed with its name, with one line per non-empty row and each cell written as its reference and value, so form layouts stay readable. Dates are detected from the file’s own number formats and written in ISO format, percentages are converted, formulas give their last saved result without recalculation, other numbers are written raw, and merged ranges are listed. Includes a zip-bomb guard and per-sheet row and column caps. The get-imap-attachment and get-imap-attachment-text descriptions now list spreadsheets. Also: plugin_version() now reads the Version header, the upgrade routine is keyed on a new schema_version() rather than the plugin version (existing installs carry their stored version over, so no routine reruns), and Requires PHP is raised to 8.2, which PhpSpreadsheet needs.
  • 2.10.1: Migrate write_log() in lh-email.php and includes/lh-email-sender.php to the canonical LH convention: add return_plugin_debugging_status() (default true) and rewrite write_log($log, $level=’info’) to emit ‘Level: [namespace] message’ via wp_json_encode for arrays/objects. lh-email-sender.php’s standalone LH_Email_sender_class also gets a new return_plugin_namespace() delegating to LH_Email_plugin::return_plugin_namespace(). Bump plugin version 2.10.0 -> 2.10.1 (header + plugin_version()). No other code changes.
  • 2.10.0: Added the create-reply-draft MCP ability, which creates a compose draft threaded into an existing conversation. It fetches the source message by its IMAP UID and fills in the subject (Re: without doubling an existing prefix), In-Reply-To and References. The recipient defaults to the original sender and can be changed, and the original can optionally be quoted below the reply. Single mode only. If the source message cannot be loaded or has no Message-ID, the draft is refused rather than created unthreaded. Single-mode sending now includes these threading headers; broadcast sending never does.
  • 2.9.9: Added the get-imap-attachment MCP ability, which fetches one attachment from an IMAP message, base64-encoded, by the part number get-imap-message already lists. It leaves the message unread, and refuses attachments over a filterable size limit (default 5 MB) rather than truncating them, checking both the reported and the actual size. Verified against live Gmail data.
  • 2.9.8: Removed the Customize menu item (BuddyPress’s email template customiser), which was not used. (Original release date not recorded.)
  • 2.9.7: Menu fixes: Email Templates now sits after LH Email’s own screens and before Situations and Customize, instead of at the very bottom, and the Emails menu is now highlighted when viewing the Situations screen. (Original release date not recorded.)
  • 2.9.6: Fixes to the 2.9.5 menu merge: Email Templates appeared at the top of the menu, the Situations screen was missing, and the Customize link led to a missing page. Situations and Customize are now added to the Emails menu directly rather than moved there, and Email Templates is placed at the end. (Original release date not recorded.)
  • 2.9.5: BuddyPress’s separate Emails menu is merged into LH Email’s own Emails menu. Its list screen is relabelled Email Templates to tell it apart from All Emails, the sent message log, and its Add New item is removed, since templates are made by editing existing ones. (Original release date not recorded.)
  • 2.9.4: BuddyPress emails can now be edited in the block editor. The BuddyPress email post type and its Situations taxonomy are now managed here instead of in lh-crm, with REST support enabled, and Situations gets its own admin screen and a column on the email list. (Original release date not recorded.)
  • 2.9.3: Fix: the IMAP unavailable notice on the Inbox screen still told admins to ask their host to enable PHP’s imap extension. It now points to the Composer dependency, which is what is actually missing when IMAP is unavailable. (Original release date not recorded.)
  • 2.9.2: Fix: get-imap-quota returned empty usage and limit figures, because the new IMAP library returns the raw server response. The quota lines are now read correctly, matching the results from before the library change. This completes the IMAP library migration, with every IMAP ability confirmed working against live Gmail data. (Original release date not recorded.)
  • 2.9.1: Fixes after the IMAP library change. Listing and searching messages failed with an empty search command. The unread check always reported messages as read. Browsing marked messages as read on the server; the client now fetches with the peek option so nothing is ever marked read. Gmail search was rejected and now works for date and sender queries. Gmail accounts now also list the [Gmail] container folder, which is cosmetic only. (Original release date not recorded.)
  • 2.9.0: IMAP now runs on the webklex/php-imap library, installed with Composer, instead of PHP’s imap extension, which is unmaintained, no longer bundled with PHP 8.4 and was behind the 2.8.4 crash. Gmail search now works. Messages are still left unread. Sorting by date is done by the server; sorting by sender, subject or size now applies to the current page only. The free-text search query is parsed into individual criteria rather than passed through whole. (Original release date not recorded.)
  • 2.8.4: Fix: listing folders crashed against Gmail, in both the list-imap-folders ability and the Inbox screen. Folders are now listed after opening INBOX, the same way every other IMAP call connects. (Original release date not recorded.)
  • 2.8.3: The Connections screen now has SMTP and IMAP tabs. IMAP connections take host, port, encryption (SSL on port 993 by default), username and password. Fixed a nonce check on connection deletion, and delete nonces are now scoped to the connection type so SMTP and IMAP connections with the same slug cannot collide. (Original release date not recorded.)
  • 2.8.2: Fix: BuddyPress emails were sent as plain text with visible HTML markup, because BuddyPress calls wp_mail() without headers. The HTML content type is now forced for BuddyPress emails, and the rendered BuddyPress template is sent as is. (Original release date not recorded.)
  • 2.8.1: The dispatch meta fields on compose emails (dispatched at, recipient count, queued count) are now exposed read-only in the REST API, as collaborative editing needs, while REST writes to them stay blocked. (Original release date not recorded.)
  • 2.8.0: IMAP support. New Emails, Inbox screen with a connection selector, message list with unread indicator, and message view with a reply button. Connections are opened read-only, so browsing never changes the mailbox. New MCP abilities: list-imap-connections, list-imap-folders, list-imap-messages, search-imap-messages (with Gmail search), get-imap-message, get-imap-headers and get-imap-quota. Shows a notice when IMAP is unavailable or no connection is set up. (Original release date not recorded.)
  • 2.7.16: Removed temporary debug logging added in 2.7.13 to 2.7.15. (Original release date not recorded; 2.7.13 to 2.7.15 were debugging builds with no separate notes.)
  • 2.7.12: Fix: the email log’s delete, retry and cancel actions are now handled before the page renders, with a proper referer check, and the remaining uses of the old action parameter were renamed. (Original release date not recorded.)
  • 2.7.11: Fix: the email log’s row actions clashed with WordPress’s reserved action parameter on admin pages; the parameter is now named lh_action. (Original release date not recorded.)
  • 2.7.10: Fix: the delete, retry and cancel row actions on the email log failed because the nonce was altered by sanitisation before it was checked. (Original release date not recorded.)
  • 2.7.9: Backwards compatibility: added the lh_membership_return_site_email_domain filter (previously provided by lh-multiple-email-accounts), which prefixes the site domain with mail. (Original release date not recorded.)
  • 2.7.8: SMTP errors such as a refused connection or failed login are now caught and recorded in the message’s error detail. (Original release date not recorded.)
  • 2.7.7: Email is now sent over SMTP using connections from LH Connections: the connection whose slug matches the From address is used, then the site default, then plain mail(). The connection’s From name and address always apply when one is found, and a PHPMailer error on an SMTP connection marks the message as failed. (Original release date not recorded.)
  • 2.7.6: Added From Name and From Email Address fields to SMTP connections. (Original release date not recorded.)
  • 2.7.5: Added an Emails, Connections screen for managing site-scoped SMTP connections, and a default-connection setting. All Emails screens now require the promote_users capability (previously edit_users). Shows a notice rather than failing when LH Connections is not active. (Original release date not recorded.)
  • 2.7.4: Added a Settings screen for choosing the default SMTP connection (requires LH Connections). (Original release date not recorded.)
  • 2.7.3: Custom email store tables with an actor registry. New compose post type with single and broadcast modes. MCP abilities: create-draft, update-draft, get-compose, list-composes and search-messages. (Original release date not recorded.)

If you like this plugin please consider making a donation to further development