The 34 security fixes from #226, cherry-picked onto release/2026 without conflicts, and the version set to 2026.10.29. This becomes the 2026.10.29 security release.
Checked locally on the branch: fmt, fork checks, build, and the imap, webdav, jmap, system, smtp and directory suites all pass. The first system run failed at the Email/import over-quota check with BlobNotFound: the test's one-second upload lifetime ran out under load. That part of the test is unchanged from release/2026, and a rerun passed.
The 34 security fixes from #226, cherry-picked onto release/2026 without conflicts, and the version set to 2026.10.29. This becomes the 2026.10.29 security release.
Checked locally on the branch: fmt, fork checks, build, and the imap, webdav, jmap, system, smtp and directory suites all pass. The first system run failed at the Email/import over-quota check with BlobNotFound: the test's one-second upload lifetime ran out under load. That part of the test is unchanged from release/2026, and a rerun passed.
Before a client signed in, IMAP and ManageSieve accepted requests and
literals up to the full configured request size (50 MB by default), and
reserved room for a literal as soon as its size was announced. Each
argument was also copied when it was handed on, and the grown buffer
was kept for the rest of the session, so a large literal cost twice its
size and stayed allocated. Anyone who could connect could make the
server hold about 100 MB per connection without signing in.
Requests before sign-in are now limited to 64 KiB, which is more than
any login, SASL response or bearer token needs. The configured size
applies once the client has signed in, and stops applying again after
UNAUTHENTICATE. Arguments are handed over without a copy, and a
literal's buffer is sized to the literal.
Test: AUTHENTICATE PLAIN {100000} before sign-in got "+ Ready for
100000 bytes." on main and now gets BAD; a receiver unit test that
found a 2 MB buffer kept after a 1 MB literal now passes.
Email/set, Email/copy and Email/import took any string up to 128
characters as a keyword, spaces, parentheses and line breaks included,
and IMAP wrote keywords into FETCH FLAGS byte for byte. A signed-in
user could store a keyword such as "x)\r\n* 1 EXPUNGE" and forge
untagged responses in the IMAP session of everyone who reads the
message, other users of a shared mailbox included. A keyword stored
from an IMAP literal or a Sieve addflag action did the same.
JMAP now refuses, with invalidProperties, a keyword that is not 1 to
255 printable ASCII characters without ( ) { ] % * " \ (RFC 8621
section 4.1.1). IMAP leaves out of FLAGS any stored keyword that is not
an atom, so keywords already stored can't inject either.
Test: on main, a keyword stored from a literal came back as
"* 1 FETCH (FLAGS (... x)" followed by a separate "* 99 EXPUNGE) ..."
line, and Email/set accepted "x)\r\n* 1 EXPUNGE"; both are refused or
left out now, and a unit test covers the name rules.
STORE checked only the right to modify messages, and only on the
selected mailbox. A user a mailbox was shared with for flagging ("w")
could also set or clear \Deleted, which needs the right to remove
messages ("t", RFC 4314), so the owner's client would expunge a
message the user wasn't allowed to delete. And because keywords belong
to the message, a user with rights on one shared mailbox could change
the flags of a message that the owner also keeps in a mailbox the user
can't touch.
On a shared account, \Deleted now needs the right to remove messages
and every other flag the right to modify them, held on every mailbox
the message is in. Flags the user may not change are left as they are
and the rest of the STORE still applies; a STORE that asks only for
changes the user has no right to make on the selected mailbox gets NO
[NOPERM] as before.
Test: with the right to modify but not remove messages in another
user's mailbox, UID STORE 1 +FLAGS (\Deleted) set the flag on main and
now gets NO; +FLAGS (\Deleted \Flagged) now sets only \Flagged.
The right to read a shared mailbox was checked only at SELECT. A user
who had another account's mailbox selected kept reading its messages,
flags and counts through FETCH, SEARCH, SORT, THREAD, NOOP and IDLE
for as long as the connection stayed open, after the owner stopped
sharing it.
Commands on a selected mailbox in another account now check the right
to read it first, and get NO [NOPERM] once it's gone; CLOSE and
UNSELECT still work. NOOP and IDLE no longer report changes in a
mailbox that can't be read. Mailboxes in the user's own account are
not checked.
Test: after the owner ran DELETEACL, UID FETCH 1:* (PREVIEW) on the
still-selected mailbox returned the message on main and now gets NO
[NOPERM]; UID SEARCH ALL too.
With plain-text sign-in turned off, POP3 refused USER, PASS, APOP and
AUTH PLAIN on an unencrypted connection, but still offered and
accepted AUTH OAUTHBEARER and AUTH XOAUTH2. A client would send its
bearer token in the clear, where anyone on the network path could read
it and use it until it expired.
Every AUTH mechanism now follows the plain-text setting, as in IMAP and
ManageSieve, and CAPA offers no mechanism on a clear-text connection
where none would be accepted.
Test: on a clear-text connection with plain-text sign-in off, CAPA
listed "SASL OAUTHBEARER XOAUTH2" on main; it lists none now, and AUTH
OAUTHBEARER, XOAUTH2 and PLAIN are each refused.
An address that tripped the sign-in ban a second time after its first
ban had run out was not banned again. The server's list of banned
addresses kept the first, expired entry instead of the new one, and the
new ban couldn't be recorded because the expired ban's record was still
there (it is removed some time later), so no other server in a cluster
heard of it either. Anyone whose ban had ended could go back to
guessing passwords at full speed.
A new ban now replaces an earlier one that ends sooner, both in memory
and on record, and is announced to the cluster as before. A longer ban
already in place, such as one an administrator set, is kept.
Test: after the first ban of 10.0.0.2 ended, 105 more failed sign-ins
from it and then a correct password signed in on main; the address is
now banned again and its record runs from the second ban.
The HTTP sign-in cache remembered every successful sign-in for the
OAuth access-token lifetime (an hour by default) from the moment it
first saw the credentials. An OAuth access token first used shortly
before it expired kept working for up to that long afterwards, and so
did a token from an external OpenID Connect provider, whose lifetime is
often a few minutes. Anyone holding such a token, a leaked one
included, could use it past its expiry.
A bearer token's sign-in is now remembered only until the token
expires: the server's own tokens carry their expiry, and a provider's
JWT its exp claim. Other sign-ins are remembered as before.
Test: a 5-second access token used once 3 seconds in was still
accepted 6 seconds in on main; it is now refused.
A JMAP WebSocket kept the access token it was opened with for as long
as the socket stayed open. An expired OAuth token, a locked or deleted
account, a changed password or a removed app password or API key all
kept working on an open socket, and requests on it saw the permissions
and shares the account had when it connected. Requests over the socket
also skipped the per-account request limits.
Before each request, and before sending queued push notifications, the
session is now checked as it stands: when the bearer token has expired,
the account is locked or gone, the password has changed, or the app
password or API key it used is gone, the server answers with an
unauthorized error and closes the socket. Requests use the account's
current permissions and count toward its request limits like any other
HTTP request.
Test: a WebSocket opened with a 5-second OAuth token still answered
Mailbox/get 6 seconds later on main; it is now refused.
The refresh-token grant didn't compare the client making the request
with the client the refresh token was issued to, and checked a client
secret only when the request named a client. A refresh token issued to
a confidential client could be redeemed with no client authentication
at all, or under another client's id. Anyone who obtained a refresh
token (from a log, a backup or a compromised app) could keep minting
access tokens without the client's secret.
The refresh token names the client it was issued to. Only that client
can now redeem it, and a confidential client has to authenticate to do
so; a request that names no client stands for the token's own client
and is authenticated as it. A request naming another client gets
invalid_grant. A refresh token issued before tokens named their client
goes to the client that authenticates now.
Test: a confidential client's refresh token was redeemed with no
client id or secret on main; it now gets invalid_client, as does the
client's id without its secret, and another client's id gets
invalid_grant.
Checking a password against a bcrypt, SHA-crypt, MD5-crypt or DES-crypt
hash ran on the async worker threads that serve every connection, and
Argon2, PBKDF2 and scrypt hashes ran on the blocking pool with no limit
on how many at once. A flood of sign-in attempts against accounts with
such hashes, which needs no account of one's own, could stall every
protocol on the server or start hundreds of threads, each holding a
memory-hard hash.
Every slow hash is now checked or computed on the blocking pool, at
most one per CPU at a time; the rest wait their turn. Plain digests
(SHA, SSHA, MD5) are cheap and still checked inline.
Test: on a single-threaded runtime, another task made no progress while
a bcrypt hash was checked on main; it now runs during every slow hash.
The OpenID Connect directory accepted any JWT the provider signed. An
ID token is minted for whichever client the user signed in to, so
anyone who signed in to another application at the same provider could
present that application's ID token here and be signed in as the user
it names. A refresh token presented as a bearer token was likewise not
told apart.
A JWT the provider marks as an ID, refresh, offline or logout token
(Keycloak's typ claim, Amazon Cognito's token_use) is now refused.
Unmarked tokens are still accepted; requiring an audience is what stops
an unmarked ID token, and stays the administrator's setting.
Test: a signed JWT with typ "ID" (or "Refresh", "Offline", or
token_use "id" or "refresh") signed in on main; it is now refused, and
access tokens still sign in.
A push subscription URL was refused only when its host was a local or
reserved IP literal. A host name that resolved to 127.0.0.1, a private
range or 169.254.169.254 passed, so any signed-in user could make the
server send the verification request, and every later push, to a
service inside its own network.
Push requests now connect only to public addresses. Host names are
checked when they resolve, which also covers redirects and a name that
changes its answer after the subscription is created, and push never
goes through a configured proxy. The shared check is for every request
whose destination outsiders choose.
Test: a subscription to https://localhost:<port>/push got its
verification request on the local listener on main; now nothing
connects.
The https address in a domain's TLS-RPT record was posted to as given,
following up to ten redirects, and the MTA-STS policy host was fetched
wherever its name resolved. Anyone who controls DNS for a domain this
server delivers to could point either at loopback or a private range
and make the server send requests into its own network, and a 307 or
308 redirect turned the report into a POST to any URL.
Both now connect only to public addresses, checked when the name
resolves, and follow no redirects. A report address that is a local IP
literal is skipped and the report goes to the record's next address.
A domain whose MTA-STS host resolves only to private addresses now has
its policy fetch fail, as if the host were unreachable.
Test: a TLS-RPT record with rua=https://localhost:<port>/tls reached
the local listener on main; now nothing connects and the report goes
to the mailto address.
For every link to a known URL shortener in a message, the spam filter
made up to three requests to follow it, with no limit per message, so
one message full of shortener links made the server issue hundreds of
sequential requests with a 5 second timeout each. The lookups skipped
certificate checks and followed plain http links as given.
A message now causes at most ten redirector lookups. Lookups are made
over https with the certificate verified, a plain http link is looked
up at its https address, and requests reach only public addresses.
Test: a message with fifteen shortener links resolved all fifteen on
main (30 URLs analyzed); now ten are resolved (25), and a plain http
shortener link is still followed.
A BDAT command made the server reserve memory for the whole chunk size
the client declared, before any of the chunk had arrived. Any client
that got as far as RCPT could send "BDAT <size just under the limit>"
and stall, holding that much memory per connection, and with the
message size limit turned off a huge size could abort the process when
the allocation failed.
Only the bytes already received are reserved now, and the buffer grows
as the rest of the chunk comes in, still within the message size limit.
Test: "BDAT 268435456" with ten bytes sent reserved 268435456 bytes on
main; now the buffer stays under 1 MiB.
A From header listing addresses in more than one domain skipped DMARC
entirely, as if no domain had a policy. Anyone sending to this server
could get a message from a domain with p=reject accepted by adding a
second address in another domain to its From header.
Each distinct author domain is now checked, and the strictest outcome
applies: a failure under the strictest policy wins, then any failure,
then no result, then a pass. So the message is rejected or quarantined
when any of its author domains asks for that, and reports and
Authentication-Results follow the same domain. A From header naming
one domain is checked as before.
Test: a message with "From: [email protected], [email protected]" failing
example.com's p=reject was queued (250) on main; now it is rejected
with 550 5.7.1.
When a domain's MTA-STS TXT record announced a new policy id, the
cached policy was dropped before the new one was fetched. If the fetch
failed, delivery went ahead with no policy at all. An attacker on the
network path who could spoof the TXT record and block the policy host
could turn off MTA-STS for a domain and downgrade delivery to it.
A policy still in the cache now stays in force until it expires when
the new policy can't be fetched or parsed, as RFC 8461 asks.
Test: after the TXT id changed and the policy fetch failed, the lookup
returned an error on main; now it returns the cached policy.
EmailSubmission removed a single Bcc header before sending: the last
one when the recipients came from the headers, the first one when the
envelope named them. Its position was also stored in 16 bits, so a Bcc
header longer than 64 KiB was only partly cut. A draft with two Bcc
headers (from Email/import or IMAP APPEND) or a very long one sent its
blind copy addresses to every recipient.
Every Bcc header of the message is now located on a fresh parse of the
message being sent and removed, whatever its size.
Test: a message with two Bcc headers delivered "Bcc: hidden_one" to the
recipients on main, and one with a Bcc header over 64 KiB delivered
the tail of its address list; now neither has any Bcc address.
Scheduling messages received by email were credited to the first From
address whenever the sender was trusted, and a sender who had signed in
to send was trusted whatever the From header said. Sign-in only binds
the envelope sender to the account, so any local user could send an
invitation, reply or cancellation in another person's name and change
the recipient's calendar. The same From address also decided whether a
message marked as spam came from a known contact.
A message from a signed-in sender is now credited to its envelope
sender, which the server already limits to the account's own
addresses. The From header is used only when it passed DMARC, and
otherwise nothing is processed as before.
Test: a signed-in user sending an invitation with another user's From
and organizer added the event to the recipient's calendar on main; now
it is refused and the calendar is unchanged.
An organizer's server sent the whole event, with every attendee, the
description and the location, to anyone who sent a REFRESH with the
event's UID, including attendees who had been removed and people who
were never invited. A REPLY for an instance the organizer hadn't
changed was stored as a new instance as long as the sender listed
itself as an attendee of the reply, so a removed attendee or a
stranger could add instances on any date, with any ORGANIZER parameters
and any other ATTENDEE lines; those attendees would then get the
organizer's next update.
A REFRESH is now answered only when the sender is an attendee of the
stored event. A REPLY for an unchanged instance needs the sender to be
an attendee of the series and the date to be one of the series' own
dates. The new instance takes its ORGANIZER, UID and SEQUENCE from the
organizer's copy and the sender's own ATTENDEE line with the answer
applied; only the people the sender delegated to are added from the
reply.
The recurring to-do test used a RECURRENCE-ID that is not a date of
its series; it now uses the series' second date.
Test: a REFRESH from someone never invited returned the full event on
main, and a removed attendee's REPLY created an instance; a REPLY that
added another attendee stored that attendee, and one for a date outside
the series stored it. Now each is refused or ignored and the
organizer's copy holds only the attendee's own answer.
Calendars and their events are numbered separately, and so are address
books and their contacts. The list of items a sharee may read added every
event or contact whose number matched a shared calendar or address book,
so sharing one calendar also exposed an unrelated event in a private
calendar, and the same for contacts. A sharee could read, copy, list
and download that item through JMAP, and find it through CalDAV,
CardDAV and free-busy queries.
Only files carry their own sharing and share their numbering with
folders, so that match now applies to files alone. Events and contacts
are shared through the calendar or address book that holds them.
Test: a sharee of one calendar read a private event that had the
shared calendar's id on main; now the event is not found and query
does not list it.
A calendar event's privacy and its hideAttendees setting were stored
and reported but never applied. Anyone a calendar was shared with saw
secret events, the full details of private events, and every attendee
of events meant to show only their owners, through CalendarEvent/get,
query, copy, changes and blob download.
For anyone but the calendar's owner, a secret event (CLASS:CONFIDENTIAL)
now doesn't exist. A private event shows only its time, recurrence and
basic metadata; matching it on title, description, location or
participants finds nothing, and copying it out is refused. An event
that hides its attendees shows only its owners and the reader, unless
the reader may change every event in its calendar; matching on its
participants finds nothing for others. Its blob, like a private
event's, is not downloadable by sharees.
Test: a sharee read a secret event, the title of a private event, and a
hidden attendee on main; now the secret event is not found, the private
one has its time only, it can't be found by title or copied, and only
the event's owner is listed as a participant.
An organizer's invitations, updates and cancellations went out as one
message listing every attendee, even when the event was set to hide
its attendees, so each attendee's mail and calendar showed everyone
else invited. The answer to an attendee's request for the event again
did the same.
For such an event the server now sends each recipient its own message
naming only the organizer, the owners and that recipient, in the
calendar data and in the summary shown in the mail.
Test: an attendee's copy of an invitation to an event hiding its
attendees listed the other attendee on main; now it lists only the
organizer and the attendee.
A sync-collection REPORT with a sync token listed every item deleted
from the account since that token, from every calendar, address book or
folder. A user a single calendar, address book or folder was shared with
saw the names of events, contacts and files deleted from the owner's
unshared collections, and the owner syncing one collection got the
deletions of all the others.
A sync now reports only deletions inside the synced collection, and for
a sharee only those under a container shared with them.
Test: webdav sync.rs, Jane syncing John's shared folder got
"/dav/file/john%40example.com/sync-private/secret-plans" on main; now
she and John get nothing, for files, calendars and address books.
A write was let through as soon as the If header held the token of any
one lock found, skipping the remaining locks on that resource and every
later resource of the request. A MOVE or COPY that named the source's
lock token overwrote a destination locked by another client, and a
DELETE of a locked folder removed resources below it locked by someone
else. A tagged If list also matched lock paths by suffix. Anyone able to
write to the account (its owner, members and sharees) could do it.
Now the resource and each locked resource below it must each be covered
by a submitted token of one of the locks on it (shared locks need only
their holder's token), and a token counts as submitted wherever it
appears in the If header.
Test: webdav lock.rs, MOVE onto a folder locked by another client with
only the source's token got "Expected 423 Locked but got 204 No
Content" on main; now 423 naming the destination's lock, the MOVE works
with both tokens, and a shared lock's holder writes with its own token.
Dead properties set with PROPPATCH and the owner given in a LOCK are
stored as sent and written into PROPFIND and lock responses. Attribute
values (and the namespace) were copied between double quotes without
escaping, so a value quoted with ' that held a " ended the attribute and
let the rest be written as markup. Any user able to set a property or
lock a resource could put elements of their choosing into the responses
other users of a shared calendar, address book or folder read. Nested
properties were also closed with the innermost element's name, giving
malformed XML.
Attribute values and the namespace are now escaped for double quotes
when stored, names that can't be written back as names are replaced,
and on output, attributes already stored are written only while they
are plain name="value" pairs. Each end tag closes the element opened
last.
Test: dav-proto dead_property_round_trip_is_well_formed got
"<note xmlns=\"urn:example:z\" a=\"x\" ><evil/>\" ..." and "expected
`</outer>`, but `</empty>` was found" on main; now the output parses
with the same elements and attribute values, and stored attributes
holding markup are cut off at it.
A sharee could COPY or MOVE a file into a shared folder holding only
the right to change the folder's properties, where PUT and MKCOL need
the right to add to it. An overwriting COPY or MOVE needed the right to
delete only the top of what it replaced, then removed everything below
it, where DELETE needs that right on every node.
What a COPY, or a MOVE from another account, created also kept the
source's ACL. A user with write access to someone's shared folder could
build a folder of their own shared with anyone, copy it in, and so
share the owner's folder contents with people the owner never chose.
Adding to a folder by COPY or MOVE now needs the right to add to it,
and overwriting needs the right to delete every node replaced. What a
COPY, or a MOVE from another account, puts in a folder takes the
folder's ACL, as an uploaded file does; a copy over an existing file
keeps that file's ACL. A MOVE within an account keeps the node's ACL.
Test: webdav acl.rs, John (properties-only on Bill's folder) copying a
file into it got "Expected 403 Forbidden but got 201 Created" on main;
with only the rights checks, the folder John copied into Bill's still
granted Jane: "Expected at least one of ...john... values, but got
{...jane...}". Now the copy is refused, the overwrite of a folder John
can't fully delete is refused, and copies and moves carry Bill's grants
only.
An addressbook-query or other DAV REPORT asking for <nresults>0</nresults>
started its countdown of results at zero and subtracted from it after
the first result, wrapping it around to the largest number: the
configured maximum number of results no longer applied, so one request
could return every item of the collection. Debug builds panicked in the
request handler instead. Any signed-in user could send it.
A limit of zero is now treated as one, and the message for an exceeded
limit names the limit actually applied.
Test: webdav card_query.rs, an addressbook-query with nresults 0 hit
"attempt to subtract with overflow" at propfind.rs and the connection
closed on main; now one result and the 507 number-of-matches-within-
limits response naming a limit of 1.
A property list that named the same property many times was rendered
once per mention for every object returned. A signed-in user could list
bodyValues a few thousand times with fetchAllBodyValues and make
Email/get or Email/parse build that many copies of every body in memory.
Every /get method, Email/get's bodyProperties and Email/parse's
properties and bodyProperties now keep each property once, in the order
first given.
Test: Email/get and Email/parse with bodyValues, textBody and partId
listed several times; on main the response held each of them three
times over, now once.
CalendarEvent/set and /copy checked the disk quota of the signed-in
user, while the bytes were stored in, and counted against, the account
written into. A sharee with write access to a calendar could fill its
owner's account past the owner's quota. FileNode/set and FileNode/copy
checked no quota at all, so any user could store files past their own
disk quota (and their tenant's). Every item of a calendar, contact or
file request was also checked against the usage stored before the
request, so one request with many items could go far over the limit.
Calendar events, contacts and files are now checked against the disk
quota (and tenant quota) of the account they are written into, the same
way mail delivery and Email/copy are, and each item is counted together
with the items before it in the same request. Each item is checked at
the size it is charged at (the stored record, not only its iCalendar or
vCard text); a file node counts its blob; an update counts what it adds.
Test: on main a sharee's 1.2 KB event went into an account with a 1 KB
quota, two events and two contacts that each fit alone but not together
were both created, and a 2 KB file was created and another copied in;
each is now refused with overQuota, while a small file still fits.
To find a new message's thread, delivery read every stored message with
the same thread name (or, for messages without a subject, every
subject-less message) and compared their references. Anyone able to send
mail could make each delivery to an account slower than the last by
sending it many messages with one subject or none.
A new message is now compared with the newest 512 messages sharing its
thread name. A reply whose thread has no message among those starts a
new thread. Duplicate detection looks at the same messages.
Test: after 513 messages with one subject, a reply to the newest still
joins its thread, and a reply to the oldest (now outside the window)
starts a new one; on main it was still compared with all of them.
Components the JSCalendar converter doesn't know are carried along as
nested jCal. A signed-in user could upload an iCalendar file with an
unknown component nested tens of thousands deep (about nine bytes per
level) and ask CalendarEvent/parse for it, or store such an event over
CalDAV and fetch it through JMAP; building and serializing the nested
value overflowed the worker's stack and aborted the whole server.
Before an event is converted for CalendarEvent/get, /parse, /set and
/copy, components nested more than 32 levels below the VCALENDAR are
dropped.
Test: converting an event nested 200,000 deep on a 2 MiB stack aborted
with a stack overflow on main; it now keeps at most 32 levels, and
CalendarEvent/parse of a blob nested 100,000 deep returns the event.
Sieve :regex patterns were compiled with the regex library's default
10 MiB size limit, so every :regex key in a user's script could hold
that much memory for as long as the script was cached. A pattern built
from variables ("${x}") was compiled again for every header or value it
was tested against: a user's script tested against a message with many
headers spent seconds of CPU on each delivery, and anyone could send
that message.
Patterns now compile to at most 1 MiB (the matcher and its lazy DFA
each); a script with a larger one fails to compile with "Invalid regular
expression". Patterns built from variables are compiled once per script
run (up to 32 kept), and a pattern that fails to compile no longer trips
a debug assertion.
Test: on main a script with ^\w{200}$ compiled, and a variable pattern
tested against 1,000 headers took 16.7 s against 18 ms for one header;
the script is now refused and 1,000 headers take as long as one.
A confidential event (CLASS:CONFIDENTIAL) was treated as secret: for
anyone the calendar was shared with it didn't exist. CalDAV clients
such as Thunderbird use CONFIDENTIAL for "show time and date only", so
their users' events vanished from sharees' calendars instead of showing
as busy time. Events are stored as iCalendar, where a JMAP "secret"
event is also CLASS:CONFIDENTIAL, so the two can't be told apart.
A confidential event, whether set over CalDAV or as JMAP "secret", now
shows sharees what a private event does: its time, recurrence and basic
metadata only. Matching it on title, description, location or
participants finds nothing, copying it out is refused, and its blob is
not downloadable by sharees. The owner still sees all of it.
Test: a sharee got notFound for a confidential event and couldn't list
it; now it is listed and shows its start and duration only, with no
title, description or participants, isn't found by title, and can't be
copied, as for a private event.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The 34 security fixes from #226, cherry-picked onto release/2026 without conflicts, and the version set to 2026.10.29. This becomes the 2026.10.29 security release.
Checked locally on the branch: fmt, fork checks, build, and the imap, webdav, jmap, system, smtp and directory suites all pass. The first system run failed at the Email/import over-quota check with BlobNotFound: the test's one-second upload lifetime ran out under load. That part of the test is unchanged from release/2026, and a rerun passed.
Before a client signed in, IMAP and ManageSieve accepted requests and literals up to the full configured request size (50 MB by default), and reserved room for a literal as soon as its size was announced. Each argument was also copied when it was handed on, and the grown buffer was kept for the rest of the session, so a large literal cost twice its size and stayed allocated. Anyone who could connect could make the server hold about 100 MB per connection without signing in. Requests before sign-in are now limited to 64 KiB, which is more than any login, SASL response or bearer token needs. The configured size applies once the client has signed in, and stops applying again after UNAUTHENTICATE. Arguments are handed over without a copy, and a literal's buffer is sized to the literal. Test: AUTHENTICATE PLAIN {100000} before sign-in got "+ Ready for 100000 bytes." on main and now gets BAD; a receiver unit test that found a 2 MB buffer kept after a 1 MB literal now passes.Email/set, Email/copy and Email/import took any string up to 128 characters as a keyword, spaces, parentheses and line breaks included, and IMAP wrote keywords into FETCH FLAGS byte for byte. A signed-in user could store a keyword such as "x)\r\n* 1 EXPUNGE" and forge untagged responses in the IMAP session of everyone who reads the message, other users of a shared mailbox included. A keyword stored from an IMAP literal or a Sieve addflag action did the same. JMAP now refuses, with invalidProperties, a keyword that is not 1 to 255 printable ASCII characters without ( ) { ] % * " \ (RFC 8621 section 4.1.1). IMAP leaves out of FLAGS any stored keyword that is not an atom, so keywords already stored can't inject either. Test: on main, a keyword stored from a literal came back as "* 1 FETCH (FLAGS (... x)" followed by a separate "* 99 EXPUNGE) ..." line, and Email/set accepted "x)\r\n* 1 EXPUNGE"; both are refused or left out now, and a unit test covers the name rules.STORE checked only the right to modify messages, and only on the selected mailbox. A user a mailbox was shared with for flagging ("w") could also set or clear \Deleted, which needs the right to remove messages ("t", RFC 4314), so the owner's client would expunge a message the user wasn't allowed to delete. And because keywords belong to the message, a user with rights on one shared mailbox could change the flags of a message that the owner also keeps in a mailbox the user can't touch. On a shared account, \Deleted now needs the right to remove messages and every other flag the right to modify them, held on every mailbox the message is in. Flags the user may not change are left as they are and the rest of the STORE still applies; a STORE that asks only for changes the user has no right to make on the selected mailbox gets NO [NOPERM] as before. Test: with the right to modify but not remove messages in another user's mailbox, UID STORE 1 +FLAGS (\Deleted) set the flag on main and now gets NO; +FLAGS (\Deleted \Flagged) now sets only \Flagged.A sharee could COPY or MOVE a file into a shared folder holding only the right to change the folder's properties, where PUT and MKCOL need the right to add to it. An overwriting COPY or MOVE needed the right to delete only the top of what it replaced, then removed everything below it, where DELETE needs that right on every node. What a COPY, or a MOVE from another account, created also kept the source's ACL. A user with write access to someone's shared folder could build a folder of their own shared with anyone, copy it in, and so share the owner's folder contents with people the owner never chose. Adding to a folder by COPY or MOVE now needs the right to add to it, and overwriting needs the right to delete every node replaced. What a COPY, or a MOVE from another account, puts in a folder takes the folder's ACL, as an uploaded file does; a copy over an existing file keeps that file's ACL. A MOVE within an account keeps the node's ACL. Test: webdav acl.rs, John (properties-only on Bill's folder) copying a file into it got "Expected 403 Forbidden but got 201 Created" on main; with only the rights checks, the folder John copied into Bill's still granted Jane: "Expected at least one of ...john... values, but got {...jane...}". Now the copy is refused, the overwrite of a folder John can't fully delete is refused, and copies and moves carry Bill's grants only.Sieve :regex patterns were compiled with the regex library's default 10 MiB size limit, so every :regex key in a user's script could hold that much memory for as long as the script was cached. A pattern built from variables ("${x}") was compiled again for every header or value it was tested against: a user's script tested against a message with many headers spent seconds of CPU on each delivery, and anyone could send that message. Patterns now compile to at most 1 MiB (the matcher and its lazy DFA each); a script with a larger one fails to compile with "Invalid regular expression". Patterns built from variables are compiled once per script run (up to 32 kept), and a pattern that fails to compile no longer trips a debug assertion. Test: on main a script with ^\w{200}$ compiled, and a variable pattern tested against 1,000 headers took 16.7 s against 18 ms for one header; the script is now refused and 1,000 headers take as long as one.