1468 lines
46 KiB
Rust
1468 lines
46 KiB
Rust
use crate::types::{AuthCredential, FolderInfo, MailError, MailServerConfig};
|
|
use flutter_rust_bridge::frb;
|
|
|
|
use mail_parser::MimeHeaders;
|
|
|
|
use lazy_static::lazy_static;
|
|
use std::collections::HashMap;
|
|
use tokio::sync::Mutex;
|
|
|
|
lazy_static! {
|
|
// Global tracker for IMAP IDLE cancellation channels, keyed by email
|
|
static ref IDLE_CANCELS: std::sync::Arc<Mutex<HashMap<String, tokio::sync::mpsc::Sender<()>>>> = std::sync::Arc::new(Mutex::new(HashMap::new()));
|
|
}
|
|
|
|
pub fn init_logger() {
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
use tracing_subscriber::prelude::*;
|
|
if let Ok(android_layer) = tracing_android::layer("KoraxRust") {
|
|
let _ = tracing_subscriber::registry()
|
|
.with(android_layer)
|
|
.try_init();
|
|
}
|
|
}
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
tracing_subscriber::fmt::init();
|
|
}
|
|
|
|
// Initialize Rustls with 'ring' as the default provider to prevent crashes
|
|
// when multiple providers (ring, aws-lc-rs) are present in the dependency graph.
|
|
// This is required for Rustls 0.23+
|
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
}
|
|
|
|
/// Step-by-step IMAP connection diagnostic. Returns a multi-line string describing
|
|
/// each phase (TCP → TLS → greeting) with timing and outcome.
|
|
/// Use this from Flutter to identify exactly where the hang is on Android.
|
|
pub async fn diagnose_imap_connection(host: String, port: u32, email: String) -> String {
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
use tokio::io::AsyncReadExt;
|
|
use tokio::net::TcpStream;
|
|
use tokio_rustls::TlsConnector;
|
|
|
|
let mut out = String::new();
|
|
out.push_str(&format!("Diagnosing connection for {}\n", email));
|
|
let mut root_cert_store = rustls::RootCertStore::empty();
|
|
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
|
|
|
let tls_config = match rustls::ClientConfig::builder_with_provider(Arc::new(
|
|
rustls::crypto::ring::default_provider(),
|
|
))
|
|
.with_safe_default_protocol_versions()
|
|
.map_err(|e| e.to_string())
|
|
{
|
|
Ok(b) => b.with_root_certificates(root_cert_store).with_no_client_auth(),
|
|
Err(e) => {
|
|
out.push_str(&format!("TLS config: FAILED ({})\n", e));
|
|
return out;
|
|
}
|
|
};
|
|
|
|
let addr = format!("{}:{}", host, port);
|
|
|
|
// Phase 1: TCP connect
|
|
let t0 = Instant::now();
|
|
let stream = match tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(&addr))
|
|
.await
|
|
{
|
|
Err(_) => {
|
|
out.push_str(&format!("TCP: TIMEOUT after 10s\n"));
|
|
return out;
|
|
}
|
|
Ok(Err(e)) => {
|
|
out.push_str(&format!("TCP: FAILED ({}) in {:.1}s\n", e, t0.elapsed().as_secs_f32()));
|
|
return out;
|
|
}
|
|
Ok(Ok(s)) => {
|
|
out.push_str(&format!("TCP: OK in {:.1}s\n", t0.elapsed().as_secs_f32()));
|
|
s
|
|
}
|
|
};
|
|
|
|
// Phase 2: TLS handshake
|
|
let connector = TlsConnector::from(Arc::new(tls_config));
|
|
let server_name = match rustls_pki_types::ServerName::try_from(host.clone())
|
|
.map_err(|_| format!("Invalid DNS name: {}", host))
|
|
.and_then(|n| Ok(n.to_owned()))
|
|
{
|
|
Ok(n) => n,
|
|
Err(e) => {
|
|
out.push_str(&format!("TLS name: FAILED ({})\n", e));
|
|
return out;
|
|
}
|
|
};
|
|
|
|
let t1 = Instant::now();
|
|
let mut tls_stream = match tokio::time::timeout(
|
|
Duration::from_secs(10),
|
|
connector.connect(server_name, stream),
|
|
)
|
|
.await
|
|
{
|
|
Err(_) => {
|
|
out.push_str(&format!("TLS: TIMEOUT after 10s\n"));
|
|
return out;
|
|
}
|
|
Ok(Err(e)) => {
|
|
out.push_str(&format!("TLS: FAILED ({}) in {:.1}s\n", e, t1.elapsed().as_secs_f32()));
|
|
return out;
|
|
}
|
|
Ok(Ok(s)) => {
|
|
out.push_str(&format!("TLS: OK in {:.1}s\n", t1.elapsed().as_secs_f32()));
|
|
s
|
|
}
|
|
};
|
|
|
|
// Phase 3: Read IMAP server greeting (first line)
|
|
let mut greeting = vec![0u8; 512];
|
|
let t2 = Instant::now();
|
|
match tokio::time::timeout(Duration::from_secs(10), tls_stream.read(&mut greeting)).await {
|
|
Err(_) => {
|
|
out.push_str(&format!("Greeting: TIMEOUT after 10s\n"));
|
|
return out;
|
|
}
|
|
Ok(Err(e)) => {
|
|
out.push_str(&format!("Greeting: READ FAILED ({}) in {:.1}s\n", e, t2.elapsed().as_secs_f32()));
|
|
return out;
|
|
}
|
|
Ok(Ok(n)) => {
|
|
let line = String::from_utf8_lossy(&greeting[..n.min(120)]).to_string();
|
|
out.push_str(&format!(
|
|
"Greeting: OK in {:.1}s — {:?}\n",
|
|
t2.elapsed().as_secs_f32(),
|
|
line.trim()
|
|
));
|
|
}
|
|
};
|
|
|
|
out
|
|
}
|
|
|
|
pub async fn discover_server(email: String) -> Result<MailServerConfig, MailError> {
|
|
crate::dns::discover_mail_server(&email)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))
|
|
}
|
|
|
|
/// A simple test function to verify we can connect and list folders.
|
|
pub async fn connect_and_list_folders(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
show_all_folders: bool,
|
|
storage_path: String,
|
|
) -> Result<Vec<FolderInfo>, MailError> {
|
|
// This call will create/cache the session in the manager
|
|
let client_arc = crate::session_manager::get_client(config, email, credential).await?;
|
|
|
|
let mut client = client_arc.lock().await;
|
|
let mut folders = client
|
|
.list_folders(show_all_folders)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
// Add unread counts from storage for all folders
|
|
if let Ok(storage) = crate::storage::Storage::new(&storage_path) {
|
|
for folder in &mut folders {
|
|
if let Ok(count) = storage.get_unread_count(&folder.name) {
|
|
folder.unread_count = count;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(folders)
|
|
}
|
|
|
|
pub fn get_total_unread_count(storage_path: String) -> i32 {
|
|
if let Ok(storage) = crate::storage::Storage::new(&storage_path) {
|
|
storage.get_total_unread_count().unwrap_or(0)
|
|
} else {
|
|
0
|
|
}
|
|
}
|
|
|
|
pub async fn get_messages(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
) -> Result<Vec<crate::types::MessageHeader>, MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let client_arc = crate::session_manager::get_client(config, email, credential).await?;
|
|
|
|
let mut client = client_arc.lock().await;
|
|
client
|
|
.select_mailbox(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
client
|
|
.fetch_headers("1:*")
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))
|
|
}
|
|
|
|
pub async fn get_message_body(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
uid: u32,
|
|
storage_path: String,
|
|
_passphrase: Option<String>,
|
|
) -> Result<crate::types::MessageBody, 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()))?;
|
|
|
|
let cached_raw = storage
|
|
.get_message_raw(&mailbox, uid)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
let body_result = if let Some(raw_bytes) = cached_raw {
|
|
tracing::debug!("Cache hit for message UID {} in {}", uid, mailbox);
|
|
parse_mime_message(&raw_bytes).await.map(|mut body| {
|
|
body.uid = uid;
|
|
body
|
|
})
|
|
} else {
|
|
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
client
|
|
.select_mailbox(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let fetch_result = client.fetch_body_raw(uid).await;
|
|
|
|
// Always close the dedicated connection, even on error.
|
|
let _ = client.logout().await;
|
|
|
|
let raw_bytes = match fetch_result {
|
|
Ok(bytes) => bytes,
|
|
Err(e) => {
|
|
let err_msg = e.to_string();
|
|
if err_msg.contains("not found on server") {
|
|
let _ = storage.delete_header(&mailbox, uid);
|
|
}
|
|
return Err(MailError::Network(err_msg));
|
|
}
|
|
};
|
|
|
|
// Save to cache
|
|
if let Err(e) = storage.save_message_raw(&mailbox, uid, &raw_bytes) {
|
|
tracing::warn!("Failed to save message body to cache: {}", e);
|
|
}
|
|
|
|
parse_mime_message(&raw_bytes).await.map(|mut body| {
|
|
body.uid = uid;
|
|
body
|
|
})
|
|
};
|
|
|
|
body_result
|
|
}
|
|
|
|
pub async fn set_message_flag(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
uid: u32,
|
|
flag: Option<String>,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
// Optimistically update local DB
|
|
if let Err(e) = storage.update_message_flag(&mailbox, uid, flag.clone()) {
|
|
tracing::warn!("Failed to update flag in local storage: {}", e);
|
|
}
|
|
|
|
// Dedicated connection — never blocked by sync_mailbox holding the shared session lock.
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let result = client
|
|
.set_message_flag(&mailbox, uid, flag)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
// Always close the dedicated connection, even on error.
|
|
let _ = client.logout().await;
|
|
|
|
result
|
|
}
|
|
|
|
pub fn save_contact(path: String, contact: crate::types::Contact) -> Result<(), MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.save_contact(&contact)
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn get_contacts(path: String) -> Result<Vec<crate::types::Contact>, MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.get_contacts()
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn delete_contact(path: String, email: String) -> Result<(), MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.delete_contact(&email)
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn get_contact_by_email(
|
|
path: String,
|
|
email: String,
|
|
) -> Result<Option<crate::types::Contact>, MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.get_contact_by_email(&email)
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub async fn append_to_sent(
|
|
config: &MailServerConfig,
|
|
email: &str,
|
|
credential: AuthCredential,
|
|
message_bytes: &[u8],
|
|
) {
|
|
let mut client = crate::imap::ImapClient::new(config.clone(), email.to_string(), credential);
|
|
if let Err(e) = client.connect().await {
|
|
tracing::warn!("append_to_sent: IMAP connect failed: {}", e);
|
|
return;
|
|
}
|
|
let folders = client.list_folders(false).await.unwrap_or_default();
|
|
let sent_folder = folders
|
|
.iter()
|
|
.find(|f| f.folder_type == crate::types::FolderType::Sent)
|
|
.map(|f| f.name.clone())
|
|
.unwrap_or_else(|| "Sent".to_string());
|
|
if let Err(e) = client
|
|
.append_message(&sent_folder, message_bytes, Some(r"\Seen".to_string()))
|
|
.await
|
|
{
|
|
tracing::warn!("append_to_sent: append to {} failed: {}", sent_folder, e);
|
|
}
|
|
let _ = client.logout().await;
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn send_email(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
to: String,
|
|
subject: String,
|
|
body: String,
|
|
attachments: Vec<crate::types::Attachment>,
|
|
encrypt: bool,
|
|
_signer_key: Option<String>,
|
|
_signer_password: Option<String>,
|
|
_storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
if encrypt {
|
|
return Err(MailError::Generic("PGP encryption not supported in mailcore. Use mailmore.".to_string()));
|
|
}
|
|
|
|
let smtp_client = crate::smtp::SmtpClient::new(config.clone(), email.clone(), credential.clone());
|
|
let message_bytes = smtp_client
|
|
.send_mail(&to, &subject, &body, attachments)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
append_to_sent(&config, &email, credential, &message_bytes).await;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn prefetch_bodies(
|
|
config: crate::types::MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
uids: Vec<u32>,
|
|
storage_path: String,
|
|
) -> 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()))?;
|
|
|
|
// Filter to only uncached UIDs before opening a connection
|
|
let uncached: Vec<u32> = uids
|
|
.into_iter()
|
|
.filter(|&uid| !matches!(storage.get_message_raw(&mailbox, uid), Ok(Some(_))))
|
|
.collect();
|
|
|
|
if uncached.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Use a DEDICATED connection so prefetch never blocks user-initiated fetches
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
client
|
|
.select_mailbox(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
for uid in uncached {
|
|
match client.fetch_body_raw(uid).await {
|
|
Ok(raw_bytes) => {
|
|
if let Err(e) = storage.save_message_raw(&mailbox, uid, &raw_bytes) {
|
|
tracing::warn!("Prefetch: Failed to save body for UID {}: {}", uid, e);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!("Prefetch: Failed to fetch body for UID {}: {}", uid, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always close the dedicated connection when prefetch is done.
|
|
let _ = client.logout().await;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn parse_mime_message(bytes: &[u8]) -> Result<crate::types::MessageBody, MailError> {
|
|
let bytes_owned = crate::imap::client::preprocess_header_bytes(bytes);
|
|
tokio::task::spawn_blocking(move || {
|
|
let parsed = mail_parser::MessageParser::default()
|
|
.parse(&bytes_owned)
|
|
.ok_or_else(|| MailError::Generic("Failed to parse MIME message".to_string()))?;
|
|
|
|
let mut attachments = Vec::new();
|
|
for part in parsed.attachments() {
|
|
let content_type = part
|
|
.content_type()
|
|
.map(|ct| {
|
|
let ctype = ct.ctype();
|
|
let subtype = ct.subtype();
|
|
format!("{}/{}", ctype, subtype.unwrap_or(""))
|
|
})
|
|
.unwrap_or_else(|| "application/octet-stream".to_string());
|
|
|
|
attachments.push(crate::types::Attachment {
|
|
name: part.attachment_name().unwrap_or("unnamed").to_string(),
|
|
content_type,
|
|
size: part.contents().len(),
|
|
content: part.contents().to_vec(),
|
|
});
|
|
}
|
|
|
|
// RFC 3156 PGP/MIME: mail_parser's attachments() does not expose the inner
|
|
// parts of a multipart/encrypted envelope. Walk all parsed parts directly to
|
|
// find the application/octet-stream ciphertext that attachments() misses.
|
|
let mut is_pgp_mime_envelope = false;
|
|
if let Some(ct) = parsed.content_type() {
|
|
if ct.ctype() == "multipart" && ct.subtype().unwrap_or("") == "encrypted" {
|
|
is_pgp_mime_envelope = true;
|
|
for part in &parsed.parts {
|
|
if let Some(pct) = part.content_type() {
|
|
if pct.ctype() == "application"
|
|
&& pct.subtype().unwrap_or("") == "octet-stream"
|
|
{
|
|
let name = part
|
|
.attachment_name()
|
|
.unwrap_or("encrypted.asc")
|
|
.to_string();
|
|
let content = part.contents().to_vec();
|
|
if !attachments
|
|
.iter()
|
|
.any(|a: &crate::types::Attachment| a.content == content)
|
|
{
|
|
attachments.push(crate::types::Attachment {
|
|
name,
|
|
content_type: "application/octet-stream".to_string(),
|
|
size: content.len(),
|
|
content,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let (pgp_status, signature_fingerprint) = detect_pgp_status(&parsed);
|
|
|
|
let from_addr = parsed.from().and_then(|f| {
|
|
f.iter()
|
|
.next()
|
|
.map(|a| a.address().unwrap_or("").to_string())
|
|
});
|
|
|
|
// For multipart/encrypted messages, body_text(0) returns raw MIME bytes
|
|
// (boundary markers, Content-Type headers, the armored PGP block) rather
|
|
// than meaningful body text. Suppress it so the caller never shows protocol
|
|
// noise to the user.
|
|
let (text_plain, text_html) = if is_pgp_mime_envelope {
|
|
(None, None)
|
|
} else {
|
|
(
|
|
parsed.body_text(0).map(|s| s.to_string()),
|
|
parsed.body_html(0).map(|s| s.to_string()),
|
|
)
|
|
};
|
|
|
|
// 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,
|
|
text_plain,
|
|
text_html,
|
|
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;
|
|
|
|
// Basic PGP detection from headers
|
|
if let Some(ct) = parsed.content_type() {
|
|
let ctype = ct.ctype();
|
|
let subtype = ct.subtype().unwrap_or("");
|
|
if ctype == "multipart" && subtype == "signed" {
|
|
pgp_status = crate::types::PgpStatus::Signed;
|
|
} else if ctype == "multipart" && subtype == "encrypted" {
|
|
pgp_status = crate::types::PgpStatus::Encrypted;
|
|
}
|
|
}
|
|
|
|
// Check text/plain body for inline PGP MESSAGE block
|
|
if pgp_status == crate::types::PgpStatus::None {
|
|
if let Some(text) = parsed.body_text(0) {
|
|
if text.contains("-----BEGIN PGP MESSAGE-----") {
|
|
pgp_status = crate::types::PgpStatus::Encrypted;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to find if signed or if we find a signature part
|
|
for part in &parsed.parts {
|
|
// Detect unnamed application/octet-stream containing PGP MESSAGE
|
|
if pgp_status == crate::types::PgpStatus::None {
|
|
let is_octet = part
|
|
.content_type()
|
|
.map(|ct| ct.ctype() == "application" && ct.subtype() == Some("octet-stream"))
|
|
.unwrap_or(false);
|
|
// Also match parts that have no content-type at all (servers strip it)
|
|
let has_no_ct = part.content_type().is_none();
|
|
if is_octet || has_no_ct {
|
|
let contents = part.contents();
|
|
if contents.starts_with(b"-----BEGIN PGP MESSAGE-----") {
|
|
pgp_status = crate::types::PgpStatus::Encrypted;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(ct) = part.content_type() {
|
|
if ct.subtype() == Some("pgp-signature") {
|
|
if pgp_status == crate::types::PgpStatus::None {
|
|
pgp_status = crate::types::PgpStatus::Signed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(pgp_status, signature_fingerprint)
|
|
}
|
|
|
|
pub async fn sync_mailbox(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
storage_path: String,
|
|
cache_days: i64,
|
|
) -> Result<Vec<crate::types::MessageHeader>, 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()))?;
|
|
|
|
// 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 =
|
|
crate::session_manager::get_client(config.clone(), email.clone(), credential.clone())
|
|
.await?;
|
|
let mut client = client_arc.lock().await;
|
|
client
|
|
.sync_mailbox(&mailbox, &storage, cache_days)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?
|
|
};
|
|
|
|
// Step 2: FLAG refresh on a dedicated connection.
|
|
let uids = sync_result.uids_for_flag_refresh;
|
|
if !uids.is_empty() {
|
|
let mut dedicated = crate::imap::ImapClient::new(config, email, credential);
|
|
match dedicated.connect().await {
|
|
Ok(()) => {
|
|
if let Err(e) = dedicated.refresh_flags(&mailbox, &uids, &storage).await {
|
|
tracing::warn!("FLAG refresh via dedicated connection failed: {}", e);
|
|
}
|
|
let _ = dedicated.logout().await;
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
"FLAG refresh: dedicated connection failed to connect: {}",
|
|
e
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(sync_result.new_headers)
|
|
}
|
|
|
|
pub async fn listen_for_updates(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
) -> Result<tokio::sync::mpsc::Receiver<bool>, MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone(), credential.clone());
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
client
|
|
.select_mailbox(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let mailbox_clone = mailbox.clone();
|
|
let email_clone = email.clone();
|
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel::<bool>(32);
|
|
|
|
let (cancel_tx, mut cancel_rx) = tokio::sync::mpsc::channel::<()>(1);
|
|
{
|
|
let mut cancels = IDLE_CANCELS.lock().await;
|
|
if let Some(old_tx) = cancels.remove(&email_clone) {
|
|
let _ = old_tx.send(()).await;
|
|
}
|
|
cancels.insert(email_clone.clone(), cancel_tx);
|
|
}
|
|
|
|
tokio::task::spawn(async move {
|
|
loop {
|
|
tokio::select! {
|
|
_ = cancel_rx.recv() => {
|
|
tracing::debug!("IDLE loop canceled for {}", email_clone);
|
|
break;
|
|
}
|
|
res = client.wait_for_update(600) => {
|
|
match res {
|
|
Ok(true) => {
|
|
tracing::debug!("IDLE: New data detected for {}", email_clone);
|
|
if tx.send(true).await.is_err() {
|
|
break;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
|
}
|
|
Ok(false) => {
|
|
tracing::debug!("IDLE: Timeout for {}, renewing...", email_clone);
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("IDLE error for {}: {}", email_clone, e);
|
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
|
if let Err(re_err) = client.reconnect().await {
|
|
tracing::error!("IDLE reconnect failed: {}", re_err);
|
|
break;
|
|
}
|
|
let _ = client.select_mailbox(&mailbox_clone).await;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
Ok(rx)
|
|
}
|
|
|
|
pub fn stop_idle_listener(email: String) {
|
|
let mut cancels = IDLE_CANCELS.blocking_lock();
|
|
if let Some(tx) = cancels.remove(&email) {
|
|
tracing::debug!("Canceling existing IDLE listener for {}", email);
|
|
let _ = tx.try_send(());
|
|
}
|
|
}
|
|
|
|
pub async fn check_connection(email: String) -> bool {
|
|
crate::session_manager::is_connected(&email).await
|
|
}
|
|
|
|
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()
|
|
} else {
|
|
mailbox
|
|
};
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let mut headers = storage
|
|
.get_headers(&mailbox)
|
|
.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> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.save_draft(&draft)
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn get_drafts(path: String) -> Result<Vec<crate::types::Draft>, MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.get_drafts()
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn delete_draft(path: String, id: i64) -> Result<(), MailError> {
|
|
let storage =
|
|
crate::storage::Storage::new(&path).map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.delete_draft(id)
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub async fn sync_drafts(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
let folders = client
|
|
.list_folders(false)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
let drafts_folder = folders
|
|
.iter()
|
|
.find(|f| f.folder_type == crate::types::FolderType::Drafts)
|
|
.map(|f| f.name.clone())
|
|
.unwrap_or_else(|| "Drafts".to_string());
|
|
|
|
client
|
|
.select_mailbox(&drafts_folder)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
let server_headers = client.fetch_headers("1:*").await.unwrap_or_default();
|
|
let server_uids: std::collections::HashSet<u32> =
|
|
server_headers.iter().map(|h| h.uid).collect();
|
|
|
|
let local_drafts = storage
|
|
.get_drafts()
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
for mut draft in local_drafts {
|
|
let mut needs_upload = false;
|
|
|
|
if let Some(uid) = draft.server_uid {
|
|
if !server_uids.contains(&uid) {
|
|
draft.server_uid = None;
|
|
draft.last_synced_at = 0;
|
|
needs_upload = true;
|
|
} else if draft.last_updated > draft.last_synced_at {
|
|
let _ = client.delete_message(&drafts_folder, uid).await;
|
|
draft.server_uid = None;
|
|
needs_upload = true;
|
|
}
|
|
} else {
|
|
needs_upload = true;
|
|
}
|
|
|
|
if needs_upload {
|
|
let mut builder = mail_builder::MessageBuilder::new();
|
|
builder = builder
|
|
.from(email.as_str())
|
|
.to(draft.to.as_str())
|
|
.subject(draft.subject.as_str())
|
|
.text_body(draft.body.as_str());
|
|
|
|
for att in &draft.attachments {
|
|
builder = builder.attachment(
|
|
att.content_type.as_str(),
|
|
att.name.as_str(),
|
|
att.content.clone(),
|
|
);
|
|
}
|
|
|
|
let message_bytes = builder
|
|
.write_to_vec()
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
let updated_local_drafts = storage
|
|
.get_drafts()
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let local_uids: std::collections::HashSet<u32> = updated_local_drafts
|
|
.iter()
|
|
.filter_map(|d| d.server_uid)
|
|
.collect();
|
|
|
|
for header in server_headers {
|
|
if !local_uids.contains(&header.uid) {
|
|
if let Ok(body) = client.fetch_body(header.uid).await {
|
|
let new_draft = crate::types::Draft {
|
|
id: None,
|
|
to: header.to.join(", "),
|
|
subject: header.subject.clone(),
|
|
body: body.text_plain.unwrap_or_default(),
|
|
attachments: vec![],
|
|
last_updated: header.date,
|
|
last_synced_at: header.date,
|
|
server_uid: Some(header.uid),
|
|
};
|
|
let _ = storage.save_draft(&new_draft);
|
|
}
|
|
}
|
|
}
|
|
|
|
let _ = client.logout().await;
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn move_message(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
from_mailbox: String,
|
|
to_mailbox: String,
|
|
uid: u32,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let from_mailbox = if from_mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
from_mailbox
|
|
};
|
|
let to_mailbox = if to_mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
to_mailbox
|
|
};
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let move_result = client
|
|
.move_message(uid, &from_mailbox, &to_mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
let _ = client.logout().await;
|
|
|
|
move_result?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
storage
|
|
.delete_header(&from_mailbox, uid)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let _ = storage.delete_drafts_by_server_uids(&[uid]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn move_messages(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
from_mailbox: String,
|
|
to_mailbox: String,
|
|
uids: Vec<u32>,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let from_mailbox = if from_mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
from_mailbox
|
|
};
|
|
let to_mailbox = if to_mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
to_mailbox
|
|
};
|
|
if uids.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let move_result = client
|
|
.move_messages(&uids, &from_mailbox, &to_mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
let _ = client.logout().await;
|
|
|
|
move_result?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
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,
|
|
credential: AuthCredential,
|
|
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, credential);
|
|
client
|
|
.connect()
|
|
.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,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
uid: u32,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let mark_result = client
|
|
.mark_as_read(&mailbox, uid)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
let _ = client.logout().await;
|
|
|
|
mark_result?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let _ = storage.update_read_status(&mailbox, uid, true);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn mark_as_unread(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
uid: u32,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let mark_result = client
|
|
.mark_as_unread(&mailbox, uid)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
let _ = client.logout().await;
|
|
|
|
mark_result?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let _ = storage.update_read_status(&mailbox, uid, false);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn mark_all_as_read(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let mark_result = client
|
|
.mark_all_as_read(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()));
|
|
|
|
let _ = client.logout().await;
|
|
|
|
mark_result?;
|
|
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let _ = storage.mark_all_as_read(&mailbox);
|
|
|
|
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,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
storage_path: String,
|
|
wipe_drafts: bool,
|
|
) -> Result<(), MailError> {
|
|
let mailbox = if mailbox.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
mailbox
|
|
};
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let result = client
|
|
.delete_all_messages(&mailbox)
|
|
.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()))?;
|
|
// 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,
|
|
credential: AuthCredential,
|
|
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(),
|
|
credential.clone(),
|
|
folder,
|
|
storage_path.clone(),
|
|
wd,
|
|
)
|
|
.await;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn create_folder(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
folder_name: String,
|
|
) -> Result<(), MailError> {
|
|
let folder_name = if folder_name.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
folder_name
|
|
};
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
let result = client.create_folder(&folder_name).await.map_err(|e| {
|
|
MailError::Generic(e.to_string())
|
|
});
|
|
let _ = client.logout().await;
|
|
result
|
|
}
|
|
|
|
pub async fn delete_folder(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
folder_name: String,
|
|
storage_path: String,
|
|
) -> Result<(), MailError> {
|
|
let folder_name = if folder_name.to_uppercase() == "INBOX" {
|
|
"INBOX".to_string()
|
|
} else {
|
|
folder_name
|
|
};
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
let result = client.delete_folder(&folder_name).await.map_err(|e| {
|
|
MailError::Generic(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
|
|
.wipe_folder(&folder_name)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn save_password(account: String, service: String, password: String) -> Result<(), MailError> {
|
|
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> {
|
|
Ok(None) // keyring not available
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub fn delete_credential(account: String, service: String) -> Result<(), MailError> {
|
|
Ok(()) // keyring not available
|
|
.map_err(|e| MailError::Generic(e.to_string()))
|
|
}
|
|
|
|
pub async fn load_older_messages(
|
|
config: MailServerConfig,
|
|
email: String,
|
|
credential: AuthCredential,
|
|
mailbox: String,
|
|
oldest_uid: u32,
|
|
batch_size: u32,
|
|
storage_path: String,
|
|
) -> Result<Vec<crate::types::MessageHeader>, MailError> {
|
|
if oldest_uid == 0 {
|
|
return Ok(vec![]);
|
|
}
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
|
|
let local_uids = storage.get_uids(&mailbox).unwrap_or_default();
|
|
|
|
let mut client = crate::imap::ImapClient::new(config, email, credential);
|
|
client
|
|
.connect()
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
client
|
|
.select_mailbox(&mailbox)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let older_uids: Vec<u32> = client
|
|
.search_uids_below(oldest_uid)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
let mut to_fetch: Vec<u32> = older_uids
|
|
.into_iter()
|
|
.filter(|uid| !local_uids.contains(uid))
|
|
.take(batch_size as usize)
|
|
.collect();
|
|
|
|
if to_fetch.is_empty() {
|
|
let _ = client.logout().await;
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
to_fetch.sort_unstable();
|
|
let range = to_fetch
|
|
.iter()
|
|
.map(|u| u.to_string())
|
|
.collect::<Vec<_>>()
|
|
.join(",");
|
|
let headers = client
|
|
.fetch_headers(&range)
|
|
.await
|
|
.map_err(|e| MailError::Network(e.to_string()))?;
|
|
|
|
let mut result = Vec::new();
|
|
for header in headers {
|
|
let _ = storage.save_header(&mailbox, &header);
|
|
result.push(header);
|
|
}
|
|
|
|
let _ = client.logout().await;
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn get_folder_stats(
|
|
email: String,
|
|
mailbox: String,
|
|
storage_path: String,
|
|
) -> Result<crate::types::FolderStats, MailError> {
|
|
let storage = crate::storage::Storage::new(&storage_path)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
let (unread, server_total, local_total) = storage
|
|
.get_folder_stats(&email, &mailbox)
|
|
.map_err(|e| MailError::Generic(e.to_string()))?;
|
|
Ok(crate::types::FolderStats {
|
|
unread,
|
|
server_total,
|
|
local_total,
|
|
})
|
|
}
|