Keep self-send cache entries until TTL so both Sent and Inbox hit

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.
This commit is contained in:
Anthony
2026-05-29 11:31:09 +02:00
parent 93f0f82a30
commit 8c1c1dfc54
+12 -15
View File
@@ -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::<TutanotaFile>()`, 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,