diff --git a/crates/admin/src/migrate.rs b/crates/admin/src/migrate.rs index 4de0896..0005e5f 100644 --- a/crates/admin/src/migrate.rs +++ b/crates/admin/src/migrate.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use bichon_core::migrate::{ count_eml_segments, do_migrate_segment, is_tantivy_index_dir, - store::{LegacyDirs, NewDirs}, + store::{LegacyDirs, NewDirs, NewIndexWriter}, }; use console::style; use dialoguer::{theme::ColorfulTheme, Confirm, Input}; @@ -326,6 +326,18 @@ pub fn handle_migration(theme: &ColorfulTheme) { .progress_chars("#>-"), ); + let mut writer = match NewIndexWriter::open(NewDirs::new( + new_index_path.clone(), + new_data_path.clone(), + )) { + Ok(w) => w, + Err(e) => { + pb.finish_with_message(format!("{}", style("Migration failed.").red())); + eprintln!("\n{} {:?}", style("✘").red().bold(), e); + return; + } + }; + let mut grand_total_migrated: usize = 0; let mut grand_total_skipped: usize = 0; @@ -337,7 +349,7 @@ pub fn handle_migration(theme: &ColorfulTheme) { match do_migrate_segment( batch_size, legacy, - NewDirs::new(new_index_path.clone(), new_data_path.clone()), + &mut writer, seg_idx, |msg| { if let Some(data) = msg.strip_prefix("TOTAL:") { @@ -407,6 +419,13 @@ pub fn handle_migration(theme: &ColorfulTheme) { pb.set_position((seg_idx + 1) as u64); } + pb.set_message(style("Finalizing indexes...").dim().to_string()); + if let Err(e) = writer.finish_writers() { + pb.finish_with_message(format!("{}", style("Migration failed.").red())); + eprintln!("\n{} {:?}", style("✘").red().bold(), e); + return; + } + pb.finish_with_message(format!( "Migration finished. Total: {}, Skipped: {}", grand_total_migrated, grand_total_skipped diff --git a/crates/core/src/cache/imap/download/flow.rs b/crates/core/src/cache/imap/download/flow.rs index cdb3842..36805a7 100644 --- a/crates/core/src/cache/imap/download/flow.rs +++ b/crates/core/src/cache/imap/download/flow.rs @@ -339,6 +339,17 @@ pub async fn fetch_and_save_full_mailbox( Ok(max_uid) } +/// Generates a synthetic UIDVALIDITY for IMAP servers that don't provide it. +/// Uses a stable hash of the mailbox name to ensure consistent IDs across sessions. +fn generate_synthetic_uidvalidity(mailbox_name: &str) -> u32 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + mailbox_name.hash(&mut hasher); + (hasher.finish() as u32).wrapping_add(1) // Avoid 0, which might be reserved +} + pub async fn reconcile_mailboxes( account: &AccountModel, remote_mailboxes: &[MailBox], @@ -366,30 +377,30 @@ pub async fn reconcile_mailboxes( break; } - let new_highest_uid = if local_mailbox.uid_validity != remote_mailbox.uid_validity { - if remote_mailbox.uid_validity.is_none() { - let err_msg = format!( - "Mailbox '{}' logic error: Server did not provide UIDVALIDITY.", - local_mailbox.name + // Handle missing UIDVALIDITY from non-compliant IMAP servers + // (e.g., Tencent Enterprise Mail, etc.) + let remote_uid_validity = match remote_mailbox.uid_validity { + Some(uid) => uid, + None => { + // Generate a synthetic UIDVALIDITY based on mailbox name + let synthetic_uid = generate_synthetic_uidvalidity(&remote_mailbox.name); + + warn!( + "Account {}: Mailbox '{}' - Server did not provide UIDVALIDITY. \ + Using synthetic UIDVALIDITY {} based on mailbox name. \ + This mailbox will be synced but may require periodic rebuilds if the server's mailbox structure changes.", + account_id, remote_mailbox.name, synthetic_uid ); - - warn!("Account {}: {}", account_id, err_msg); - - DownloadState::update_folder_progress( - account_id, - remote_mailbox.name.clone(), - 0, - 0, - FolderStatus::Failed, - Some(err_msg.clone()), - )?; - DownloadState::append_session_error(account_id, err_msg)?; - continue; + + synthetic_uid } + }; + + let new_highest_uid = if local_mailbox.uid_validity != Some(remote_uid_validity) { info!( "Account {}: Mailbox '{}' detected with changed uid_validity (local: {:#?}, remote: {:#?}). \ The mailbox data may be invalid, resetting its envelopes and rebuilding the cache.", - account_id, local_mailbox.name, &local_mailbox.uid_validity, &remote_mailbox.uid_validity + account_id, local_mailbox.name, &local_mailbox.uid_validity, &remote_uid_validity ); DownloadState::update_folder_progress( @@ -443,6 +454,10 @@ pub async fn reconcile_mailboxes( let mut updated = remote_mailbox.clone(); updated.highest_uid = new_highest_uid; + // Update uid_validity with the resolved value (either from server or synthetic) + if updated.uid_validity.is_none() { + updated.uid_validity = Some(remote_uid_validity); + } mailboxes_to_update.push(updated); } //The metadata of this mailbox must only be updated after a successful synchronization; diff --git a/crates/core/src/migrate/mod.rs b/crates/core/src/migrate/mod.rs index 02f15aa..80255cd 100644 --- a/crates/core/src/migrate/mod.rs +++ b/crates/core/src/migrate/mod.rs @@ -121,7 +121,7 @@ fn is_dir_not_empty(path: &PathBuf) -> std::io::Result { pub fn do_migrate_segment( batch_size: u32, legacy: LegacyDirs, - new_dirs: NewDirs, + writer: &mut NewIndexWriter, segment_index: usize, mut on_progress: F, ) -> BichonResult<()> @@ -226,8 +226,6 @@ where drop(envelope_index); // ── Phase 2: process EML docs, streaming one at a time ───────────── - let mut writer = NewIndexWriter::open(new_dirs)?; - let mut total_migrated = 0usize; let mut total_skipped = 0usize; @@ -308,7 +306,6 @@ where chunk_start = chunk_end; } - writer.finish_writers()?; on_progress(&format!("DONE:{}:{}", total_migrated, total_skipped)); Ok(()) }