Skip to main content

max / goingson

9.4 KB · 305 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 Self {
135 imap_client: ImapClient::with_password(account, &account.password),
136 smtp_client: SmtpClient::new(account),
137 }
138 }
139 }
140
141 #[async_trait]
142 impl EmailProvider for ImapProvider {
143 async fn test_connection(&self) -> Result<String, String> {
144 self.imap_client.test_connection().await?;
145 self.smtp_client.test_connection().await?;
146 Ok("IMAP and SMTP connection successful".to_string())
147 }
148
149 async fn list_folders(&self) -> Result<Vec<String>, String> {
150 self.imap_client.list_folders().await
151 }
152
153 async fn sync_emails(
154 &self,
155 since: Option<DateTime<Utc>>,
156 limit: u32,
157 archive_folder: &str,
158 ) -> Result<ProviderSyncResult, String> {
159 let mut debug_parts = Vec::new();
160
161 // Sync inbox
162 let (inbox_emails, inbox_debug) = self
163 .imap_client
164 .fetch_emails_from_folder_debug("INBOX", since)
165 .await?;
166 debug_parts.push(format!("INBOX: {}", inbox_debug));
167
168 // Sync archive
169 let archive_result = self
170 .imap_client
171 .fetch_emails_from_folder_debug(archive_folder, since)
172 .await;
173
174 let archive_emails = match archive_result {
175 Ok((emails, debug)) => {
176 debug_parts.push(format!("Archive: {}", debug));
177 emails
178 }
179 Err(e) => {
180 debug_parts.push(format!("Archive error: {}", e));
181 Vec::new()
182 }
183 };
184
185 Ok(ProviderSyncResult {
186 inbox_emails: inbox_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(),
187 archive_emails: archive_emails.into_iter().take(limit as usize).map(UnifiedEmail::from).collect(),
188 debug_info: Some(debug_parts.join(" | ")),
189 })
190 }
191
192 async fn send_email(
193 &self,
194 to: &str,
195 subject: &str,
196 body: &str,
197 ) -> Result<String, String> {
198 use crate::email::smtp_client::SendParams;
199 self.smtp_client.send_message(&SendParams {
200 to, cc: None, bcc: None, subject, body,
201 in_reply_to: None, references: None,
202 attachments: Vec::new(),
203 }).await
204 }
205
206 async fn archive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> {
207 let uid: u32 = email_id
208 .parse()
209 .map_err(|_| "Invalid email ID".to_string())?;
210 self.imap_client.archive_message(uid, archive_folder).await
211 }
212
213 async fn unarchive_email(&self, email_id: &str, archive_folder: &str) -> Result<(), String> {
214 let uid: u32 = email_id
215 .parse()
216 .map_err(|_| "Invalid email ID".to_string())?;
217 self.imap_client.unarchive_message(uid, archive_folder).await
218 }
219 }
220
221 /// JMAP provider implementation.
222 pub struct JmapProvider {
223 client: JmapClient,
224 }
225
226 impl JmapProvider {
227 pub fn new(session_url: &str, access_token: &str) -> Result<Self, String> {
228 Ok(Self {
229 client: JmapClient::new(session_url, access_token)?,
230 })
231 }
232
233 /// Creates a JMAP provider from an email account.
234 pub fn from_account(account: &EmailAccount) -> Result<Self, String> {
235 let session_url = account
236 .jmap_session_url
237 .as_ref()
238 .ok_or_else(|| "No JMAP session URL configured".to_string())?;
239 let access_token = account
240 .oauth2_access_token
241 .as_ref()
242 .ok_or_else(|| "No access token available".to_string())?;
243
244 Self::new(session_url, access_token)
245 }
246
247 /// Updates the access token (after refresh).
248 pub fn update_token(&mut self, access_token: &str) {
249 self.client.update_token(access_token);
250 }
251 }
252
253 #[async_trait]
254 impl EmailProvider for JmapProvider {
255 async fn test_connection(&self) -> Result<String, String> {
256 // JMAP test_connection is handled specially in commands/email.rs
257 // via test_jmap_account which has direct access to the account
258 Err("JMAP test_connection requires mutable access - use session discovery directly".to_string())
259 }
260
261 async fn list_folders(&self) -> Result<Vec<String>, String> {
262 Err("JMAP list_folders requires mutable access - use mailbox listing directly".to_string())
263 }
264
265 async fn sync_emails(
266 &self,
267 _since: Option<DateTime<Utc>>,
268 _limit: u32,
269 _archive_folder: &str,
270 ) -> Result<ProviderSyncResult, String> {
271 Err("JMAP sync requires mutable access - use JmapClient directly".to_string())
272 }
273
274 async fn send_email(
275 &self,
276 _to: &str,
277 _subject: &str,
278 _body: &str,
279 ) -> Result<String, String> {
280 Err("JMAP send requires mutable access - use JmapClient directly".to_string())
281 }
282
283 async fn archive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> {
284 Err("JMAP archive requires mutable access - use JmapClient directly".to_string())
285 }
286
287 async fn unarchive_email(&self, _email_id: &str, _archive_folder: &str) -> Result<(), String> {
288 Err("JMAP unarchive requires mutable access - use JmapClient directly".to_string())
289 }
290 }
291
292 /// Creates the appropriate provider for an email account.
293 pub fn create_provider(account: &EmailAccount) -> Result<Box<dyn EmailProvider>, String> {
294 if account.auth_type.uses_jmap() {
295 Ok(Box::new(JmapProvider::from_account(account)?))
296 } else {
297 Ok(Box::new(ImapProvider::new(account)))
298 }
299 }
300
301 /// Determines if an account uses JMAP (and should use JmapClient directly).
302 pub fn uses_jmap(account: &EmailAccount) -> bool {
303 account.auth_type.uses_jmap()
304 }
305