Repository navigation
Releases: karatektus/pl_mail
Release list
v0.3.8
Image: ghcr.io/karatektus/pl_mail:0.3.8
The image runs PHP 8.4.27, which fixes CVE-2026-109618 and CVE-2026-109619. Every earlier image, v0.3.7 included, ran PHP 8.4.26, which is open to both. One of them can be reached by any multipart request, with no sign-in and no upload form needed, and the advisory considers FrankenPHP susceptible. Exploit code is public, so update. plMail itself is unchanged: the FrankenPHP base image the build starts from now carries the fixed PHP, and this release is that rebuild. No schema change. #280.
v0.3.7
Image: ghcr.io/karatektus/pl_mail:0.3.7
Single sign-on can be the only way in. A new setting under Admin → SSO, Single sign-on only (or OIDC_DISABLE_PASSWORD_LOGIN=true in the environment), hides the password form and makes the server refuse a password sign-in, so everybody comes in through the provider. It is in force only while a provider is set up, so a provider that was removed cannot leave an install with no way in, and the box can be ticked only by an administrator who has signed in through the provider themselves. If the provider is down, php bin/console app:sso:allow-password-login brings the form back. App passwords for mail clients and the phone apps, and sessions that are already open, are not affected. Off by default. Schema change: one boolean column on oidc_config, false for every existing install. #169.
A reply threads for the person who gets it. A reply sent from plMail, in the web composer or over JMAP, reached the recipient without In-Reply-To or References, so Outlook, Gmail and Apple Mail showed it as a new conversation instead of part of the one it answered. The draft always held both; the mail built from it never carried them. They are written now, and References always ends with the message answered, as RFC 5322 asks, even when the draft's own list left it out. A mail that is not a reply is unchanged, and replies already sent stay as they went out. No schema change. #206, by @phdelodder.
A send that fails for a passing reason is tried again, and one that finally fails is marked on the draft. Every failed send used to be dropped at once, whatever the reason, so a one-minute Gmail rate limit or a short outage lost the mail and only a toast said so. A rate limit, a provider failing on its side, a busy or greylisting mail server, or a connection that never opened is now tried again for up to two minutes, keeping the wait Gmail or Outlook asks for, and nothing is said while it is. A refusal, such as an unknown recipient or a message too large, is said at once, as before. When the answer to a send is lost after the provider had the mail, it is never sent again without checking: Gmail and Outlook accounts look for it in Sent first, and SMTP accounts report it as possibly sent. A send that finally fails puts a red warning triangle on the draft in Drafts, with the reason, and the same triangle on Drafts in the sidebar while any draft has one; the toast now says why. The migration adds four nullable columns and a partial index to message; it is additive and needs no backfill. #125.
IMAP mail with a repeated Date header is imported, and Proton Mail Bridge folders sync. A message carrying its Date header two or three times (some gateways add their own) could not be read by the IMAP library: it stopped with an error, and that took the rest of its page of the sync or import with it, every time. Such a message is now read with only the first of each repeated Date, Message-ID, In-Reply-To and References, and the stored source keeps every line the server sent. A message that still cannot be read costs only itself: it is tried again on the next syncs and then skipped, like other unreadable mail. And Proton Mail Bridge answered the search for new mail with "no such message", which failed that folder's sync on every poll; that answer now counts as no new mail. No schema change. #219, reported by @sponger544.
Releases are published on GitHub, so you can follow them in a feed reader. Each tagged release now gets a page under Releases with its notes from this changelog and the image to pull, once that image is published. Subscribe to https://github.com/karatektus/pl_mail/releases.atom, or use Watch → Custom → Releases on GitHub. Releases from v0.3.0 on have a page. Nothing changes in the image. Discussion #226, asked by @adrianfurgol.
A parcel on its way no longer says Delivered, and an order number is not taken for its tracking number. A FedEx or UPS mail that said the parcel "will be delivered tomorrow", or offered a text "when it's delivered", gave a card marked Delivered beside tomorrow's date, because any "delivered" in the mail counted. Only a delivery the mail says has happened counts now. A FedEx mail for an eBay purchase also put the eBay order number on the card instead of the tracking number, since both are twelve digits, so Track led nowhere; a number labelled "Order number", "Reference", "Bestellnummer" or the like is no longer read as a tracking number. A card wrongly marked Delivered is put right by the carrier's next mail or by app:backfill insights --since=7d; a card that already carries an order number stays until you dismiss it from its ⋮ menu. No schema change. #120, reported by @sbach89.
Amazon parcels from amazon.co.uk are on the radar, on the right day. Parcels were read from the German, American and French Amazon shops only, so an "Out for delivery" mail from amazon.co.uk gave no card. amazon.co.uk, amazon.ca, amazon.com.au, amazon.in and amazon.sg are now read the same way, and the card's Track button opens the shop that sent the mail. A delivery date such as "23/10/2026" is now read from the shops of countries that write the day first; it used to give a card with no date. And "arriving today" or "tomorrow" is counted from the day on your calendar, in your timezone from Settings → General, instead of the day in UTC, which put a parcel a day early for a mail that came in on an Australian morning and a day late for one on an American evening. That also applies to the carriers' own mail ("Zustellung heute"). Mail that is already in plMail gets its card, or its corrected day, when app:backfill insights --since=7d reads it again. No schema change. #207.
A short outage of the AI host no longer leaves mail without a category for good. When the assistant's host could not be reached, the message was still marked as asked, so the catch-up never offered it again: a one-minute outage left 327 messages with no AI category and no second attempt. A message is now left unmarked, and offered again by the catch-up a quarter of an hour later, when the call timed out, the connection was refused, or the host answered 401, 403, 408, 429 or any 5xx. A host that stays down is tried at that pace and no faster. Held mail is still released at once under the rules' answer. Everything else is marked as before and not asked again: an answer, an answer that is no use, a model the host does not have (404), and a refusal of the request itself (400, 413, 422), which would only come back the same. Mail marked during an earlier outage stays marked. No schema change. #222, by @phdelodder.
The weekly cleanup works on installs that moved their storage, and clears old thumbnails. app:prune:blobs looked for leftover files under a fixed var/, so on an install that sets APP_STORAGE_DIR to another directory it found nothing and reclaimed nothing. It now looks where the files are written. The preview thumbnails of attachments are kept there too now, beside the attachments, instead of under a fixed var/; on such an install the old var/attachments/thumbs directory can be deleted, and the thumbnails are drawn again when they are next shown. The cleanup also deletes the thumbnails of attachments that no longer exist, which nothing removed before. No schema change. #232, #233.
The installed app has the icon you picked, on iPhone and Android too. The tab icon followed Settings → Appearance → Logo, but an app installed from the browser or added to a home screen kept the wax seal, because its icons came from fixed files. The install manifest and the iOS home-screen icon now name your choice. Your browser draws them as PNGs when you pick a logo and uploads them for your account, so the image needs nothing new to draw them; a logo picked before this is drawn by the next page you open. An app that is already installed keeps its old icon until the browser refreshes it or you reinstall. No migration. #141, building on #92 by @Flouss.
The weekly cleanup no longer deletes profile pictures, signature images and backgrounds. Every Sunday at 05:00 app:prune:blobs removed week-old files it found no record for anywhere under var/uploads, var/attachments and var/raw. Profile pictures, signature images and appearance background images are kept under var/uploads without such a record, so they were deleted along with the real leftovers. The cleanup now only looks in the per-account directories that mail attachments, original messages and staged uploads are written to, and leaves everything else on those volumes alone, including directories added later. Files it already deleted cannot be brought back by plMail: restore them from a backup of the volume, or upload them again. No schema change. #214.
Reconnecting a service in the setup wizard no longer fails. Opening the Integrations step for a service that was already connected and saving it again answered with an error page, because the step built a second connection with the same name instead of editing the first. It now edits the existing one, and a blank secret still keeps the stored one. The settings page had the same error when a second connection to a service (a work Nextcloud next...
v0.3.6
Image: ghcr.io/karatektus/pl_mail:0.3.6
Several Gmail accounts import without failing on a duplicate key. With more than one Gmail account connected, mail one of them forwarded to another (or that both received) was merged into the first account's copy, and that copy's Gmail id was replaced with the second mailbox's. The first account then stored the message again, and the next import batch to reach it failed with duplicate key value violates unique constraint "uniq_message_gmail_id_account", was fetched again on every retry and used up the Gmail quota, so 429 rateLimitExceeded followed. A message another Gmail account holds is now left with that account and written down as such (app:gmail:skipped lists it as held by another Gmail account). Messages this already happened to stay listed twice. #196, by @karatektus.
A label a rule puts on new mail shows on its conversation. A rule with Apply label labelled the arriving message, but the conversation lost the label again in the same step, so it was missing from that label's list and from a tab filled by it. A message joining a conversation plMail already had was affected unless it was the first one that label was ever applied to on the account. Mail labelled before this update gets its conversation's label back the next time any label in that conversation changes. #185, by @karatektus.
A conversation no longer splits when its replies are synced first. When replies arrived before the message they answer, which is the usual case right after connecting an IMAP account (the Inbox is synced before Sent Items), each reply that found nothing to hang on started a conversation of its own, and the message that arrived later started another: a four-message exchange could be listed as three conversations, until app:backfill rethread was run. A message now also brings in the mail that already answers it, so a conversation ends up as one whatever order its folders are synced in. The conversation that was already in your list is the one kept, with its star, its tab and its labels. Conversations split before this update stay split until the next app:backfill rethread. Migration: one index on message, built on boot; on a large mailbox the first start after the update takes longer while it is built, and new mail waits for it. #176, by @phdelodder.
IMAP folders that never synced now do. On some servers, Dovecot among them, a folder's UIDVALIDITY (the number that says whether its message numbers still mean what they did) is above 2147483647, and plMail could not store a number that large: every sync of such a folder stopped with "value … is out of range for type integer" in the worker log, and its mail never arrived. Other folders of the same account were not affected. Migration: mailbox.uid_validity is widened from INT to BIGINT, which rewrites the small mailbox table once on boot; no stored value changes. Reversible: rolling back forgets a folder's UIDVALIDITY that only the wider column can hold, and the next sync records it again. #174, by @phdelodder.
Release notes after an update, and a feedback form. The first time you open plMail after it has been updated, a dialog lists what changed since the version you last saw: the highlights first, then every other change as a headline you can open. The ? in the top bar opens it again at any time, with a dot on it while there is something you have not seen, and the whole changelog is under Full changelog. It never opens on a fresh install, only after an update; if you would rather not be shown it, tick Don't show this after updates, and Settings → General → What's new turns it back on. Send feedback, in the dialog and under the ?, sends a mail from your own account, with your plMail version and, if you tick it, a screenshot of the page you were on; a copy stays in your Sent folder. It goes to feedback@pluetzner.de, or to APP_FEEDBACK_TO where that is set. Migration: everybody who already has an account is recorded as having seen 0.3.1, so their first look is every release since; nothing else changes in the schema. #166, by @karatektus.
Drafts from Outlook and Gmail open in the editor. A draft that arrived by sync (from Outlook or Gmail) was listed in its conversation, and clicking it gave a "Not found" page: it had been marked as already sent, which the editor refuses to open. It is now kept as the unsent draft it is, like one written in plMail. The visible side effect is that a conversation holding such a draft is no longer moved up the list by the draft's date, the same as for a draft written here. A draft you then send from Outlook or Gmail is recognised as sent, and no longer offered for editing. Migration: drafts already stored that the provider still files as drafts are made openable too; one that was sent elsewhere before the upgrade is left as it was. #164, by @Flouss.
Pull to sync on phones. Reaching for the button at the top of the screen is no longer the only way: pulling the mail list down from its top now syncs it, through the same sync as that button. Drag past the Release to sync indicator and let go, and the sync icon spins while the run lasts. It is for a finger on a phone-width screen only. A mouse, a trackpad and wider windows behave as before, a list scrolled down scrolls as it always did, and a pull while the sidebar is open does nothing. Reduced motion removes the indicator's slide. No schema change, no deployment change. #163, by @Flouss.
Notifications only for newly arrived mail. The notification on a phone or browser was sent for every change to a message — opening a conversation, marking it read, starring, archiving, a spam move, a copy of something you sent — and again a few minutes later when Outlook reported your own reading back, and it always read "New mail". A device switched on under Settings → Notifications is now told when mail arrives in the Inbox of a mailbox that has finished importing, and about nothing else. Spam, drafts, sent copies and an import in progress no longer notify, and neither does a message whose arrival time plMail cannot work out from its headers. Reminders from Calendar alerts and the update notice for administrators are separate pushes and unchanged. Apps that register through JMAP, and an Android phone through Firebase, are still told about every change: they ask what moved. Migration: one boolean column on jmap_push_subscription, false by default; devices registered through the in-app toggle (an id starting web-) are switched to the new behaviour once, so nobody has to turn notifications off and on. Reversible. #162, by @Flouss.
Outlook no longer reports moves that did not happen. Microsoft sends a message again whenever anything about it changes — plMail marking it read is enough — and each of those was written to the change log as a move into the folder the message was already in. Only a message whose folder actually changed is recorded now. The effect runs one way: a JMAP client, and an Android phone through Firebase, are told about fewer Email and Thread changes from an Outlook mailbox, and none that describe nothing. No migration. #162, by @Flouss.
Category tabs no longer add browser history entries. A tab switch, in the inbox or on an account's page, replaces the current entry, as Gmail does, so the browser's Back leaves the inbox instead of stepping back through every tab you looked at. The address still follows the tab. No migration. #161, by @Flouss.
Read and star changes made in Outlook sync to plMail. An Outlook account follows each folder with a change feed, and the feed was begun asking for a message's id, folder and Message-ID only. Exchange reports a change in the properties a feed asks for, so a message whose only change was being read, unread, flagged or unflagged was not reported at all: mail handled in Outlook, on a phone or by a rule stayed as plMail had last seen it. (The other direction, plMail to Outlook, was not affected.) The feed now asks for both. A feed already running keeps what it was begun with, so after the upgrade every Outlook folder is listed once more and followed from a new feed; the account shows as importing until that pass is done, which costs its listing pages and fetches nothing it already has. Each message listed is also compared with the server's read state and star, so the few that drifted while nothing was listening move to match the server: a message plMail has as read that Outlook still has unread goes back to unread, and the other way round, so a badge can go up as well as down. Migration: clears the stored change feeds of Outlook accounts; nothing else changes in the schema. #114, by @Flouss.
Fixed saving attachments to Paperless-ngx. The "Save to…" chooser listed your Paperless tags as folders to open, but a tag has nothing inside it, so clicking one only reloaded the same list and the dialog seemed to do nothing. A tag is now the choice itself: click it and the attachment is filed under it, or pick No tag to file it under none. The same goes for the folder a filter saves to. The upload itself was also refused ("No file was submitted") because it was sent in a form Paperless could not read; it now goes through. Paperless still turns down file types it cannot consume...
v0.3.5
Image: ghcr.io/karatektus/pl_mail:0.3.5
Password managers no longer fill client-secret fields. Opening Admin → SSO, or a Google, Microsoft or integration provider form, could put your mail address in the client ID and your password in the secret field, one press of Save away from being stored; with that stopped, a click in the field still had Chrome list every saved password and Bitwarden offer to generate one, and Bitwarden opened its menu in the text fields below. All of it followed from the field being a password input, which no attribute talks every browser and extension out of. Client secrets, API tokens and mail-server passwords are now text fields drawn as dots, and a text field that asks for nothing says autocomplete="off". The sign-in form and the forms that set a login password are unchanged.
Sign in with OpenID Connect (Authentik, Keycloak, …). Enter the provider's issuer, client ID and secret under the new Admin → SSO and the login page offers a "Sign in with …" button above the password form, which stays; the OIDC_* variables do the same for an install configured from its compose file, and what is saved in the admin area wins. An account is found by its address the first time — only when the provider says it verified that address — and by the provider's subject from then on, so changing the address at the provider later changes nothing here. A switch creates an account for somebody plMail has never seen; it is off by default. Suspension, the login throttle and two-factor apply as they do to a password. Setup, with Authentik step by step, is in Single sign-on. app:user:oidc-unlink forgets a link from the console, for somebody deleted and re-added at the provider. Migration: two nullable columns and a unique index on user, and a new one-row table oidc_config; nothing backfilled, reversible. #106, by @jo2904.
Wider Happening soon panel. The panel opened at the narrowest dialog width, so a fortnight of bookings pushed the undated cards under "Also spotted" far down the scroll. It now opens wide, and on a screen wide enough for it the list and "On your radar" stay on the left while "Also spotted" sits to their right. On a phone it stacks as before. No migration. #83, by @Flouss.
The calendar shows times in your own time zone. A calendar you subscribe to from Google or a CalDAV server arrives with the time zone that account holds for it, and a shared one is often set to UTC there. When such a calendar was your default, the whole calendar page was drawn in UTC, so an appointment at 11:30 Paris time showed at 09:30 — the stored time was right, only the clock it was printed on was not. The page, and the dot in the top bar that decides what is left of today, now use your own time zone (the Time zone setting, else the install's default) whenever the default calendar is a synced one. A calendar created here keeps the zone it was given. No migration; nothing stored changes.
Inbox unread count in the browser tab on every page. The "(3)" in front of the tab's title appeared on the Inbox and vanished on Settings, the calendar and the admin pages, although the sidebar there still showed the same number. It stays now, and updates on sync like the Inbox's does. Mail folders are unchanged: each shows its own figure or none, so Trash, Sent and Archive do not borrow the Inbox's. No migration. #72, by @Flouss.
Fixed missing attachments on Outlook and Hotmail mail. A message with a file on it arrived in plMail with no attachment at all, no paperclip, nothing to download, for every Outlook.com, Hotmail and Microsoft 365 account. The list of a message's attachments was asked of Microsoft in a form it rejects, and a refused list was read as "none", silently. The request is fixed, and a message that Microsoft says has attachments but whose list does not come back is now logged as a warning instead of passing unnoticed. Mail that has already been synced stays as it was stored, without its attachments, until it is fetched again; mail that arrives from now on is correct. No migration. #77, by @Flouss.
Parcels picked up at a relay point show as delivered. The mail that says "Votre colis a été retiré" or "… récupéré par son destinataire" named no stage the radar knew, so the card stayed "announced" after the parcel was in its recipient's hands. Both now read as Delivered; the same words negated ("n'a pas été retiré", "n'a pas été récupéré par son destinataire") do not, and neither do those words about something other than the parcel. No migration.
Fixed the carrier shown for Pickup relay parcels. The Pickup relay network (network1.pickup.fr, network2.pickup.fr) hands over the parcels of several carriers, and its mail gave every parcel whose number has the international S10 shape the carrier "Deutsche Post", whoever was shipping it. A card now takes its carrier from the sender's name when that starts with Chronopost or Colissimo, with that carrier's tracking link, and is otherwise headed "Pickup" with no carrier and no link. What the number itself says still counts: a number ending in FR is La Poste's, and Colissimo's own shape is Colissimo's. Cards already on the radar correct themselves the next time their mail is read, no duplicates. No migration.
Re-reading old mail no longer resets a parcel's status. A card keeps the newest statement of its series, but "newest" meant the last mail read, not the latest dated. app:backfill insights reads by the order mail was stored, which for an Outlook import is newest-first, so a parcel's "out for delivery" mail was read before its "announced" one and the older statement overwrote it. A mail older than the one a card already stands on now leaves the card alone; mail of the same instant, or a card whose mail was deleted, behave as before. No migration.
Cards from old mail no longer look new. The cards without a date of their own — GitHub activity, mostly — are listed for the last seven days, measured from when the card was created. That equals the mail's age only while cards are made as mail arrives: app:backfill insights, or an import, creates them from old mail now, so a month-old notification took a place among the newest twenty for a week. The window and the order now follow the date of the card's mail, and the card's own date only when its mail was deleted. No migration.
One-time codes leave Happening soon once they expire. When a code's mail says how long it lasts, the card drops off when that time passes. When it says nothing — a password manager's, many shops' — the card had no date to drop off by and was listed like any undated card, for seven days, with the code on it. Such a code is now offered for fifteen minutes from its mail; it is still on the mail itself. Codes that state an expiry, and every other kind of card, are unchanged. No migration.
Insights read the whole of an Outlook mail. For an HTML mail, Microsoft Graph gives the text as a preview cut at 255 characters, and that preview is what the card extractors (parcels, codes, flights, tickets, invoices) read instead of the mail. A tracking number further down — in a Pickup or IKEA relay notice, say, or a GLS mail with the number under a heading — never produced a card. They now read the mail's HTML for an Outlook account, and the preview only when the HTML has no words in it. Mail already stored is read again by app:backfill insights --since=…. No migration.
GLS France parcels are recognised. GLS France prints no tracking number of a recognisable shape: its mails carry an eight-character code under "Numéro de colis" (Q7XK2M4P), and the sender was not known either. Mail from gls-france.com is now read for that code, behind that label only, and the card links to GLS France's own tracking page. Eight bare digits, or a code in mail from anyone else, are still not read. No migration.
Fixed wrong invoice numbers and merged invoice cards. A billing mail whose footer says "Manage invoice notifications" had that line read as an invoice number: "invoice no" matched the start of "notifications" and the rest of the word became the number. Every bill that sender sent got the same number and so the same card, and each overwrote the last. "Invoice no" and "invoice number" now have to be whole words. No migration.
Fixed sign-out not sticking. A page's background requests that were still on their way when you pressed Sign out could sign you straight back in through "Keep me signed in". Its tokens are now kept on the server, one per device, and signing out deletes this device's; other devices stay signed in. Everybody who relied on "Keep me signed in" is asked to sign in once more after the update. One table is added; the migration is reversible.
Sender name per address. Settings → Aliases has a sender name field under each address; left empty, the address sends under the account's name as before. The name was already shown in the From selector for an alias that had one (a phone app could set it), but the mail itself went out under the account's name regardless — the selector and the recipient now see the same one, in the browser and from the apps. No migration. #70, reported by @Node815.
Replies are sent from the address the mail came to. Mail that came in for an alias is answered from that alias, with that alias's signature, instead of every reply opening on the primary address; answering your own sent mail keeps the address it was sent from. A domain whose every address reaches the mailbox can be added under Settings → Aliases → Catch-all domains, and mail to any address at it...
v0.3.4
Image: ghcr.io/karatektus/pl_mail:0.3.4
Fixed Gmail imports stuck on Google's quota. Google lowered the per-user limit to 6,000 units a minute for projects created since May 2026 and raised the cost of fetching a message with its body from 5 units to 60 (its table says 20, which is the cost without the body); plMail paced imports for 15,000 at the old cost, so on a new project most of what an import asked for was refused, the log filled with rate-limit warnings and an import could stop without resuming. plMail now reads the limit out of Google's first refusal and keeps to it — nothing to configure — counts a message at its real cost, fetches five to a request so a minute's worth is never asked for in one second, and shares one allowance between its workers. An account Google still refuses at the right limit is slowed down and speeds up again by itself. An import longer than an hour no longer fetches again what an earlier pass already brought in. A refusal is no longer an error: whatever met it — a batch, an import page, a sync, a label change being pushed — waits out the minute and carries on. The import line in the background-work indicator now shows the quota, roughly how much of it is in use, how many messages a minute are coming in, how long the rest of the import takes, and says so when Google has paused it. No migration. #90, reported by @karamanliev. Your own tabs on an account's page. Clicking an account in the sidebar offered the five built-in tabs and none of the ones you made. Mail filed in one of yours is in that tab only — not in Primary or any other — so on an account's page it was in no tab at all. The page now has the same tabs as the top-level Inbox, yours included, each shown while it holds mail in that account, and ?tab= accepts yours. Counts are per account; a link to a tab that is not yours, or was deleted, opens Primary as it does in the Inbox. No migration.
Fixed the browser tab icon in Firefox. Settings → Appearance → Logo changed the icon in the app but not in the tab, which kept showing the wax seal. Every page also linked a fixed 192px wax-seal PNG as a second tab icon, and Firefox prefers that to the SVG for a tab. The PNG is gone from the tab icons; the home-screen and install icons are unchanged. No migration. #74, by @Flouss.
Fixed messages with invalid bytes not being stored. Over IMAP, a mail whose text was labelled us-ascii but written in Latin-1 — "Grüße" from a mailer nobody configured — was refused by the database ("invalid byte sequence for encoding UTF8: 0xfc"), asked for again on each of the next syncs and then given up on, so it never appeared. The same happened with such a byte in a Message-ID, In-Reply-To or References header, and in an attachment's content type or content ID. All of them are now read the way a subject or a file name with that byte already was, and the message arrives with its umlauts. A message that was given up on before this is not fetched again by itself. No migration. #81
Clearer read/unread button. On an unread row, "Mark as read" wore the closed envelope, while the same action above a selection wears the open one — which looked like a Promotions problem only because that is where mail is unread. A row's button now matches the toolbar: open envelope to mark read, closed envelope to mark unread. No migration. #61, by @Flouss.
New Gmail and Microsoft accounts get their own colour. Accounts added with a password were already given the first colour no other account was wearing; accounts connected through Google or Microsoft skipped that step and all kept the first colour, so their dots in the sidebar and the corner mark on every message row looked the same. They now take the first free colour like any other. Accounts you connected before this keep the colour they have; the swatch on the account's row in Settings → Mail accounts changes it. No migration. #60, by @Flouss.
Readable mail list beside an open message. A row went to a single line once the list was about 575px wide, which left the subject around 120px and its preview wrapped next to it ("Facture pour Co…") whenever the reading pane was open, or on a large phone. A row now goes to one line only once the list is 75rem (about 1200px) wide. Below that it is two lines — sender and subject, then the preview across the whole row — and on a list narrower than about 575px, a phone, the three it always was. Not less, because a one-line row in a 65rem list (the list beside the reading pane on a 2560px screen) still cut real subjects after about 30 characters. Archive, delete, snooze and the other row actions appear on hover in two- and three-line rows too, instead of only in one-line rows. A two-line row has no unread dot; like a one-line row it says unread with a bold sender and subject. Also fixed: with the two-line preview setting on, a one-line row wrapped its preview anyway. No setting; no migration. #62, by @Flouss. The calendar button can skip a view. The button in the top bar has always been a three-way switch: mail, then the calendar beside the mail, then the calendar full width. Settings → Calendar → Behaviour has one choice, Calendar button, that can make it a two-way one. Full width only goes between the mail and the calendar full width, one press each way, and clicking a mail link while the calendar is full width goes back to the mail instead of dropping into a split. Split only goes between the mail and the calendar beside it. All three is the default, so the button behaves as before until you change it. Nothing is added to the database. #80, by @Flouss.
Unread counts on inbox tabs. The number is the sidebar's Inbox badge split by category, so the tabs add up to it, and it appears on every tab, the one you are on included, and updates in place on sync. Before, an unread conversation in Promotions made the Inbox badge read 1 while Primary, the tab that opens, showed nothing unread, and the only hint on the strip was a colour on the Promotions icon. The "3 new" pill is unchanged: it says what arrived that you have not been shown, the unread count says what is unread. On for everyone; Settings → Appearance → Message list switches it off per person. One column is added to the user table; the migration is reversible. #54, by @Flouss.
Change an account's colour. The coloured dot beside each account, and the mark on its messages in a mixed list, was picked for you when the account was added and could never be changed. Settings → Mail accounts now has a colour swatch on every row — Gmail and Outlook accounts included, which the Edit form is closed to — that opens the palette of eight. Two accounts may share a colour if you want them to read as a pair. No migration. #69, by @Flouss.
An open message stays open when the list changes. With the reading pane on the right, choosing another folder, switching a category tab, turning the page, sorting or searching closed the message you were reading, although it had never covered the list. It stays now, and the back arrow closes it and leaves you on the folder you moved to. The open row is marked again when you return to the folder it is in. With one pane at a time nothing changes: there the message has covered the list, so choosing a folder still closes it. No migration. #63, by @Flouss.
Sidebar "+" buttons appear on hover. Both were on screen all the time. They also appear when reached with the keyboard, and stay visible on a touch screen, where there is no pointer to reveal them with. #66, by @Flouss.
Separate model host for semantic search. Admin → AI has a tick box, Search uses a different model host, with its own provider, address, token and extra headers. Off by default. It is for running the two models on different machines: a hosted model for writing and a small one at home for search, so the mailbox is indexed on a machine you own and nothing is billed per message, or the reverse.
- The index is kept when the search model has the same name on the new host. A vector belongs to its model, not to the address it was made at.
- Each host gets only its own token and headers. A search host without a token is sent none.
- Test asks both hosts, each for the model that lives on it.
Five columns are added to ai_settings, unused until the box is ticked; the migration is reversible.
Custom headers for the model host. For a reverse proxy with basic auth, a zero-trust gateway, a routing gateway with its own key, or a provider that takes a project or tenant header. Admin → AI has an Extra headers box under the token: one Name: value per line, sent with every request for either provider, stored encrypted. The page lists the names on file and never shows a value again. They are dropped when the address or the provider changes, like the token. One column is added to ai_settings; the migration is reversible.
Any OpenAI-compatible language model. Admin → AI has a Provider field: Ollama, as before, or OpenAI-compatible, which covers llama.cpp, vLLM, LM Studio, LiteLLM, hosted providers and Ollama under /v1. Writing help, summaries, sorting and search by meaning all work with either. The address is the API root including its version, such as https://api.example.com/v1, and the token is the API key.
- Ollama is still the default and ...
v0.3.3
Image: ghcr.io/karatektus/pl_mail:0.3.3
Fixed the emoji picker in Safari and on iPhone. Nearly every cell of the grid was blank, with a handful of emoji scattered through it, and an emoji typed into a message from the iOS keyboard could not be seen in the message. plMail ships its own emoji font so the picker looks the same everywhere, in a format Safari does not draw; the browser found each emoji in the font and painted nothing. The font is now only used where the browser can draw it. On an iPhone, iPad and in Safari on a Mac the picker and the message show Apple's own emoji instead. #39.
New accounts import their newest mail first. Adding an account used to bring its whole history in through the sync itself: every IMAP folder from its oldest message to its newest, or every Gmail message or Microsoft folder listed before one message was fetched, in one job on the queue all new mail arrives on. A large account showed an empty Inbox for most of an hour, new mail on every other account waited behind it, and the worker doing it was shown as dead. #42.
- Newest first, Inbox first. The most recent mail of the Inbox arrives within about a minute, then the rest of it, then Sent, Drafts and your own folders, the archive, and Spam and the bin last. Several accounts added together take turns, so each has a usable Inbox early.
- Its own queue and worker. History is imported on
ingest_backlogby a newworker-backlogprocess, a page at a time. Mail arriving now stays oningestand is never behind it. The worker starts by itself; there is nothing to configure. - You can see it. A chip beside the search box says Importing and how far along it is while an import runs. It opens a list of each importing account with how much is in and which folder it is on, and says so when an import is waiting.
- Repeated sync requests collapse. The quarter-hour poll, a push and the Sync button used to queue a full sync each, one behind the other. A request a later sync has already covered is skipped.
- One message that will not import costs itself. It is retried with its page five times and then passed over, with an error in the log, instead of stopping its folder.
This covers IMAP, Gmail and Microsoft accounts alike. Folders that had already synced are untouched; a folder that was in the middle of its first sync when you upgrade is picked up again from the top, and what it had stored is not fetched twice. One migration adds four columns to the folder table.
Two things change for Microsoft accounts beyond the import. A folder whose sync state expired on Microsoft's side used to be read again from end to end inside the sync; it is handed to the import instead. And a message deleted in Outlook while another folder's sync was failing is now removed here once that folder answers, where it used to stay behind: in no list, and counted in every total.
The Background work button updates live. It never did on a mail page: the controller that refreshes it was not loaded there, so a bulk action's progress showed whatever it had reached when the page was drawn. Where it was loaded, it refreshed once and then not again, and a refresh shut the open list. All three are fixed.
Separate password for sending. The account form has one password field, as before, and under the SMTP settings a new Use a different password for sending, which reveals a second. Zoho issues an app password per protocol, and an account set up with one of them synced and then failed every send on authentication, with nowhere to enter the other. The username stays shared. The connection test uses both, and a config backup carries both. One migration adds a column to the account table; it runs by itself and rewrites nothing. #42.
Fixed one message stopping an IMAP folder's sync. A multipart message whose header puts spaces around the equals sign — boundary = "…", which the standard allows and some mailers write — could not be read, and because that failed while a page of mail was being fetched, the whole folder's sync failed with no content found on every poll until the message was deleted on the server. Nothing newer in that folder arrived, and nothing in plMail said so. Such a message is read now. And a message that cannot be read for any other reason no longer takes its folder with it: the rest of the page is stored, the message is asked for again on the next five syncs, and then it is skipped with an error in the log naming the folder and the UID. A folder that was stuck this way catches up by itself on the next poll after upgrading. No migration. #45.
Reading pane. Opening a message used to replace the list; Settings → Reading pane now offers Right, which puts the message beside the list on a screen that has room for both. The open row is marked, the back arrow closes the message and leaves the list where it was, and the divider between the two can be dragged (or moved with the arrow keys; double-click resets it). Its position is stored as a percentage of the mail card, 25 to 75, rather than in pixels, so it keeps its meaning when the calendar pane opens or is resized. It is drawn only while the card is at least 52rem wide — a narrow window, a phone, or a docked calendar that leaves the mail too little room fall back to one pane at a time. Off by default, so nothing changes until someone chooses it. Per person, not part of an exported theme, and carried by a config backup. No migration.
Settings has a Features group, with a page each for the reading pane and the keyboard shortcuts. The shortcuts switch moved there from General.
Simpler selection toolbar. Archive, Delete and Move to stay; star, mark as read or unread, snooze, label and spam are under More. Nine buttons in a row were wider than the list whenever it shared its card — with a docked calendar, or now with a message open beside it — and the row was cut off at the edge. The keys still reach all of them directly.
Fixed shortcuts after ticking a row. A click left the focus on the row's checkbox, which counted as typing, so e after ticking three rows did nothing; rows ticked with x were unaffected.
A message opened by its own address (a reload, a bookmark) has no list behind it, so with the reading pane on the right it is shown beside the Inbox, whichever folder it was opened from.
Category tabs fit on phones. On a narrow screen the tabs (Primary, Social, Promotions, …) ran into each other, labels underneath the next tab's icon. They now keep their size and the strip scrolls sideways inside itself, opening on the tab you are on. No migration.
The Immich picker hides Live Photo videos. Immich keeps it as an asset of its own, hidden from its timeline and without a preview, so the picker showed it as a grey tile that could only be attached as a three-second clip — on a library full of iPhone photos, a large share of the grid. Library, search, album and person views now leave those clips out. Archived photos stay, and so does anything a server that predates the visibility field sends. No migration. #46.
Gmail-style keyboard shortcuts. j and k move through a list, o opens, u goes back, x selects, e archives, # deletes, s stars, r a f reply, reply to all and forward, c writes, / searches, g then a letter goes to a list, z undoes. ? shows all of them, and so does the keyboard button above a mail list. A key acts on the selection, otherwise on the open conversation, otherwise on the highlighted row, and does exactly what the button it stands for does. Nothing happens while you are typing or while a dialog or menu is open. On for everyone; Settings → Keyboard shortcuts switches them off per person. No migration. #36.
a replies on a message that has only one recipient, where there is no Reply to all to press. The toolbar above a selection has a Star button, and s presses it; before, s did nothing with rows ticked. An open conversation has a Snooze button in its toolbar, which is what b presses there.
Config backups include sorting settings. Settings → Categories — rules or the assistant, whether that overrules the provider's own categories, and whether the inbox has tabs at all — was never written into the file, so a restore put everyone back on rules, the provider's categories and a tabbed inbox. All three now travel with the user. Backups made before this still restore, onto those same defaults. No migration.
French parcel tracking. Colissimo, Chronopost, Mondial Relay, Colis Privé, DPD France and Cainiao mail now yields a parcel card, with their number shapes, French wording for the stage and for the day ("sera livré après-demain", "prévue le 14/11"), and Amazon.fr alongside the other Amazon shops — its button now opens amazon.fr, not the German site. A parcel waiting at a relay point or locker is a new stage, Ready for pickup. The satisfaction survey a carrier sends after delivery is ignored, where it would otherwise have turned a delivered card back. A French S10 number from a shop of unknown carrier reads as La Poste instead of Deutsche Post. Not read yet: GLS France and the Pickup relay network (no number to recognise), and "returned" and "attempt failed". Existing mail is not re-read on its own; app:backfill insights picks it up. No migration.
Choose which IMAP folders to sync. Settings → Accounts has a new Choose which folders to sync button on every IMAP account's row. It opens the account's folders with a switch beside each: switch one off and plMail st...
v0.3.2
Image: ghcr.io/karatektus/pl_mail:0.3.2
Security
Security: outsiders can no longer change your calendar. Reported by @jhuber in #34, who found everything in this section by reading the code. Two holes, neither of which exposed data or ran code, both of which let an outsider put misleading entries on a calendar unnoticed.
- A meeting could be moved or cancelled by anyone who knew its identifier. Everyone invited to a meeting has that identifier, and so does anyone a copy was forwarded to. A cancellation or an update from any of them was applied, and the changed meeting still showed as accepted. A change is now applied only when it comes from the address that sent the invitation, or from the organiser the event names. From anyone else nothing changes, and a card above the message says so and offers Apply the change. The same now protects meetings mirrored from a connected calendar.
- A booking in a mail's markup was put on the calendar on the strength of an unchecked From line. A stranger could add a "flight", and a forged airline address with a real booking number could cancel the real one. A booking is now added unasked only when your mail server vouches for the sender; otherwise it is offered on a card with Add to calendar. For IMAP accounts the server's verdict is believed when its name is on the same domain as the IMAP host.
- Mail in Spam or the bin is no longer read for events. A message moved back out is read then.
Mail styles no longer leak into replies and prints. Inline styles are kept because in mail they are the design, and in the reading pane that is harmless: the message is in a sandboxed frame. The quote in the reply composer and the print page are not framed, and a mail using position: fixed could cover the composer with content of its own. Such declarations are now removed, and both places clip what is inside them. Also from #34.
The default TRUSTED_PROXIES is documented for what it is. It trusts every private address so a reverse proxy works unconfigured, which also lets another machine on the LAN forge its address and step around the per-address limits on login and booking. The per-account login limit is unaffected. The default is unchanged — narrowing it blindly would break sign-in behind a proxy — and Reverse proxy now says how and when to narrow it. Also from #34.
Reminders no longer reach Google readable. A calendar reminder pushed to an Android device through Firebase carried the event's title as plain JSON, although the code said it was encrypted — that was only true of browser push. Anything with content in it is now sealed to a key the app registers, the same way a browser's push is, and a device that has registered none is not sent it at all. State changes carry no content and are unchanged. The Android app needs the version that registers a key to show these reminders; older versions never showed them. Also from #34.
Nothing already on a calendar is removed or changed by upgrading. One new nullable column, event_source_link.hold_reason. See Invitations and events from mail.
Fixed: a fresh install could only be opened on the machine it runs on. The stock compose.yaml served https://localhost and nothing else. Opened from another machine by address or hostname, the browser reported an SSL protocol error, which unlike a certificate warning cannot be clicked through, so a headless server could not be set up at all without first setting SERVER_NAME. The default now answers on every name and address with the same self-signed certificate, and the browser shows its usual warning. Live updates follow as well: the hub address is taken from the public address chosen during setup, where the stock file used to pin it to localhost. An install that sets SERVER_NAME or MERCURE_PUBLIC_URL itself is unchanged. See Docker → Starting it.
Mark as spam button. It sits beside Delete: among a row's buttons, in an open conversation's toolbar and above a selection. On one conversation it opens a short menu — move it to Spam, or move it and also create a filter that sends everything from this sender, or from the sender's whole domain, to Spam from now on. The domain filter asks once more before it acts, and for a mail provider's domain such as gmail.com it is behind More options first. The filter is an ordinary one under Settings → Filters. Undo takes back the move and a filter the same click created. Until now the only ways to say "this is spam" were Move to and dragging onto the Spam row. See Mail → Mark as spam.
Filters can match a sender exactly. Two new conditions sit beside "From contains": From is exactly, which matches the sender's whole address, and Sender's domain is, which matches everything after the @. "From contains" looks for the text anywhere in the address or the display name, so a filter for a@x.de also caught ba@x.de, and anybody could match it by putting that address in their display name. The spam button's filters use the new conditions. Existing filters are unchanged. JMAP clients get the same two as fromAddress and fromDomain in Email/query.
Message templates. Settings → Templates is a new section: write a message once, file it in a folder, and insert it from a new button in the compose window. The folder tree opens with one folder per mail account, and the account you are writing from has its templates listed first. A template can hold variables — the recipient's first name, full name and address, your own name and address, a signature, and dates with an offset and a format of their own ("14 days ago", written out long or as dd.MM.yyyy). Dates, sender and signature are filled in when the template is inserted. Recipient variables wait for a recipient: they stay in the message as placeholders until someone is in To, and Send asks first if one is still open. The compose window can also save the message on screen as a new template. Two new tables, mail_template and template_folder; nothing existing changes. Removing a mail account keeps its templates and moves them to the top level. See Templates.
Templates are available to JMAP clients. A new capability, urn:plmail:params:jmap:templates, with Template/get, Template/set, TemplateFolder/get, TemplateFolder/set and Template/render. The first four read and write the same templates and folders as Settings → Templates, through the same sanitiser and the same ownership rules. Template/render returns a template ready to insert for the account being written from: dates, sender and signature filled in, the recipient too when the client supplies one, and a list of the recipient variables that are still open. The Session lists the variables and date formats an editor may offer. The apps do not use any of this yet. See Client development → Templates.
A demo mailbox starts with templates. A visitor to a public demo gets six templates along with the seeded mail and calendar: a quote follow-up, an invoice reminder, an out-of-office and others, filed at the top level, under the demo account and in a folder inside it. They use every variable between them. app:test:seed-demo seeds the same six. Nothing changes on a normal install.
Fixed: an emoji picked in a reply you had not clicked into yet did not appear. A reply opens with the cursor on its empty first line, and whatever a toolbar button inserted there — an emoji, an inline image, and now a template — went somewhere that is never drawn: the button seemed to do nothing, and the reply stayed empty. Typing was never affected, and neither was a message you had clicked into first. The insertion now lands on the line.
Usage figures for public demos. In demo mode the admin area has a new section, Demo visitors: how many demo sessions were started in the last hour, 24 hours, 7 days and 30 days, how many different visitors started them, the total so far and how many have a mailbox right now. No address is stored. What is kept per session is a keyed hash of the visitor's address with its last part cut off, and that hash is deleted after 30 days. The demo's privacy notice says so. None of this exists on a normal install: the section is not in the navigation, its address answers 404 and nothing is recorded. See Demo mode.
Fixed messages growing without end. Some mail sizes its outer block as "one screen tall". Inside plMail a message sits in a frame that is made as tall as its content, so the block grew with the frame, the frame grew to fit the block, and the message kept getting longer, pushing everything below it away. Sizes given in screen heights are now ignored inside a message, and a message whose content merely follows the frame is no longer resized again.
Inbox as one list, without tabs. Under Settings → General → Categories, Inbox tabs can be switched to One list: no Primary, Social, Promotions and the rest, just the inbox, newest first. Per person, and tabs stay on unless you change it. Mail is still filed in the background, so switching back shows sorted tabs at once; if the assistant is what sorts your mail it keeps doing so, and the card says that when the tabs are off. "Select all in this view" then means the whole inbox. Apps can read the choice from the JMAP session as inboxTabs. #29.
A round of first-impression fixes. All reported by @pmdevelopment in #37.
- **A selection surv...
v0.3.1
Image: ghcr.io/karatektus/pl_mail:0.3.1
Mail with no plain-text part is read for insights. Parcels, invoices, codes and the rest were looked for in a message's plain text only, and a shop's shipping notice is very often HTML and nothing else: the tracking number was on the page and plMail was reading an empty string. Such mail is now read as the text its HTML shows — without stylesheets, hidden Outlook blocks or the addresses behind links. A report of a missed insight carries that text too; for these mails it used to arrive empty.
- A date announced as the day the shop hands the parcel to the carrier ("Voraussichtliche Übergabe an den Versanddienstleister") is no longer taken for the delivery date.
- Mail already stored is not re-read on its own;
app:backfill insightsreads it again (the last 24 hours unless--sincesays otherwise).
A calendar file somebody mails you is offered, not added. Two changes that belong together:
- A file attached as an ordinary file is read. An appointment sent as
termin.icswas only recognised when the sender also declared it as calendar data. Recruiting and booking systems often attach it as a plain file, and then the mail offered a download and nothing else. It is now recognised by its name too. - Calendar data that does not invite you waits for a yes. A file with no organiser and no guests, or an invitation addressed to somebody else, used to go straight onto the calendar — which let anybody put things there by sending mail. The card above the message now asks: Add to calendar or No thanks. No reply is sent to anyone. Invitations addressed to you, bookings and your own events are unchanged, and events of this kind already on your calendar stay there.
app:backfill events picks up mail that arrived before this (the last 24 hours unless --since says otherwise).
An Amazon order is on the radar from the moment it is placed. The parcel card appeared with the dispatch mail; the order confirmation before it ("Bestellt: …", "Ordered: …") was not read at all, although it already names the order and the day it is promised for. It now makes the card, with a new first stage, Ordered, and the later mails about the same order move that card on. German mails that promise a weekday ("Zustellung: Freitag") get their date as well.
Importing a large Gmail mailbox no longer runs the account out of quota. Google allows each Gmail account a fixed amount of work per minute, and since 0.3.0 an import fetched old mail fast enough to use all of it. The import itself recovered quietly; what failed was the regular check for new mail on the same account, with "Quota exceeded … Units per minute per user" in the log and new mail arriving late until the import was through. Fetching is now paced to stay well inside the allowance, so an import of a very large mailbox takes somewhat longer and new mail keeps arriving while it runs.
Admin → Performance no longer blames plMail for mail that was late before it got here. "How late mail arrives" measured from the date a message carries, which is the sender's clock: a newsletter dated half an hour before it was sent, or a first message held back by greylisting, was shown as plMail taking that long. It now measures from the moment your provider accepted the message. On the installation this was found on, an account showing "95% within 15 min" had in fact stored all of its inbox mail within a minute.
- Inbox mail and the rest are counted apart. Providers only announce mail that lands in the inbox; spam and filtered mail wait for the quarter-hourly check by design, and used to put a fifteen-minute tail on accounts whose push was working. They now have a column of their own.
- The slowest messages are listed, in a section you click open, one to a row: where each was filed, what brought it in (push, IDLE, the schedule, the sync button) and how long it had already been under way before your provider had it. No subject and no sender, like the rest of the page.
- What brought a message in is recorded from this version on; older mail shows a dash. The last week of mail is given its accept time in the background shortly after the update, so the panel may be sparse for a few minutes.
- This adds three columns and an index to the message table. On a very large mailbox, building the index makes this start-up take a little longer than usual.
A label name that differs from an existing one only in capitals is refused. "work" could be created beside "Work". Gmail treats the two as one label and rejected the second, but only later, in the background: the new label existed in plMail, could be put on mail, and never reached Gmail, with an error in the log and nothing on screen. plMail now refuses it when you create or rename the label, in the browser and from the apps, and says which label is in the way. Renaming a label in the browser had no check at all and could take another label's exact name; it has the same one now.
- If you already have such a pair, nothing is changed for you. Delete or rename the newer one; until you do, putting it on Gmail mail keeps failing in the background.
"Move to" is available to apps, not just the browser. The Android and iOS apps talk to plMail over JMAP, which had no way to ask for a move: an app could only add one label and remove another itself, which files mail in two places on a plain IMAP account and cannot take mail out of Trash or Spam properly. Thread/set now accepts a moveTo instruction that runs the same code the browser's button does. Nothing changes in the web interface. For client authors: Building a client → Moving.
Admin → Performance: how long things take, in one place. A new section directly below System. System says whether plMail is working; this says how long it took.
- How late mail arrives. Per account, the time between the provider receiving a message and plMail storing it. Seconds means push is working; a typical delay of five minutes or more is highlighted, because that account is living on the quarter-hourly poll — which "works", so nothing else would tell you.
- What each worker has waiting. The queue numbers arranged by the process that handles them: how many jobs wait, how long the oldest has waited, how long the job in hand has run.
- New mail waiting for the assistant. The line under the hold switch in Admin → AI says "typically 6 s, 95% within 13 s" — which shows that one message took twice as long as the rest and nothing about why. Here each held message is a row: how long it was kept back, how long it queued before the assistant was asked, how long the model call took, and how much of that call the host spent loading the model. A message that was slow because the model had been unloaded says so.
- Model host. Moved here from Admin → AI, unchanged. The search index backfill stayed under AI, where its Start and Pause belong.
Rows name accounts; nothing about any mail's content is shown. See Administration → Performance.
Before you upgrade
- One migration, applied on boot. Three nullable columns on
message; no row is rewritten, so it is instant on a mailbox of any size. - Mail sorted before this release has no durations and shows a dash in the new table.
v0.3.0
Image: ghcr.io/karatektus/pl_mail:0.3.0
Adding an account no longer puts new mail behind hours of follow-up work. Everything plMail does to a message after fetching it — sorting by the assistant, event extraction, insight cards, read receipts — was queued beside the fetching, on the one worker that fetches mail. A first import of a large mailbox therefore queued that work for every message it brought in, oldest first, and new mail waited behind it; with the assistant sorting mail that was a model call per message and could be hours. Fetching mail is now the only thing that worker does.
- Follow-up work has its own queues and workers.
worker-livehandles mail that has just arrived on an account whose import is over, and nothing else, so new mail is never behind an import.worker-enrichhandles an import's recent mail and then, only when that is done, old mail.worker-releaseonly ends the wait described below. See Mail ingest → What follows a message. - Only mail from the last 30 days gets follow-up work as it is imported. Older mail is stored, threaded, filed by the ordinary rules and run through your own rules, as before — but no events, insight cards or receipt handling are derived from it.
app:backfill insightsandapp:backfill event-extractionread history if you want it. - Old mail is sorted by the assistant after the import, not during it — newest first, in small batches, in the background, and only once every account of yours has finished importing.
- New mail waits for the assistant before it appears in a tab. If you let the assistant sort your mail, a message used to appear where the rules put it and move a few seconds later when the assistant answered. It is now kept out of the inbox tabs for those seconds and appears once, in the right tab. The wait is capped — 60 seconds by default — and past the cap the mail is shown where the rules put it, and moved if the assistant answers later. Mail from a first import is never held.
- Admin → AI has a switch for that wait, and shows what it costs. Wait for the assistant before showing new mail in a tab turns it off, Wait at most sets the cap (2–300 seconds), and the line underneath reports how long mail was actually held over the last 7 days and how often the wait ran out. See Administration.
"Move to" files a conversation under a label in one step. There was a label button, which adds a label and leaves the mail where it is, and dragging onto a folder, which only works with a pointer and a sidebar in reach. Filing mail the way Gmail does — label it and get it out of the list you are in — took two actions. A folder button beside the label button now does it in one, for a selection in the list and for an open conversation: the label you pick goes on, and the label of the list you were looking at comes off. From the inbox that is "label and archive"; from a label it swaps one label for the other; from Trash or Spam it takes the mail out of the bin without putting it back in the inbox. Everything else the conversation wears stays.
- The picker offers the Inbox, your labels, Spam and Trash, and applies the one you click at once. The list you are already in, a label the conversation already has, and the bin for mail already in it are left off. See Mail → Move to.
- Archive, label changes and moves can be undone. Each shows an Undo for eight seconds, and it puts back exactly the labels every conversation had, rather than reversing the action — so an already-archived conversation that was ticked along with the rest stays archived. Dragging onto a folder gets the same Undo. An action over a whole view runs in the background and has none.
- Labelling a selection is one request, not one per conversation. The toolbar's label menu posted once for every ticked row; it posts once now, which is also why there is one toast and one Undo for the lot.
Fixed
- Signing in with a two-factor code could leave you on the code form, its button reading "Checking…" for good. The code had been accepted — reloading showed the inbox. While the code form is on screen the page behind it still fetches things, and each of those requests, turned away because the sign-in was not finished, was remembered as "where you were going". After a correct code you were sent to the last of them. When that was the request that refreshes the live-update cookie, which has no page to show, the browser stayed where it was. It happened on installations whose live updates were failing, because that is when the page keeps making that request. Only a page you were actually opening is remembered now, and a part of a page loaded in the background is not one.
- Starting plMail took minutes on a NAS with spinning disks. After "Statement statistics are enabled" the log went quiet for two minutes and more before the web server came up. At every start each container set access-control entries on its cache directory, which is part of the image — and changing anything about an image file makes Docker copy the whole file into the container and flush it to disk. Two thousand files, one synchronous write each, in every container at once. Nothing needed those entries: the image already makes the directory writable. They are no longer set. Only installs running as root were affected, which is why the TrueNAS catalogue app was not.
- And thirty seconds before that, on the compose files in this repository. The web server and workers wait for the database to report healthy, Docker's first health probe comes one interval after the container starts, and the interval was never set — so it was the default thirty seconds, and sixty when Postgres was still replaying its log at the first look. It is five seconds now. If you keep your own copy of
compose.yamlortruenas.compose.yaml, addinterval: 5sandretries: 30to thedatabasehealth check. - A busy Gmail or Outlook quota no longer raises an alarm, and no longer costs mail. When a provider was still rate-limiting after five retries, the job was logged at the highest level with a full stack trace and parked on the failed queue — whatever the job was. For a sync that only looks for what is new, that was noise: the next sync resumes from the same point. For a job fetching particular messages it was worse than it looked: nothing asks for those messages again, so they stayed missing until somebody retried the parked job by hand. A throttled sync is now dropped with one warning, and a throttled fetch is put back to try again a quarter of an hour later, up to ten times, before it is allowed to fail.
- A fresh install had no live updates until the container was next restarted. The inbox did not refresh by itself and the browser console showed a blocked connection to
https://localhost. The address typed into the install form was saved, but the web server had started before it existed and kept telling pages to reach the live-update hub at the built-in default. The install form now ends by restarting the web server, on a page that waits until it is back and then opens the inbox. - The "plMail is restarting" page waits for the server instead of guessing. It used to reload after six seconds whether or not anything was listening yet, and on a slower machine that showed the browser's own connection error.
Before you upgrade
- One migration, applied on boot. It adds two columns to
messageand two toai_settings, and changes no existing row. - Holding new mail for the assistant is on by default, and applies only to people who sort by assistant. Switch it off under Admin → AI if your model host is slow.
- The worker container runs three more processes,
worker-live,worker-releaseandworker-enrich. Nothing to configure on the standard images. If you run your own consumers instead ofapp:work, addmessenger:consume enrich_live,messenger:consume releaseandmessenger:consume enrich enrich_backlog— without them nothing sorts mail, extracts events or indexes for search. - Jobs already queued at upgrade are worked through where they are, on
ingest, once.