mirror of
https://github.com/chatmail/core.git
synced 2026-08-18 06:36:29 +03:00
Compare commits
3 Commits
link2xt/ap
...
hpk/draft-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c69c6fa4ec | ||
|
|
85fd53add2 | ||
|
|
4116d7579b |
515
draft/keyupdate-draft.md
Normal file
515
draft/keyupdate-draft.md
Normal file
@@ -0,0 +1,515 @@
|
||||
# A keyupdate push channel to maintain reliable chat connectivity
|
||||
|
||||
[Multi-relay support for chat profiles was added in March 2026][multi-relay],
|
||||
allowing them to use multiple relays for receiving and sending messages.
|
||||
While instant onboarding is being extended to multi-relay onboarding ([#8444]),
|
||||
adding and removing relays automatically is not settled,
|
||||
not least because changing a relay is unsafe today.
|
||||
This draft proposes a keyupdate push channel
|
||||
that shares our current key with our contacts when it changes,
|
||||
without waiting for a chat interaction.
|
||||
It helps keep chats connected now,
|
||||
and makes automatic relay changes safe enough to design later.
|
||||
|
||||
|
||||
## Problems of maintaining reliable chat connectivity today
|
||||
|
||||
A profile's relay list lives inside its own key,
|
||||
as a signed notation that travels with the key.
|
||||
A contact sends to the addresses contained in the key, signed by the key holder.
|
||||
Rooted in the [Autocrypt 1](https://autocrypt.org) inline key-distribution specification,
|
||||
there is no central directory, no probe and no removal notice,
|
||||
so a contact's copy is only refreshed by traffic:
|
||||
a message we send them, or a group message from someone else
|
||||
that gossips our key onwards.
|
||||
|
||||
Reachability therefore decays exactly where traffic is thin.
|
||||
Three situations turn that decay into a cut conversation,
|
||||
or into costs of preventing one:
|
||||
|
||||
- **Mutual silence.**
|
||||
If neither side has written since our relays changed,
|
||||
the contact keeps the old list indefinitely.
|
||||
Their next message goes to a relay we no longer read,
|
||||
and they get silence rather than a bounce because the old account still exists.
|
||||
Even if a bounce comes, it's hard to effectively act on it for users.
|
||||
[#7878] is a user report of exactly this, and the reporter's objection
|
||||
is the design point:
|
||||
"people changing relays shouldn't need to know to write in every room
|
||||
they're in just to make sure everyone gets the update".
|
||||
It was closed expecting [#7865] to fix it, and [#7865] did land:
|
||||
the relay list is now carried in the key.
|
||||
That settled what gets distributed, not when,
|
||||
which is the part still missing.
|
||||
|
||||
- **Deliberate relay removal.**
|
||||
Users remove relays for good reasons:
|
||||
privacy, distrust of a provider, or wanting to use relays they chose.
|
||||
Here the cut would be immediate and total:
|
||||
every contact still on the old list would write to an address nobody reads.
|
||||
It does not happen today only because core doesn't really remove a relay,
|
||||
preventing lost messages at the costs of the "unpublished relay" workaround below.
|
||||
|
||||
- **Adding relays during degradation.**
|
||||
Relays sometimes get added precisely when the existing ones stop working,
|
||||
so the announcement would have to travel
|
||||
at the moment reachability is already impaired.
|
||||
A contact who does not learn the new relay
|
||||
keeps writing to an address that may already be dead.
|
||||
|
||||
|
||||
What exists today only narrows the gap.
|
||||
MDN key refresh ([#8481]) covers contacts who read our messages without
|
||||
replying, so the exposed population is mutual silence
|
||||
rather than all quiet contacts.
|
||||
Unpublished relays buy time, and only for removal.
|
||||
|
||||
|
||||
### The "unpublished relay" workaround is its own problem
|
||||
|
||||
"Remove" today marks a relay *unpublished*: the address stops being advertised,
|
||||
but the relay keeps a full IMAP connection and keeps receiving.
|
||||
This is deliberate ([#8384]) and it does buy safety for the cases above.
|
||||
The price is that users cannot end their relationship with a relay:
|
||||
|
||||
- **Retention is unpredictable in both directions.**
|
||||
Cleanup requires 90 days both since the removal itself
|
||||
and since the last message received there,
|
||||
and every fetched message resets the second clock,
|
||||
with no filter for blocked contacts or spam,
|
||||
so a removed relay that keeps getting mail is kept indefinitely.
|
||||
Adding a relay at the five-relay cap does the opposite
|
||||
and hard-deletes the least recently used unpublished one at once.
|
||||
|
||||
- **A relay the user removed keeps being used.**
|
||||
Unpublished transports get an IMAP loop like any other
|
||||
and count against the same cap,
|
||||
so a profile can hold connections to five relays,
|
||||
four of them ones the user pressed "Remove" on.
|
||||
Chatmail relays only delete accounts after a period without any login,
|
||||
and core keeps logging in,
|
||||
so the account there outlives the removal as well.
|
||||
|
||||
- **Nothing the user can do shortens it.**
|
||||
Real deletion is documented as internal and test-only.
|
||||
Users are told at removal time that the relay is phased out,
|
||||
but not for how long,
|
||||
and not that we keep connecting and receiving there.
|
||||
[#8384] records the friction from a user: a removed relay still advertising
|
||||
TURN servers, and a permanent connection error with "no way to get rid of
|
||||
it", patched as a symptom in [#8550] while the relay itself stayed.
|
||||
|
||||
This grows with multi-relay setups, and multi-relay onboarding ([#8444]) brings them.
|
||||
The [privacy notes being drafted for the apps][privacy notes]
|
||||
would somehow need to describe this behaviour, and the description would read badly.
|
||||
|
||||
Unpublished relays exist to protect exactly the contacts
|
||||
the new keyupdate push channel would reach,
|
||||
so the channel would let us drop the concept completely.
|
||||
|
||||
|
||||
## Moving on from Autocrypt1: A keyupdate push channel
|
||||
|
||||
We should be able to hand our current key to our contacts
|
||||
without waiting for a conversation to happen.
|
||||
The straightforward way is to address such a message per recipient,
|
||||
in chunks of a few dozen contacts, as sketched in [#8588].
|
||||
This draft argues for a different way of sending key updates,
|
||||
starting with a simple observation:
|
||||
|
||||
**A keyupdate needs to reach everyone holding a copy of our key,
|
||||
and that audience already shares a secret: the identity key of a contact**.
|
||||
From that key we can derive a secret deterministically,
|
||||
so everyone already holding the key computes the same value,
|
||||
and nobody else can.
|
||||
That lets us re-use the symmetric broadcast encryption core already has:
|
||||
one signed, symmetrically encrypted message to many recipients at once,
|
||||
with the secret derived rather than generated and handed out to subscribers.
|
||||
|
||||
In chatmail clients "public" keys are by default hidden identities,
|
||||
only transmitted in encrypted messages, with the goal of preventing
|
||||
a curious or abusive relay operator to track identities.
|
||||
|
||||
What using automatically derived secrets and existing broadcast encryption buys:
|
||||
|
||||
- No new header names, no new key distribution or bookkeeping protocol (phew!),
|
||||
only a new `Chat-Content` value.
|
||||
|
||||
- Network cost is largely independent of recipient count.
|
||||
A keyupdate is a few KB, rendered once per relay change,
|
||||
and addresses only travel as `RCPT TO` commands in the SMTP transaction.
|
||||
Chatmail relays take 1000 addresses per submission,
|
||||
so for most profiles today a keyupdate is a single upload,
|
||||
and each upload stays well under 100KB on the wire.
|
||||
|
||||
- Recipients learn nothing about our contact list
|
||||
because nothing in the message is recipient-specific:
|
||||
one password-encrypted session key packet instead of one per recipient,
|
||||
and a signature that names only its issuer,
|
||||
with no `intended recipient fingerprint` subpackets.
|
||||
The submitting relay still sees the envelope metadata, as it does today.
|
||||
|
||||
- Contacts acquired at any point in the past are reached.
|
||||
The secret comes from the key itself and not from a shared session,
|
||||
so a contact from years ago can decrypt an update sent today,
|
||||
given they have updated to a post-keyupdate app release.
|
||||
|
||||
Delivery would not have to be complete to be useful.
|
||||
In group chats, cooperative Autocrypt gossip spreads what arrived:
|
||||
members who received the fresher key pass it on to the others,
|
||||
whenever one of them next writes to the group.
|
||||
|
||||
Pushing keyupdates removes the last reason to keep "unpublished relays" around,
|
||||
so "Remove" can mean actually removed, like users intend it.
|
||||
It would also unblock automatic relay management.
|
||||
What ships under that name today is initial onboarding only
|
||||
([#8444], still off by default): no rotation, no removal.
|
||||
Designing those proved to be hard while changing a relay is unsafe,
|
||||
because any automatic change would silently cut off
|
||||
the contacts who do not hear about it.
|
||||
Keyupdates would lift that constraint and could land in the next release,
|
||||
well before automatic addition/removal mechanics are settled.
|
||||
|
||||
|
||||
## Keyupdates are decryptable forever, so MUST only contain key updates
|
||||
|
||||
Anyone who ever obtained our "public" key could derive the secret
|
||||
and decrypt these messages, forever.
|
||||
Nothing here is ephemeral and the secret never rotates,
|
||||
so blocking or deleting a contact does not take that ability away.
|
||||
|
||||
However, deriving the secret is not the same as getting the message:
|
||||
Keyupdates go only to our own contacts (see below),
|
||||
so the wider set only matters for someone who also obtains a copy,
|
||||
a relay in the path for example.
|
||||
|
||||
This is acceptable because the payload is "contacts-public" anyway:
|
||||
it is the key and its relay list,
|
||||
the same data we hand out in every chat where we participate.
|
||||
Whoever can derive the secret already holds an earlier copy of that key,
|
||||
so what a keyupdate adds for them is the current relay list.
|
||||
It follows that **the keyupdate channel must never carry user generated data
|
||||
or metadata besides the public key itself.**
|
||||
|
||||
|
||||
## When a keyupdate goes out
|
||||
|
||||
Sending should be driven by a diff, not by an event.
|
||||
A device records the relay list it last announced,
|
||||
and a keyupdate is due only when the current relay list differs from it.
|
||||
That single decision gives us the rest:
|
||||
|
||||
- **Debounced.**
|
||||
Changes within a short window should go out as one message,
|
||||
so an automatic relay add/remove yields one keyupdate,
|
||||
and an add followed by a removal of the same yields none.
|
||||
|
||||
- **Short debouncing for user observability**:
|
||||
Around 30 seconds may be a good enough debounce window to start with.
|
||||
It is long enough that several changes usually travel in one keyupdate
|
||||
(current initial onboarding adds up to three relays sequentially),
|
||||
and short enough that someone changing relays by hand
|
||||
can still watch the effect arrive with chat peers,
|
||||
which is worth more than optimising against a few small extra messages.
|
||||
|
||||
- **Behind real traffic.**
|
||||
The message should leave from the SMTP loop once its queue is drained,
|
||||
so a keyupdate never delays a user message
|
||||
and is only attempted on a connection that just proved to work.
|
||||
|
||||
- **Nothing on upgrade.**
|
||||
Existing profiles must start with their current relay list
|
||||
already recorded as announced.
|
||||
Otherwise the keyupdate release would send unnecessary noise
|
||||
without any actual relay changes.
|
||||
|
||||
- **Exactly one device in a multi-device setup announces.**
|
||||
A device applying a relay change received over
|
||||
multi-device sync should record the resulting list as announced without sending.
|
||||
That also systematically prevents a device catching up on a backlog of old sync
|
||||
messages from announcing historical states.
|
||||
|
||||
|
||||
## Cryptographic and implementation considerations
|
||||
|
||||
The keyupdate design needs no new cryptographic primitives,
|
||||
only the broadcast machinery named above with a different secret.
|
||||
Both symmetric secrets core has today, for broadcast channels and for
|
||||
securejoin, are random values shared out of band by QR code or invite.
|
||||
Deriving one from public key material instead
|
||||
is the part most worth scrutinising.
|
||||
|
||||
**The construction of the derived secret:**
|
||||
|
||||
- Derive the secret as a canonical `keyupdate/` followed by the hex of
|
||||
`SHA256("keyupdate" || <primary key packet body>)`,
|
||||
where the body is the OpenPGP primary key packet without its packet header:
|
||||
one octet version, four octets big-endian creation time,
|
||||
one octet algorithm, for v6 a four-octet length of the key material,
|
||||
then the key material.
|
||||
This layout is normative and must be pinned by test vectors,
|
||||
not left as "whatever the OpenPGP library happens to serialize".
|
||||
|
||||
- Send it as an ordinary [RFC 9580] password-encrypted message.
|
||||
Nothing about the format is specific to keyupdates,
|
||||
it is what any OpenPGP implementation writes for a passphrase:
|
||||
one [v6 SKESK] packet whose [salted S2K]
|
||||
(type 1, SHA-256, eight random salt bytes)
|
||||
turns the secret, via HKDF, into the key
|
||||
that wraps a fresh random session key,
|
||||
plus one [SEIPDv2] packet, AES-128 in OCB mode, ZLIB compressed,
|
||||
signed by the sender.
|
||||
AES-128 is what all our symmetrically encrypted messages already use.
|
||||
Carry our key in a protected `Autocrypt` header,
|
||||
next to a protected `Chat-Content: key-update` header.
|
||||
|
||||
Properties that follow, and requirements they imply:
|
||||
|
||||
- **Who can read it is not who we send it to.**
|
||||
Anyone holding a copy of our key can derive the secret (see above),
|
||||
including people who got it by gossip or vCard.
|
||||
What we choose is the recipient set:
|
||||
unblocked key-contacts we share an accepted 1:1,
|
||||
group or subscribed-channel chat with.
|
||||
Subscribers of our own channels should be left out,
|
||||
because they could be massive and it doesn't contribute to better chat connectivity.
|
||||
Receivers in turn can accept from any unblocked key-contact.
|
||||
|
||||
- **Retroactive.**
|
||||
The primary key packet is fixed when the key is generated,
|
||||
and re-signing with a new relay list does not touch it,
|
||||
so the secret is stable for the lifetime of the key.
|
||||
|
||||
- **v4 and v6 keys both work.**
|
||||
The derivation reads the serialized key body, not the fingerprint,
|
||||
so it does not depend on the fingerprint algorithm.
|
||||
The body layout does differ between the two versions, so the digest differs per version.
|
||||
Both derivations should be pinned by test vectors,
|
||||
since deployed contacts recompute them from their stored copies.
|
||||
|
||||
- **Only update the key the secret was derived from.**
|
||||
A keyupdate must be signed by that key and carry an update to it,
|
||||
and anything else is dropped.
|
||||
Without the rule anyone holding a contact's key could send
|
||||
a stream of messages carrying freshly generated keys in the Autocrypt
|
||||
header, each silently creating a contact the user never sees,
|
||||
because keyupdates are trashed.
|
||||
The signature is not what makes the key credible, though,
|
||||
and it is not what limits the audience either,
|
||||
see *Keyupdates could come from anywhere but are signed anyway* below.
|
||||
|
||||
- **Domain-separated.**
|
||||
A v6 fingerprint is also a SHA-256 over the same key material,
|
||||
but the two preimages are already disjoint,
|
||||
so the hashed `keyupdate` prefix is documentation rather than protection.
|
||||
The `keyupdate/` prefix on the password string does carry weight:
|
||||
it separates these secrets from the securejoin and broadcast secrets
|
||||
that share the same trial-decryption pool.
|
||||
|
||||
- **Replay changes nothing.**
|
||||
An old update can be replayed forever.
|
||||
The relay list lives in the direct key signature,
|
||||
and certificate merging keeps the one with the newest creation time,
|
||||
on a tie the one already stored.
|
||||
The rest of the certificate is fixed today:
|
||||
a key carries one encryption subkey that never rotates,
|
||||
so an old copy differs from the current one only in that signature.
|
||||
Rotating Autocrypt 2 subkeys might need a fresh look at this.
|
||||
|
||||
- **Trial decryption must stay bounded.**
|
||||
A symmetrically encrypted message carries no hint of which secret opens it,
|
||||
so a receiver has to try every secret it knows until one works,
|
||||
and the cost of a single failed attempt matters.
|
||||
Core already restricts symmetric decryption to a single ESK packet with a
|
||||
salted S2K, so an attacker cannot make it expensive with an iterated S2K
|
||||
or a stack of session keys.
|
||||
A single hash is on purpose here: the secret is a full 256-bit digest
|
||||
rather than a passphrase, so hardening would buy nothing.
|
||||
What is not bounded is one certificate parse per unblocked key-contact,
|
||||
which any sender could trigger with undecryptable garbage.
|
||||
Keyupdate secrets should therefore be tried last,
|
||||
after the securejoin and broadcast secrets.
|
||||
Parsing and deriving on demand was measured at under a second
|
||||
for a thousand contacts, so the secrets need no caching layer.
|
||||
|
||||
- **One body, many deliveries.**
|
||||
The body would be rendered once and independently from the contact count.
|
||||
The SMTP envelope still lists every distinct relay address,
|
||||
chunked at whatever limit the relay advertises over IMAP METADATA.
|
||||
The relay therefore learns the keyupdate addressee set,
|
||||
though it can track our send and receive history anyway
|
||||
and largely observes a similar set over time.
|
||||
|
||||
- **Invisible on arrival.**
|
||||
The key should be applied on the normal Autocrypt path
|
||||
and the message then trashed rather than filed:
|
||||
no chat, no counter, and no refresh of the sender's "last seen",
|
||||
because an invisible message should not light up an online dot.
|
||||
|
||||
- **Old clients stay quiet.**
|
||||
Without the secret the message is undecryptable,
|
||||
and with `force_encryption` being true by default
|
||||
unsigned incoming mail is discarded before any chat is touched.
|
||||
A user who turns encryption enforcement off,
|
||||
and who already has a plain-address chat with our address,
|
||||
may see an undecipherable message there.
|
||||
No new chat and no contact request should ever be created.
|
||||
Old and new clients can therefore ship together,
|
||||
with coverage growing as clients update.
|
||||
|
||||
- **Not sent to our own devices.**
|
||||
We inform about relay changes through regular multi-device sync,
|
||||
because changing transports involves private credentials for accessing a relay address,
|
||||
and because we perform some merging on concurrent transport additions.
|
||||
|
||||
|
||||
### Keyupdates could come from anywhere but are signed anyway
|
||||
|
||||
The relay list in a keyupdate lives in a direct key self-signature,
|
||||
and certificate merging verifies it and prefers the newest one,
|
||||
so the resulting certificate is as trustworthy whether the keyupdate
|
||||
arrived signed or unsigned, from the key owner or from a stranger.
|
||||
Merging, not signing, is the cryptographic gate for every certificate we get,
|
||||
so conceptually we could accept keyupdates from anyone.
|
||||
However, the secret is derived from the key,
|
||||
so we only ever ingest keyupdates from contacts whose certificate we store,
|
||||
an audience fixed by construction rather than by policy.
|
||||
|
||||
Keyupdates should be signed
|
||||
for implementation simplicity and a smaller attack surface:
|
||||
every past holder of the key can derive the secret,
|
||||
so the AEAD tag says only that the writer was one of them
|
||||
while the signature narrows it to the key owner,
|
||||
and core takes the sender contact from it,
|
||||
treating a message without one as unencrypted and discarding it.
|
||||
Unsigned keyupdates stay conceptually defensible,
|
||||
but they would need an exception from the rule
|
||||
that a contact-bound secret implies exactly one signature by that contact,
|
||||
which broadcast channels and securejoin rest on too, and that is not worth it.
|
||||
|
||||
The same reasoning bounds a possible extension:
|
||||
If we let our key-contacts gossip us other keys,
|
||||
a keyupdate can carry `Autocrypt-Gossip` headers,
|
||||
constrained to contacts we already share so no unknown one is added.
|
||||
Conceptually that is fine but requires more implementation changes,
|
||||
including the sender side of gossiping keyupdates,
|
||||
so it is best considered separately from a first keyupdate release.
|
||||
|
||||
|
||||
## Out of scope: envelope SMTP failures can terminate all sending
|
||||
|
||||
A permanently refused `RCPT TO` would fail the whole SMTP transaction,
|
||||
and core then drops the queued message
|
||||
without attempting the remaining chunks,
|
||||
so with one envelope a single dead address
|
||||
would cost the announcement for everyone behind it.
|
||||
Chatmail relays never get there: they accept every recipient
|
||||
and report delivery failures afterwards as DSNs.
|
||||
The hazard is real only on a deployment that rejects unknown or
|
||||
over-quota recipients at `RCPT TO`,
|
||||
and it affects regular group messages and even 1:1 chats today,
|
||||
so it is out of scope for keyupdates, which carry less critical data.
|
||||
Losing all relays on both sides at once is out of scope too,
|
||||
but for a different reason: nothing we send can help there,
|
||||
see the last section.
|
||||
|
||||
|
||||
## Open questions
|
||||
|
||||
- Should a keyupdate be rate limited beyond coalescing,
|
||||
for a profile whose relay list flaps on its own?
|
||||
Manual fiddling is bounded by the person
|
||||
doing it, but future automatic relay add/remove needs to think about limits.
|
||||
|
||||
- What should happen to delivery status notifications for dead addresses in
|
||||
a large envelope? They arrive per address and refer to one Message-ID
|
||||
but we don't do much with them. We could probably evolve to exclude such
|
||||
bounced addresses from future key updates but it shouldn't block
|
||||
a first key update implementation.
|
||||
|
||||
- Is the sending set right? Narrowing the keyupdate recipient set
|
||||
by activity would shrink the envelope, but it would also disclose
|
||||
to the relay which of our contacts are close ones.
|
||||
Note that a quiet contact is indistinguishable from one who left,
|
||||
so dropping them silently loses their next message.
|
||||
|
||||
|
||||
## What a reliable keyupdate rollout might open up
|
||||
|
||||
None of the following sections is proposed here.
|
||||
But each of them becomes thinkable once changing relay setups is safer.
|
||||
|
||||
|
||||
### Maximizing reliable deletion ("forward secrecy") with Autocrypt 2
|
||||
|
||||
The keyupdate channel is not necessarily only about relays.
|
||||
Autocrypt 2 ([#8317]) introduces expiring, rotating subkeys,
|
||||
whose distribution has a similar shape as the problem above:
|
||||
a contact we never write to ends up not having our "reliable deletion" encryption keys.
|
||||
The described keyupdate mechanism could help,
|
||||
or at least it could play into the cadence of updates ...
|
||||
|
||||
|
||||
### Forward-rotating through the relay network
|
||||
|
||||
Today a relay change feels like something to be survived.
|
||||
If keyupdates make it routine, it becomes something we could choose to do:
|
||||
Better relays appear, and a profile could drift towards them
|
||||
instead of staying wherever it happened to be born.
|
||||
A safe frequency is capped by how reliably keyupdates land,
|
||||
which is the interesting part:
|
||||
rotation is exactly as feasible as the keyupdate channel is reliable.
|
||||
|
||||
|
||||
### Immortal chat connectivity: surviving total relay loss
|
||||
|
||||
One case is already out of reach of anything we send directly.
|
||||
In [#8329] both sides lost all their relays at once,
|
||||
so any announcement would go to addresses the chat partner can not access anymore.
|
||||
Cooperative Autocrypt gossip can still bridge that
|
||||
if some mutual contact remains reachable from both sides,
|
||||
which is what the reporters ended up relying on,
|
||||
and failing that an out-of-band re-exchange restores contact,
|
||||
which works since [#8358], rerunning the full securejoin protocol
|
||||
when the address is outdated.
|
||||
|
||||
Harder still is a whole region losing all its relays at once:
|
||||
nobody can announce anything to anybody, no push channel helps,
|
||||
and there may be no mutual contact left to bridge it.
|
||||
But new relays, for example in sprouting mesh networks, might become available
|
||||
and wouldn't it be useful to re-establish chat connectivity with those
|
||||
who might be able to help you, or where you can pool resources?
|
||||
|
||||
Interestingly, a keyupdate message is not addressed to anyone in particular.
|
||||
It is one ciphertext readable by whoever holds the sender's key,
|
||||
so it does not have to be directly delivered to be useful:
|
||||
something parked now can be picked up later.
|
||||
Making it discoverable without handing everyone a way to enumerate and track profiles
|
||||
is an interesting enough challenge to make cryptographers have exciting discussions.
|
||||
But during emergencies people really just want things to work
|
||||
and we could ask users in some way "It seems you lost access to all chats.
|
||||
Do you want to enable emergency recovery mode?"
|
||||
|
||||
We could then ephemerally park keyupdates at all accessible relays,
|
||||
and scan what relay-stored key updates are decryptable for us.
|
||||
As soon as chat connectivity recovers for first bunches of people,
|
||||
they spread it further through cooperative Autocrypt gossip,
|
||||
re-establishing chats over time, scaling chat connectivity for everyone.
|
||||
|
||||
|
||||
[#7865]: https://github.com/chatmail/core/issues/7865 "Distribute information about relays in the key signature"
|
||||
[#7878]: https://github.com/chatmail/core/issues/7878 "DeltaChat seems to sometimes propagate the incorrect relays for users who switched relays"
|
||||
[#8317]: https://github.com/chatmail/core/issues/8317 "Autocrypt 2 support"
|
||||
[#8329]: https://github.com/chatmail/core/issues/8329 "Scanning QR for existing contact doesn't update relay list"
|
||||
[#8358]: https://github.com/chatmail/core/pull/8358 "fix: Rerun the full securejoin protocol if the address was outdated"
|
||||
[#8384]: https://github.com/chatmail/core/issues/8384 "Remove hidden relays automatically"
|
||||
[#8444]: https://github.com/chatmail/core/pull/8444 "feat: Basic multi-relay onboarding"
|
||||
[#8481]: https://github.com/chatmail/core/pull/8481 "Improve and speed up autocrypt/pgp gossipping with MDNs"
|
||||
[#8550]: https://github.com/chatmail/core/pull/8550 "fix: multi relay connectivity"
|
||||
[#8588]: https://github.com/chatmail/core/pull/8588 "feat: Key update messages"
|
||||
[multi-relay]: https://delta.chat/en/2026-03-31-zero#maximizing-availability-and-resilience-through-multi-path-delivery "Maximizing availability and resilience through multi-path delivery"
|
||||
[privacy notes]: https://github.com/deltachat/deltachat-pages/pull/1385 "Privacy notes being drafted for the apps"
|
||||
[RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html "OpenPGP"
|
||||
[v6 SKESK]: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.3.2 "RFC 9580 5.3.2: Version 6 Symmetric Key Encrypted Session Key Packet Format"
|
||||
[SEIPDv2]: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.13.2 "RFC 9580 5.13.2: Version 2 Symmetrically Encrypted and Integrity Protected Data Packet Format"
|
||||
[salted S2K]: https://www.rfc-editor.org/rfc/rfc9580.html#section-3.7.1.2 "RFC 9580 3.7.1.2: Salted S2K"
|
||||
@@ -2181,26 +2181,3 @@ async fn test_load_shared_secrets_with_legacy_state() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reception of a message from Apple Mail.
|
||||
///
|
||||
/// The message is a multiplart/mixed message
|
||||
/// with a text part and multipart/related part.
|
||||
///
|
||||
/// multipart/related part has an HTML and images,
|
||||
/// but images are not referenced with blob:applewebdata://
|
||||
/// instead of cid: URLs.
|
||||
///
|
||||
/// See also related test_many_images and test-data/message/many_images_amazon_via_apple_mail.eml
|
||||
/// for a similar message, but images properly referenced in HTML.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_apple_blob() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = &tcm.alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/apple-blob.eml");
|
||||
let mimeparser = MimeMessage::from_bytes(context, &raw[..])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(mimeparser.parts.len(), 4);
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
Return-Path: <bob@example.net>
|
||||
Content-Type: multipart/alternative; boundary=Apple-Mail-23186EA7-751B-466A-B348-44BA06879E29
|
||||
Content-Transfer-Encoding: 7bit
|
||||
From: Bob <bob@example.net>
|
||||
Mime-Version: 1.0 (1.0)
|
||||
Subject: Glitchy app on ipad after recent update
|
||||
Message-Id: <CC7CF9B0-AD8F-4468-9E2E-437D53C1DB12@example.org>
|
||||
Date: Sat, 15 Aug 2026 21:02:24 +0100
|
||||
To: alice@example.org
|
||||
|
||||
|
||||
--Apple-Mail-23186EA7-751B-466A-B348-44BA06879E29
|
||||
Content-Type: text/plain;
|
||||
charset=us-ascii
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Hello. See first attached image.=20
|
||||
|
||||
Foo bar. See images 2,3.=20
|
||||
|
||||
|
||||
|
||||
--Apple-Mail-23186EA7-751B-466A-B348-44BA06879E29
|
||||
Content-Type: multipart/related;
|
||||
type="text/html";
|
||||
boundary=Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
|
||||
--Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3
|
||||
Content-Type: text/html;
|
||||
charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html class=3D"apple-mail-supports-explicit-dark-mode"><head><meta http-equi=
|
||||
v=3D"content-type" content=3D"text/html; charset=3Dutf-8"></head><body dir=3D=
|
||||
"auto"><div dir=3D"ltr"><meta http-equiv=3D"content-type" content=3D"text/ht=
|
||||
ml; charset=3Dutf-8">Hello. </div><div=
|
||||
dir=3D"ltr"><br></div><div dir=3D"ltr">See first attached image. </div><div dir=3D"ltr"><br></div><div dir=3D=
|
||||
"ltr">See images 2,3. </div><div dir=3D"ltr"><div dir=3D"ltr=
|
||||
"><br><div>-- </div><div><br></div><div><img=
|
||||
alt=3D"image0.jpeg" src=3D"blob:applewebdata://245de37b-ccc3-4dd2-b75b-ed66=
|
||||
efe8c6a7/243a0795-5043-4b6f-b332-551f5a30ff38" data-unique-identifier=3D""><=
|
||||
img alt=3D"image1.jpeg" src=3D"blob:applewebdata://245de37b-ccc3-4dd2-b75b-e=
|
||||
d66efe8c6a7/678adee9-cd13-4967-aecf-1a1096a3e7ab" data-unique-identifier=3D"=
|
||||
"><img alt=3D"image2.jpeg" src=3D"blob:applewebdata://245de37b-ccc3-4dd2-b75=
|
||||
b-ed66efe8c6a7/f9b066ca-8f0d-431d-9686-362ccdf9d32d" data-unique-identifier=3D=
|
||||
""><br></div></div></div></body></html>=
|
||||
|
||||
--Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3
|
||||
Content-Type: image/jpeg;
|
||||
name=image0.jpeg;
|
||||
x-apple-part-url="mf_2AD17D5E-F4E6-4B11-A716-8B20BB3543B4/L0/001"
|
||||
Content-Disposition: inline;
|
||||
filename=image0.jpeg
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Id: <mf_2AD17D5E-F4E6-4B11-A716-8B20BB3543B4/L0/001>
|
||||
|
||||
/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK
|
||||
CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU
|
||||
FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAeAB4DAREA
|
||||
AhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMB
|
||||
AAIQAxAAAAGqQAAAAAAAf//EABQQAQAAAAAAAAAAAAAAAAAAAED/2gAIAQEAAQUCB//EABQRAQAA
|
||||
AAAAAAAAAAAAAAAAAED/2gAIAQMBAT8BB//EABQRAQAAAAAAAAAAAAAAAAAAAED/2gAIAQIBAT8B
|
||||
B//EABQQAQAAAAAAAAAAAAAAAAAAAED/2gAIAQEABj8CB//EABQQAQAAAAAAAAAAAAAAAAAAAED/
|
||||
2gAIAQEAAT8hB//aAAwDAQACAAMAAAAQkkkkkkkk/8QAFBEBAAAAAAAAAAAAAAAAAAAAQP/aAAgB
|
||||
AwEBPxAH/8QAFBEBAAAAAAAAAAAAAAAAAAAAQP/aAAgBAgEBPxAH/8QAFBABAAAAAAAAAAAAAAAA
|
||||
AAAAQP/aAAgBAQABPxAH/9k=
|
||||
--Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3
|
||||
Content-Type: image/jpeg;
|
||||
name=image1.jpeg;
|
||||
x-apple-part-url="mf_314093B0-563C-414A-BE40-A83A2FFCDAB9/L0/001"
|
||||
Content-Disposition: inline;
|
||||
filename=image1.jpeg
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Id: <mf_314093B0-563C-414A-BE40-A83A2FFCDAB9/L0/001>
|
||||
|
||||
/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK
|
||||
CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU
|
||||
FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAyADIDAREA
|
||||
AhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMB
|
||||
AAIQAxAAAAGqQAAAAAAAAAAAAAAAAAAAAAAAD//EABQQAQAAAAAAAAAAAAAAAAAAAFD/2gAIAQEA
|
||||
AQUCR//EABQRAQAAAAAAAAAAAAAAAAAAAFD/2gAIAQMBAT8BR//EABQRAQAAAAAAAAAAAAAAAAAA
|
||||
AFD/2gAIAQIBAT8BR//EABQQAQAAAAAAAAAAAAAAAAAAAFD/2gAIAQEABj8CR//EABQQAQAAAAAA
|
||||
AAAAAAAAAAAAAFD/2gAIAQEAAT8hR//aAAwDAQACAAMAAAAQkkkkkkkkkkkkkkkkkkkkkkkkn//E
|
||||
ABQRAQAAAAAAAAAAAAAAAAAAAFD/2gAIAQMBAT8QR//EABQRAQAAAAAAAAAAAAAAAAAAAFD/2gAI
|
||||
AQIBAT8QR//EABQQAQAAAAAAAAAAAAAAAAAAAFD/2gAIAQEAAT8QR//Z
|
||||
|
||||
--Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3
|
||||
Content-Type: image/jpeg;
|
||||
name=image2.jpeg;
|
||||
x-apple-part-url="mf_303F2F37-BE85-44A2-87A5-83B49DE9EA6C/L0/001"
|
||||
Content-Disposition: inline;
|
||||
filename=image2.jpeg
|
||||
Content-Transfer-Encoding: base64
|
||||
Content-Id: <mf_303F2F37-BE85-44A2-87A5-83B49DE9EA6C/L0/001>
|
||||
|
||||
/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsK
|
||||
CwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQU
|
||||
FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCABkAGQDAREA
|
||||
AhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAj/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMB
|
||||
AAIQAxAAAAGqQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAD//EABQQAQAAAAAAAAAAAAAAAAAAAHD/2gAIAQEAAQUCKf/EABQRAQAA
|
||||
AAAAAAAAAAAAAAAAAHD/2gAIAQMBAT8BKf/EABQRAQAAAAAAAAAAAAAAAAAAAHD/2gAIAQIBAT8B
|
||||
Kf/EABQQAQAAAAAAAAAAAAAAAAAAAHD/2gAIAQEABj8CKf/EABQQAQAAAAAAAAAAAAAAAAAAAHD/
|
||||
2gAIAQEAAT8hKf/aAAwDAQACAAMAAAAQkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk
|
||||
kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkn//EABQRAQAAAAAAAAAAAAAAAAAAAHD/2gAI
|
||||
AQMBAT8QKf/EABQRAQAAAAAAAAAAAAAAAAAAAHD/2gAIAQIBAT8QKf/EABQQAQAAAAAAAAAAAAAA
|
||||
AAAAAHD/2gAIAQEAAT8QKf/Z
|
||||
--Apple-Mail-E9473C4A-5698-46EE-ABDF-6F38D4C3ACC3--
|
||||
|
||||
--Apple-Mail-23186EA7-751B-466A-B348-44BA06879E29--
|
||||
Reference in New Issue
Block a user