The picture brought its own X into the corner of the dialog body, just
below the modal's X. It is gone; while a picture is up, the modal's X is
relabelled "Close the screenshot" and its first press puts the picture
back, the second closes What's new, as Escape already did.
Closes#187
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Leave a message another Gmail account holds with that account
With several Gmail accounts connected, mail forwarded from one to another
reaches the second still Delivered-To the first and is attributed there.
Both merges in SyncGmailMessageBatchHandler then found the first account's
own row by Message-ID and wrote the second mailbox's Gmail id over its own:
they were written for a sibling that syncs over IMAP, and the sibling map is
every active account. The first account's sync then stored the message a
second time, and a later merge by Message-ID picked the other of the two
rows and gave it an id its twin still held: a duplicate key on
uniq_message_gmail_id_account, the whole batch failed, and every retry
fetched it again until Gmail answered 429 (discussion #168).
A row is now looked up by its Gmail id before either merge. A merge only
gives an id to a row of a non-Gmail account that has none yet, or has this
carrier's; anything else is recorded as held elsewhere, so the import counts
it and stops asking for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Link the changelog entry to its PR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rules run after threading and before the batch's flush, and ThreadLabelSynchronizer took the union over $thread->messages, which did not yet hold the message the threader had just attached. A rule's label was taken straight off the conversation again. The sync now counts the messages that point at the thread in memory (collection, identity map and pending inserts).
Closes#184
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A milestone branch on its way to main (milestone/v0.4.0, #182) collects
work that was agreed and reviewed piece by piece, each piece closing its
own issue. It has no issue of its own to name, and listing the pieces'
issues only works once each has landed, so the required linked-issue
check refused it. `deployment` joins `bug`, `chore` and `dependencies` as
a label that needs none.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Threading looked only from a reply to the message it answers, so replies synced before that message (INBOX before Sent Items) opened threads of their own and stayed there. MessageThreader now also looks the other way: after a message gets its thread, mail that already names its Message-ID in In-Reply-To or References is folded in by the new ThreadMerger. The older thread survives, keeping its JMAP id, summary, insights, star, snooze, labels and pinned tab. A GIN expression index makes the reverse lookup an index probe.
Review follow-up: moved mail is added to the surviving thread's collection so a label rule later in the same batch keeps its labels, plus tests for that, bracketed legacy references and provider-keyed survivors.
Closes#172
Co-authored-by: Philippe Delodder <philippe@delodder.be>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mailbox.uid_validity becomes a BIGINT, so IMAP folders whose UIDVALIDITY is above the signed INT range sync again. Reversible migration; the mapping stays ?int (DBAL 4 hydrates a bigint as an int).
Closes#171
Co-Authored-By: Philippe Delodder <philippe@delodder.be>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking a highlight's screenshot opened the PNG in a new tab. It now grows
out of its thumbnail until it fills the What's new dialog, with an X; the X,
Escape or Back shrink it back, and What's new stays open.
Back goes through a history entry of its own (assets/history_layer.js),
imported first in app.js so its popstate listener runs before Turbo's, which
would otherwise restore the page and close the dialog.
Closes#179
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* The ? menu draws above the calendar, and a main build announces its newest release
The menu was an ordinary dropdown inside the header's z-20 stacking
context, so the calendar's day headings painted over it. It now opens in
the top layer through ui--user-menu, like the account menu, and closes
when an item is chosen.
A build that is not a tag (main, sha-…, a checkout) counted as no
release, so installs that follow main never saw the dialog. It now
counts as the newest tagged release in its own changelog; Unreleased is
still never shown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Feedback with a screenshot stays in the dialog, whatever comes back
Sending with a screenshot closed the dialog without the sent screen. The
form is now posted with fetch() and its answer drawn into the dialog;
anything that is not the dialog (a proxy refusing a large body, a server
error) is said in the form with its status, and the dialog stays.
The screenshot is a JPEG at most 1600 px wide instead of a full-size
PNG, a fraction of the size, so a reverse proxy's default body limit is
far away. The From is the first field of the account that is an
address; an account with none is refused in the form instead of
throwing at send time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Find the open conversation's toolbar by type in the keyboard-shortcuts spec
The spec looked for the conversation by id alone, and the messages
inside it carry the same attribute with their own ids. On CI a message
id equalled the conversation's, so two elements matched and the strict
locator failed on the first try and the retry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* What's new opens after an update, with a way to send feedback
The release notes from CHANGELOG.md, read from the image, in a dialog that
opens by itself the first time somebody uses plMail after an update and
never on a fresh install. A ? in the top bar opens it again, with a dot
while something is unseen; "Don't show this after updates" silences it,
and Settings -> General -> What's new undoes that. Highlights are marked
in the changelog with an HTML comment and may carry a screenshot taken by
tests/e2e/whats-new-screenshots.spec.ts.
Send feedback is a mail from one of the person's own accounts to
APP_FEEDBACK_TO (feedback@pluetzner.de by default), with an optional
rating and a screenshot of the page behind the dialog.
Existing users are recorded as having seen 0.3.1 by a migration; anyone
created later starts on the release running at their first request.
Closes#165.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plain changelog headlines from 0.3.2 on, and highlights with screenshots
Every bold headline users see under "New to you" now says what changed in
a few words. Each release gets a handful of highlights, eight of them
with a picture cropped to the card or pane they are about, taken by
tests/e2e/whats-new-screenshots.spec.ts.
Relative links in the notes keep their words and lose the target, and
the parsed changelog's cache key carries a format version so a new
image never serves the previous one's parse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep highlight pictures for ten releases, and none that nothing names
Every file in public/whats-new ships in every image. ChangelogReaderTest
now fails on a picture no highlight names, and on one whose release is
more than ten back; the release pull request that crosses the line
deletes it and leaves the entry a highlight without a picture.
Documented in CONTRIBUTING.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Credit #166 in the changelog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Match the Back button by its whole name in the back-navigation specs
Playwright matches a string name as a substring, so "Back" also found the
new "Help and feedback" button in the top bar, which comes first, and the
specs opened its menu instead of going back to the list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Synced drafts no longer get a sentAt, so the editor opens them. A migration clears it on rows the provider still files as drafts, and a draft that turns up in Sent is settled as sent.
Takes over #123.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pulling the mail list down from its top, with a finger below md, runs the topbar's sync. A shared touch-gesture module decides what a touch is and who owns it, for the row swipes and long-press to come. Mouse, trackpad and wider windows are unchanged.
Takes over #115.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The in-app Web Push device is now told only when mail lands in the Inbox of a mailbox that has finished importing, and Outlook stops recording folder moves for messages that did not move. JMAP clients and FCM phones keep every StateChange. Migration: jmap_push_subscription.arrivals_only, set once for in-app devices.
Takes over #112.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A streaming controller's set_time_limit() outlived its test in the single PHPUnit process and killed slower runs part-way. A PHPUnit extension resets it after every test.
Co-authored-by: pmdevelopment <4947485+pmdevelopment@users.noreply.github.com>
* feat(mail): replace the history entry when switching category tabs on a phone
Below md, a category tab navigates the inbox list frame with Turbo's
replace action, so the browser's Back leaves the inbox instead of
stepping back through every tab. At md and up the link keeps advance,
so desktop behaviour is unchanged.
The choice is made on click by the ui--tab-history controller, not on
connect: the tab strip is morphed on every list refresh, and a morph
removes attributes the server did not render.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
* feat(mail): category tabs replace the history entry at every width
#150 asked for this on a phone only; the maintainer wants it everywhere,
as Gmail does. With no breakpoint to ask about, the tab links render
data-turbo-action="replace" from the server, so the click-time controller
goes: a server-rendered attribute survives the strip's morph on its own.
The account page's tab strip, added on main since, gets the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): credit the tab history entry to #161, which took over #113
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Compose files keep only a short header; the reasoning lives in
docs/install. truenas.compose.yaml is gone (the TrueNAS catalog app
replaces it) and so is compose.prod.yaml (compose.yaml already runs the
published prod image). compose.override.yaml.dist becomes compose.dev.yaml,
passed with -f, so there is no untracked copy that compose loads on its
own. Docs, CODESTYLE, CONTRIBUTING and the entrypoint's error text follow.
Every remaining file resolves to the same `docker compose config` as
before.
Closes#30
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Settings → Profile opens the same file picker as attaching to a mail, in an avatar mode: albums, people, search and paging, one picture at a time, files that cannot be a picture greyed out. The choice is posted straight back to the profile form, and the connection is matched against the user's own. From Paperless-ngx the original is taken rather than the archived PDF. The setup wizard keeps its small grid for now.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Save to… chooser drew Paperless tags as folders to open, and clicking one did nothing. A DestinationKind (folder, album, tag) now decides how the chooser is drawn: a tag is the choice itself, with a No tag row. The upload is sent as a string with a Content-Length instead of streamed, which Paperless read as an empty form ("No file was submitted", #79).
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Below 36rem of pane width the date, the menu and the arrow move onto a row of their own under the sender, so the sender gets the full width. A container query on the message list, so it follows the pane, not the window.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The linked-issue check now fails a PR that closes no issue, unless it is labelled bug, chore or dependencies, and re-runs on label changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every job of the E2E workflow ran `composer install`, whose post-install
importmap:install fetched all 45 packages from cdn.jsdelivr.net: about 270
requests per run. jsDelivr builds a `+esm` bundle on request, and now and
then one times out ("Idle timeout reached for .../openpgp@6.3.2/+esm",
"Failed to connect to cdn.jsdelivr.net"), which failed the job before a
test ran.
assets/vendor/ is now cached, keyed by importmap.php. importmap:install
skips a package already there at the version importmap.php names, so the
CDN is asked once per change to the importmap. When it still fails, the
install is tried up to three times; a retry finds vendor/ in place and
only reruns the scripts.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Issue forms, a PR template, area labels and a linked-issue warning on PRs, and CONTRIBUTING.md "How work gets in": a feature needs an accepted issue first, milestones are releases, main is protected. The roadmap checklist moved to issues #126-#130.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Archived mail had no list to be browsed in, only search. All Mail lists every conversation outside Spam and the bin (what arrived, what was sent, drafts and archived mail), as Gmail's All Mail does, across the reader's active accounts or within one.
It is LabelRole::AllMail, a view and not a place, like Unread: nothing attaches it to mail, it is created hidden when label settings is opened, it is bound to no account and it refuses moves. Its eye in Settings → Mail → Labels shows or hides both the row beside Drafts and the row at the top of each account's folders. Archiving or snoozing from it leaves the row in place. One list scope, `all_mail`, with the account id as its value or empty for every account; AllMailView holds the rules the row buttons, the bulk toolbar, Move to and select-all ask.
No migration.
Co-authored-by: shady2k <3366415+shady2k@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clicking an account in the sidebar opened a plain list, so on an Outlook account (or any account) the category tabs were only on the top-level Inbox. An account's page can now draw the same tabs, the reader's own included, chosen by the same rule as the unified inbox and counted from that account's mail alone. The strip is plainer than the unified one: no "new" pills, sender hints, unread tint or drop targets, since those count every account. It scrolls the open tab into view on a phone.
Settings → Mail → Categories has a new "Tabs on each account" switch, off by default. Off, an account's page stays its whole inbox in one list, as before, and a stale `?tab=` link opens that too. The switch is offered only while "Inbox tabs" is on. Stored in the user's settings bag and carried by a config backup. No migration.
Co-authored-by: Walid Mani <15911342+Flouss@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One per-user switch, off until somebody turns it on. With it on,
MessageRenderer treats every sender as if it were on the image allowlist,
so no message asks before loading its pictures. They still come through the
image proxy, so a sender learns that a message was opened but not the
reader's address. Mail in Spam stays blocked, as it does for trusted senders.
Kept apart from the per-sender allowlist: no "Stop showing" line for a
sender nobody trusted, and switching it off leaves those entries as they
were. Stored in the user's settings bag and carried by a config backup.
No migration.
Closes#124.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OidcAuthenticator issued no RememberMeBadge, so an OIDC login rested on the
PHP session alone: a browser-session cookie, a file cleaned up after the 24
minute session.gc_maxlifetime, and lost with the container on every deploy.
The badge now carries the firewall's remember-me parameter, because the
callback has no form to tick it on; always_remember_me would instead make the
password form's unticked checkbox meaningless. The token is stored in the
database and a remember-me cookie is still challenged by two-factor.
Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
bulk-select-all's "the pager agrees with the rows after a bulk action"
failed now and then under load. Not the job: it archived the 67
conversations in six seconds. A nudge from the job started a list
refresh while it was still writing, the job's "done" arrived while that
fetch was out, and _refreshList() returned early because one was
already in flight. The fetch came back without the archive, and nothing
was left to refresh the list — it showed the archived conversations
until something unrelated happened.
A request that arrives during a refresh is now remembered and taken
once the current fetch lands, keeping the most urgent of whatever asked
in the meantime. list-refresh-queue.spec.ts holds the first fetch at the
network so the second request always arrives during it; it fails
without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compose-formatting's "typed emoticons are left exactly as typed" failed
about a third of the time under load. The signature spec saved a
signature on the worker's user and never took it away, so whichever spec
ran next on that worker composed with one. The emoji test clicked the
editor and assumed the caret landed at the end of the body; with a
signature it lands on the empty line above it, the text went in before
the signature, and the exact assertion failed.
Both ends fixed: the signature spec clears the signature after each
test, and the emoji test places the caret at the end of the body
itself instead of trusting a click.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The delete form had Turbo switched off, which is what asks Turbo's
confirm question — so a tab and its filing went at one click. It goes
through Turbo now, as the account list's delete does.
The tab card is headed "Tabs": the sorting card's first switch is
already called "Inbox tabs".
inbox-categories.spec.ts makes a tab, moves it with the buttons and
reloads to the same order, and switches Forums off and finds no Forums
tab in the inbox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A phone could see the five built-in categories and nothing of how the
reader arranged them. urn:plmail:params:jmap:inboxtabs adds:
- InboxTab/get and /changes: every tab in the reader's order, built-ins
with isEnabled, the reader's own with their name. Per user, answered
by every account; a change is recorded in each account's log so push
and /changes work as they do for labels.
- Thread.inboxTab: the tab the web shows a conversation in — the
reader's own, the built-in, or Primary for a switched-off category.
- Email/query threadInboxTab: the same list the web shows under a key.
- Thread/set { inboxTab }: a move between tabs, pinned like a drop.
A tab the reader does not have is refused.
Thread.category and threadCategory are unchanged for clients that know
only the five.
The recompute statements wrote every conversation's category back
whether it changed or not and recorded nothing, so a re-sort — any of
the sorting choices, and now switching a tab — left phones drawing the
old tabs. They update only what moves and return it, and every caller
records those as Thread changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What sorts mail into tabs and the tabs themselves were two cards under
General. They are their own page now, in the Mail group: the sorting
card, then every tab on one list in the order the inbox draws them.
- Built-in tabs carry an On/Off switch. Off releases conversations
somebody dragged into the category and queues the re-sort the other
sorting choices queue; on queues it too, and the mail comes back.
- The reader's own tabs keep rename and delete.
- Everything but Primary is dragged into place, or moved with buttons
from a keyboard or a phone, as on the account list. Primary sits
above the list: always first, never off.
The order endpoint keeps only keys the reader has, so a request cannot
plant a key naming somebody else's category.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One order across the built-in tabs and the reader's own, and a set of
built-ins switched off, both beside the other sorting choices on the
user (CategorySorting). The order replaces the categories' own position
column, which never shipped: the strip is one list, so its order is too.
Primary is always first and cannot be switched off — it is where the
rest falls back to.
A switched-off category takes no mail. Whatever would have gone there —
by the header rules, Google's label or the assistant — is in Primary,
and the message panel says it would have been that category. The
answer is mapped rather than the next rule consulted: "Forums is off"
does not make a mailing list Promotions for its List-Unsubscribe.
Until the re-sort a switch queues has run, and for a conversation
somebody pinned there, Primary lists and counts the switched-off
category's conversations too, so nothing is in no tab at all.
InboxTabs is where order and on/off are applied, so the strip, the drop
targets and every key that resolves agree. A restore rewrites the
order's own-tab keys to the categories' new ids.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A filter only filed Gmail-labelled mail while the reader had asked
plMail's sorting to overrule Google's. Google's category and plMail's
rules are both guesses, and the override decides between them; a filter
is an instruction the reader wrote, and outranks both. Out of the filer,
the threader and the two recompute statements alike.
A new tab could be called "Werbung" beside the built-in Werbung: the
reserved names were the enum's English values only. Every interface
language's label is refused now.
The per-account recompute sorted every message on the installation for
each account it was asked about. It reads only that account's now —
the same rows, since a conversation belongs to one account — which
matters more once switching a tab on or off re-sorts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The migration is dated after the ones main has run since, under a name
no open branch uses (#109 and #112 hold the earlier slots today).
- The inbox's query ceiling is one above the shared one again: main
raised that to 28 for the sidebar's change counter, and the own-tabs
read is still the one statement the inbox adds on top.
- French for the new strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The listing that confirms nothing is missing waited out the same hour
that lets batches drain, so a finished import showed "Importing · 100%"
for up to an hour. When Gmail's current total is accounted for — stored,
carried into another account, or declined — that listing now goes after
ten minutes. Each check costs one getProfile, which also brings the
total up to date; the count on the line no longer runs past it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Gmail collecting mail@… and that mailbox also connected, its
messages are stored on (or merged into) the other account, carrying
this Gmail account as gmailCarrierAccount. The import line counted only
this account's rows and skipped messages, so it sat at 65,010 of 76,018
with the queue empty and an estimate of two hours left — short by
exactly what the other account held. Nothing was being fetched; the
backfill itself already treats those ids as synced.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The French interface came with a note that appointments in French mail
were left for later. They are read now, for every user, since the
interface language says nothing about the language of the mail:
- The date detector and its gate know the French months and weekdays,
"1er", 14h30 / 14 h 30 / "à 9h" / "vers 15 heures" / midi, demain,
après-demain (before demain, as übermorgen before morgen),
aujourd'hui, ce soir, and "mardi prochain" either way round. A bare
single-digit "2h" is not a time: beside a date it is a length.
- A French reader reads 04/08/2026 day first, as a German one does.
- French quote attributions ("… a écrit :") and forwarded headers
("Envoyé :", "Message transféré") are not appointments.
- Durations: "2 heures", "une heure".
- Invoices: facture, net à payer, montant dû, échéance, numéro de
facture, and "Total HT" is not the total.
- Login codes: code de vérification and its relatives, "valable 10
minutes", and French order and customer numbers are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Gmail import now takes in Spam and Trash, and everything that reads
new mail for its meaning would have read those too. Message::isSetAside()
says whether a message is filed in either, and the pipeline asks it:
ClassifyMailStep no language-model categorisation
the hold not kept out of the tabs awaiting a verdict
that is not coming
ExtractInsightsStep no parcel, invoice or other cards
ExtractEventsStep no events read out of it; a forged invitation
in Spam stays out of the calendar
ProposeEventsStep no proposals
contact harvesting Spam only: a spammer is not a correspondent,
and somebody whose mail was deleted still is
The three read-for-meaning steps share RecentMailPolicy::recentIds(),
which leaves set-aside mail out unless asked. ReadReceiptStep asks:
pairing a receipt with its mail reads nothing, and a receipt filtered
into Spam is still that mail's.
Unchanged: storing, threading, the rule-based categoriser, the user's
filter rules, JMAP state. It applies to every provider, not only Gmail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72d3956b keeps a message for the Gmail account when nothing else claims
it. One case makes that a duplicate: Gmail collected the message from
another connected account (Gmailify or "check mail from other
accounts"), that account's own sync holds it, and none of its address
survived in the headers — a Bcc'd newsletter, say. The old rule dropped
it; the new one would have stored it a second time.
When a message reaches the Gmail account only because nobody claimed
it, the user's other accounts are asked for the same Message-ID first,
and a copy found there is enriched (gmailId, labels) as a Gmailify
sibling's is. A message whose headers name the Gmail account itself is
not looked up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`app:gmail:skipped` on a real mailbox (#90) showed what the ownership
filter was dropping: the account owner's own mail.
202 sent from "send mail as" addresses (From: pl@pmdevelopment.de)
89 forwarded in from an address not connected here
43 Gmail drafts
14 received, with no recipient header naming anyone
4 addressed to the googlemail.com spelling of the account
6 chats
resolveOwningAccount() kept a message only when its headers named the
account. It now keeps one for the account unless another connected
account has the better claim (a Gmailify sibling named in its headers):
SENT or DRAFT the sibling the From names, else this account
received resolveReceivedAccount()'s answer, else this account;
with Gmailify attribution switched off, still only mail
addressed to this account
CHAT nobody, and recorded as before
GmailAddressFilter reads googlemail.com as gmail.com.
The listing never asked for Spam and Trash although its comment said
it did: messages.list leaves both out without includeSpamTrash, and
getProfile's messagesTotal, the import's total, counts them. How much
of the 11,200 that mailbox was short of this accounts for is not known
yet: its import was still running when it was counted.
A finished import does not list again, so the migration clears the
record of declined messages (chats aside) and every Gmail account's
backfill markers. Each lists its mailbox once more, fetching only what
it does not hold.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ajoute 'fr' à AppLocale et à framework.enabled_locales, qui doivent
rester synchronisés (le commentaire de translation.yaml le dit). Le
sélecteur de langue des réglages et celui de l'installation lisent
AppLocale::cases() : « 🇫🇷 Français » y apparaît sans autre
modification.
Deux réglages suivent la langue et devaient connaître le français :
- ClockFormat::forLocale() : le français écrit 14 h 00, donc 24 heures
par défaut comme l'allemand. Sans ça, un utilisateur francophone qui
n'a rien choisi verrait « 2 PM ». Testé dans ClockFormatResolverTest.
- Le sélecteur d'emoji charge ses noms et mots-clés par langue (la
recherche « cœur » ne trouve rien dans les données anglaises).
assets/emoji/fr/data.json vient de emoji-picker-element-data, comme
en/ et de/. J'ai vérifié que ces deux fichiers sont identiques octet
pour octet à ceux publiés par le paquet, donc fr/ vient bien de la
même source. Le gabarit choisit maintenant de, fr ou en d'après la
langue au lieu du seul test « starts with 'de' ».
Laissé tel quel : DeterministicDateDetector, qui lit les dates dans le
contenu des mails selon leur langue, et non la langue de l'interface.
Apprendre les dates françaises (« le 4 août ») est un autre chantier.
Docs : README, features/appearance.md (et sa traduction allemande, avec
le hash mis à jour), et une entrée de CHANGELOG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nYWPc4bWacb79hm4ytfni
Les 3 118 clés de messages.en.yaml, plus les domaines validators et
security, traduits en français. Le catalogue seul ne change rien à
l'écran : la locale est déclarée au commit suivant. CatalogueParityTest
vérifie désormais le français comme l'allemand et le pirate. C'est le
seul garde-fou, car Symfony retombe sans bruit sur l'anglais quand une
clé manque.
Choix de traduction, pour qui reprendra ces fichiers :
- Vouvoiement, l'usage des logiciels en français, alors que
l'allemand tutoie. Aucun test ne l'impose, contrairement à
GermanIsInformalTest.
- « Agenda » pour calendar, « libellé » pour label, « suivi » pour
star (comme Gmail en français), « mettre en attente » pour snooze,
« courrier » pour mail quand c'est le contenu et « mail » pour un
message. Les termes des consoles Google et Microsoft sont repris tels
que leurs interfaces françaises les affichent.
- Apostrophe typographique (’) partout dans le texte. Avec une
apostrophe droite, chaque chaîne entre guillemets simples devrait la
doubler, et un oubli casse le fichier entier. L'espace avant « : » à
la française oblige aussi à mettre la valeur entre guillemets :
security.fr.yaml ne se chargeait plus à cause de ça (« A colon cannot
be used in an unquoted mapping value »). C'est corrigé et vérifié en
chargeant les trois fichiers.
- Placeholders (%count%, {skinTone}, {{ limit }}) et formes plurielles
({0}…|{1}…|]1,Inf[…) contrôlés clé par clé contre l'anglais : aucun
écart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018nYWPc4bWacb79hm4ytfni