diff --git a/rust/src/cli.rs b/rust/src/cli.rs index cdb8a4e..abb9159 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -28,6 +28,11 @@ pub fn parse_command_line() -> CommandLineArgs { .help("Maximum number of messages to process per mailbox per run (0 = no limit)") .value_parser(clap::value_parser!(u32)) .action(ArgAction::Set)) + .arg(Arg::new("dry-run") + .short('n') + .long("dry-run") + .help("Show what would be done without making any changes") + .action(ArgAction::SetTrue)) .arg(Arg::new("generate-bash-completion") .long("generate-bash-completion") .help("Generate bash completion script and exit") @@ -44,6 +49,7 @@ pub fn parse_command_line() -> CommandLineArgs { CommandLineArgs { config_path: matches.get_one::("config").map(|s| s.clone()), max_messages: matches.get_one::("max-messages").copied(), + dry_run: matches.get_flag("dry-run"), generate_bash_completion: matches.get_flag("generate-bash-completion"), help: false, // Using clap's built-in help } @@ -83,7 +89,7 @@ _{}_completions() {{ if [[ $cur == -* ]]; then # Complete with available options - local opts="-c --config -m --max-messages -h --help --generate-bash-completion" + local opts="-c --config -m --max-messages -n --dry-run -h --help --generate-bash-completion" COMPREPLY=($(compgen -W "$opts" -- "$cur")) return fi diff --git a/rust/src/config.rs b/rust/src/config.rs index 209a120..38a6f38 100644 --- a/rust/src/config.rs +++ b/rust/src/config.rs @@ -107,6 +107,7 @@ impl MailSource { pub struct CommandLineArgs { pub config_path: Option, pub max_messages: Option, + pub dry_run: bool, pub generate_bash_completion: bool, pub help: bool, } @@ -283,6 +284,7 @@ mod tests { let args = CommandLineArgs { config_path: None, max_messages: None, + dry_run: false, generate_bash_completion: false, help: false, }; diff --git a/rust/src/main.rs b/rust/src/main.rs index 2f15cbe..ed2c83a 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -36,6 +36,10 @@ async fn run(args: mail2couch::config::CommandLineArgs) -> Result<()> { info!("Maximum messages per mailbox: unlimited"); } + if args.dry_run { + info!("šŸ” DRY-RUN MODE: No changes will be made to CouchDB"); + } + // Display configuration summary print_config_summary(&config); diff --git a/rust/src/sync.rs b/rust/src/sync.rs index 8fdec27..29e9e95 100644 --- a/rust/src/sync.rs +++ b/rust/src/sync.rs @@ -119,8 +119,12 @@ impl SyncCoordinator { let db_name = generate_database_name(&source.name, &source.user); info!("Using database: {}", db_name); - // Create database if it doesn't exist - self.couch_client.create_database(&db_name).await?; + // Create database if it doesn't exist (skip in dry-run mode) + if !self.args.dry_run { + self.couch_client.create_database(&db_name).await?; + } else { + info!("šŸ” DRY-RUN: Would create database {}", db_name); + } // Connect to IMAP server let mut imap_client = ImapClient::connect(source.clone()).await?; @@ -212,22 +216,33 @@ impl SyncCoordinator { let mailbox_info = imap_client.select_mailbox(mailbox).await?; debug!("Selected mailbox {}: {} messages", mailbox, mailbox_info.exists); - // Get last sync metadata - let since_date = match self.couch_client.get_sync_metadata(db_name, mailbox).await { - Ok(metadata) => { - info!(" Found sync metadata, last sync: {}", metadata.last_sync_time); - Some(metadata.last_sync_time) - } - Err(_) => { - info!(" No sync metadata found, performing full sync"); - // Parse since date from message filter if provided - source.message_filter.since.as_ref() - .and_then(|since_str| { - DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z") - .map(|dt| dt.with_timezone(&Utc)) - .ok() - }) + // Get last sync metadata (skip in dry-run mode) + let since_date = if !self.args.dry_run { + match self.couch_client.get_sync_metadata(db_name, mailbox).await { + Ok(metadata) => { + info!(" Found sync metadata, last sync: {}", metadata.last_sync_time); + Some(metadata.last_sync_time) + } + Err(_) => { + info!(" No sync metadata found, performing full sync"); + // Parse since date from message filter if provided + source.message_filter.since.as_ref() + .and_then(|since_str| { + DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z") + .map(|dt| dt.with_timezone(&Utc)) + .ok() + }) + } } + } else { + info!(" šŸ” DRY-RUN: Would check for sync metadata"); + // In dry-run mode, use config since date if available + source.message_filter.since.as_ref() + .and_then(|since_str| { + DateTime::parse_from_str(&format!("{} 00:00:00 +0000", since_str), "%Y-%m-%d %H:%M:%S %z") + .map(|dt| dt.with_timezone(&Utc)) + .ok() + }) }; // Search for messages using server-side IMAP SEARCH with keyword filtering when possible @@ -257,17 +272,21 @@ impl SyncCoordinator { }; info!(" Found {} messages matching search criteria", message_uids.len()); - // Handle sync mode - check for deleted messages + // Handle sync mode - check for deleted messages (skip in dry-run mode) let mut messages_deleted = 0; if source.mode == "sync" { - messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await - .unwrap_or_else(|e| { - warn!(" Failed to handle deleted messages: {}", e); - 0 - }); - - if messages_deleted > 0 { - info!(" šŸ—‘ļø Deleted {} messages that no longer exist on server", messages_deleted); + if !self.args.dry_run { + messages_deleted = self.handle_deleted_messages(db_name, mailbox, &message_uids).await + .unwrap_or_else(|e| { + warn!(" Failed to handle deleted messages: {}", e); + 0 + }); + + if messages_deleted > 0 { + info!(" šŸ—‘ļø Deleted {} messages that no longer exist on server", messages_deleted); + } + } else { + info!(" šŸ” DRY-RUN: Would check for deleted messages in sync mode"); } } @@ -312,54 +331,74 @@ impl SyncCoordinator { // Extract UID before moving the document let uid_str = mail_doc.source_uid.clone(); - // Store the message document first - match self.couch_client.store_mail_document(db_name, mail_doc).await { - Ok(doc_id) => { - messages_stored += 1; - - // Store attachments if any exist - if !attachments.is_empty() { - for (filename, content_type, data) in attachments { - match self.couch_client.store_attachment( - db_name, - &doc_id, - &filename, - &content_type, - &data, - ).await { - Ok(_) => { - debug!(" Stored attachment: {}", filename); - } - Err(e) => { - warn!(" Failed to store attachment {}: {}", filename, e); + // Store the message document first (skip in dry-run mode) + if !self.args.dry_run { + match self.couch_client.store_mail_document(db_name, mail_doc).await { + Ok(doc_id) => { + messages_stored += 1; + + // Store attachments if any exist + if !attachments.is_empty() { + for (filename, content_type, data) in attachments { + match self.couch_client.store_attachment( + db_name, + &doc_id, + &filename, + &content_type, + &data, + ).await { + Ok(_) => { + debug!(" Stored attachment: {}", filename); + } + Err(e) => { + warn!(" Failed to store attachment {}: {}", filename, e); + } } } } + + // Parse UID from source_uid + if let Ok(uid) = uid_str.parse::() { + last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid))); + } } - - // Parse UID from source_uid - if let Ok(uid) = uid_str.parse::() { - last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid))); + Err(e) => { + warn!(" Failed to store message {}: {}", uid_str, e); + messages_skipped += 1; } } - Err(e) => { - warn!(" Failed to store message {}: {}", uid_str, e); - messages_skipped += 1; + } else { + // In dry-run mode, simulate successful storage + messages_stored += 1; + debug!(" šŸ” DRY-RUN: Would store message {} (Subject: {})", + uid_str, mail_doc.subject); + + if !attachments.is_empty() { + debug!(" šŸ” DRY-RUN: Would store {} attachments", attachments.len()); + } + + // Parse UID from source_uid + if let Ok(uid) = uid_str.parse::() { + last_uid = Some(last_uid.map_or(uid, |prev: u32| prev.max(uid))); } } } - // Update sync metadata + // Update sync metadata (skip in dry-run mode) if let Some(uid) = last_uid { - let sync_metadata = SyncMetadata::new( - mailbox.to_string(), - start_time, - uid, - messages_stored, - ); + if !self.args.dry_run { + let sync_metadata = SyncMetadata::new( + mailbox.to_string(), + start_time, + uid, + messages_stored, + ); - if let Err(e) = self.couch_client.store_sync_metadata(db_name, &sync_metadata).await { - warn!(" Failed to store sync metadata: {}", e); + if let Err(e) = self.couch_client.store_sync_metadata(db_name, &sync_metadata).await { + warn!(" Failed to store sync metadata: {}", e); + } + } else { + info!(" šŸ” DRY-RUN: Would update sync metadata (last UID: {}, {} messages)", uid, messages_stored); } } @@ -422,7 +461,11 @@ impl SyncCoordinator { /// Print summary of sync results pub fn print_sync_summary(&self, results: &[SourceSyncResult]) { - info!("\nšŸŽ‰ Synchronization completed!"); + if self.args.dry_run { + info!("\nšŸ” DRY-RUN completed!"); + } else { + info!("\nšŸŽ‰ Synchronization completed!"); + } info!("{}", "=".repeat(50)); let mut total_sources = 0; @@ -434,24 +477,45 @@ impl SyncCoordinator { total_mailboxes += result.mailboxes_processed; total_messages += result.total_messages; - info!( - "šŸ“§ {}: {} mailboxes, {} messages (database: {})", - result.source_name, - result.mailboxes_processed, - result.total_messages, - result.database - ); + if self.args.dry_run { + info!( + "šŸ“§ {}: {} mailboxes, {} messages found (database: {})", + result.source_name, + result.mailboxes_processed, + result.total_messages, + result.database + ); + } else { + info!( + "šŸ“§ {}: {} mailboxes, {} messages (database: {})", + result.source_name, + result.mailboxes_processed, + result.total_messages, + result.database + ); + } } info!("{}", "=".repeat(50)); - info!( - "šŸ“Š Total: {} sources, {} mailboxes, {} messages", - total_sources, total_mailboxes, total_messages - ); + if self.args.dry_run { + info!( + "šŸ“Š DRY-RUN Total: {} sources, {} mailboxes, {} messages found", + total_sources, total_mailboxes, total_messages + ); + } else { + info!( + "šŸ“Š Total: {} sources, {} mailboxes, {} messages", + total_sources, total_mailboxes, total_messages + ); + } if let Some(max) = self.args.max_messages { info!("āš ļø Message limit was applied: {} per mailbox", max); } + + if self.args.dry_run { + info!("šŸ” No changes were made to CouchDB"); + } } } @@ -493,6 +557,7 @@ mod tests { let args = CommandLineArgs { config_path: None, max_messages: Some(10), + dry_run: false, generate_bash_completion: false, help: false, };