mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
Surface mail attachments over IMAP as multipart/mixed parts
When the prefetch loop loads a mail's body, also load every entity in `mail.attachments`, decrypt the blob data with the new SDK helper and embed each attachment as a base64 part of a `multipart/mixed` RFC 2822 message. Thunderbird now renders PDFs / images / etc. inline rather than showing a body-only mail with no hint anything was attached. * `mail_to_rfc2822` now takes a slice of (TutanotaFile, &[u8]) and emits a multi-part envelope when it's non-empty; the simple text/html case is unchanged. The boundary is derived from the mail's IdTuple so the cached .eml.enc bytes stay stable across rewrites. * `MailBackend::load_attachments` is the new trait method; the TutaSession implementation loads each File via `crypto_client.load` (auto-decrypted via the file's own `_ownerEncSessionKey`) then asks the new `MailFacade::load_file_attachment_data` for the concatenated decrypted bytes. * `prefetch_details` does a best-effort fetch — partial failure logs a warning and ships the body alone, on the assumption the user can re-open later and the next sweep will retry. A new unit test asserts the multipart structure (boundary, body part, attachment part with name/MIME/filename, closing boundary).
This commit is contained in:
@@ -333,7 +333,7 @@ async fn apply_mail_set_entry_create(
|
|||||||
);
|
);
|
||||||
let rfc2822 = details
|
let rfc2822 = details
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|d| crate::mail::mail_to_rfc2822(&mail, Some(d)));
|
.map(|d| crate::mail::mail_to_rfc2822(&mail, Some(d), &[]));
|
||||||
let mut stored = StoredMail {
|
let mut stored = StoredMail {
|
||||||
mail,
|
mail,
|
||||||
details: details.clone(),
|
details: details.clone(),
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ impl ImapSession {
|
|||||||
let rfc = mail_to_rfc2822(
|
let rfc = mail_to_rfc2822(
|
||||||
&self.mails[idx].mail,
|
&self.mails[idx].mail,
|
||||||
self.mails[idx].details.as_ref(),
|
self.mails[idx].details.as_ref(),
|
||||||
|
&[],
|
||||||
);
|
);
|
||||||
self.mails[idx].rfc2822 = Some(rfc);
|
self.mails[idx].rfc2822 = Some(rfc);
|
||||||
} else if self.mails[idx].rfc2822.is_none() {
|
} else if self.mails[idx].rfc2822.is_none() {
|
||||||
@@ -660,7 +661,7 @@ impl ImapSession {
|
|||||||
|
|
||||||
let details = sm.details.or(old_details);
|
let details = sm.details.or(old_details);
|
||||||
let rfc2822 = sm.rfc2822.or(old_rfc).unwrap_or_else(|| {
|
let rfc2822 = sm.rfc2822.or(old_rfc).unwrap_or_else(|| {
|
||||||
mail_to_rfc2822(&sm.mail, details.as_ref())
|
mail_to_rfc2822(&sm.mail, details.as_ref(), &[])
|
||||||
});
|
});
|
||||||
|
|
||||||
self.mails.push(CachedMail {
|
self.mails.push(CachedMail {
|
||||||
@@ -1485,6 +1486,14 @@ mod tests {
|
|||||||
let key = mail._id.as_ref().map(|id| id.element_id.to_string()).unwrap_or_default();
|
let key = mail._id.as_ref().map(|id| id.element_id.to_string()).unwrap_or_default();
|
||||||
Ok(self.details.lock().unwrap().get(&key).cloned())
|
Ok(self.details.lock().unwrap().get(&key).cloned())
|
||||||
}
|
}
|
||||||
|
async fn load_attachments(
|
||||||
|
&self,
|
||||||
|
_mail: &Mail,
|
||||||
|
) -> Result<Vec<(tutasdk::entities::generated::tutanota::TutanotaFile, Vec<u8>)>, String> {
|
||||||
|
// The mock has no attachment fixtures; the IMAP-session tests
|
||||||
|
// only exercise body/header paths.
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String> {
|
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String> {
|
||||||
Ok(vec![inbox_folder()])
|
Ok(vec![inbox_folder()])
|
||||||
}
|
}
|
||||||
@@ -1603,8 +1612,8 @@ mod tests {
|
|||||||
|
|
||||||
let backend = Arc::new(MockBackend::with_mails(vec![m1.clone(), m2.clone()]));
|
let backend = Arc::new(MockBackend::with_mails(vec![m1.clone(), m2.clone()]));
|
||||||
let store = MailStore::new();
|
let store = MailStore::new();
|
||||||
let rfc1 = crate::mail::mail_to_rfc2822(&m1, Some(&d1));
|
let rfc1 = crate::mail::mail_to_rfc2822(&m1, Some(&d1), &[]);
|
||||||
let rfc2 = crate::mail::mail_to_rfc2822(&m2, Some(&d2));
|
let rfc2 = crate::mail::mail_to_rfc2822(&m2, Some(&d2), &[]);
|
||||||
store.set_folder_list(vec![inbox_folder()]).await;
|
store.set_folder_list(vec![inbox_folder()]).await;
|
||||||
store.set_folder("inbox", vec![
|
store.set_folder("inbox", vec![
|
||||||
StoredMail { mail: m1, details: Some(d1), rfc2822: Some(rfc1), uid: 1 },
|
StoredMail { mail: m1, details: Some(d1), rfc2822: Some(rfc1), uid: 1 },
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use tutasdk::entities::generated::tutanota::{Mail, MailAddress, MailDetails};
|
use tutasdk::entities::generated::tutanota::{Mail, MailAddress, MailDetails, TutanotaFile};
|
||||||
|
|
||||||
pub fn mail_to_rfc2822(mail: &Mail, details: Option<&MailDetails>) -> String {
|
/// One decrypted attachment as it lands in the RFC 2822 we serve over IMAP:
|
||||||
|
/// the [`TutanotaFile`] entity (for name + MIME type + cid) and the raw
|
||||||
|
/// decrypted bytes (for the body of the part).
|
||||||
|
pub type AttachmentPart<'a> = (&'a TutanotaFile, &'a [u8]);
|
||||||
|
|
||||||
|
pub fn mail_to_rfc2822(
|
||||||
|
mail: &Mail,
|
||||||
|
details: Option<&MailDetails>,
|
||||||
|
attachments: &[AttachmentPart<'_>],
|
||||||
|
) -> String {
|
||||||
let mut msg = String::with_capacity(4096);
|
let mut msg = String::with_capacity(4096);
|
||||||
|
|
||||||
let date_str = format_rfc2822_date(mail.receivedDate.as_millis());
|
let date_str = format_rfc2822_date(mail.receivedDate.as_millis());
|
||||||
@@ -38,10 +47,6 @@ pub fn mail_to_rfc2822(mail: &Mail, details: Option<&MailDetails>) -> String {
|
|||||||
msg.push_str(&format!("To: {}\r\n", format_address(first)));
|
msg.push_str(&format!("To: {}\r\n", format_address(first)));
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.push_str("MIME-Version: 1.0\r\n");
|
|
||||||
msg.push_str("Content-Type: text/html; charset=UTF-8\r\n");
|
|
||||||
msg.push_str("Content-Transfer-Encoding: base64\r\n");
|
|
||||||
|
|
||||||
if let Some(ref id) = mail._id {
|
if let Some(ref id) = mail._id {
|
||||||
msg.push_str(&format!(
|
msg.push_str(&format!(
|
||||||
"Message-ID: <{}.{}@tutabridge.local>\r\n",
|
"Message-ID: <{}.{}@tutabridge.local>\r\n",
|
||||||
@@ -49,19 +54,76 @@ pub fn mail_to_rfc2822(mail: &Mail, details: Option<&MailDetails>) -> String {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
msg.push_str("\r\n");
|
msg.push_str("MIME-Version: 1.0\r\n");
|
||||||
|
|
||||||
let body_text = details
|
let body_text = details
|
||||||
.and_then(|d| d.body.compressedText.as_deref().or(d.body.text.as_deref()))
|
.and_then(|d| d.body.compressedText.as_deref().or(d.body.text.as_deref()))
|
||||||
.unwrap_or("<p>(No body available)</p>");
|
.unwrap_or("<p>(No body available)</p>");
|
||||||
|
|
||||||
let encoded = base64_encode_body(body_text.as_bytes());
|
if attachments.is_empty() {
|
||||||
msg.push_str(&encoded);
|
msg.push_str("Content-Type: text/html; charset=UTF-8\r\n");
|
||||||
msg.push_str("\r\n");
|
msg.push_str("Content-Transfer-Encoding: base64\r\n");
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
msg.push_str(&base64_encode_body(body_text.as_bytes()));
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
} else {
|
||||||
|
// The boundary is derived from the mail's element id so the same
|
||||||
|
// mail always produces the same MIME boundary — keeps `.eml.enc`
|
||||||
|
// bytes stable across rewrites.
|
||||||
|
let boundary = build_boundary(mail);
|
||||||
|
msg.push_str(&format!(
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"{}\"\r\n",
|
||||||
|
boundary
|
||||||
|
));
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
msg.push_str("This is a multi-part message in MIME format.\r\n");
|
||||||
|
|
||||||
|
msg.push_str(&format!("--{}\r\n", boundary));
|
||||||
|
msg.push_str("Content-Type: text/html; charset=UTF-8\r\n");
|
||||||
|
msg.push_str("Content-Transfer-Encoding: base64\r\n\r\n");
|
||||||
|
msg.push_str(&base64_encode_body(body_text.as_bytes()));
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
|
||||||
|
for (file, data) in attachments {
|
||||||
|
msg.push_str(&format!("--{}\r\n", boundary));
|
||||||
|
let mime = file.mimeType.as_deref().unwrap_or("application/octet-stream");
|
||||||
|
let name_encoded = encode_header_value(&file.name);
|
||||||
|
msg.push_str(&format!(
|
||||||
|
"Content-Type: {}; name=\"{}\"\r\n",
|
||||||
|
mime, name_encoded
|
||||||
|
));
|
||||||
|
msg.push_str("Content-Transfer-Encoding: base64\r\n");
|
||||||
|
msg.push_str(&format!(
|
||||||
|
"Content-Disposition: attachment; filename=\"{}\"\r\n",
|
||||||
|
name_encoded
|
||||||
|
));
|
||||||
|
if let Some(ref cid) = file.cid {
|
||||||
|
// Some Tuta files (inline images) carry a Content-ID — propagate
|
||||||
|
// it so HTML `<img src="cid:…">` references still resolve.
|
||||||
|
msg.push_str(&format!("Content-ID: <{}>\r\n", cid));
|
||||||
|
}
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
msg.push_str(&base64_encode_body(data));
|
||||||
|
msg.push_str("\r\n");
|
||||||
|
}
|
||||||
|
msg.push_str(&format!("--{}--\r\n", boundary));
|
||||||
|
}
|
||||||
|
|
||||||
msg
|
msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a MIME boundary that is stable for a given mail and unlikely to
|
||||||
|
/// collide with payload bytes. Format: `=_TutaBridge_<list>_<elem>` where the
|
||||||
|
/// ids are the mail's `IdTuple` — they contain only base64-ext characters
|
||||||
|
/// (so safe in a Content-Type header) and uniquely identify the mail.
|
||||||
|
fn build_boundary(mail: &Mail) -> String {
|
||||||
|
if let Some(ref id) = mail._id {
|
||||||
|
format!("=_TutaBridge_{}_{}", id.list_id, id.element_id)
|
||||||
|
} else {
|
||||||
|
"=_TutaBridge_unknown".to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn format_address(addr: &MailAddress) -> String {
|
pub(crate) fn format_address(addr: &MailAddress) -> String {
|
||||||
if addr.name.is_empty() {
|
if addr.name.is_empty() {
|
||||||
addr.address.clone()
|
addr.address.clone()
|
||||||
@@ -373,7 +435,7 @@ mod tests {
|
|||||||
_errors: Default::default(),
|
_errors: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let rfc = mail_to_rfc2822(&mail, None);
|
let rfc = mail_to_rfc2822(&mail, None, &[]);
|
||||||
|
|
||||||
assert!(rfc.contains("Date: Wed, 25 Dec 2024 12:37:25 +0000\r\n"));
|
assert!(rfc.contains("Date: Wed, 25 Dec 2024 12:37:25 +0000\r\n"));
|
||||||
assert!(rfc.contains("From: Alice <alice@tuta.com>\r\n"));
|
assert!(rfc.contains("From: Alice <alice@tuta.com>\r\n"));
|
||||||
@@ -484,7 +546,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let rfc = mail_to_rfc2822(&mail, Some(&details));
|
let rfc = mail_to_rfc2822(&mail, Some(&details), &[]);
|
||||||
|
|
||||||
assert!(rfc.contains("From: sender@tuta.com\r\n"));
|
assert!(rfc.contains("From: sender@tuta.com\r\n"));
|
||||||
assert!(rfc.contains("To: Bob <bob@example.com>, charlie@example.com\r\n"));
|
assert!(rfc.contains("To: Bob <bob@example.com>, charlie@example.com\r\n"));
|
||||||
@@ -494,4 +556,124 @@ mod tests {
|
|||||||
base64::engine::general_purpose::STANDARD.encode(b"<p>Hello World</p>");
|
base64::engine::general_purpose::STANDARD.encode(b"<p>Hello World</p>");
|
||||||
assert!(rfc.contains(&body_b64));
|
assert!(rfc.contains(&body_b64));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mail_to_rfc2822_with_attachment_emits_multipart() {
|
||||||
|
use tutasdk::date::DateTime;
|
||||||
|
use tutasdk::entities::generated::tutanota::{Body, Recipients, TutanotaFile};
|
||||||
|
use tutasdk::IdTupleGenerated;
|
||||||
|
|
||||||
|
let mail = Mail {
|
||||||
|
_id: Some(IdTupleGenerated::new(
|
||||||
|
test_id("list_att"),
|
||||||
|
test_id("elem_att"),
|
||||||
|
)),
|
||||||
|
_permissions: test_id("perm_att"),
|
||||||
|
_format: 0,
|
||||||
|
_ownerEncSessionKey: None,
|
||||||
|
subject: "With Attachment".to_string(),
|
||||||
|
receivedDate: DateTime::from_millis(0),
|
||||||
|
state: 2,
|
||||||
|
unread: false,
|
||||||
|
confidential: false,
|
||||||
|
replyType: 0,
|
||||||
|
_ownerGroup: None,
|
||||||
|
differentEnvelopeSender: None,
|
||||||
|
listUnsubscribe: false,
|
||||||
|
movedTime: None,
|
||||||
|
phishingStatus: 0,
|
||||||
|
authStatus: None,
|
||||||
|
method: 0,
|
||||||
|
recipientCount: 1,
|
||||||
|
encryptionAuthStatus: None,
|
||||||
|
_ownerKeyVersion: None,
|
||||||
|
processingState: 0,
|
||||||
|
processNeeded: false,
|
||||||
|
sendAt: None,
|
||||||
|
serverClassificationData: None,
|
||||||
|
_kdfNonce: None,
|
||||||
|
sender: MailAddress {
|
||||||
|
_id: None,
|
||||||
|
name: "Alice".to_string(),
|
||||||
|
address: "alice@tuta.com".to_string(),
|
||||||
|
contact: None,
|
||||||
|
_errors: Default::default(),
|
||||||
|
},
|
||||||
|
attachments: vec![],
|
||||||
|
conversationEntry: IdTupleGenerated::new(
|
||||||
|
test_id("conv_l"),
|
||||||
|
test_id("conv_e"),
|
||||||
|
),
|
||||||
|
firstRecipient: Some(MailAddress {
|
||||||
|
_id: None,
|
||||||
|
name: "".to_string(),
|
||||||
|
address: "bob@example.com".to_string(),
|
||||||
|
contact: None,
|
||||||
|
_errors: Default::default(),
|
||||||
|
}),
|
||||||
|
mailDetails: None,
|
||||||
|
mailDetailsDraft: None,
|
||||||
|
bucketKey: None,
|
||||||
|
sets: vec![],
|
||||||
|
clientSpamClassifierResult: None,
|
||||||
|
_errors: Default::default(),
|
||||||
|
};
|
||||||
|
let details = MailDetails {
|
||||||
|
_id: None,
|
||||||
|
sentDate: DateTime::from_millis(0),
|
||||||
|
authStatus: 0,
|
||||||
|
replyTos: vec![],
|
||||||
|
recipients: Recipients {
|
||||||
|
_id: None,
|
||||||
|
toRecipients: vec![],
|
||||||
|
ccRecipients: vec![],
|
||||||
|
bccRecipients: vec![],
|
||||||
|
},
|
||||||
|
headers: None,
|
||||||
|
body: Body {
|
||||||
|
_id: None,
|
||||||
|
text: Some("<p>The body</p>".to_string()),
|
||||||
|
compressedText: None,
|
||||||
|
_errors: Default::default(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let file = TutanotaFile {
|
||||||
|
_id: Some(IdTupleGenerated::new(
|
||||||
|
test_id("file_list"),
|
||||||
|
test_id("file_elem"),
|
||||||
|
)),
|
||||||
|
_permissions: test_id("file_perm"),
|
||||||
|
_format: 0,
|
||||||
|
_ownerEncSessionKey: None,
|
||||||
|
name: "doc.pdf".to_string(),
|
||||||
|
size: 5,
|
||||||
|
mimeType: Some("application/pdf".to_string()),
|
||||||
|
_ownerGroup: None,
|
||||||
|
cid: None,
|
||||||
|
_ownerKeyVersion: None,
|
||||||
|
_kdfNonce: None,
|
||||||
|
parent: None,
|
||||||
|
subFiles: None,
|
||||||
|
blobs: vec![],
|
||||||
|
_errors: Default::default(),
|
||||||
|
};
|
||||||
|
let data: &[u8] = b"PDFDA";
|
||||||
|
let attachments: Vec<super::AttachmentPart> = vec![(&file, data)];
|
||||||
|
let rfc = mail_to_rfc2822(&mail, Some(&details), &attachments);
|
||||||
|
|
||||||
|
assert!(rfc.contains("Content-Type: multipart/mixed; boundary=\"=_TutaBridge_list_att_elem_att\""));
|
||||||
|
assert!(rfc.contains("--=_TutaBridge_list_att_elem_att\r\n"));
|
||||||
|
// Body part: text/html base64
|
||||||
|
assert!(rfc.contains("Content-Type: text/html; charset=UTF-8\r\n"));
|
||||||
|
let body_b64 =
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(b"<p>The body</p>");
|
||||||
|
assert!(rfc.contains(&body_b64));
|
||||||
|
// Attachment part
|
||||||
|
assert!(rfc.contains("Content-Type: application/pdf; name=\"doc.pdf\""));
|
||||||
|
assert!(rfc.contains("Content-Disposition: attachment; filename=\"doc.pdf\""));
|
||||||
|
let pdf_b64 = base64::engine::general_purpose::STANDARD.encode(data);
|
||||||
|
assert!(rfc.contains(&pdf_b64));
|
||||||
|
// Closing boundary
|
||||||
|
assert!(rfc.ends_with("--=_TutaBridge_list_att_elem_att--\r\n"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use tokio::sync::{watch, RwLock};
|
use tokio::sync::{watch, RwLock};
|
||||||
use tutasdk::entities::generated::tutanota::{Mail, MailDetails};
|
use tutasdk::entities::generated::tutanota::{Mail, MailDetails, TutanotaFile};
|
||||||
|
|
||||||
use crate::mail::mail_to_rfc2822;
|
use crate::mail::mail_to_rfc2822;
|
||||||
use crate::store::{LocalStore, MailMetadata};
|
use crate::store::{LocalStore, MailMetadata};
|
||||||
@@ -453,10 +453,10 @@ async fn load_cached_folder(
|
|||||||
}
|
}
|
||||||
Some(eml)
|
Some(eml)
|
||||||
},
|
},
|
||||||
Ok(None) => Some(mail_to_rfc2822(&mail, None)),
|
Ok(None) => Some(mail_to_rfc2822(&mail, None, &[])),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to read cached eml {}: {e}", meta.element_id);
|
warn!("Failed to read cached eml {}: {e}", meta.element_id);
|
||||||
Some(mail_to_rfc2822(&mail, None))
|
Some(mail_to_rfc2822(&mail, None, &[]))
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -529,7 +529,7 @@ pub(crate) async fn sync_folder(
|
|||||||
uid,
|
uid,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let rfc2822 = mail_to_rfc2822(mail, None);
|
let rfc2822 = mail_to_rfc2822(mail, None, &[]);
|
||||||
updated.push(StoredMail {
|
updated.push(StoredMail {
|
||||||
mail: mail.clone(),
|
mail: mail.clone(),
|
||||||
details: None,
|
details: None,
|
||||||
@@ -611,7 +611,29 @@ async fn prefetch_details(
|
|||||||
let result = retry(|| backend.load_mail_details(mail)).await;
|
let result = retry(|| backend.load_mail_details(mail)).await;
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(details)) => {
|
Ok(Some(details)) => {
|
||||||
let rfc2822 = mail_to_rfc2822(mail, Some(&details));
|
// Best-effort attachment fetch: if any blob fails to load we
|
||||||
|
// still ship the body — the user can re-open the mail later
|
||||||
|
// and the prefetch sweep will retry. Empty `mail.attachments`
|
||||||
|
// short-circuits inside the backend so this is free for the
|
||||||
|
// overwhelmingly common "no attachments" case.
|
||||||
|
let attachments_owned = match backend.load_attachments(mail).await {
|
||||||
|
Ok(atts) => atts,
|
||||||
|
Err(e) => {
|
||||||
|
warn!(
|
||||||
|
"Failed to load attachments for {}: {e}",
|
||||||
|
mail._id
|
||||||
|
.as_ref()
|
||||||
|
.map(|id| id.element_id.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
);
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let attachment_refs: Vec<(&TutanotaFile, &[u8])> = attachments_owned
|
||||||
|
.iter()
|
||||||
|
.map(|(f, d)| (f, d.as_slice()))
|
||||||
|
.collect();
|
||||||
|
let rfc2822 = mail_to_rfc2822(mail, Some(&details), &attachment_refs);
|
||||||
if let Some(id) = mail._id.as_ref() {
|
if let Some(id) = mail._id.as_ref() {
|
||||||
let eid = id.element_id.to_string();
|
let eid = id.element_id.to_string();
|
||||||
|
|
||||||
@@ -632,7 +654,7 @@ async fn prefetch_details(
|
|||||||
// `MailDetailsDraft` (legacy / malformed). Persist a
|
// `MailDetailsDraft` (legacy / malformed). Persist a
|
||||||
// headers-only `.eml` and mark the row done so we don't
|
// headers-only `.eml` and mark the row done so we don't
|
||||||
// re-attempt every prefetch sweep.
|
// re-attempt every prefetch sweep.
|
||||||
let rfc2822 = mail_to_rfc2822(mail, None);
|
let rfc2822 = mail_to_rfc2822(mail, None, &[]);
|
||||||
if let Some(id) = mail._id.as_ref() {
|
if let Some(id) = mail._id.as_ref() {
|
||||||
let eid = id.element_id.to_string();
|
let eid = id.element_id.to_string();
|
||||||
if let Err(e) = local_store.write_eml(&eid, &rfc2822) {
|
if let Err(e) = local_store.write_eml(&eid, &rfc2822) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use tutasdk::bindings::rest_client::RestClient;
|
|||||||
use tutasdk::crypto_entity_client::CryptoEntityClient;
|
use tutasdk::crypto_entity_client::CryptoEntityClient;
|
||||||
use tutasdk::entities::generated::tutanota::{
|
use tutasdk::entities::generated::tutanota::{
|
||||||
DraftCreateData, DraftData, DraftRecipient, Mail, MailBox, MailDetails, MailDetailsBlob,
|
DraftCreateData, DraftData, DraftRecipient, Mail, MailBox, MailDetails, MailDetailsBlob,
|
||||||
MailSetEntry, SendDraftData, SendDraftParameters,
|
MailSetEntry, SendDraftData, SendDraftParameters, TutanotaFile,
|
||||||
};
|
};
|
||||||
use tutasdk::folder_system::{FolderSystem, MailSetKind};
|
use tutasdk::folder_system::{FolderSystem, MailSetKind};
|
||||||
use tutasdk::services::generated::tutanota::{DraftService, SendDraftService};
|
use tutasdk::services::generated::tutanota::{DraftService, SendDraftService};
|
||||||
@@ -67,6 +67,15 @@ pub trait MailBackend: Send + Sync {
|
|||||||
json: &str,
|
json: &str,
|
||||||
) -> Result<Option<MailDetails>, String>;
|
) -> Result<Option<MailDetails>, String>;
|
||||||
async fn load_mail_details(&self, mail: &Mail) -> Result<Option<MailDetails>, String>;
|
async fn load_mail_details(&self, mail: &Mail) -> Result<Option<MailDetails>, String>;
|
||||||
|
/// Load and decrypt every attachment of `mail`. Returns a vector of
|
||||||
|
/// `(file metadata, decrypted bytes)` in the order they appear in
|
||||||
|
/// `mail.attachments`. Empty if the mail has no attachments. Errors are
|
||||||
|
/// per-mail (no partial returns): if any one attachment fails the whole
|
||||||
|
/// call returns `Err` so the caller can decide to retry the prefetch.
|
||||||
|
async fn load_attachments(
|
||||||
|
&self,
|
||||||
|
mail: &Mail,
|
||||||
|
) -> Result<Vec<(TutanotaFile, Vec<u8>)>, String>;
|
||||||
/// Enumerate all mail folders (system + custom, with hierarchy).
|
/// Enumerate all mail folders (system + custom, with hierarchy).
|
||||||
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String>;
|
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String>;
|
||||||
async fn set_unread_status(&self, mail_ids: Vec<IdTupleGenerated>, unread: bool) -> Result<(), String>;
|
async fn set_unread_status(&self, mail_ids: Vec<IdTupleGenerated>, unread: bool) -> Result<(), String>;
|
||||||
@@ -292,6 +301,24 @@ impl TutaSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_attachments_impl(
|
||||||
|
&self,
|
||||||
|
mail: &Mail,
|
||||||
|
) -> Result<Vec<(TutanotaFile, Vec<u8>)>, ApiCallError> {
|
||||||
|
if mail.attachments.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let crypto_client = self.crypto_client();
|
||||||
|
let mail_facade = self.logged_in.mail_facade();
|
||||||
|
let mut out: Vec<(TutanotaFile, Vec<u8>)> = Vec::with_capacity(mail.attachments.len());
|
||||||
|
for file_id in &mail.attachments {
|
||||||
|
let file: TutanotaFile = crypto_client.load(file_id).await?;
|
||||||
|
let data = mail_facade.load_file_attachment_data(&file).await?;
|
||||||
|
out.push((file, data));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_mail_impl(&self, msg: &ParsedMessage) -> Result<(), ApiCallError> {
|
async fn send_mail_impl(&self, msg: &ParsedMessage) -> Result<(), ApiCallError> {
|
||||||
let randomizer = RandomizerFacade::from_core(rand_core::OsRng);
|
let randomizer = RandomizerFacade::from_core(rand_core::OsRng);
|
||||||
let session_key: GenericAesKey = Aes256Key::generate(&randomizer).into();
|
let session_key: GenericAesKey = Aes256Key::generate(&randomizer).into();
|
||||||
@@ -403,6 +430,13 @@ impl MailBackend for TutaSession {
|
|||||||
.map_err(|e| format!("{e}"))
|
.map_err(|e| format!("{e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_attachments(
|
||||||
|
&self,
|
||||||
|
mail: &Mail,
|
||||||
|
) -> Result<Vec<(TutanotaFile, Vec<u8>)>, String> {
|
||||||
|
self.load_attachments_impl(mail).await.map_err(|e| format!("{e}"))
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String> {
|
async fn list_folders(&self) -> Result<Vec<FolderInfo>, String> {
|
||||||
let mailbox = self.load_mailbox().await.map_err(|e| format!("{e}"))?;
|
let mailbox = self.load_mailbox().await.map_err(|e| format!("{e}"))?;
|
||||||
let folder_system = self.load_folders(&mailbox).await.map_err(|e| format!("{e}"))?;
|
let folder_system = self.load_folders(&mailbox).await.map_err(|e| format!("{e}"))?;
|
||||||
|
|||||||
+1
-1
Submodule tuta-repo updated: 42170313a7...00266f32d4
Reference in New Issue
Block a user