Skip to main content

max / goingson

9.7 KB · 309 lines History Blame Raw
1 //! Email provider abstraction for IMAP vs JMAP.
2 //!
3 //! Provides a unified interface for email operations regardless of
4 //! the underlying protocol (IMAP/SMTP or JMAP).
5
6 use async_trait::async_trait;
7 use chrono::{DateTime, Utc};
8 use goingson_core::EmailAccount;
9
10 use super::imap_client::{ImapClient, ParsedEmail};
11 use super::smtp_client::SmtpClient;
12 use crate::jmap::{email::JmapParsedEmail, JmapClient};
13
14 /// Unified parsed email structure from any provider.
15 #[derive(Debug, Clone)]
16 pub struct UnifiedEmail {
17 /// Provider-specific ID (IMAP UID as string, or JMAP ID)
18 pub provider_id: String,
19 /// Message-ID header
20 pub message_id: Option<String>,
21 /// In-Reply-To header
22 pub in_reply_to: Option<String>,
23 /// Source folder name
24 pub source_folder: String,
25 /// From address
26 pub from: String,
27 /// To address
28 pub to: String,
29 /// Subject
30 pub subject: String,
31 /// Body text
32 pub body: String,
33 /// Received date
34 pub date: DateTime<Utc>,
35 /// Whether email is read (JMAP only, IMAP doesn't fetch flags)
36 pub is_read: bool,
37 }
38
39 impl From<ParsedEmail> for UnifiedEmail {
40 fn from(email: ParsedEmail) -> Self {
41 Self {
42 provider_id: email.imap_uid.to_string(),
43 message_id: email.message_id,
44 in_reply_to: email.in_reply_to,
45 source_folder: email.source_folder,
46 from: email.from,
47 to: email.to,
48 subject: email.subject,
49 body: email.body,
50 date: email.date,
51 is_read: email.is_read,
52 }
53 }
54 }
55
56 impl From<JmapParsedEmail> for UnifiedEmail {
57 fn from(email: JmapParsedEmail) -> Self {
58 Self {
59 provider_id: email.jmap_id,
60 message_id: email.message_id,
61 in_reply_to: email.in_reply_to,
62 source_folder: email.source_folder,
63 from: email.from,
64 to: email.to,
65 subject: email.subject,
66 body: email.body,
67 date: email.date,
68 is_read: email.is_read,
69 }
70 }
71 }
72
73 /// Sync result from a provider.
74 #[derive(Debug, Clone)]
75 pub struct ProviderSyncResult {
76 /// Emails from inbox
77 pub inbox_emails: Vec<UnifiedEmail>,
78 /// Emails from archive
79 pub archive_emails: Vec<UnifiedEmail>,
80 /// Debug info
81 pub debug_info: Option<String>,
82 }
83
84 /// Trait for email providers.
85 #[async_trait]
86 pub trait EmailProvider: Send + Sync {
87 /// Tests the connection to the provider.
88 async fn test_connection(&self) -> Result<String, String>;
89
90 /// Lists available folders/mailboxes.
91 async fn list_folders(&self) -> Result<Vec<String>, String>;
92
93 /// Fetches emails for sync.
94 async fn sync_emails(
95 &self,
96 since: Option<DateTime<Utc>>,
97 limit: u32,
98 archive_folder: &str,
99 ) -> Result<ProviderSyncResult, String>;
100
101 /// Sends an email.
102 async fn send_email(
103 &self,
104 to: &str,
105 subject: &str,
106 body: &str,
107 ) -> Result<String, String>;
108
109 /// Archives an email (moves from inbox to archive).
110 async fn archive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String>;
111
112 /// Unarchives an email (moves from archive to inbox).
113 async fn unarchive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String>;
114
115 /// Marks an email as read (JMAP only, no-op for IMAP).
116 async fn mark_read(&self, _email_id: &str) -> Result<(), String> {
117 Ok(()) // Default no-op
118 }
119
120 /// Marks an email as unread (JMAP only, no-op for IMAP).
121 async fn mark_unread(&self, _email_id: &str) -> Result<(), String> {
122 Ok(()) // Default no-op
123 }
124 }
125
126 /// IMAP/SMTP provider implementation.
127 pub struct ImapProvider {
128 imap_client: ImapClient,
129 smtp_client: SmtpClient,
130 }
131
132 impl ImapProvider {
133 pub fn new(account: &EmailAccount) -> Self {
134 // Prefer the keychain; fall back to the legacy plaintext column only
135 // for accounts created before credentials moved to secure storage.
136 let password = crate::oauth::CredentialStore::get_password(account.id.into())
137 .unwrap_or_else(|| account.password.clone());
138 Self {
139 imap_client: ImapClient::with_password(account, &password),
140 smtp_client: SmtpClient::with_password(account, &password),
141 }
142 }
143 }
144
145 #[async_trait]
146 impl EmailProvider for ImapProvider {
147 async fn test_connection(&self) -> Result<String, String> {
148 self.imap_client.test_connection().await?;
149 self.smtp_client.test_connection().await?;
150 Ok("IMAP and SMTP connection successful".to_string())
151 }
152
153 async fn list_folders(&self) -> Result<Vec<String>, String> {
154 self.imap_client.list_folders().await
155 }
156
157 async fn sync_emails(
158 &self,
159 since: Option<DateTime<Utc>>,
160 limit: u32,
161 archive_folder: &str,
162 ) -> Result<ProviderSyncResult, String> {
163 let mut debug_parts = Vec::new();
164
165 // Sync inbox
166 let (inbox_emails, inbox_debug) = self
167 .imap_client
168 .fetch_emails_from_folder_debug("INBOX", since)
169 .await?;
170 debug_parts.push(format!("INBOX: {}", inbox_debug));
171
172 // Sync archive
173 let archive_result = self
174 .imap_client
175 .fetch_emails_from_folder_debug(archive_folder, since)
176 .await;
177
178 let archive_emails = match archive_result {
179 Ok((emails, debug)) => {
180 debug_parts.push(format!("Archive: {}", debug));
181 emails
182 }
183 Err(e) => {
184 debug_parts.push(format!("Archive error: {}", e));
185 Vec::new()
186 }
187 };
188
189 Ok(ProviderSyncResult {
190 inbox_emails: inbox_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(),
191 archive_emails: archive_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(),
192 debug_info: Some(debug_parts.join(" | ")),
193 })
194 }
195
196 async fn send_email(
197 &self,
198 to: &str,
199 subject: &str,
200 body: &str,
201 ) -> Result<String, String> {
202 use crate::email::smtp_client::SendParams;
203 self.smtp_client.send_message(&SendParams {
204 to, cc: None, bcc: None, subject, body,
205 in_reply_to: None, references: None,
206 attachments: Vec::new(),
207 }).await
208 }
209
210 async fn archive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> {
211 let uid: u32 = email_id
212 .parse()
213 .map_err(|_| "Invalid email ID".to_string())?;
214 self.imap_client.archive_message(uid, archive_folder).await
215 }
216
217 async fn unarchive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> {
218 let uid: u32 = email_id
219 .parse()
220 .map_err(|_| "Invalid email ID".to_string())?;
221 self.imap_client.unarchive_message(uid, archive_folder).await
222 }
223 }
224
225 /// JMAP provider implementation.
226 pub struct JmapProvider {
227 client: JmapClient,
228 }
229
230 impl JmapProvider {
231 pub fn new(session_url: &str, access_token: &str) -> Result<Self, String> {
232 Ok(Self {
233 client: JmapClient::new(session_url, access_token)?,
234 })
235 }
236
237 /// Creates a JMAP provider from an email account.
238 pub fn from_account(account: &EmailAccount) -> Result<Self, String> {
239 let session_url = account
240 .jmap_session_url
241 .as_ref()
242 .ok_or_else(|| "No JMAP session URL configured".to_string())?;
243 let access_token = account
244 .oauth2_access_token
245 .as_ref()
246 .ok_or_else(|| "No access token available".to_string())?;
247
248 Self::new(session_url, access_token)
249 }
250
251 /// Updates the access token (after refresh).
252 pub fn update_token(&mut self, access_token: &str) {
253 self.client.update_token(access_token);
254 }
255 }
256
257 #[async_trait]
258 impl EmailProvider for JmapProvider {
259 async fn test_connection(&self) -> Result<String, String> {
260 // JMAP test_connection is handled specially in commands/email.rs
261 // via test_jmap_account which has direct access to the account
262 Err("JMAP test_connection requires mutable access - use session discovery directly".to_string())
263 }
264
265 async fn list_folders(&self) -> Result<Vec<String>, String> {
266 Err("JMAP list_folders requires mutable access - use mailbox listing directly".to_string())
267 }
268
269 async fn sync_emails(
270 &self,
271 _since: Option<DateTime<Utc>>,
272 _limit: u32,
273 _archive_folder: &str,
274 ) -> Result<ProviderSyncResult, String> {
275 Err("JMAP sync requires mutable access - use JmapClient directly".to_string())
276 }
277
278 async fn send_email(
279 &self,
280 _to: &str,
281 _subject: &str,
282 _body: &str,
283 ) -> Result<String, String> {
284 Err("JMAP send requires mutable access - use JmapClient directly".to_string())
285 }
286
287 async fn archive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> {
288 Err("JMAP archive requires mutable access - use JmapClient directly".to_string())
289 }
290
291 async fn unarchive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> {
292 Err("JMAP unarchive requires mutable access - use JmapClient directly".to_string())
293 }
294 }
295
296 /// Creates the appropriate provider for an email account.
297 pub fn create_provider(account: &EmailAccount) -> Result<Box<dyn EmailProvider>, String> {
298 if account.auth_type.uses_jmap() {
299 Ok(Box::new(JmapProvider::from_account(account)?))
300 } else {
301 Ok(Box::new(ImapProvider::new(account)))
302 }
303 }
304
305 /// Determines if an account uses JMAP (and should use JmapClient directly).
306 pub fn uses_jmap(account: &EmailAccount) -> bool {
307 account.auth_type.uses_jmap()
308 }
309