sync: mailcore 0.2.1-beta

This commit is contained in:
Eugen Kaparulin
2026-06-22 21:54:48 +03:00
parent c76d1197d0
commit c48a14d4e8
10 changed files with 827 additions and 130 deletions

View File

@@ -1,4 +1,4 @@
use crate::types::{FolderInfo, MailError, MailServerConfig};
use crate::types::{AuthCredential, FolderInfo, MailError, MailServerConfig};
use flutter_rust_bridge::frb;
use mail_parser::MimeHeaders;
@@ -13,7 +13,19 @@ lazy_static! {
}
pub fn init_logger() {
tracing_subscriber::fmt::init();
#[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.
@@ -21,6 +33,170 @@ pub fn init_logger() {
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 → XOAUTH2 → NOOP) 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();
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()
));
}
};
// Phase 4: Token fetch
let t3 = Instant::now();
let token = match tokio::time::timeout(
Duration::from_secs(10),
crate::oauth::get_valid_token(&email),
)
.await
{
Err(_) => {
out.push_str(&format!("OAuth2 token: TIMEOUT after 10s\n"));
return out;
}
Ok(Err(e)) => {
out.push_str(&format!("OAuth2 token: FAILED ({}) in {:.1}s\n", e, t3.elapsed().as_secs_f32()));
return out;
}
Ok(Ok(t)) => {
out.push_str(&format!(
"OAuth2 token: OK in {:.1}s (len={})\n",
t3.elapsed().as_secs_f32(),
t.len()
));
t
}
};
// Phase 5: XOAUTH2 authenticate (raw IMAP)
use tokio::io::AsyncWriteExt;
let auth_str = format!("user={}\x01auth=Bearer {}\x01\x01", email, token);
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(auth_str.as_bytes());
let cmd = format!("A001 AUTHENTICATE XOAUTH2 {}\r\n", encoded);
let t4 = Instant::now();
if let Err(e) = tokio::time::timeout(Duration::from_secs(5), tls_stream.write_all(cmd.as_bytes())).await {
out.push_str(&format!("AUTHENTICATE write: {:?}\n", e));
return out;
}
let mut resp = vec![0u8; 2048];
match tokio::time::timeout(Duration::from_secs(10), tls_stream.read(&mut resp)).await {
Err(_) => {
out.push_str(&format!("AUTHENTICATE response: TIMEOUT after 10s in {:.1}s\n", t4.elapsed().as_secs_f32()));
}
Ok(Err(e)) => {
out.push_str(&format!("AUTHENTICATE response: FAILED ({}) in {:.1}s\n", e, t4.elapsed().as_secs_f32()));
}
Ok(Ok(n)) => {
let line = String::from_utf8_lossy(&resp[..n.min(200)]).to_string();
out.push_str(&format!(
"AUTHENTICATE response in {:.1}s: {:?}\n",
t4.elapsed().as_secs_f32(),
line.trim()
));
}
}
out
}
pub async fn discover_server(email: String) -> Result<MailServerConfig, MailError> {
crate::dns::discover_mail_server(&email)
.await
@@ -31,12 +207,12 @@ pub async fn discover_server(email: String) -> Result<MailServerConfig, MailErro
pub async fn connect_and_list_folders(
config: MailServerConfig,
email: String,
password: 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, password).await?;
let client_arc = crate::session_manager::get_client(config, email, credential).await?;
let mut client = client_arc.lock().await;
let mut folders = client
@@ -67,7 +243,7 @@ pub fn get_total_unread_count(storage_path: String) -> i32 {
pub async fn get_messages(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
) -> Result<Vec<crate::types::MessageHeader>, MailError> {
let mailbox = if mailbox.to_uppercase() == "INBOX" {
@@ -76,7 +252,7 @@ pub async fn get_messages(
mailbox
};
let client_arc = crate::session_manager::get_client(config, email, password).await?;
let client_arc = crate::session_manager::get_client(config, email, credential).await?;
let mut client = client_arc.lock().await;
client
@@ -92,7 +268,7 @@ pub async fn get_messages(
pub async fn get_message_body(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uid: u32,
storage_path: String,
@@ -117,9 +293,9 @@ pub async fn get_message_body(
body
})
} else {
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone());
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -161,7 +337,7 @@ pub async fn get_message_body(
pub async fn set_message_flag(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uid: u32,
flag: Option<String>,
@@ -176,9 +352,9 @@ pub async fn set_message_flag(
}
// Dedicated connection — never blocked by sync_mailbox holding the shared session lock.
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -231,11 +407,11 @@ pub fn get_contact_by_email(
pub async fn append_to_sent(
config: &MailServerConfig,
email: &str,
password: &str,
credential: AuthCredential,
message_bytes: &[u8],
) {
let mut client = crate::imap::ImapClient::new(config.clone(), email.to_string());
if let Err(e) = client.connect(password).await {
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;
}
@@ -258,7 +434,7 @@ pub async fn append_to_sent(
pub async fn send_email(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
to: String,
subject: String,
body: String,
@@ -272,19 +448,19 @@ pub async fn send_email(
return Err(MailError::Generic("PGP encryption not supported in mailcore. Use mailmore.".to_string()));
}
let client = crate::smtp::SmtpClient::new(config.clone(), email.clone());
let message_bytes = client
.send_mail(&password, &to, &subject, &body, attachments)
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, &password, &message_bytes).await;
append_to_sent(&config, &email, credential, &message_bytes).await;
Ok(())
}
pub async fn prefetch_bodies(
config: crate::types::MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uids: Vec<u32>,
storage_path: String,
@@ -309,9 +485,9 @@ pub async fn prefetch_bodies(
}
// Use a DEDICATED connection so prefetch never blocks user-initiated fetches
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
client
@@ -561,7 +737,7 @@ fn detect_pgp_status(parsed: &mail_parser::Message) -> (crate::types::PgpStatus,
pub async fn sync_mailbox(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
storage_path: String,
cache_days: i64,
@@ -583,7 +759,7 @@ pub async fn sync_mailbox(
// Step 1: Run sync on the shared session.
let sync_result = {
let client_arc =
crate::session_manager::get_client(config.clone(), email.clone(), password.clone())
crate::session_manager::get_client(config.clone(), email.clone(), credential.clone())
.await?;
let mut client = client_arc.lock().await;
client
@@ -595,8 +771,8 @@ pub async fn sync_mailbox(
// 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);
match dedicated.connect(&password).await {
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);
@@ -618,7 +794,7 @@ pub async fn sync_mailbox(
pub async fn listen_for_updates(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
) -> Result<tokio::sync::mpsc::Receiver<bool>, MailError> {
let mailbox = if mailbox.to_uppercase() == "INBOX" {
@@ -627,9 +803,9 @@ pub async fn listen_for_updates(
mailbox
};
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone());
let mut client = crate::imap::ImapClient::new(config.clone(), email.clone(), credential.clone());
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
client
@@ -637,7 +813,6 @@ pub async fn listen_for_updates(
.await
.map_err(|e| MailError::Network(e.to_string()))?;
let password_clone = password.clone();
let mailbox_clone = mailbox.clone();
let email_clone = email.clone();
@@ -674,7 +849,7 @@ pub async fn listen_for_updates(
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(&password_clone).await {
if let Err(re_err) = client.reconnect().await {
tracing::error!("IDLE reconnect failed: {}", re_err);
break;
}
@@ -754,12 +929,12 @@ pub fn delete_draft(path: String, id: i64) -> Result<(), MailError> {
pub async fn sync_drafts(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
storage_path: String,
) -> Result<(), MailError> {
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -869,7 +1044,7 @@ pub async fn sync_drafts(
pub async fn move_message(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
from_mailbox: String,
to_mailbox: String,
uid: u32,
@@ -885,9 +1060,9 @@ pub async fn move_message(
} else {
to_mailbox
};
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -913,7 +1088,7 @@ pub async fn move_message(
pub async fn move_messages(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
from_mailbox: String,
to_mailbox: String,
uids: Vec<u32>,
@@ -933,9 +1108,9 @@ pub async fn move_messages(
return Ok(());
}
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -961,7 +1136,7 @@ pub async fn move_messages(
pub async fn delete_messages_permanently(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uids: Vec<u32>,
storage_path: String,
@@ -969,9 +1144,9 @@ pub async fn delete_messages_permanently(
if uids.is_empty() {
return Ok(());
}
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -1009,7 +1184,7 @@ pub fn mark_as_read_local(
pub async fn mark_as_read(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uid: u32,
storage_path: String,
@@ -1020,9 +1195,9 @@ pub async fn mark_as_read(
mailbox
};
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -1045,7 +1220,7 @@ pub async fn mark_as_read(
pub async fn mark_as_unread(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
uid: u32,
storage_path: String,
@@ -1056,9 +1231,9 @@ pub async fn mark_as_unread(
mailbox
};
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -1081,7 +1256,7 @@ pub async fn mark_as_unread(
pub async fn mark_all_as_read(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
storage_path: String,
) -> Result<(), MailError> {
@@ -1091,9 +1266,9 @@ pub async fn mark_all_as_read(
mailbox
};
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -1135,7 +1310,7 @@ pub fn prepare_delete_all_messages(
pub async fn delete_all_messages(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
storage_path: String,
wipe_drafts: bool,
@@ -1146,9 +1321,9 @@ pub async fn delete_all_messages(
mailbox
};
let mut client = crate::imap::ImapClient::new(config, email.clone());
let mut client = crate::imap::ImapClient::new(config, email.clone(), credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
@@ -1177,7 +1352,7 @@ pub async fn delete_all_messages(
pub async fn resume_pending_wipes(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
storage_path: String,
) -> Result<(), MailError> {
let storage = crate::storage::Storage::new(&storage_path)
@@ -1189,7 +1364,7 @@ pub async fn resume_pending_wipes(
let _ = delete_all_messages(
config.clone(),
email.clone(),
password.clone(),
credential.clone(),
folder,
storage_path.clone(),
wd,
@@ -1202,7 +1377,7 @@ pub async fn resume_pending_wipes(
pub async fn create_folder(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
folder_name: String,
) -> Result<(), MailError> {
let folder_name = if folder_name.to_uppercase() == "INBOX" {
@@ -1210,9 +1385,9 @@ pub async fn create_folder(
} else {
folder_name
};
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
let result = client.create_folder(&folder_name).await.map_err(|e| {
@@ -1225,7 +1400,7 @@ pub async fn create_folder(
pub async fn delete_folder(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
folder_name: String,
storage_path: String,
) -> Result<(), MailError> {
@@ -1234,9 +1409,9 @@ pub async fn delete_folder(
} else {
folder_name
};
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
let result = client.delete_folder(&folder_name).await.map_err(|e| {
@@ -1270,7 +1445,7 @@ pub fn delete_credential(account: String, service: String) -> Result<(), MailErr
pub async fn load_older_messages(
config: MailServerConfig,
email: String,
password: String,
credential: AuthCredential,
mailbox: String,
oldest_uid: u32,
batch_size: u32,
@@ -1284,9 +1459,9 @@ pub async fn load_older_messages(
let local_uids = storage.get_uids(&mailbox).unwrap_or_default();
let mut client = crate::imap::ImapClient::new(config, email);
let mut client = crate::imap::ImapClient::new(config, email, credential);
client
.connect(&password)
.connect()
.await
.map_err(|e| MailError::Network(e.to_string()))?;
client
@@ -1337,11 +1512,61 @@ pub fn get_folder_stats(
) -> 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) = storage
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,
})
}
/// Populate the in-memory OAuth2 token store from Flutter secure storage.
/// Call at startup for every OAuth2 account with the persisted refresh token.
/// Rust will perform the first token refresh automatically on the next IMAP connect.
pub fn init_oauth2_session(email: String, refresh_token: String) {
crate::oauth::init_session(&email, &refresh_token);
}
/// Remove all OAuth2 tokens for an account from the in-memory store.
/// Call when removing an OAuth2 account.
pub fn delete_oauth2_tokens(email: String) -> Result<(), MailError> {
crate::oauth::delete_tokens(&email);
Ok(())
}
/// Provide OAuth2 client credentials. Must be called once at startup before
/// any Google sign-in or token refresh. Lives in the private mailmore layer.
pub fn init_google_credentials(client_id: String, client_secret: String) {
crate::oauth::init_credentials(&client_id, &client_secret);
}
/// Provide Desktop/mobile PKCE client credentials for browser-based OAuth flow.
pub fn init_google_desktop_credentials(client_id: String, client_secret: String) {
crate::oauth::init_desktop_credentials(&client_id, &client_secret);
}
/// Exchange a Google server-side authorization code for access + refresh tokens.
/// Returns (access_token, refresh_token) — the caller must persist the refresh
/// token via Flutter secure storage for use across app restarts.
pub async fn exchange_google_auth_code(
email: String,
server_auth_code: String,
) -> Result<Vec<String>, MailError> {
let (access, refresh) =
crate::oauth::exchange_auth_code(&email, &server_auth_code).await?;
Ok(vec![access, refresh])
}
/// Exchange an authorization code from the desktop PKCE flow.
/// Returns (email, access_token, refresh_token).
pub async fn exchange_google_auth_code_desktop(
code: String,
redirect_uri: String,
code_verifier: String,
) -> Result<Vec<String>, MailError> {
let (email, access, refresh) =
crate::oauth::exchange_auth_code_desktop(&code, &redirect_uri, &code_verifier).await?;
Ok(vec![email, access, refresh])
}