diff --git a/go/mail2couch b/go/mail2couch new file mode 100755 index 0000000..cd300c5 Binary files /dev/null and b/go/mail2couch differ diff --git a/rust/config-test-rust-no-filter.json b/rust/config-test-rust-no-filter.json new file mode 100644 index 0000000..518c5cd --- /dev/null +++ b/rust/config-test-rust-no-filter.json @@ -0,0 +1,54 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "Rust Wildcard All Folders Test", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["*"], + "exclude": ["Drafts", "Trash"] + }, + "messageFilter": {} + }, + { + "name": "Rust Work Pattern Test", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "syncuser", + "password": "syncpass", + "mode": "sync", + "folderFilter": { + "include": ["Work*", "Important*", "INBOX"], + "exclude": ["*Temp*"] + }, + "messageFilter": {} + }, + { + "name": "Rust Specific Folders Only", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "archiveuser", + "password": "archivepass", + "mode": "archive", + "folderFilter": { + "include": ["INBOX", "Sent", "Personal"], + "exclude": [] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file diff --git a/rust/config-test-rust-single.json b/rust/config-test-rust-single.json new file mode 100644 index 0000000..292bfc9 --- /dev/null +++ b/rust/config-test-rust-single.json @@ -0,0 +1,24 @@ +{ + "couchDb": { + "url": "http://localhost:5984", + "user": "admin", + "password": "password" + }, + "mailSources": [ + { + "name": "Rust Wildcard All Folders Test", + "enabled": true, + "protocol": "imap", + "host": "localhost", + "port": 3143, + "user": "testuser1", + "password": "password123", + "mode": "archive", + "folderFilter": { + "include": ["*"], + "exclude": ["Drafts", "Trash"] + }, + "messageFilter": {} + } + ] +} \ No newline at end of file diff --git a/rust/src/imap.rs b/rust/src/imap.rs index 8bb22cd..80ebd00 100644 --- a/rust/src/imap.rs +++ b/rust/src/imap.rs @@ -377,8 +377,8 @@ impl ImapClient { Ok(uid_vec) } - /// Fetch message by UID - pub async fn fetch_message(&mut self, uid: u32, mailbox: &str) -> Result { + /// Fetch message by UID with attachment data + pub async fn fetch_message(&mut self, uid: u32, mailbox: &str) -> Result<(MailDocument, Vec<(String, String, Vec)>)> { let session = self.session.as_mut() .ok_or_else(|| anyhow!("Not connected to IMAP server"))?; @@ -401,8 +401,8 @@ impl ImapClient { } } - /// Fetch multiple messages by UIDs - pub async fn fetch_messages(&mut self, uids: &[u32], max_count: Option, mailbox: &str) -> Result> { + /// Fetch multiple messages by UIDs with attachment data + pub async fn fetch_messages(&mut self, uids: &[u32], max_count: Option, mailbox: &str) -> Result)>)>> { if uids.is_empty() { return Ok(Vec::new()); } @@ -447,7 +447,7 @@ impl ImapClient { for (i, message) in fetched_messages.iter().enumerate() { if let Some(&uid) = uids_to_fetch.get(i) { match self.parse_message(message, uid, mailbox).await { - Ok(doc) => mail_documents.push(doc), + Ok((doc, attachments)) => mail_documents.push((doc, attachments)), Err(e) => { log::warn!("Failed to parse message {}: {}", uid, e); } @@ -458,8 +458,8 @@ impl ImapClient { Ok(mail_documents) } - /// Parse a raw IMAP message into a MailDocument - async fn parse_message(&self, message: &Fetch, uid: u32, mailbox: &str) -> Result { + /// Parse a raw IMAP message into a MailDocument with attachment data + async fn parse_message(&self, message: &Fetch, uid: u32, mailbox: &str) -> Result<(MailDocument, Vec<(String, String, Vec)>)> { let body = message.body() .ok_or_else(|| ImapError::Parsing("No message body found".to_string()))?; @@ -507,19 +507,16 @@ impl ImapClient { has_attachments, ); - // Add attachment stubs if any exist - if !attachment_stubs.is_empty() { - mail_doc.attachments = Some(attachment_stubs); - } + // Don't store attachment metadata in the document + // CouchDB will handle this when we store attachments separately + // This matches the Go implementation approach - // Store the attachment data separately (we'll return it for processing) - // Note: In practice, we'd store these via CouchDB after the document is created - // For now, we'll just log that we found attachments + // Log attachment information if !attachment_data.is_empty() { log::info!("Found {} attachments for message {}", attachment_data.len(), uid); } - Ok(mail_doc) + Ok((mail_doc, attachment_data)) } /// Extract email addresses from a parsed message @@ -626,26 +623,26 @@ impl ImapClient { // Get the content data (try different methods based on content type) let body_data = if let Some(text_content) = part.get_text_contents() { // Text-based attachments + log::debug!("Found text attachment content: {} bytes", text_content.len()); text_content.as_bytes().to_vec() } else { - // For binary data, we'll need to handle this differently - // For now, create a placeholder to indicate the attachment exists + // For now, skip attachments without text content + // TODO: Implement binary attachment support with proper mail-parser API + log::debug!("Skipping non-text attachment for part {} (content-type: {})", index, content_type.c_type); vec![] }; let content_type_str = content_type.c_type.to_string(); - // Create attachment stub with metadata - let attachment_stub = AttachmentStub { - content_type: content_type_str.clone(), - length: if body_data.is_empty() { None } else { Some(body_data.len() as u64) }, - stub: Some(true), // Indicates data will be stored separately - }; - - attachment_stubs.insert(filename.clone(), attachment_stub); - - // Store the binary data for later processing (if we have it) + // Only create attachment stub if we have actual data if !body_data.is_empty() { + let attachment_stub = AttachmentStub { + content_type: content_type_str.clone(), + length: Some(body_data.len() as u64), + stub: None, // Will be stored as actual attachment data + }; + + attachment_stubs.insert(filename.clone(), attachment_stub); attachment_data.push((filename, content_type_str, body_data)); } } diff --git a/rust/src/sync.rs b/rust/src/sync.rs index 0229afc..0e89378 100644 --- a/rust/src/sync.rs +++ b/rust/src/sync.rs @@ -271,7 +271,7 @@ impl SyncCoordinator { let mut messages_skipped = 0; let mut last_uid = None; - for mail_doc in messages { + for (mail_doc, attachments) in messages { // Apply message filters if !should_process_message(&mail_doc, &source.message_filter) { messages_skipped += 1; @@ -281,10 +281,31 @@ impl SyncCoordinator { // Extract UID before moving the document let uid_str = mail_doc.source_uid.clone(); - // Store the message + // Store the message document first match self.couch_client.store_mail_document(db_name, mail_doc).await { - Ok(_) => { + 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)));