# 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 **Mail read, unread, starred or unstarred in Outlook now reaches 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. **Saving an attachment to Paperless-ngx works.** 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. **A message you read in plMail is read in Outlook too.** 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. **A message's header no longer breaks into pieces in a narrow pane.** 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 the folder it came from 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. **All Mail, if you want it: every conversation outside Spam and the bin, in one list (#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. **An Outlook account's read, unread, starred, moved and deleted mail reaches an open page 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. **Choosing a profile picture from Immich, Paperless-ngx or another connected service opens the same picker as attaching to a mail.** 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. **Remote images can be shown in every message without asking (#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. **An account's own inbox can have the category tabs (#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. **Signing in through single sign-on keeps you signed in, as "Keep me signed in" does on the password form.** 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. **A Gmail import takes in everything in the mailbox, not only mail addressed to the account.** 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 kept, not worked on.** 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. **plMail speaks French.** 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. **Inbox tabs are yours to arrange: switch the built-in ones off, make your own, and put them in any order.** 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 **Browsers and password managers kept offering your logins in 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 through Authentik, Keycloak or any other OpenID Connect provider.** 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. **Happening soon is wider, with Also spotted beside the list.** 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 no longer reads its times on a synced calendar's clock.** 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. **The browser tab shows the Inbox's unread count on every page that is not a mail folder.** 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. **Attachments on Outlook and Hotmail mail show up.** 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. **A parcel collected at a relay point now reads 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. **A parcel handed over at a Pickup relay no longer says "Deutsche Post".** 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. **Reading old mail again could turn a parcel's card back to its first 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. **A card made from old mail looked like new mail on Happening soon.** 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. **A one-time code stayed on Happening soon, readable, for a week.** 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. **A parcel, a code or a date in an Outlook mail was only found if it sat in the first few lines.** 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. **A GLS France parcel never appeared on the radar.** 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. **An invoice card could be numbered "TIFICATIONS", and two bills from one sender became one card.** 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. **Signing out holds.** 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. **An address can send under its own name.** 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. **A reply goes out from the address the mail was sent 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. **A signature can be pasted as HTML, and its images travel with the mail.** 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 `