sync: mailcore 0.2.1-beta
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
use crate::types::{FolderInfo, MailError, MailServerConfig};
|
||||
use flutter_rust_bridge::frb;
|
||||
|
||||
use mail_parser::MimeHeaders;
|
||||
|
||||
@@ -418,6 +419,12 @@ pub async fn parse_mime_message(bytes: &[u8]) -> Result<crate::types::MessageBod
|
||||
)
|
||||
};
|
||||
|
||||
// Extract sender public key:
|
||||
// 1. Try Autocrypt header first (RFC 7929)
|
||||
let sender_public_key = extract_autocrypt_key(&parsed)
|
||||
// 2. Fall back to application/pgp-keys attachment
|
||||
.or_else(|| extract_pgp_keys_attachment(&parsed));
|
||||
|
||||
Ok(crate::types::MessageBody {
|
||||
uid: 0, // No UID for raw bytes
|
||||
from: from_addr,
|
||||
@@ -426,12 +433,77 @@ pub async fn parse_mime_message(bytes: &[u8]) -> Result<crate::types::MessageBod
|
||||
attachments,
|
||||
pgp_status,
|
||||
signature_fingerprint,
|
||||
sender_public_key,
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|e| Err(MailError::Generic(format!("Task spawn error: {}", e))))
|
||||
}
|
||||
|
||||
/// Parse the Autocrypt header and return the ASCII-armored public key, or None.
|
||||
///
|
||||
/// RFC 7929 / Autocrypt Level 1: the `keydata=` attribute holds the raw bytes
|
||||
/// of the exported OpenPGP certificate, Base64-encoded (no line breaks, no armor).
|
||||
/// We re-armor them so callers can use the result directly with Sequoia.
|
||||
fn extract_autocrypt_key(parsed: &mail_parser::Message) -> Option<String> {
|
||||
let header_value = parsed
|
||||
.headers()
|
||||
.iter()
|
||||
.find(|h| h.name.as_str().eq_ignore_ascii_case("Autocrypt"))
|
||||
.and_then(|h| match &h.value {
|
||||
mail_parser::HeaderValue::Text(t) => Some(t.as_ref().to_string()),
|
||||
mail_parser::HeaderValue::TextList(list) => list.first().map(|s| s.to_string()),
|
||||
_ => None,
|
||||
})?;
|
||||
|
||||
// Extract keydata= value (may be followed by ; or end of string)
|
||||
let lower = header_value.to_lowercase();
|
||||
let keydata_start = lower.find("keydata=")?;
|
||||
let rest = &header_value[keydata_start + "keydata=".len()..];
|
||||
let keydata_b64: String = rest
|
||||
.split(';')
|
||||
.next()?
|
||||
.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.collect();
|
||||
|
||||
if keydata_b64.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Re-armor: wrap the raw base64 in PGP armor header/footer
|
||||
// (The keydata is already the raw exported cert bytes in base64; we split
|
||||
// into 64-char lines for canonical OpenPGP ASCII Armor format.)
|
||||
let wrapped: String = keydata_b64
|
||||
.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(64)
|
||||
.map(|chunk| chunk.iter().collect::<String>())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Some(format!(
|
||||
"-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n{wrapped}\n-----END PGP PUBLIC KEY BLOCK-----\n"
|
||||
))
|
||||
}
|
||||
|
||||
/// Look for the first MIME part with Content-Type application/pgp-keys and
|
||||
/// return its content decoded as UTF-8 (the ASCII-armored public key block).
|
||||
fn extract_pgp_keys_attachment(parsed: &mail_parser::Message) -> Option<String> {
|
||||
for part in &parsed.parts {
|
||||
let is_pgp_keys = part
|
||||
.content_type()
|
||||
.map(|ct| {
|
||||
ct.ctype().eq_ignore_ascii_case("application")
|
||||
&& ct.subtype().map(|s| s.eq_ignore_ascii_case("pgp-keys")).unwrap_or(false)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if is_pgp_keys {
|
||||
return std::str::from_utf8(part.contents()).ok().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn detect_pgp_status(parsed: &mail_parser::Message) -> (crate::types::PgpStatus, Option<String>) {
|
||||
let mut pgp_status = crate::types::PgpStatus::None;
|
||||
let signature_fingerprint = None;
|
||||
@@ -502,6 +574,12 @@ pub async fn sync_mailbox(
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
|
||||
// Skip syncing folders pending a full wipe — avoids repopulating messages
|
||||
// before the IMAP expunge completes.
|
||||
if storage.is_folder_pending_wipe(&email, &mailbox) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Step 1: Run sync on the shared session.
|
||||
let sync_result = {
|
||||
let client_arc =
|
||||
@@ -626,6 +704,7 @@ pub async fn check_connection(email: String) -> bool {
|
||||
pub fn get_cached_messages(
|
||||
storage_path: String,
|
||||
mailbox: String,
|
||||
account_email: String,
|
||||
) -> Result<Vec<crate::types::MessageHeader>, MailError> {
|
||||
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
||||
"INBOX".to_string()
|
||||
@@ -634,9 +713,18 @@ pub fn get_cached_messages(
|
||||
};
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
storage
|
||||
let mut headers = storage
|
||||
.get_headers(&mailbox)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
// Ensure account_email is set on every returned header. Rows migrated from
|
||||
// older schema versions will have an empty string in the column; overwrite
|
||||
// them here so callers always get a non-empty value.
|
||||
for h in &mut headers {
|
||||
if h.account_email.is_empty() {
|
||||
h.account_email = account_email.clone();
|
||||
}
|
||||
}
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
pub fn save_draft(path: String, draft: crate::types::Draft) -> Result<i64, MailError> {
|
||||
@@ -737,10 +825,11 @@ pub async fn sync_drafts(
|
||||
.write_to_vec()
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
|
||||
if let Ok(_) = client
|
||||
if let Ok(uid) = client
|
||||
.append_message(&drafts_folder, &message_bytes, Some(r"\Draft".to_string()))
|
||||
.await
|
||||
{
|
||||
draft.server_uid = Some(uid);
|
||||
draft.last_synced_at = draft.last_updated;
|
||||
let _ = storage.save_draft(&draft);
|
||||
}
|
||||
@@ -813,7 +902,10 @@ pub async fn move_message(
|
||||
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
let _ = storage.delete_header(&from_mailbox, uid);
|
||||
storage
|
||||
.delete_header(&from_mailbox, uid)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
let _ = storage.delete_drafts_by_server_uids(&[uid]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -858,11 +950,62 @@ pub async fn move_messages(
|
||||
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
let _ = storage.delete_headers(&from_mailbox, &uids);
|
||||
storage
|
||||
.delete_headers(&from_mailbox, &uids)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
let _ = storage.delete_drafts_by_server_uids(&uids);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_messages_permanently(
|
||||
config: MailServerConfig,
|
||||
email: String,
|
||||
password: String,
|
||||
mailbox: String,
|
||||
uids: Vec<u32>,
|
||||
storage_path: String,
|
||||
) -> Result<(), MailError> {
|
||||
if uids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut client = crate::imap::ImapClient::new(config, email);
|
||||
client
|
||||
.connect(&password)
|
||||
.await
|
||||
.map_err(|e| MailError::Network(e.to_string()))?;
|
||||
|
||||
let result = client
|
||||
.delete_messages_permanently(&mailbox, &uids)
|
||||
.await
|
||||
.map_err(|e| MailError::Network(e.to_string()));
|
||||
|
||||
let _ = client.logout().await;
|
||||
result?;
|
||||
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
storage
|
||||
.delete_headers(&mailbox, &uids)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn mark_as_read_local(
|
||||
storage_path: String,
|
||||
mailbox: String,
|
||||
uid: u32,
|
||||
) -> Result<(), MailError> {
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
storage
|
||||
.update_read_status(&mailbox, uid, true)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mark_as_read(
|
||||
config: MailServerConfig,
|
||||
email: String,
|
||||
@@ -970,12 +1113,32 @@ pub async fn mark_all_as_read(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[frb(sync)]
|
||||
pub fn prepare_delete_all_messages(
|
||||
email: String,
|
||||
mailbox: String,
|
||||
storage_path: String,
|
||||
wipe_drafts: bool,
|
||||
) -> Result<(), MailError> {
|
||||
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
||||
"INBOX".to_string()
|
||||
} else {
|
||||
mailbox
|
||||
};
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
storage
|
||||
.set_folder_pending_wipe(&email, &mailbox, wipe_drafts)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
pub async fn delete_all_messages(
|
||||
config: MailServerConfig,
|
||||
email: String,
|
||||
password: String,
|
||||
mailbox: String,
|
||||
storage_path: String,
|
||||
wipe_drafts: bool,
|
||||
) -> Result<(), MailError> {
|
||||
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
||||
"INBOX".to_string()
|
||||
@@ -1000,11 +1163,42 @@ pub async fn delete_all_messages(
|
||||
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
// Wipe local cache (idempotent — also done by prepare, covers the resume path).
|
||||
let _ = storage.wipe_folder(&mailbox);
|
||||
let _ = storage.update_server_total(&email, &mailbox, 0);
|
||||
if wipe_drafts {
|
||||
let _ = storage.wipe_drafts();
|
||||
}
|
||||
let _ = storage.clear_folder_pending_wipe(&email, &mailbox);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resume_pending_wipes(
|
||||
config: MailServerConfig,
|
||||
email: String,
|
||||
password: String,
|
||||
storage_path: String,
|
||||
) -> Result<(), MailError> {
|
||||
let storage = crate::storage::Storage::new(&storage_path)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
let pending = storage
|
||||
.get_pending_wipes(&email)
|
||||
.map_err(|e| MailError::Generic(e.to_string()))?;
|
||||
for (folder, wd) in pending {
|
||||
let _ = delete_all_messages(
|
||||
config.clone(),
|
||||
email.clone(),
|
||||
password.clone(),
|
||||
folder,
|
||||
storage_path.clone(),
|
||||
wd,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_folder(
|
||||
config: MailServerConfig,
|
||||
email: String,
|
||||
@@ -1059,17 +1253,17 @@ pub async fn delete_folder(
|
||||
}
|
||||
|
||||
pub fn save_password(account: String, service: String, password: String) -> Result<(), MailError> {
|
||||
crate::secrets::SecretManager::set_password(&account, &service, &password)
|
||||
Err(crate::types::MailError::Generic("keyring not available".into()))?; Ok(())
|
||||
.map_err(|e| MailError::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn get_password(account: String, service: String) -> Result<Option<String>, MailError> {
|
||||
crate::secrets::SecretManager::get_password(&account, &service)
|
||||
Ok(None) // keyring not available
|
||||
.map_err(|e| MailError::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
pub fn delete_credential(account: String, service: String) -> Result<(), MailError> {
|
||||
crate::secrets::SecretManager::delete_password(&account, &service)
|
||||
Ok(()) // keyring not available
|
||||
.map_err(|e| MailError::Generic(e.to_string()))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user