9 Commits
Author SHA1 Message Date
ea70139077 Send In-Reply-To and References on a reply (#227)
* Send In-Reply-To and References on a reply

buildEmail() never wrote them, so every reply from the web composer or
JMAP reached the recipient as a new conversation. References ends with
the parent even when the draft's list lacks it (RFC 5322 3.6.4).

Closes #206

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* References ends with the parent even when the draft listed it earlier

normaliseList() keeps the first of a repeated id, so a draft whose
References already named the parent somewhere other than last (a JMAP
client's own order) sent a chain that did not end with it. The parent is
now taken out of the list before it is appended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Paul Lützner <karatektus@gmail.com>
2026-10-20 10:49:20 +02:00
e1200fdd6c Mail the AI host could not be reached for is asked again by the catch-up (#231)
The categorisation stamp was written even when the call never reached a model, so a short outage left every message in flight without a category for good. A timeout, an unreachable host or a refusal by 401, 403, 408, 429 or a 5xx now leaves the message unstamped for the catch-up; an answer, a 404 and a refusal of the request itself (400, 413, 422) are stamped as before. Closes #222.

Co-authored-by: Philippe Delodder <philippe@delodder.be>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 07:42:34 +02:00
50c842cc9d Reconnecting an integration edits it, and a taken name is a form error (#217)
The setup wizard built a new connection every time its Integrations step was saved, so saving it again collided with the unique key on (user, provider, name) and answered with a 500. It now edits the oldest existing connection for that service. The settings page had the same 500 for a second connection that kept the default name; a UniqueEntity constraint now reports it on the name field. Closes #216.

Co-authored-by: Philippe Delodder <philippe@delodder.be>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:35:50 +02:00
7fb2c8f38c Inline images in a message load through a signed link (#218)
The message body renders in a sandbox that sends no session cookie, so every cid: image was a 302 to the login page. They now load through /mail/inline/{id}, signed with an expiry of one to two days, for the message's own image parts only; parts labelled octet-stream are served when their bytes are an image. Closes #213.

Co-authored-by: Philippe Delodder <philippe@delodder.be>
Co-authored-by: MontyWei <62734446+montywei@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:03:04 +02:00
b453b79eb4 Single sign-on only: hide the password form and refuse password sign-ins (#210)
* Single sign-on only: hide the password form and refuse password sign-ins

Adds Admin -> SSO 'Single sign-on only' (and OIDC_DISABLE_PASSWORD_LOGIN).
The login page drops the password form and LoginFormAuthenticator refuses a
password sign-in before looking the user up. It only applies while a whole
provider is in force, the saving administrator must already be linked to the
provider, and app:sso:allow-password-login brings the form back from the
console. Config restore leaves it off. Off by default; one boolean column on
oidc_config.

Closes #169

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Single sign-on only: require a link to the provider being saved

The admin form let the box be ticked by an administrator linked to any
provider, including the one the page held before. Switching providers and
ticking it in one save made a provider they had never used the only way in.
It now compares the stored issuer with the one on the page, without a
trailing slash, as OidcDiscovery does.

Tests for what had none: the console command (clears the setting, keeps the
provider), a restore over a row that had it on, the link to another
provider, and a control showing the same password signs in with the setting
off. The "right credentials" test now posts the right password.

Docs: there is no iOS app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Docs: put the iOS app back in the single sign-on page

The previous commit dropped it on the wrong assumption that there is no
iOS app. There is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Paul Lützner <karatektus@gmail.com>
2026-10-19 13:09:12 +02:00
Philippe Delodder cbe14ea5ae Drop a header block the server repeats at the top of the body (#177)
Closes #173.

Co-authored-by: Philippe Delodder <7259755+phdelodder@users.noreply.github.com>
2026-10-16 18:48:07 +02:00
Philippe Delodder dda54a3876 Outlook's <![endif]--> no longer leaks into a message as text (#178)
Closes #175.

Co-authored-by: Philippe Delodder <7259755+phdelodder@users.noreply.github.com>
2026-10-16 18:35:03 +02:00
7a5a7350bf A conversation no longer splits when its replies are synced first (#176)
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>
2026-10-16 13:11:37 +02:00
7c9bf694ff Store a UIDVALIDITY above 2147483647 (#174)
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>
2026-10-16 12:49:28 +02:00