* 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>
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>
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>
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>
* 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>
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>