protonmail_client/
client.rs1use std::marker::PhantomData;
55
56use crate::config::ImapConfig;
57use crate::connection::{self, ImapSession};
58use crate::error::{Error, Result};
59use crate::flag::Flag;
60use crate::folder::Folder;
61use chrono::NaiveDate;
62use email_extract::{Email, parse_email};
63use futures::{StreamExt, pin_mut};
64use tracing::{info, warn};
65
66#[derive(Debug, Clone, Copy)]
70pub struct ReadOnly;
71
72#[derive(Debug, Clone, Copy)]
74pub struct ReadWrite;
75
76pub struct ProtonClient<M = ReadOnly> {
87 config: ImapConfig,
88 _mode: PhantomData<M>,
89}
90
91impl<M> ProtonClient<M> {
92 #[must_use]
93 pub const fn new(config: ImapConfig) -> Self {
94 Self {
95 config,
96 _mode: PhantomData,
97 }
98 }
99}
100
101impl<M: Send + Sync> ProtonClient<M> {
104 pub async fn list_folders(&self) -> Result<Vec<String>> {
110 let mut session = connection::connect(&self.config).await?;
111
112 let mut folder_stream = session
113 .list(Some(""), Some("*"))
114 .await
115 .map_err(|e| Error::Imap(format!("List folders failed: {e}")))?;
116
117 let mut names = Vec::new();
118 while let Some(item) = folder_stream.next().await {
119 if let Ok(name) = item {
120 names.push(name.name().to_string());
121 }
122 }
123 drop(folder_stream);
124
125 session.logout().await.ok();
126 Ok(names)
127 }
128
129 pub async fn fetch_uid(&self, folder: &Folder, uid: u32) -> Result<Email> {
136 let mut session = connection::connect(&self.config).await?;
137 connection::select(&mut session, folder.as_str()).await?;
138
139 let email = Self::fetch_single(&mut session, uid).await?;
140
141 session.logout().await.ok();
142 Ok(email)
143 }
144
145 pub async fn fetch_unseen(&self, folder: &Folder) -> Result<Vec<Email>> {
151 self.search(folder, "UNSEEN").await
152 }
153
154 pub async fn fetch_all(&self, folder: &Folder) -> Result<Vec<Email>> {
160 self.search(folder, "ALL").await
161 }
162
163 pub async fn fetch_last_n(&self, folder: &Folder, n: usize) -> Result<Vec<Email>> {
170 let mut session = connection::connect(&self.config).await?;
171 connection::select(&mut session, folder.as_str()).await?;
172
173 let uids = session
174 .uid_search("ALL")
175 .await
176 .map_err(|e| Error::Imap(format!("Search failed: {e}")))?;
177
178 let mut uid_list: Vec<u32> = uids.into_iter().collect();
179 uid_list.sort_unstable();
180
181 let start = uid_list.len().saturating_sub(n);
182 let recent_uids = &uid_list[start..];
183
184 if recent_uids.is_empty() {
185 session.logout().await.ok();
186 return Ok(vec![]);
187 }
188
189 info!("Fetching {} most recent messages", recent_uids.len());
190
191 let mut emails = Self::fetch_by_uids(&mut session, recent_uids).await?;
192 emails.sort_by(|a, b| b.date.cmp(&a.date));
193
194 session.logout().await.ok();
195 Ok(emails)
196 }
197
198 pub async fn fetch_date_range(
206 &self,
207 folder: &Folder,
208 since: NaiveDate,
209 before: NaiveDate,
210 ) -> Result<Vec<Email>> {
211 let since_str = since.format("%-d-%b-%Y").to_string();
212 let before_str = before.format("%-d-%b-%Y").to_string();
213 let query = format!("SINCE {since_str} BEFORE {before_str}");
214
215 let mut emails = self.search(folder, &query).await?;
216 emails.sort_by(|a, b| b.date.cmp(&a.date));
217 Ok(emails)
218 }
219
220 pub async fn search(&self, folder: &Folder, query: &str) -> Result<Vec<Email>> {
226 let mut session = connection::connect(&self.config).await?;
227 connection::select(&mut session, folder.as_str()).await?;
228
229 let uids = session
230 .uid_search(query)
231 .await
232 .map_err(|e| Error::Imap(format!("Search failed: {e}")))?;
233
234 let uid_list: Vec<u32> = uids.into_iter().collect();
235 if uid_list.is_empty() {
236 session.logout().await.ok();
237 return Ok(vec![]);
238 }
239
240 info!("Found {} messages matching '{}'", uid_list.len(), query);
241
242 let emails = Self::fetch_by_uids(&mut session, &uid_list).await?;
243
244 session.logout().await.ok();
245 Ok(emails)
246 }
247
248 async fn fetch_by_uids(session: &mut ImapSession, uids: &[u32]) -> Result<Vec<Email>> {
251 let mut emails = Vec::new();
252
253 for uid in uids {
254 match Self::fetch_single(session, *uid).await {
255 Ok(email) => emails.push(email),
256 Err(e) => {
257 warn!("Failed to fetch UID {}: {}", uid, e);
258 }
259 }
260 }
261
262 Ok(emails)
263 }
264
265 async fn fetch_single(session: &mut ImapSession, uid: u32) -> Result<Email> {
266 let uid_set = format!("{uid}");
267 let mut messages = session
268 .uid_fetch(&uid_set, "(BODY.PEEK[])")
269 .await
270 .map_err(|e| Error::Imap(format!("Fetch failed: {e}")))?;
271
272 if let Some(msg_result) = messages.next().await {
273 let msg = msg_result.map_err(|e| Error::Imap(format!("Fetch error: {e}")))?;
274 if let Some(body) = msg.body() {
275 return parse_email(uid, body).map_err(|e| Error::Parse(e.to_string()));
276 }
277 }
278
279 Err(Error::Imap(format!("No body found for UID {uid}")))
280 }
281}
282
283impl ProtonClient<ReadWrite> {
286 pub async fn move_to_folder(&self, uid: u32, from: &Folder, to: &Folder) -> Result<()> {
295 let mut session = connection::connect(&self.config).await?;
296 connection::select(&mut session, from.as_str()).await?;
297
298 let uid_set = format!("{uid}");
299
300 session
302 .uid_copy(&uid_set, to.as_str())
303 .await
304 .map_err(|e| Error::Imap(format!("Copy failed: {e}")))?;
305
306 let mut store_stream = session
308 .uid_store(&uid_set, "+FLAGS (\\Deleted)")
309 .await
310 .map_err(|e| Error::Imap(format!("Store +Deleted failed: {e}")))?;
311 while store_stream.next().await.is_some() {}
312 drop(store_stream);
313
314 {
316 let expunge_stream = session
317 .expunge()
318 .await
319 .map_err(|e| Error::Imap(format!("Expunge failed: {e}")))?;
320 pin_mut!(expunge_stream);
321 while expunge_stream.next().await.is_some() {}
322 }
323
324 session.logout().await.ok();
325 Ok(())
326 }
327
328 pub async fn add_flag(&self, uid: u32, folder: &Folder, flag: &Flag) -> Result<()> {
334 let mut session = connection::connect(&self.config).await?;
335 connection::select(&mut session, folder.as_str()).await?;
336
337 let uid_set = format!("{uid}");
338 let store_arg = format!("+FLAGS ({})", flag.as_imap_str());
339
340 let mut stream = session
341 .uid_store(&uid_set, &store_arg)
342 .await
343 .map_err(|e| Error::Imap(format!("Store failed: {e}")))?;
344 while stream.next().await.is_some() {}
345 drop(stream);
346
347 session.logout().await.ok();
348 Ok(())
349 }
350
351 pub async fn remove_flag(&self, uid: u32, folder: &Folder, flag: &Flag) -> Result<()> {
357 let mut session = connection::connect(&self.config).await?;
358 connection::select(&mut session, folder.as_str()).await?;
359
360 let uid_set = format!("{uid}");
361 let store_arg = format!("-FLAGS ({})", flag.as_imap_str());
362
363 let mut stream = session
364 .uid_store(&uid_set, &store_arg)
365 .await
366 .map_err(|e| Error::Imap(format!("Store failed: {e}")))?;
367 while stream.next().await.is_some() {}
368 drop(stream);
369
370 session.logout().await.ok();
371 Ok(())
372 }
373
374 pub async fn archive(&self, uid: u32, from: &Folder) -> Result<()> {
380 self.move_to_folder(uid, from, &Folder::Archive).await
381 }
382
383 pub async fn unmark_all_read(&self, folder: &Folder) -> Result<()> {
390 let mut session = connection::connect(&self.config).await?;
391 connection::select(&mut session, folder.as_str()).await?;
392
393 let uids = session
394 .uid_search("SEEN")
395 .await
396 .map_err(|e| Error::Imap(format!("Search failed: {e}")))?;
397
398 let uid_list: Vec<u32> = uids.into_iter().collect();
399 if uid_list.is_empty() {
400 session.logout().await.ok();
401 return Ok(());
402 }
403
404 let uid_set = uid_list
405 .iter()
406 .map(ToString::to_string)
407 .collect::<Vec<_>>()
408 .join(",");
409
410 let mut stream = session
411 .uid_store(&uid_set, "-FLAGS (\\Seen)")
412 .await
413 .map_err(|e| Error::Imap(format!("Store failed: {e}")))?;
414 while stream.next().await.is_some() {}
415 drop(stream);
416
417 session.logout().await.ok();
418 Ok(())
419 }
420}