# Changelog Notable changes per release. plMail runs its migrations automatically on boot, so anything that changes the schema irreversibly is called out explicitly. The published image tags: `latest` follows the most recent release below, `main` follows the tip of the default branch, and `sha-…` pins one commit. ## Unreleased **Releases are published on GitHub, so you can follow them in a feed reader.** Each tagged release now gets a page under [Releases](https://github.com/karatektus/pl_mail/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](https://github.com/karatektus/pl_mail/discussions/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](https://github.com/karatektus/pl_mail/issues/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](https://github.com/karatektus/pl_mail/issues/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](https://github.com/karatektus/pl_mail/issues/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](https://github.com/karatektus/pl_mail/issues/232), [#233](https://github.com/karatektus/pl_mail/issues/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](https://github.com/karatektus/pl_mail/issues/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](https://github.com/karatektus/pl_mail/issues/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 to the home one) kept the default name; it now asks for another name instead. No schema change. [#216](https://github.com/karatektus/pl_mail/issues/216), by @phdelodder. **Inline images in a message show up again.** Pictures that a mail carries inside itself, such as a logo or the icons in a booking confirmation, rendered as empty boxes: the message body is shown in a sandbox that sends no session cookie, so each image was answered with a redirect to the login page. They now load through a signed link (`/mail/inline/{id}`) that only answers for an image of the message being read, and nothing that is not a picture. The link stops working after a day or two, so one that leaks through a forward or a log does not open the image for good; the message signs a fresh one each time it is shown. Images labelled `application/octet-stream` or `image/octet-stream` load the same way when their bytes are a picture. Messages already in the mailbox are fixed too, no backfill needed. No schema change. **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](https://github.com/karatektus/pl_mail/issues/169). **A deleted label no longer comes back after the next sync.** On a plain IMAP account, and for a folder on Outlook, deleting a label left its folder on the mail server, and the next sync made the label again. Deleting one now deletes the folder there too. When there is mail in it, the delete dialog first asks where that mail goes — Inbox, Archive, Trash or one of your other folders — moves it there, deletes the folder once the server says it is empty, and only then removes the label. This runs in the background and shows in the activity indicator, which names the folder it is waiting for; a folder that still holds mail on the server is never deleted, and a server that refuses the delete ends it at once with its reason. A folder still being imported is deleted only once its import has finished. A folder plMail creates on an IMAP server (an Archive or Trash it did not have yet) no longer shows as "Importing" in the activity indicator. Deleting an Outlook category never reached Outlook at all, and now does; labels nested under a Gmail label are deleted in Gmail with it. No schema change. [#40](https://github.com/karatektus/pl_mail/issues/40). **Inline images labelled `application/octet-stream` show up.** Many mailers do not say what an inline image is — they label it `application/octet-stream`, or `image/octet-stream`, which is not a registered type at all and is what Vaultwarden's notification mails put on their logo — and the attachment route served every such part as a download, so the image never appeared in the message. For those labels only, the file's own bytes now decide, held to the same allow-list as everything else: a PNG, JPEG, GIF or WebP is served inline, while SVG, HTML and anything unrecognised are still downloads. The image proxy already did this for unlabelled remote images and now shares the same check, so a remote image labelled `image/octet-stream` is now also identified from its bytes and served under its real type, rather than passed through under the label it came with. No schema change. Part of [#213](https://github.com/karatektus/pl_mail/issues/213). **A mail can go to Bcc recipients only.** Sending with nobody in To was refused with "Add at least one recipient before sending", even with addresses in Cc or Bcc: the window counted all three, but the server only counted To. It now counts all three as well. A mail with no recipient at all is still refused, and a draft may still be saved with none. No schema change. **Outlook's `` no longer shows up in a message.** Mail written for Outlook hides parts of itself behind conditional comments, and one form of them ends in a marker that is not inside a comment. A mail that also had a comment containing `--` anywhere in it was read by an older parser in one step of the rendering, which turned that marker into text, so a parcel notification could show `` in the middle of a sentence. The same mail could also lose a link around a table. Every comment is now removed before that step, and the marker with it. Mail already in the mailbox is repaired on request: `php bin/console app:backfill safe-html-conditional-comments` writes the shown copy of every message that has the marker in it again from the sender's original, and `safe-html-table-links` does the same for the links. No schema change. [#175](https://github.com/karatektus/pl_mail/issues/175), by @phdelodder. **One X on a What's new screenshot.** A screenshot opened full size brought its own X into the corner of the dialog, just below the dialog's X. Now there is only the dialog's: pressed once it puts the screenshot back, pressed again it closes What's new, the same as Escape. [#187](https://github.com/karatektus/pl_mail/issues/187). **An event that ends when it starts no longer runs into the next day.** A calendar feed or CalDAV event whose end is its start, such as a reminder at 23:59, was given an hour, as if no end had been stated. Other clients show it as 23:59–23:59, and here it ran past midnight and showed again on the next day. It now gets one minute, which keeps it on its own day. An event with no end at all still gets an hour, but the hour stops at midnight. Events already synced are corrected when the publisher next changes them, or right away by removing the calendar and adding it again. [#170](https://github.com/karatektus/pl_mail/issues/170). **Some IMAP messages no longer show their own headers above the content.** A Stalwart server answers some messages' body with the whole message, headers included, and plMail stored that as the body: the message opened with `Delivered-To:`, `Received:` and the rest of its header block printed as text, and its original held the headers twice. An exact repeat of the header block at the top of the body is now dropped before the message is read. Mail already stored that way is repaired by `app:backfill repeated-headers`, from the stored original and without asking the server; it reads the original of every IMAP message once. No migration. [#173](https://github.com/karatektus/pl_mail/issues/173), by @phdelodder. ## v0.3.6 — 2026-10-16 **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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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, a ZIP for one, and says so. Nextcloud, Google Drive, OneDrive, Dropbox, Immich and Google Photos choose exactly as before. No migration. [#78](https://github.com/karatektus/pl_mail/pull/78), by @Flouss. **Mail read in plMail is marked read in Outlook.** On Microsoft accounts, marking mail read (or starring it) is sent to Outlook by a background job, and that job used to work out the new state by reading the message back from plMail's database. When the job started a moment before the change had been saved, it read the old state and told Outlook the message was still unread: Outlook was touched, nothing changed, and an hour later the next sync brought the "unread" back into plMail. In a bulk mark-read several messages were affected. The job now carries what you chose. Archive, trash and label changes are unchanged, and a retried push still sends the current state rather than the original choice. Messages already stuck this way are not repaired; mark them read again. Gmail and IMAP jobs already carried the change themselves. No migration. [#111](https://github.com/karatektus/pl_mail/pull/111), by @Flouss. **Fixed the message header in narrow panes.** On a phone, or with the reading pane on the right and dragged narrow, the sender's address was cut mid-word across three lines while the date and the buttons took half the width. Below 36rem of pane width the date, the menu and the arrow now sit on a row of their own under the header, so the sender gets the full width. It follows the width of the pane, not of the window. Wider panes are unchanged. No migration. **Outlook mail moved in another app leaves its old folder here too.** A message marked as spam (or archived, or deleted) in the phone's mail app was taken out of its old folder and put in the new one, but its conversation kept the old folder as well. The message sat in Spam while the inbox list went on showing it. A conversation now sits in exactly the folders its messages do, so the move shows in the list at the next sync. Conversations already left that way are put right the next time one of their messages changes folder. [#121](https://github.com/karatektus/pl_mail/pull/121), by @Flouss. **Optional All Mail view (#57).** Archived mail had no list of its own to be browsed in, only search. All Mail holds it, together with what arrived, what you sent and your drafts, as Gmail's All Mail does, leaving out only Spam and the bin. It is off until you switch it on with its eye in Settings → Mail → Labels; then it is a row beside Drafts for every account together, and a row at the top of each account's folders for that account alone. Archiving or snoozing from it leaves the conversation in the list. A conversation with several labels is listed once. No migration. **Outlook changes appear without a reload.** Marking a message unread in Outlook on the web moved the unread counts in the sidebar and left the list showing the old row until the page was reloaded; moving or deleting one there did the same. A sync that only changed such a message ingested no mail, and "an account changed" was announced only after a sync had ingested something, so the open list was never told. It is told now, once per sync, and only when a row really changed: a server that agrees with what plMail already has says nothing. [#122](https://github.com/karatektus/pl_mail/pull/122), by @Flouss. **Profile pictures use the full file picker.** Settings → Profile showed a small grid of its own: the first couple of dozen images at the top of the service, with no albums, no people, no search, no way to scroll on. Each connection's button now opens the full picker, with "Use as profile picture" where a mail has "Attach", and one picture can be chosen at a time. A PDF, or a file the service says is over 4 MB, is shown greyed out rather than hidden; Immich and Google Photos do not report sizes, so a large photo from them is still refused after it is chosen, with the reason under the picture field. A scanned photograph from Paperless-ngx is now taken as the original — it used to arrive as the archived PDF and be refused. The setup wizard's profile step still has the small grid, because the picker would replace the wizard it is inside. No migration. **Option to always show remote images (#124).** Settings → Mail → Images has one switch, **Always show remote images**, off until you turn it on. With it on, no message asks before loading its pictures; they still come through plMail's image proxy, so a sender learns that the message was opened but not your address. Mail in Spam stays blocked as before, and the senders you trusted one by one are kept for when it is switched off again. The choice lives in your user settings and a config backup carries it. No migration. **Category tabs in an account's own inbox (#55).** Clicking an account in the sidebar opened a plain list, so on an Outlook account — or any account — the tabs were nowhere to be found unless you went back to the top-level Inbox. The categories were being worked out all along; that page just never drew them. Settings → Mail → Categories now has **Tabs on each account**, off until you turn it on: with it on, an account's page shows Primary, Social, Promotions, Updates, Forums and your own tabs for that account's mail; off, it stays the account's whole inbox in one list, as before. It is offered only while **Inbox tabs** is on, and a config backup carries it. The tabs on this page are plainer than the top-level Inbox's: no "new" pills, sender hints or unread tint (those add up every account), and no dropping a conversation onto a tab. No migration. **Single sign-on keeps you signed in.** A single sign-on sign-in left nothing but the PHP session holding you in: a cookie that ends with the browser, kept in a file that is cleaned up after 24 minutes without a request and that goes with the container whenever the server is updated. Which is why it signed people out every few hours. It now sets the same 60-day remember-me cookie, stored in the database, so a lapsed session is picked up again without a visit to the provider. Accounts with two-factor authentication are still asked for their code when the cookie signs them back in. Everyone signed in through the provider before the upgrade signs in once more to get the cookie. **Gmail import includes all mail in the mailbox.** plMail kept a Gmail message only when its headers named the account, and on a real mailbox (`app:gmail:skipped`) what it dropped was the account owner's own mail: 202 messages sent from "send mail as" addresses, 89 forwarded in from an address not connected here, 43 drafts and 4 addressed to the `googlemail.com` spelling of the address. A message in a Gmail mailbox is now that account's unless another connected account has a better claim to it; drafts arrive as drafts, and `googlemail.com` is read as `gmail.com`. Only chats are still left out. The import also fetches Spam and Trash now, which it said it did and never asked Google for — the reason an import could not reach Gmail's own total. Every Gmail account lists its mailbox once more after the upgrade to take these in, and shows as importing until that pass is done. A message that names nobody and that Gmail collected from another connected account is merged into that account's copy rather than listed twice. **Migration:** clears the record of skipped Gmail messages except chats, and the backfill markers of Gmail accounts. [#90](https://github.com/karatektus/pl_mail/issues/90), reported by @karamanliev. The progress line also counts mail the import stored on another connected account — Gmail collecting a mailbox that is connected here too — so it no longer stops short of Gmail's total by that mailbox's size. Once everything is in, the import confirms it within about ten minutes instead of showing "Importing · 100%" for up to an hour, and the count no longer runs past a total that new mail has outgrown. **Spam and Trash are no longer processed.** Mail arriving in either — from any provider, and now from a Gmail import — is no longer categorised by the language model, held back from the inbox tabs, read for parcels, invoices or meetings, or offered as a calendar event, and senders of spam no longer become contacts. A forged invitation in Spam stays out of the calendar. Read receipts filtered into Spam are still matched to the mail they are about. No migration. **French translation.** Choose **Français** under **Settings → General → Language**, or when creating the first account. The whole interface is translated, including the admin panel, the validation and sign-in messages and the emoji picker's search; French also defaults to the 24-hour clock, as German does. Mail written in French is read as German and English mail already was, whatever language the interface is in: an appointment like "mardi prochain à 14h30" is offered for the calendar, a "facture" with its "net à payer" becomes an invoice card, and a "code de vérification" a login-code card. A slashed date such as 04/08/2026 is read day first for a French reader. No migration. **Customisable inbox tabs.** The new Settings → Categories, in the Mail group, holds everything about the tabs: what sorts mail into them (the card that used to sit under General), which of the five built-in tabs are on, the tabs you make yourself, and the order of all of them — dragged, or moved with buttons from a keyboard or a phone. Primary is always first and always on. A tab you switch off takes no mail: the header rules, Google's labels and the assistant all file what would have gone there under Primary, and switching it back on re-sorts and brings it back. A tab of your own is filled by a filter whose action is **File in category**; mail filed there leaves the built-in tab it would otherwise be in, and the tab shows while it holds mail, takes drag-and-drop, and counts and badges like the others. A filter files Gmail mail too, whatever you chose about Google's own sorting. Only mail whose sender passes authentication is filed, so a forged "your parcel is waiting" never lands in a tab named after the real carrier; on a mailbox where plMail cannot tell which server vouches for the mail such a filter files nothing, and the filters page says so. Mail stays in a tab when the filter that put it there is changed or deleted. The Android app and other JMAP clients can draw the same tabs and move mail between them (`urn:plmail:params:jmap:inboxtabs`), and a re-sort that moves conversations between tabs now tells them — a phone used to keep showing a conversation where it had been. Re-sorting also reads only the mailbox being re-sorted, where it used to sort every user's mail once per account. **Migration:** one new table, a nullable column each on messages and conversations and two settings columns on users, added without rewriting any mail. With tabs on, every inbox page and sidebar-counts poll makes one more indexed read. [#87](https://github.com/karatektus/pl_mail/pull/87), by @Flouss. ## v0.3.5 — 2026-10-13 **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](docs/providers/oidc.md). `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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/pull/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](https://github.com/karatektus/pl_mail/issues/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 is then answered from that address without adding each one as an alias (not offered for Gmail and Outlook accounts, which send only as addresses the provider knows). A meeting invitation sent to such an address is yours to answer. Only the address a mail actually reached you by counts as yours, not the whole domain, so somebody else with a mailbox at the same domain stays in your reply-all and is never answered as. Reopening a draft saved from an alias no longer puts it back on the primary address. Typing an arbitrary From address into a new message is not part of this. The phone apps build their replies themselves and get the same answer from the server: `Email/get` has a `replyFrom` property, the identity that answers a mail, returned when asked for by name, and `EmailSubmission/set` accepts the identity it names for a catch-all address. A draft saved by an app now keeps its sender: `Email/set` takes `identityId` on a draft (announced as `draftIdentity` in the session), so a reply written from an alias no longer comes back from Drafts on the primary address. Apps already installed are unaffected and go on answering from the primary address until they are updated. No migration. [#41](https://github.com/karatektus/pl_mail/issues/41), reported by @YoGo9. **HTML signatures with images.** The signature editor has two views, Rendered and HTML. HTML takes the source; Rendered shows it as it will be stored, cleaned by the server, with a line underneath naming what was removed (usually a `