From 8c1c1dfc540f72e6c9959a7465ee59a677cd508b Mon Sep 17 00:00:00 2001 From: Anthony Date: Fri, 29 May 2026 11:31:09 +0200 Subject: [PATCH] Keep self-send cache entries until TTL so both Sent and Inbox hit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original implementation consumed the cache entry on first lookup, which meant the prefetch sweep that ran first (typically the Sent copy) got its multipart envelope rebuilt correctly, but the second sweep (the Inbox copy, which is the one suffering from the `File._ownerEncSessionKey` race) found an empty cache and fell back to the failing `crypto_client.load` path — leaving its .eml body-only. A self-send produces `load_attachments` calls for both folder copies of the same envelope, so the cache must serve as many lookups as arrive within the TTL. Clone the cached attachments out of the entry instead of removing it; the existing TTL (1h) + soft cap (50 entries) keep memory bounded. Also move the cache insert ahead of `DraftService.post` so the WS event for the inbox copy cannot beat the insert. Live-verified: both Sent and Inbox copies of a self-sent mail with a PDF attachment now expose a proper `multipart/mixed` BODYSTRUCTURE with the file part — no more session-key-transient retry storms in the prefetch logs. --- crates/bridge/src/tuta.rs | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/crates/bridge/src/tuta.rs b/crates/bridge/src/tuta.rs index 776bd7e..f4e9399 100644 --- a/crates/bridge/src/tuta.rs +++ b/crates/bridge/src/tuta.rs @@ -535,6 +535,14 @@ impl TutaSession { } async fn send_mail_impl(&self, msg: &ParsedMessage) -> Result<(), ApiCallError> { + // Pre-populate the self-send cache *before* we hit any Tuta API, + // so the inbox-side WS event has zero chance of beating the + // insert (`SendDraftService.post` on the server emits the event + // before its own response reaches us). + if !msg.attachments.is_empty() && is_self_recipient(msg, &self.email) { + self.cache_self_send_attachments(msg).await; + } + let randomizer = RandomizerFacade::from_core(rand_core::OsRng); let session_key: GenericAesKey = Aes256Key::generate(&randomizer).into(); @@ -620,18 +628,6 @@ impl TutaSession { .await?; log::info!("Mail sent, message_id: {}", send_return.messageId); - - // Cache attachments for self-sends — the inbox copy of the mail - // that is about to arrive via the event bus will try to decrypt - // these blobs via `crypto_client.load::()`, which - // perpetually fails because Tuta's server never populates - // `File._ownerEncSessionKey` for the recipient-side copy when the - // recipient is also the sender. We already hold the plaintext bytes - // and session keys here; cache them keyed by the same envelope - // metadata that will appear on the inbox copy. - if !msg.attachments.is_empty() && is_self_recipient(msg, &self.email) { - self.cache_self_send_attachments(msg).await; - } Ok(()) } @@ -691,9 +687,10 @@ impl TutaSession { cache.remove(&key); return None; } - // The cache entry is consumed: once we've materialised the - // multipart envelope on disk, future fetches read it from there. - let attachments = cache.remove(&key)?.attachments; + // Clone, do NOT consume — a self-send produces two `load_attachments` + // calls (one for the Sent copy, one for the Inbox copy) and both + // need to render multipart. The TTL handles eviction. + let attachments = entry.attachments.clone(); log::info!( "Self-send cache hit for {:?} ({} attachment(s))", mail.subject,