//! Email provider abstraction for IMAP vs JMAP. //! //! Provides a unified interface for email operations regardless of //! the underlying protocol (IMAP/SMTP or JMAP). use async_trait::async_trait; use chrono::{DateTime, Utc}; use goingson_core::EmailAccount; use super::imap_client::{ImapClient, ParsedEmail}; use super::smtp_client::SmtpClient; use crate::jmap::{email::JmapParsedEmail, JmapClient}; /// Unified parsed email structure from any provider. #[derive(Debug, Clone)] pub struct UnifiedEmail { /// Provider-specific ID (IMAP UID as string, or JMAP ID) pub provider_id: String, /// Message-ID header pub message_id: Option, /// In-Reply-To header pub in_reply_to: Option, /// Source folder name pub source_folder: String, /// From address pub from: String, /// To address pub to: String, /// Subject pub subject: String, /// Body text pub body: String, /// Received date pub date: DateTime, /// Whether email is read (JMAP only, IMAP doesn't fetch flags) pub is_read: bool, } impl From for UnifiedEmail { fn from(email: ParsedEmail) -> Self { Self { provider_id: email.imap_uid.to_string(), message_id: email.message_id, in_reply_to: email.in_reply_to, source_folder: email.source_folder, from: email.from, to: email.to, subject: email.subject, body: email.body, date: email.date, is_read: email.is_read, } } } impl From for UnifiedEmail { fn from(email: JmapParsedEmail) -> Self { Self { provider_id: email.jmap_id, message_id: email.message_id, in_reply_to: email.in_reply_to, source_folder: email.source_folder, from: email.from, to: email.to, subject: email.subject, body: email.body, date: email.date, is_read: email.is_read, } } } /// Sync result from a provider. #[derive(Debug, Clone)] pub struct ProviderSyncResult { /// Emails from inbox pub inbox_emails: Vec, /// Emails from archive pub archive_emails: Vec, /// Debug info pub debug_info: Option, } /// Trait for email providers. #[async_trait] pub trait EmailProvider: Send + Sync { /// Tests the connection to the provider. async fn test_connection(&self) -> Result; /// Lists available folders/mailboxes. async fn list_folders(&self) -> Result, String>; /// Fetches emails for sync. async fn sync_emails( &self, since: Option>, limit: u32, archive_folder: &str, ) -> Result; /// Sends an email. async fn send_email( &self, to: &str, subject: &str, body: &str, ) -> Result; /// Archives an email (moves from inbox to archive). async fn archive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String>; /// Unarchives an email (moves from archive to inbox). async fn unarchive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String>; /// Marks an email as read (JMAP only, no-op for IMAP). async fn mark_read(&self, _email_id: &str) -> Result<(), String> { Ok(()) // Default no-op } /// Marks an email as unread (JMAP only, no-op for IMAP). async fn mark_unread(&self, _email_id: &str) -> Result<(), String> { Ok(()) // Default no-op } } /// IMAP/SMTP provider implementation. pub struct ImapProvider { imap_client: ImapClient, smtp_client: SmtpClient, } impl ImapProvider { pub fn new(account: &EmailAccount) -> Self { Self { imap_client: ImapClient::with_password(account, &account.password), smtp_client: SmtpClient::new(account), } } } #[async_trait] impl EmailProvider for ImapProvider { async fn test_connection(&self) -> Result { self.imap_client.test_connection().await?; self.smtp_client.test_connection().await?; Ok("IMAP and SMTP connection successful".to_string()) } async fn list_folders(&self) -> Result, String> { self.imap_client.list_folders().await } async fn sync_emails( &self, since: Option>, limit: u32, archive_folder: &str, ) -> Result { let mut debug_parts = Vec::new(); // Sync inbox let (inbox_emails, inbox_debug) = self .imap_client .fetch_emails_from_folder_debug("INBOX", since) .await?; debug_parts.push(format!("INBOX: {}", inbox_debug)); // Sync archive let archive_result = self .imap_client .fetch_emails_from_folder_debug(archive_folder, since) .await; let archive_emails = match archive_result { Ok((emails, debug)) => { debug_parts.push(format!("Archive: {}", debug)); emails } Err(e) => { debug_parts.push(format!("Archive error: {}", e)); Vec::new() } }; Ok(ProviderSyncResult { inbox_emails: inbox_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(), archive_emails: archive_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(), debug_info: Some(debug_parts.join(" | ")), }) } async fn send_email( &self, to: &str, subject: &str, body: &str, ) -> Result { use crate::email::smtp_client::SendParams; self.smtp_client.send_message(&SendParams { to, cc: None, bcc: None, subject, body, in_reply_to: None, references: None, attachments: Vec::new(), }).await } async fn archive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> { let uid: u32 = email_id .parse() .map_err(|_| "Invalid email ID".to_string())?; self.imap_client.archive_message(uid, archive_folder).await } async fn unarchive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> { let uid: u32 = email_id .parse() .map_err(|_| "Invalid email ID".to_string())?; self.imap_client.unarchive_message(uid, archive_folder).await } } /// JMAP provider implementation. pub struct JmapProvider { client: JmapClient, } impl JmapProvider { pub fn new(session_url: &str, access_token: &str) -> Result { Ok(Self { client: JmapClient::new(session_url, access_token)?, }) } /// Creates a JMAP provider from an email account. pub fn from_account(account: &EmailAccount) -> Result { let session_url = account .jmap_session_url .as_ref() .ok_or_else(|| "No JMAP session URL configured".to_string())?; let access_token = account .oauth2_access_token .as_ref() .ok_or_else(|| "No access token available".to_string())?; Self::new(session_url, access_token) } /// Updates the access token (after refresh). pub fn update_token(&mut self, access_token: &str) { self.client.update_token(access_token); } } #[async_trait] impl EmailProvider for JmapProvider { async fn test_connection(&self) -> Result { // JMAP test_connection is handled specially in commands/email.rs // via test_jmap_account which has direct access to the account Err("JMAP test_connection requires mutable access - use session discovery directly".to_string()) } async fn list_folders(&self) -> Result, String> { Err("JMAP list_folders requires mutable access - use mailbox listing directly".to_string()) } async fn sync_emails( &self, _since: Option>, _limit: u32, _archive_folder: &str, ) -> Result { Err("JMAP sync requires mutable access - use JmapClient directly".to_string()) } async fn send_email( &self, _to: &str, _subject: &str, _body: &str, ) -> Result { Err("JMAP send requires mutable access - use JmapClient directly".to_string()) } async fn archive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> { Err("JMAP archive requires mutable access - use JmapClient directly".to_string()) } async fn unarchive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> { Err("JMAP unarchive requires mutable access - use JmapClient directly".to_string()) } } /// Creates the appropriate provider for an email account. pub fn create_provider(account: &EmailAccount) -> Result, String> { if account.auth_type.uses_jmap() { Ok(Box::new(JmapProvider::from_account(account)?)) } else { Ok(Box::new(ImapProvider::new(account))) } } /// Determines if an account uses JMAP (and should use JmapClient directly). pub fn uses_jmap(account: &EmailAccount) -> bool { account.auth_type.uses_jmap() }