feat: add comprehensive Rust implementation with feature parity

This commit completes the Rust implementation of mail2couch with full feature
parity to the Go version, including:

- Complete IMAP client with TLS support and retry logic
- Advanced email parsing with MIME multipart support using mail-parser
- Email attachment extraction and CouchDB storage
- Sync mode implementation with deleted message handling
- Enhanced error handling and retry mechanisms
- Identical command-line interface with bash completion
- Test configurations for both implementations

The Rust implementation now provides:
- Memory safety and type safety guarantees
- Modern async/await patterns with tokio/async-std
- Comprehensive error handling with anyhow/thiserror
- Structured logging and progress reporting
- Performance optimizations and retry logic

Test configurations created:
- rust/config-test-rust.json - Rust implementation test config
- go/config-test-go.json - Go implementation test config
- test-config-comparison.md - Detailed comparison documentation
- test-both-implementations.sh - Automated testing script

Both implementations can now be tested side-by-side with identical
configurations to validate feature parity and performance.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-02 20:27:14 +02:00
commit 7b98efe06b
8 changed files with 1086 additions and 100 deletions

View file

@ -26,6 +26,7 @@ pub struct MailboxSyncResult {
pub messages_processed: u32,
pub messages_stored: u32,
pub messages_skipped: u32,
pub messages_deleted: u32,
pub last_uid: Option<u32>,
pub sync_time: DateTime<Utc>,
}
@ -148,13 +149,24 @@ impl SyncCoordinator {
match self.sync_mailbox(&mut imap_client, &db_name, mailbox, source).await {
Ok(result) => {
info!(
" ✅ {}: {} processed, {} stored, {} skipped",
result.mailbox,
result.messages_processed,
result.messages_stored,
result.messages_skipped
);
if result.messages_deleted > 0 {
info!(
" ✅ {}: {} processed, {} stored, {} skipped, {} deleted",
result.mailbox,
result.messages_processed,
result.messages_stored,
result.messages_skipped,
result.messages_deleted
);
} else {
info!(
" ✅ {}: {} processed, {} stored, {} skipped",
result.mailbox,
result.messages_processed,
result.messages_stored,
result.messages_skipped
);
}
total_messages += result.messages_processed;
mailbox_results.push(result);
}
@ -214,12 +226,27 @@ impl SyncCoordinator {
let message_uids = imap_client.search_messages(since_date.as_ref()).await?;
info!(" Found {} messages to process", message_uids.len());
// Handle sync mode - check for deleted messages
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 message_uids.is_empty() {
return Ok(MailboxSyncResult {
mailbox: mailbox.to_string(),
messages_processed: 0,
messages_stored: 0,
messages_skipped: 0,
messages_deleted,
last_uid: None,
sync_time: start_time,
});
@ -238,7 +265,7 @@ impl SyncCoordinator {
};
// Fetch and process messages
let messages = imap_client.fetch_messages(uids_to_process, self.args.max_messages).await?;
let messages = imap_client.fetch_messages(uids_to_process, self.args.max_messages, mailbox).await?;
let mut messages_stored = 0;
let mut messages_skipped = 0;
@ -289,11 +316,58 @@ impl SyncCoordinator {
messages_processed: uids_to_process.len() as u32,
messages_stored,
messages_skipped,
messages_deleted,
last_uid,
sync_time: start_time,
})
}
/// Handle deleted messages in sync mode
/// Compares UIDs from IMAP server with stored messages in CouchDB
/// and deletes messages that no longer exist on the server
async fn handle_deleted_messages(
&mut self,
db_name: &str,
mailbox: &str,
current_server_uids: &[u32],
) -> Result<u32> {
// Get all stored message UIDs for this mailbox from CouchDB
let stored_uids = self.get_stored_message_uids(db_name, mailbox).await?;
if stored_uids.is_empty() {
return Ok(0); // No stored messages to delete
}
// Find UIDs that exist in CouchDB but not on the server
let server_uid_set: std::collections::HashSet<u32> = current_server_uids.iter().cloned().collect();
let mut deleted_count = 0;
for stored_uid in stored_uids {
if !server_uid_set.contains(&stored_uid) {
// This message was deleted from the server, remove it from CouchDB
let doc_id = format!("{}_{}", mailbox, stored_uid);
match self.couch_client.delete_document(db_name, &doc_id).await {
Ok(_) => {
debug!(" Deleted document: {}", doc_id);
deleted_count += 1;
}
Err(e) => {
warn!(" Failed to delete document {}: {}", doc_id, e);
}
}
}
}
Ok(deleted_count)
}
/// Get all stored message UIDs for a mailbox from CouchDB
async fn get_stored_message_uids(&self, db_name: &str, mailbox: &str) -> Result<Vec<u32>> {
// Use the CouchDB client method to get stored UIDs
self.couch_client.get_mailbox_uids(db_name, mailbox).await
}
/// Print summary of sync results
pub fn print_sync_summary(&self, results: &[SourceSyncResult]) {
info!("\n🎉 Synchronization completed!");