1use std::{
7 cmp::max,
8 cmp::min,
9 collections::{BTreeMap, BTreeSet, HashMap},
10 iter::Peekable,
11 mem::take,
12 sync::atomic::Ordering,
13 time::{Duration, UNIX_EPOCH},
14};
15
16use anyhow::{Context as _, Result, bail, ensure, format_err};
17use async_channel::{self, Receiver, Sender};
18use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
19use deltachat_contact_tools::ContactAddress;
20use futures::{FutureExt as _, TryStreamExt};
21use futures_lite::FutureExt;
22use num_traits::FromPrimitive;
23use ratelimit::Ratelimit;
24use url::Url;
25
26use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
27use crate::chat::{self, ChatId, ChatIdBlocked};
28use crate::chatlist_events;
29use crate::config::Config;
30use crate::constants::{self, Blocked, Chattype, ShowEmails};
31use crate::contact::{Contact, ContactId, Modifier, Origin};
32use crate::context::Context;
33use crate::events::EventType;
34use crate::headerdef::{HeaderDef, HeaderDefMap};
35use crate::log::{LogExt, error, info, warn};
36use crate::login_param::{
37 ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
38};
39use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
40use crate::mimeparser;
41use crate::net::proxy::ProxyConfig;
42use crate::net::session::SessionStream;
43use crate::oauth2::get_oauth2_access_token;
44use crate::push::encrypt_device_token;
45use crate::receive_imf::{
46 ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
47};
48use crate::scheduler::connectivity::ConnectivityStore;
49use crate::stock_str;
50use crate::tools::{self, create_id, duration_to_str, time};
51
52pub(crate) mod capabilities;
53mod client;
54mod idle;
55pub mod scan_folders;
56pub mod select_folder;
57pub(crate) mod session;
58
59use client::{Client, determine_capabilities};
60use mailparse::SingleInfo;
61use session::Session;
62
63pub(crate) const GENERATED_PREFIX: &str = "GEN_";
64
65const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
66 MESSAGE-ID \
67 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
68 )])";
69const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
70const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
71
72#[derive(Debug)]
73pub(crate) struct Imap {
74 pub(crate) idle_interrupt_receiver: Receiver<()>,
75
76 addr: String,
78
79 lp: Vec<ConfiguredServerLoginParam>,
81
82 password: String,
84
85 proxy_config: Option<ProxyConfig>,
87
88 strict_tls: bool,
89
90 oauth2: bool,
91
92 authentication_failed_once: bool,
93
94 pub(crate) connectivity: ConnectivityStore,
95
96 conn_last_try: tools::Time,
97 conn_backoff_ms: u64,
98
99 ratelimit: Ratelimit,
107}
108
109#[derive(Debug)]
110struct OAuth2 {
111 user: String,
112 access_token: String,
113}
114
115#[derive(Debug)]
116pub(crate) struct ServerMetadata {
117 pub comment: Option<String>,
120
121 pub admin: Option<String>,
124
125 pub iroh_relay: Option<Url>,
126
127 pub ice_servers: String,
134
135 pub ice_servers_expiration_timestamp: i64,
138}
139
140impl async_imap::Authenticator for OAuth2 {
141 type Response = String;
142
143 fn process(&mut self, _data: &[u8]) -> Self::Response {
144 format!(
145 "user={}\x01auth=Bearer {}\x01\x01",
146 self.user, self.access_token
147 )
148 }
149}
150
151#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
152pub enum FolderMeaning {
153 Unknown,
154
155 Spam,
157 Inbox,
158 Mvbox,
159 Trash,
160
161 Virtual,
168}
169
170impl FolderMeaning {
171 pub fn to_config(self) -> Option<Config> {
172 match self {
173 FolderMeaning::Unknown => None,
174 FolderMeaning::Spam => None,
175 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
176 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
177 FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
178 FolderMeaning::Virtual => None,
179 }
180 }
181}
182
183struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
184 inner: Peekable<T>,
185}
186
187impl<T, I> From<I> for UidGrouper<T>
188where
189 T: Iterator<Item = (i64, u32, String)>,
190 I: IntoIterator<IntoIter = T>,
191{
192 fn from(inner: I) -> Self {
193 Self {
194 inner: inner.into_iter().peekable(),
195 }
196 }
197}
198
199impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
200 type Item = (String, Vec<i64>, String);
202
203 fn next(&mut self) -> Option<Self::Item> {
204 let (_, _, folder) = self.inner.peek().cloned()?;
205
206 let mut uid_set = String::new();
207 let mut rowid_set = Vec::new();
208
209 while uid_set.len() < 1000 {
210 if let Some((start_rowid, start_uid, _)) = self
212 .inner
213 .next_if(|(_, _, start_folder)| start_folder == &folder)
214 {
215 rowid_set.push(start_rowid);
216 let mut end_uid = start_uid;
217
218 while let Some((next_rowid, next_uid, _)) =
219 self.inner.next_if(|(_, next_uid, next_folder)| {
220 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
221 })
222 {
223 end_uid = next_uid;
224 rowid_set.push(next_rowid);
225 }
226
227 let uid_range = UidRange {
228 start: start_uid,
229 end: end_uid,
230 };
231 if !uid_set.is_empty() {
232 uid_set.push(',');
233 }
234 uid_set.push_str(&uid_range.to_string());
235 } else {
236 break;
237 }
238 }
239
240 Some((folder, rowid_set, uid_set))
241 }
242}
243
244impl Imap {
245 pub fn new(
249 lp: Vec<ConfiguredServerLoginParam>,
250 password: String,
251 proxy_config: Option<ProxyConfig>,
252 addr: &str,
253 strict_tls: bool,
254 oauth2: bool,
255 idle_interrupt_receiver: Receiver<()>,
256 ) -> Self {
257 Imap {
258 idle_interrupt_receiver,
259 addr: addr.to_string(),
260 lp,
261 password,
262 proxy_config,
263 strict_tls,
264 oauth2,
265 authentication_failed_once: false,
266 connectivity: Default::default(),
267 conn_last_try: UNIX_EPOCH,
268 conn_backoff_ms: 0,
269 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
271 }
272 }
273
274 pub async fn new_configured(
276 context: &Context,
277 idle_interrupt_receiver: Receiver<()>,
278 ) -> Result<Self> {
279 let param = ConfiguredLoginParam::load(context)
280 .await?
281 .context("Not configured")?;
282 let proxy_config = ProxyConfig::load(context).await?;
283 let strict_tls = param.strict_tls(proxy_config.is_some());
284 let imap = Self::new(
285 param.imap.clone(),
286 param.imap_password.clone(),
287 proxy_config,
288 ¶m.addr,
289 strict_tls,
290 param.oauth2,
291 idle_interrupt_receiver,
292 );
293 Ok(imap)
294 }
295
296 pub(crate) async fn connect(
302 &mut self,
303 context: &Context,
304 configuring: bool,
305 ) -> Result<Session> {
306 let now = tools::Time::now();
307 let until_can_send = max(
308 min(self.conn_last_try, now)
309 .checked_add(Duration::from_millis(self.conn_backoff_ms))
310 .unwrap_or(now),
311 now,
312 )
313 .duration_since(now)?;
314 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
315 if !ratelimit_duration.is_zero() {
316 warn!(
317 context,
318 "IMAP got rate limited, waiting for {} until can connect.",
319 duration_to_str(ratelimit_duration),
320 );
321 let interrupted = async {
322 tokio::time::sleep(ratelimit_duration).await;
323 false
324 }
325 .race(self.idle_interrupt_receiver.recv().map(|_| true))
326 .await;
327 if interrupted {
328 info!(
329 context,
330 "Connecting to IMAP without waiting for ratelimit due to interrupt."
331 );
332 }
333 }
334
335 info!(context, "Connecting to IMAP server.");
336 self.connectivity.set_connecting(context);
337
338 self.conn_last_try = tools::Time::now();
339 const BACKOFF_MIN_MS: u64 = 2000;
340 const BACKOFF_MAX_MS: u64 = 80_000;
341 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
342 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
343 (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
344 ));
345 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
346
347 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
348 let mut first_error = None;
349 for lp in login_params {
350 info!(context, "IMAP trying to connect to {}.", &lp.connection);
351 let connection_candidate = lp.connection.clone();
352 let client = match Client::connect(
353 context,
354 self.proxy_config.clone(),
355 self.strict_tls,
356 connection_candidate,
357 )
358 .await
359 .context("IMAP failed to connect")
360 {
361 Ok(client) => client,
362 Err(err) => {
363 warn!(context, "{err:#}.");
364 first_error.get_or_insert(err);
365 continue;
366 }
367 };
368
369 self.conn_backoff_ms = BACKOFF_MIN_MS;
370 self.ratelimit.send();
371
372 let imap_user: &str = lp.user.as_ref();
373 let imap_pw: &str = &self.password;
374
375 let login_res = if self.oauth2 {
376 info!(context, "Logging into IMAP server with OAuth 2.");
377 let addr: &str = self.addr.as_ref();
378
379 let token = get_oauth2_access_token(context, addr, imap_pw, true)
380 .await?
381 .context("IMAP could not get OAUTH token")?;
382 let auth = OAuth2 {
383 user: imap_user.into(),
384 access_token: token,
385 };
386 client.authenticate("XOAUTH2", auth).await
387 } else {
388 info!(context, "Logging into IMAP server with LOGIN.");
389 client.login(imap_user, imap_pw).await
390 };
391
392 match login_res {
393 Ok(mut session) => {
394 let capabilities = determine_capabilities(&mut session).await?;
395
396 let session = if capabilities.can_compress {
397 info!(context, "Enabling IMAP compression.");
398 let compressed_session = session
399 .compress(|s| {
400 let session_stream: Box<dyn SessionStream> = Box::new(s);
401 session_stream
402 })
403 .await
404 .context("Failed to enable IMAP compression")?;
405 Session::new(compressed_session, capabilities)
406 } else {
407 Session::new(session, capabilities)
408 };
409
410 let mut lock = context.server_id.write().await;
412 lock.clone_from(&session.capabilities.server_id);
413
414 self.authentication_failed_once = false;
415 context.emit_event(EventType::ImapConnected(format!(
416 "IMAP-LOGIN as {}",
417 lp.user
418 )));
419 self.connectivity.set_preparing(context);
420 info!(context, "Successfully logged into IMAP server.");
421 return Ok(session);
422 }
423
424 Err(err) => {
425 let imap_user = lp.user.to_owned();
426 let message = stock_str::cannot_login(context, &imap_user).await;
427
428 warn!(context, "IMAP failed to login: {err:#}.");
429 first_error.get_or_insert(format_err!("{message} ({err:#})"));
430
431 let _lock = context.wrong_pw_warning_mutex.lock().await;
433 if err.to_string().to_lowercase().contains("authentication") {
434 if self.authentication_failed_once
435 && !configuring
436 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
437 {
438 let mut msg = Message::new_text(message);
439 if let Err(e) = chat::add_device_msg_with_importance(
440 context,
441 None,
442 Some(&mut msg),
443 true,
444 )
445 .await
446 {
447 warn!(context, "Failed to add device message: {e:#}.");
448 } else {
449 context
450 .set_config_internal(Config::NotifyAboutWrongPw, None)
451 .await
452 .log_err(context)
453 .ok();
454 }
455 } else {
456 self.authentication_failed_once = true;
457 }
458 } else {
459 self.authentication_failed_once = false;
460 }
461 }
462 }
463 }
464
465 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
466 }
467
468 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
473 let configuring = false;
474 let mut session = match self.connect(context, configuring).await {
475 Ok(session) => session,
476 Err(err) => {
477 self.connectivity.set_err(context, &err);
478 return Err(err);
479 }
480 };
481
482 let folders_configured = context
483 .sql
484 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
485 .await?;
486 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
487 let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
488 false => session.is_chatmail(),
489 true => context.get_config_bool(Config::IsChatmail).await?,
490 };
491 let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
492 self.configure_folders(context, &mut session, create_mvbox)
493 .await?;
494 }
495
496 Ok(session)
497 }
498
499 pub async fn fetch_move_delete(
504 &mut self,
505 context: &Context,
506 session: &mut Session,
507 watch_folder: &str,
508 folder_meaning: FolderMeaning,
509 ) -> Result<()> {
510 if !context.sql.is_open().await {
511 bail!("IMAP operation attempted while it is torn down");
513 }
514
515 let msgs_fetched = self
516 .fetch_new_messages(context, session, watch_folder, folder_meaning)
517 .await
518 .context("fetch_new_messages")?;
519 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
520 context.scheduler.interrupt_ephemeral_task().await;
525 }
526
527 session
528 .move_delete_messages(context, watch_folder)
529 .await
530 .context("move_delete_messages")?;
531
532 Ok(())
533 }
534
535 pub(crate) async fn fetch_new_messages(
539 &mut self,
540 context: &Context,
541 session: &mut Session,
542 folder: &str,
543 folder_meaning: FolderMeaning,
544 ) -> Result<bool> {
545 if should_ignore_folder(context, folder, folder_meaning).await? {
546 info!(context, "Not fetching from {folder:?}.");
547 session.new_mail = false;
548 return Ok(false);
549 }
550
551 let create = false;
552 let folder_exists = session
553 .select_with_uidvalidity(context, folder, create)
554 .await
555 .with_context(|| format!("Failed to select folder {folder:?}"))?;
556 if !folder_exists {
557 return Ok(false);
558 }
559
560 if !session.new_mail {
561 info!(context, "No new emails in folder {folder:?}.");
562 return Ok(false);
563 }
564 session.new_mail = false;
565
566 let mut read_cnt = 0;
567 loop {
568 let (n, fetch_more) = self
569 .fetch_new_msg_batch(context, session, folder, folder_meaning)
570 .await?;
571 read_cnt += n;
572 if !fetch_more {
573 return Ok(read_cnt > 0);
574 }
575 }
576 }
577
578 async fn fetch_new_msg_batch(
580 &mut self,
581 context: &Context,
582 session: &mut Session,
583 folder: &str,
584 folder_meaning: FolderMeaning,
585 ) -> Result<(usize, bool)> {
586 let uid_validity = get_uidvalidity(context, folder).await?;
587 let old_uid_next = get_uid_next(context, folder).await?;
588 info!(
589 context,
590 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
591 );
592
593 let uids_to_prefetch = 500;
594 let msgs = session
595 .prefetch(old_uid_next, uids_to_prefetch)
596 .await
597 .context("prefetch")?;
598 let read_cnt = msgs.len();
599
600 let download_limit = context.download_limit().await?;
601 let mut uids_fetch = Vec::<(u32, bool )>::with_capacity(msgs.len() + 1);
602 let mut uid_message_ids = BTreeMap::new();
603 let mut largest_uid_skipped = None;
604 let delete_target = context.get_delete_msgs_target().await?;
605
606 for (uid, ref fetch_response) in msgs {
608 let headers = match get_fetch_headers(fetch_response) {
609 Ok(headers) => headers,
610 Err(err) => {
611 warn!(context, "Failed to parse FETCH headers: {err:#}.");
612 continue;
613 }
614 };
615
616 let message_id = prefetch_get_message_id(&headers);
617
618 let delete = if let Some(message_id) = &message_id {
629 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
630 .await?
631 .is_some_and(|(_msg_id, deleted)| deleted)
632 } else {
633 false
634 };
635
636 let message_id = message_id.unwrap_or_else(create_message_id);
639
640 if delete {
641 info!(context, "Deleting locally deleted message {message_id}.");
642 }
643
644 let _target;
645 let target = if delete {
646 &delete_target
647 } else {
648 _target = target_folder(context, folder, folder_meaning, &headers).await?;
649 &_target
650 };
651
652 context
653 .sql
654 .execute(
655 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
656 VALUES (?1, ?2, ?3, ?4, ?5)
657 ON CONFLICT(folder, uid, uidvalidity)
658 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
659 target=excluded.target",
660 (&message_id, &folder, uid, uid_validity, target),
661 )
662 .await?;
663
664 if folder == target
671 && folder_meaning != FolderMeaning::Spam
676 && prefetch_should_download(
677 context,
678 &headers,
679 &message_id,
680 fetch_response.flags(),
681 )
682 .await.context("prefetch_should_download")?
683 {
684 match download_limit {
685 Some(download_limit) => uids_fetch.push((
686 uid,
687 fetch_response.size.unwrap_or_default() > download_limit,
688 )),
689 None => uids_fetch.push((uid, false)),
690 }
691 uid_message_ids.insert(uid, message_id);
692 } else {
693 largest_uid_skipped = Some(uid);
694 }
695 }
696
697 if !uids_fetch.is_empty() {
698 self.connectivity.set_working(context);
699 }
700
701 let (sender, receiver) = async_channel::unbounded();
702
703 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
704 let mailbox_uid_next = session
705 .selected_mailbox
706 .as_ref()
707 .with_context(|| format!("Expected {folder:?} to be selected"))?
708 .uid_next
709 .unwrap_or_default();
710
711 let update_uids_future = async {
712 let mut largest_uid_fetched: u32 = 0;
713
714 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
715 largest_uid_fetched = max(largest_uid_fetched, uid);
716 if let Some(received_msg) = received_msg_opt {
717 received_msgs.push(received_msg)
718 }
719 }
720
721 largest_uid_fetched
722 };
723
724 let actually_download_messages_future = async {
725 let sender = sender;
726 let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
727 let mut fetch_partially = false;
728 uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
729 for (uid, fp) in uids_fetch {
730 if fp != fetch_partially {
731 session
732 .fetch_many_msgs(
733 context,
734 folder,
735 uids_fetch_in_batch.split_off(0),
736 &uid_message_ids,
737 fetch_partially,
738 sender.clone(),
739 )
740 .await
741 .context("fetch_many_msgs")?;
742 fetch_partially = fp;
743 }
744 uids_fetch_in_batch.push(uid);
745 }
746
747 anyhow::Ok(())
748 };
749
750 let (largest_uid_fetched, fetch_res) =
751 tokio::join!(update_uids_future, actually_download_messages_future);
752
753 let mut new_uid_next = largest_uid_fetched + 1;
759 let fetch_more = fetch_res.is_ok() && {
760 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
761 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
765
766 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
767
768 prefetch_uid_next < mailbox_uid_next
769 };
770 if new_uid_next > old_uid_next {
771 set_uid_next(context, folder, new_uid_next).await?;
772 }
773
774 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
775
776 if !received_msgs.is_empty() {
777 context.emit_event(EventType::IncomingMsgBunch);
778 }
779
780 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
781
782 fetch_res?;
785
786 Ok((read_cnt, fetch_more))
787 }
788
789 pub(crate) async fn fetch_existing_msgs(
795 &mut self,
796 context: &Context,
797 session: &mut Session,
798 ) -> Result<()> {
799 add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
800 .await
801 .context("failed to get recipients from the movebox")?;
802 add_all_recipients_as_contacts(context, session, Config::ConfiguredInboxFolder)
803 .await
804 .context("failed to get recipients from the inbox")?;
805
806 info!(context, "Done fetching existing messages.");
807 Ok(())
808 }
809}
810
811impl Session {
812 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
814 let all_folders = self
815 .list_folders()
816 .await
817 .context("listing folders for resync")?;
818 for folder in all_folders {
819 let folder_meaning = get_folder_meaning(&folder);
820 if !matches!(
821 folder_meaning,
822 FolderMeaning::Virtual | FolderMeaning::Unknown
823 ) {
824 self.resync_folder_uids(context, folder.name(), folder_meaning)
825 .await?;
826 }
827 }
828 Ok(())
829 }
830
831 pub(crate) async fn resync_folder_uids(
838 &mut self,
839 context: &Context,
840 folder: &str,
841 folder_meaning: FolderMeaning,
842 ) -> Result<()> {
843 let uid_validity;
844 let mut msgs = BTreeMap::new();
846
847 let create = false;
848 let folder_exists = self
849 .select_with_uidvalidity(context, folder, create)
850 .await?;
851 if folder_exists {
852 let mut list = self
853 .uid_fetch("1:*", RFC724MID_UID)
854 .await
855 .with_context(|| format!("Can't resync folder {folder}"))?;
856 while let Some(fetch) = list.try_next().await? {
857 let headers = match get_fetch_headers(&fetch) {
858 Ok(headers) => headers,
859 Err(err) => {
860 warn!(context, "Failed to parse FETCH headers: {}", err);
861 continue;
862 }
863 };
864 let message_id = prefetch_get_message_id(&headers);
865
866 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
867 msgs.insert(
868 uid,
869 (
870 rfc724_mid,
871 target_folder(context, folder, folder_meaning, &headers).await?,
872 ),
873 );
874 }
875 }
876
877 info!(
878 context,
879 "resync_folder_uids: Collected {} message IDs in {folder}.",
880 msgs.len(),
881 );
882
883 uid_validity = get_uidvalidity(context, folder).await?;
884 } else {
885 warn!(context, "resync_folder_uids: No folder {folder}.");
886 uid_validity = 0;
887 }
888
889 context
891 .sql
892 .transaction(move |transaction| {
893 transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
894 for (uid, (rfc724_mid, target)) in &msgs {
895 transaction.execute(
898 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
899 VALUES (?1, ?2, ?3, ?4, ?5)
900 ON CONFLICT(folder, uid, uidvalidity)
901 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
902 target=excluded.target",
903 (rfc724_mid, folder, uid, uid_validity, target),
904 )?;
905 }
906 Ok(())
907 })
908 .await?;
909 Ok(())
910 }
911
912 async fn delete_message_batch(
915 &mut self,
916 context: &Context,
917 uid_set: &str,
918 row_ids: Vec<i64>,
919 ) -> Result<()> {
920 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
922 .await?;
923 context
924 .sql
925 .transaction(|transaction| {
926 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
927 for row_id in row_ids {
928 stmt.execute((row_id,))?;
929 }
930 Ok(())
931 })
932 .await
933 .context("Cannot remove deleted messages from imap table")?;
934
935 context.emit_event(EventType::ImapMessageDeleted(format!(
936 "IMAP messages {uid_set} marked as deleted"
937 )));
938 Ok(())
939 }
940
941 async fn move_message_batch(
944 &mut self,
945 context: &Context,
946 set: &str,
947 row_ids: Vec<i64>,
948 target: &str,
949 ) -> Result<()> {
950 if self.can_move() {
951 match self.uid_mv(set, &target).await {
952 Ok(()) => {
953 context
955 .sql
956 .transaction(|transaction| {
957 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
958 for row_id in row_ids {
959 stmt.execute((row_id,))?;
960 }
961 Ok(())
962 })
963 .await
964 .context("Cannot delete moved messages from imap table")?;
965 context.emit_event(EventType::ImapMessageMoved(format!(
966 "IMAP messages {set} moved to {target}"
967 )));
968 return Ok(());
969 }
970 Err(err) => {
971 if context.should_delete_to_trash().await? {
972 error!(
973 context,
974 "Cannot move messages {} to {}, no fallback to COPY/DELETE because \
975 delete_to_trash is set. Error: {:#}",
976 set,
977 target,
978 err,
979 );
980 return Err(err.into());
981 }
982 warn!(
983 context,
984 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
985 set,
986 target,
987 err
988 );
989 }
990 }
991 }
992
993 let copy = !context.is_trash(target).await?;
996 if copy {
997 info!(
998 context,
999 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1000 );
1001 self.uid_copy(&set, &target).await?;
1002 } else {
1003 error!(
1004 context,
1005 "Server does not support MOVE, fallback to DELETE {} to {}", set, target,
1006 );
1007 }
1008 context
1009 .sql
1010 .transaction(|transaction| {
1011 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1012 for row_id in row_ids {
1013 stmt.execute((row_id,))?;
1014 }
1015 Ok(())
1016 })
1017 .await
1018 .context("Cannot plan deletion of messages")?;
1019 if copy {
1020 context.emit_event(EventType::ImapMessageMoved(format!(
1021 "IMAP messages {set} copied to {target}"
1022 )));
1023 }
1024 Ok(())
1025 }
1026
1027 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1031 let rows = context
1032 .sql
1033 .query_map_vec(
1034 "SELECT id, uid, target FROM imap
1035 WHERE folder = ?
1036 AND target != folder
1037 ORDER BY target, uid",
1038 (folder,),
1039 |row| {
1040 let rowid: i64 = row.get(0)?;
1041 let uid: u32 = row.get(1)?;
1042 let target: String = row.get(2)?;
1043 Ok((rowid, uid, target))
1044 },
1045 )
1046 .await?;
1047
1048 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1049 let create = false;
1054 let folder_exists = self
1055 .select_with_uidvalidity(context, folder, create)
1056 .await?;
1057 ensure!(folder_exists, "No folder {folder}");
1058
1059 if target.is_empty() {
1061 self.delete_message_batch(context, &uid_set, rowid_set)
1062 .await
1063 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1064 } else {
1065 self.move_message_batch(context, &uid_set, rowid_set, &target)
1066 .await
1067 .with_context(|| {
1068 format!(
1069 "cannot move batch of messages {:?} to folder {:?}",
1070 &uid_set, target
1071 )
1072 })?;
1073 }
1074 }
1075
1076 if let Err(err) = self.maybe_close_folder(context).await {
1079 warn!(context, "Failed to close folder: {err:#}.");
1080 }
1081
1082 Ok(())
1083 }
1084
1085 pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
1087 context.send_sync_msg().await?;
1088 while let Some((id, mime, msg_id, attempts)) = context
1089 .sql
1090 .query_row_optional(
1091 "SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
1092 (),
1093 |row| {
1094 let id: i64 = row.get(0)?;
1095 let mime: String = row.get(1)?;
1096 let msg_id: MsgId = row.get(2)?;
1097 let attempts: i64 = row.get(3)?;
1098 Ok((id, mime, msg_id, attempts))
1099 },
1100 )
1101 .await
1102 .context("Failed to SELECT from imap_send")?
1103 {
1104 let res = self
1105 .append(folder, Some("(\\Seen)"), None, mime)
1106 .await
1107 .with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
1108 .log_err(context);
1109 if res.is_ok() {
1110 msg_id.set_delivered(context).await?;
1111 }
1112 const MAX_ATTEMPTS: i64 = 2;
1113 if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
1114 context
1115 .sql
1116 .execute("DELETE FROM imap_send WHERE id=?", (id,))
1117 .await
1118 .context("Failed to delete from imap_send")?;
1119 } else {
1120 context
1121 .sql
1122 .execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
1123 .await
1124 .context("Failed to update imap_send.attempts")?;
1125 res?;
1126 }
1127 }
1128 Ok(())
1129 }
1130
1131 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1133 let rows = context
1134 .sql
1135 .query_map_vec(
1136 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1137 WHERE imap.id = imap_markseen.id AND target = folder
1138 ORDER BY folder, uid",
1139 [],
1140 |row| {
1141 let rowid: i64 = row.get(0)?;
1142 let uid: u32 = row.get(1)?;
1143 let folder: String = row.get(2)?;
1144 Ok((rowid, uid, folder))
1145 },
1146 )
1147 .await?;
1148
1149 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1150 let create = false;
1151 let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
1152 Err(err) => {
1153 warn!(
1154 context,
1155 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1156 );
1157 continue;
1158 }
1159 Ok(folder_exists) => folder_exists,
1160 };
1161 if !folder_exists {
1162 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1163 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1164 warn!(
1165 context,
1166 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1167 );
1168 continue;
1169 } else {
1170 info!(
1171 context,
1172 "Marked messages {} in folder {} as seen.", uid_set, folder
1173 );
1174 }
1175 context
1176 .sql
1177 .transaction(|transaction| {
1178 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1179 for rowid in rowid_set {
1180 stmt.execute((rowid,))?;
1181 }
1182 Ok(())
1183 })
1184 .await
1185 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1186 }
1187
1188 Ok(())
1189 }
1190
1191 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1193 if !self.can_condstore() {
1194 info!(
1195 context,
1196 "Server does not support CONDSTORE, skipping flag synchronization."
1197 );
1198 return Ok(());
1199 }
1200
1201 let create = false;
1202 let folder_exists = self
1203 .select_with_uidvalidity(context, folder, create)
1204 .await
1205 .context("Failed to select folder")?;
1206 if !folder_exists {
1207 return Ok(());
1208 }
1209
1210 let mailbox = self
1211 .selected_mailbox
1212 .as_ref()
1213 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1214
1215 if mailbox.highest_modseq.is_none() {
1218 info!(
1219 context,
1220 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1221 );
1222 return Ok(());
1223 }
1224
1225 let mut updated_chat_ids = BTreeSet::new();
1226 let uid_validity = get_uidvalidity(context, folder)
1227 .await
1228 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1229 let mut highest_modseq = get_modseq(context, folder)
1230 .await
1231 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1232 let mut list = self
1233 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1234 .await
1235 .context("failed to fetch flags")?;
1236
1237 let mut got_unsolicited_fetch = false;
1238
1239 while let Some(fetch) = list
1240 .try_next()
1241 .await
1242 .context("failed to get FETCH result")?
1243 {
1244 let uid = if let Some(uid) = fetch.uid {
1245 uid
1246 } else {
1247 info!(context, "FETCH result contains no UID, skipping");
1248 got_unsolicited_fetch = true;
1249 continue;
1250 };
1251 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1252 if is_seen {
1253 if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
1254 .await
1255 .with_context(|| {
1256 format!("failed to update seen status for msg {folder}/{uid}")
1257 })?
1258 {
1259 updated_chat_ids.insert(chat_id);
1260 }
1261 }
1262
1263 if let Some(modseq) = fetch.modseq {
1264 if modseq > highest_modseq {
1265 highest_modseq = modseq;
1266 }
1267 } else {
1268 warn!(context, "FETCH result contains no MODSEQ");
1269 }
1270 }
1271 drop(list);
1272
1273 if got_unsolicited_fetch {
1274 self.new_mail = true;
1279 }
1280
1281 set_modseq(context, folder, highest_modseq)
1282 .await
1283 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1284 if !updated_chat_ids.is_empty() {
1285 context.on_archived_chats_maybe_noticed();
1286 }
1287 for updated_chat_id in updated_chat_ids {
1288 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1289 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1290 }
1291
1292 Ok(())
1293 }
1294
1295 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1297 let mut uids: Vec<_> = self
1298 .uid_search(get_imap_self_sent_search_command(context).await?)
1299 .await?
1300 .into_iter()
1301 .collect();
1302 uids.sort_unstable();
1303
1304 let mut result = Vec::new();
1305 for (_, uid_set) in build_sequence_sets(&uids)? {
1306 let mut list = self
1307 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1308 .await
1309 .context("IMAP Could not fetch")?;
1310
1311 while let Some(msg) = list.try_next().await? {
1312 match get_fetch_headers(&msg) {
1313 Ok(headers) => {
1314 if let Some(from) = mimeparser::get_from(&headers) {
1315 if context.is_self_addr(&from.addr).await? {
1316 result.extend(mimeparser::get_recipients(&headers));
1317 }
1318 }
1319 }
1320 Err(err) => {
1321 warn!(context, "{}", err);
1322 continue;
1323 }
1324 };
1325 }
1326 }
1327 Ok(result)
1328 }
1329
1330 pub(crate) async fn fetch_many_msgs(
1345 &mut self,
1346 context: &Context,
1347 folder: &str,
1348 request_uids: Vec<u32>,
1349 uid_message_ids: &BTreeMap<u32, String>,
1350 fetch_partially: bool,
1351 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1352 ) -> Result<()> {
1353 if request_uids.is_empty() {
1354 return Ok(());
1355 }
1356
1357 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1358 info!(
1359 context,
1360 "Starting a {} FETCH of message set \"{}\".",
1361 if fetch_partially { "partial" } else { "full" },
1362 set
1363 );
1364 let mut fetch_responses = self
1365 .uid_fetch(
1366 &set,
1367 if fetch_partially {
1368 BODY_PARTIAL
1369 } else {
1370 BODY_FULL
1371 },
1372 )
1373 .await
1374 .with_context(|| {
1375 format!("fetching messages {} from folder \"{}\"", &set, folder)
1376 })?;
1377
1378 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1381
1382 let mut count = 0;
1383 for &request_uid in &request_uids {
1384 let mut fetch_response = uid_msgs.remove(&request_uid);
1386
1387 while fetch_response.is_none() {
1389 let Some(next_fetch_response) = fetch_responses
1390 .try_next()
1391 .await
1392 .context("Failed to process IMAP FETCH result")?
1393 else {
1394 break;
1396 };
1397
1398 if let Some(next_uid) = next_fetch_response.uid {
1399 if next_uid == request_uid {
1400 fetch_response = Some(next_fetch_response);
1401 } else if !request_uids.contains(&next_uid) {
1402 info!(
1409 context,
1410 "Skipping not requested FETCH response for UID {}.", next_uid
1411 );
1412 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1413 warn!(context, "Got duplicated UID {}.", next_uid);
1414 }
1415 } else {
1416 info!(context, "Skipping FETCH response without UID.");
1417 }
1418 }
1419
1420 let fetch_response = match fetch_response {
1421 Some(fetch) => fetch,
1422 None => {
1423 warn!(
1424 context,
1425 "Missed UID {} in the server response.", request_uid
1426 );
1427 continue;
1428 }
1429 };
1430 count += 1;
1431
1432 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1433 let (body, partial) = if fetch_partially {
1434 (fetch_response.header(), fetch_response.size) } else {
1436 (fetch_response.body(), None) };
1438
1439 if is_deleted {
1440 info!(context, "Not processing deleted msg {}.", request_uid);
1441 received_msgs_channel.send((request_uid, None)).await?;
1442 continue;
1443 }
1444
1445 let body = if let Some(body) = body {
1446 body
1447 } else {
1448 info!(
1449 context,
1450 "Not processing message {} without a BODY.", request_uid
1451 );
1452 received_msgs_channel.send((request_uid, None)).await?;
1453 continue;
1454 };
1455
1456 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1457
1458 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1459 error!(
1460 context,
1461 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1462 request_uid
1463 );
1464 continue;
1465 };
1466
1467 info!(
1468 context,
1469 "Passing message UID {} to receive_imf().", request_uid
1470 );
1471 let res = receive_imf_inner(
1472 context,
1473 rfc724_mid,
1474 body,
1475 is_seen,
1476 partial.map(|msg_size| (msg_size, None)),
1477 )
1478 .await;
1479 let received_msg = if let Err(err) = res {
1480 warn!(context, "receive_imf error: {:#}.", err);
1481 if partial.is_some() {
1482 return Err(err);
1483 }
1484 receive_imf_inner(
1485 context,
1486 rfc724_mid,
1487 body,
1488 is_seen,
1489 Some((body.len().try_into()?, Some(format!("{err:#}")))),
1490 )
1491 .await?
1492 } else {
1493 res?
1494 };
1495 received_msgs_channel
1496 .send((request_uid, received_msg))
1497 .await?;
1498 }
1499
1500 while fetch_responses
1507 .try_next()
1508 .await
1509 .context("Failed to drain FETCH responses")?
1510 .is_some()
1511 {}
1512
1513 if count != request_uids.len() {
1514 warn!(
1515 context,
1516 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1517 count,
1518 request_uids.len(),
1519 request_uids,
1520 );
1521 } else {
1522 info!(
1523 context,
1524 "Successfully received {} UIDs.",
1525 request_uids.len()
1526 );
1527 }
1528 }
1529
1530 Ok(())
1531 }
1532
1533 pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
1539 if !self.can_metadata() {
1540 return Ok(());
1541 }
1542
1543 let mut lock = context.metadata.write().await;
1544 if let Some(ref mut old_metadata) = *lock {
1545 let now = time();
1546
1547 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1549 return Ok(());
1550 }
1551
1552 info!(context, "ICE servers expired, requesting new credentials.");
1553 let mailbox = "";
1554 let options = "";
1555 let metadata = self
1556 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1557 .await?;
1558 let mut got_turn_server = false;
1559 for m in metadata {
1560 if m.entry == "/shared/vendor/deltachat/turn" {
1561 if let Some(value) = m.value {
1562 match create_ice_servers_from_metadata(context, &value).await {
1563 Ok((parsed_timestamp, parsed_ice_servers)) => {
1564 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1565 old_metadata.ice_servers = parsed_ice_servers;
1566 got_turn_server = false;
1567 }
1568 Err(err) => {
1569 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1570 }
1571 }
1572 }
1573 }
1574 }
1575
1576 if !got_turn_server {
1577 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1579 old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
1580 }
1581 return Ok(());
1582 }
1583
1584 info!(
1585 context,
1586 "Server supports metadata, retrieving server comment and admin contact."
1587 );
1588
1589 let mut comment = None;
1590 let mut admin = None;
1591 let mut iroh_relay = None;
1592 let mut ice_servers = None;
1593 let mut ice_servers_expiration_timestamp = 0;
1594
1595 let mailbox = "";
1596 let options = "";
1597 let metadata = self
1598 .get_metadata(
1599 mailbox,
1600 options,
1601 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1602 )
1603 .await?;
1604 for m in metadata {
1605 match m.entry.as_ref() {
1606 "/shared/comment" => {
1607 comment = m.value;
1608 }
1609 "/shared/admin" => {
1610 admin = m.value;
1611 }
1612 "/shared/vendor/deltachat/irohrelay" => {
1613 if let Some(value) = m.value {
1614 if let Ok(url) = Url::parse(&value) {
1615 iroh_relay = Some(url);
1616 } else {
1617 warn!(
1618 context,
1619 "Got invalid URL from iroh relay metadata: {:?}.", value
1620 );
1621 }
1622 }
1623 }
1624 "/shared/vendor/deltachat/turn" => {
1625 if let Some(value) = m.value {
1626 match create_ice_servers_from_metadata(context, &value).await {
1627 Ok((parsed_timestamp, parsed_ice_servers)) => {
1628 ice_servers_expiration_timestamp = parsed_timestamp;
1629 ice_servers = Some(parsed_ice_servers);
1630 }
1631 Err(err) => {
1632 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1633 }
1634 }
1635 }
1636 }
1637 _ => {}
1638 }
1639 }
1640 let ice_servers = if let Some(ice_servers) = ice_servers {
1641 ice_servers
1642 } else {
1643 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1645 create_fallback_ice_servers(context).await?
1646 };
1647
1648 *lock = Some(ServerMetadata {
1649 comment,
1650 admin,
1651 iroh_relay,
1652 ice_servers,
1653 ice_servers_expiration_timestamp,
1654 });
1655 Ok(())
1656 }
1657
1658 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1660 if context.push_subscribed.load(Ordering::Relaxed) {
1661 return Ok(());
1662 }
1663
1664 let Some(device_token) = context.push_subscriber.device_token().await else {
1665 return Ok(());
1666 };
1667
1668 if self.can_metadata() && self.can_push() {
1669 let old_encrypted_device_token =
1670 context.get_config(Config::EncryptedDeviceToken).await?;
1671
1672 let device_token_changed = old_encrypted_device_token.is_none()
1674 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1675
1676 let new_encrypted_device_token;
1677 if device_token_changed {
1678 let encrypted_device_token = encrypt_device_token(&device_token)
1679 .context("Failed to encrypt device token")?;
1680
1681 let encrypted_device_token_len = encrypted_device_token.len();
1685
1686 context
1692 .set_config_internal(Config::DeviceToken, Some(&device_token))
1693 .await?;
1694 context
1695 .set_config_internal(
1696 Config::EncryptedDeviceToken,
1697 Some(&encrypted_device_token),
1698 )
1699 .await?;
1700
1701 if encrypted_device_token_len <= 4096 {
1702 new_encrypted_device_token = Some(encrypted_device_token);
1703 } else {
1704 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1714 new_encrypted_device_token = None;
1715 }
1716 } else {
1717 new_encrypted_device_token = old_encrypted_device_token;
1718 }
1719
1720 if let Some(encrypted_device_token) = new_encrypted_device_token {
1723 let folder = context
1724 .get_config(Config::ConfiguredInboxFolder)
1725 .await?
1726 .context("INBOX is not configured")?;
1727
1728 self.run_command_and_check_ok(&format_setmetadata(
1729 &folder,
1730 &encrypted_device_token,
1731 ))
1732 .await
1733 .context("SETMETADATA command failed")?;
1734
1735 context.push_subscribed.store(true, Ordering::Relaxed);
1736 }
1737 } else if !context.push_subscriber.heartbeat_subscribed().await {
1738 let context = context.clone();
1739 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1741 }
1742
1743 Ok(())
1744 }
1745}
1746
1747fn format_setmetadata(folder: &str, device_token: &str) -> String {
1748 let device_token_len = device_token.len();
1749 format!(
1750 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1751 )
1752}
1753
1754impl Session {
1755 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1761 if flag == "\\Deleted" {
1762 self.selected_folder_needs_expunge = true;
1763 }
1764 let query = format!("+FLAGS ({flag})");
1765 let mut responses = self
1766 .uid_store(uid_set, &query)
1767 .await
1768 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1769 while let Some(_response) = responses.try_next().await? {
1770 }
1772 Ok(())
1773 }
1774
1775 async fn configure_mvbox<'a>(
1784 &mut self,
1785 context: &Context,
1786 folders: &[&'a str],
1787 create_mvbox: bool,
1788 ) -> Result<Option<&'a str>> {
1789 self.maybe_close_folder(context).await?;
1792
1793 for folder in folders {
1794 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1795 let res = self.examine(&folder).await;
1796 if res.is_ok() {
1797 info!(
1798 context,
1799 "MVBOX-folder {:?} successfully selected, using it.", &folder
1800 );
1801 self.close().await?;
1802 let create = false;
1805 let folder_exists = self
1806 .select_with_uidvalidity(context, folder, create)
1807 .await?;
1808 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1809 return Ok(Some(folder));
1810 }
1811 }
1812
1813 if !create_mvbox {
1814 return Ok(None);
1815 }
1816 for folder in folders {
1819 match self
1820 .select_with_uidvalidity(context, folder, create_mvbox)
1821 .await
1822 {
1823 Ok(_) => {
1824 info!(context, "MVBOX-folder {} created.", folder);
1825 return Ok(Some(folder));
1826 }
1827 Err(err) => {
1828 warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1829 }
1830 }
1831 }
1832 Ok(None)
1833 }
1834}
1835
1836impl Imap {
1837 pub(crate) async fn configure_folders(
1838 &mut self,
1839 context: &Context,
1840 session: &mut Session,
1841 create_mvbox: bool,
1842 ) -> Result<()> {
1843 let mut folders = session
1844 .list(Some(""), Some("*"))
1845 .await
1846 .context("list_folders failed")?;
1847 let mut delimiter = ".".to_string();
1848 let mut delimiter_is_default = true;
1849 let mut folder_configs = BTreeMap::new();
1850
1851 while let Some(folder) = folders.try_next().await? {
1852 info!(context, "Scanning folder: {:?}", folder);
1853
1854 if let Some(d) = folder.delimiter() {
1856 if delimiter_is_default && !d.is_empty() && delimiter != d {
1857 delimiter = d.to_string();
1858 delimiter_is_default = false;
1859 }
1860 }
1861
1862 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1863 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1864 if let Some(config) = folder_meaning.to_config() {
1865 folder_configs.insert(config, folder.name().to_string());
1867 } else if let Some(config) = folder_name_meaning.to_config() {
1868 folder_configs
1870 .entry(config)
1871 .or_insert_with(|| folder.name().to_string());
1872 }
1873 }
1874 drop(folders);
1875
1876 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1877
1878 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1879 let mvbox_folder = session
1880 .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1881 .await
1882 .context("failed to configure mvbox")?;
1883
1884 context
1885 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1886 .await?;
1887 if let Some(mvbox_folder) = mvbox_folder {
1888 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1889 context
1890 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1891 .await?;
1892 }
1893 for (config, name) in folder_configs {
1894 context.set_config_internal(config, Some(&name)).await?;
1895 }
1896 context
1897 .sql
1898 .set_raw_config_int(
1899 constants::DC_FOLDERS_CONFIGURED_KEY,
1900 constants::DC_FOLDERS_CONFIGURED_VERSION,
1901 )
1902 .await?;
1903
1904 info!(context, "FINISHED configuring IMAP-folders.");
1905 Ok(())
1906 }
1907}
1908
1909impl Session {
1910 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1919 use UnsolicitedResponse::*;
1920 use async_imap::imap_proto::Response;
1921 use async_imap::imap_proto::ResponseCode;
1922
1923 let folder = self.selected_folder.as_deref().unwrap_or_default();
1924 let mut should_refetch = false;
1925 while let Ok(response) = self.unsolicited_responses.try_recv() {
1926 match response {
1927 Exists(_) => {
1928 info!(
1929 context,
1930 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1931 );
1932 should_refetch = true;
1933 }
1934
1935 Expunge(_) | Recent(_) => {}
1936 Other(ref response_data) => {
1937 match response_data.parsed() {
1938 Response::Fetch { .. } => {
1939 info!(
1940 context,
1941 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1942 );
1943 should_refetch = true;
1944 }
1945
1946 Response::Done {
1949 code: Some(ResponseCode::CopyUid(_, _, _)),
1950 ..
1951 } => {}
1952
1953 _ => {
1954 info!(context, "{folder:?}: got unsolicited response {response:?}")
1955 }
1956 }
1957 }
1958 _ => {
1959 info!(context, "{folder:?}: got unsolicited response {response:?}")
1960 }
1961 }
1962 }
1963 Ok(should_refetch)
1964 }
1965}
1966
1967async fn should_move_out_of_spam(
1968 context: &Context,
1969 headers: &[mailparse::MailHeader<'_>],
1970) -> Result<bool> {
1971 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1972 return Ok(true);
1983 }
1984
1985 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1986 if msg.chat_blocked != Blocked::Not {
1987 return Ok(false);
1989 }
1990 } else {
1991 let from = match mimeparser::get_from(headers) {
1992 Some(f) => f,
1993 None => return Ok(false),
1994 };
1995 let (from_id, blocked_contact, _origin) =
1997 match from_field_to_contact_id(context, &from, None, true, true)
1998 .await
1999 .context("from_field_to_contact_id")?
2000 {
2001 Some(res) => res,
2002 None => {
2003 warn!(
2004 context,
2005 "Contact with From address {:?} cannot exist, not moving out of spam", from
2006 );
2007 return Ok(false);
2008 }
2009 };
2010 if blocked_contact {
2011 return Ok(false);
2013 }
2014
2015 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2016 if chat_id_blocked.blocked != Blocked::Not {
2017 return Ok(false);
2018 }
2019 } else if from_id != ContactId::SELF {
2020 return Ok(false);
2022 }
2023 }
2024
2025 Ok(true)
2026}
2027
2028async fn spam_target_folder_cfg(
2033 context: &Context,
2034 headers: &[mailparse::MailHeader<'_>],
2035) -> Result<Option<Config>> {
2036 if !should_move_out_of_spam(context, headers).await? {
2037 return Ok(None);
2038 }
2039
2040 if needs_move_to_mvbox(context, headers).await?
2041 || context.get_config_bool(Config::OnlyFetchMvbox).await?
2044 {
2045 Ok(Some(Config::ConfiguredMvboxFolder))
2046 } else {
2047 Ok(Some(Config::ConfiguredInboxFolder))
2048 }
2049}
2050
2051pub async fn target_folder_cfg(
2054 context: &Context,
2055 folder: &str,
2056 folder_meaning: FolderMeaning,
2057 headers: &[mailparse::MailHeader<'_>],
2058) -> Result<Option<Config>> {
2059 if context.is_mvbox(folder).await? {
2060 return Ok(None);
2061 }
2062
2063 if folder_meaning == FolderMeaning::Spam {
2064 spam_target_folder_cfg(context, headers).await
2065 } else if folder_meaning == FolderMeaning::Inbox
2066 && needs_move_to_mvbox(context, headers).await?
2067 {
2068 Ok(Some(Config::ConfiguredMvboxFolder))
2069 } else {
2070 Ok(None)
2071 }
2072}
2073
2074pub async fn target_folder(
2075 context: &Context,
2076 folder: &str,
2077 folder_meaning: FolderMeaning,
2078 headers: &[mailparse::MailHeader<'_>],
2079) -> Result<String> {
2080 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2081 Some(config) => match context.get_config(config).await? {
2082 Some(target) => Ok(target),
2083 None => Ok(folder.to_string()),
2084 },
2085 None => Ok(folder.to_string()),
2086 }
2087}
2088
2089async fn needs_move_to_mvbox(
2090 context: &Context,
2091 headers: &[mailparse::MailHeader<'_>],
2092) -> Result<bool> {
2093 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2094 if !context.get_config_bool(Config::IsChatmail).await?
2095 && has_chat_version
2096 && headers
2097 .get_header_value(HeaderDef::AutoSubmitted)
2098 .filter(|val| val.eq_ignore_ascii_case("auto-generated"))
2099 .is_some()
2100 {
2101 if let Some(from) = mimeparser::get_from(headers) {
2102 if context.is_self_addr(&from.addr).await? {
2103 return Ok(true);
2104 }
2105 }
2106 }
2107 if !context.get_config_bool(Config::MvboxMove).await? {
2108 return Ok(false);
2109 }
2110
2111 if headers
2112 .get_header_value(HeaderDef::AutocryptSetupMessage)
2113 .is_some()
2114 {
2115 return Ok(false);
2118 }
2119
2120 if has_chat_version {
2121 Ok(true)
2122 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2123 match parent.is_dc_message {
2124 MessengerMessage::No => Ok(false),
2125 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2126 }
2127 } else {
2128 Ok(false)
2129 }
2130}
2131
2132fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2139 const SPAM_NAMES: &[&str] = &[
2141 "spam",
2142 "junk",
2143 "Correio electrónico não solicitado",
2144 "Correo basura",
2145 "Lixo",
2146 "Nettsøppel",
2147 "Nevyžádaná pošta",
2148 "No solicitado",
2149 "Ongewenst",
2150 "Posta indesiderata",
2151 "Skräp",
2152 "Wiadomości-śmieci",
2153 "Önemsiz",
2154 "Ανεπιθύμητα",
2155 "Спам",
2156 "垃圾邮件",
2157 "垃圾郵件",
2158 "迷惑メール",
2159 "스팸",
2160 ];
2161 const TRASH_NAMES: &[&str] = &[
2162 "Trash",
2163 "Bin",
2164 "Caixote do lixo",
2165 "Cestino",
2166 "Corbeille",
2167 "Papelera",
2168 "Papierkorb",
2169 "Papirkurv",
2170 "Papperskorgen",
2171 "Prullenbak",
2172 "Rubujo",
2173 "Κάδος απορριμμάτων",
2174 "Корзина",
2175 "Кошик",
2176 "ゴミ箱",
2177 "垃圾桶",
2178 "已删除邮件",
2179 "휴지통",
2180 ];
2181 let lower = folder_name.to_lowercase();
2182
2183 if lower == "inbox" {
2184 FolderMeaning::Inbox
2185 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2186 FolderMeaning::Spam
2187 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2188 FolderMeaning::Trash
2189 } else {
2190 FolderMeaning::Unknown
2191 }
2192}
2193
2194fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2195 for attr in folder_attrs {
2196 match attr {
2197 NameAttribute::Trash => return FolderMeaning::Trash,
2198 NameAttribute::Junk => return FolderMeaning::Spam,
2199 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2200 NameAttribute::Extension(label) => {
2201 match label.as_ref() {
2202 "\\Spam" => return FolderMeaning::Spam,
2203 "\\Important" => return FolderMeaning::Virtual,
2204 _ => {}
2205 };
2206 }
2207 _ => {}
2208 }
2209 }
2210 FolderMeaning::Unknown
2211}
2212
2213pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2214 match get_folder_meaning_by_attrs(folder.attributes()) {
2215 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2216 meaning => meaning,
2217 }
2218}
2219
2220fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2222 match prefetch_msg.header() {
2223 Some(header_bytes) => {
2224 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2225 Ok(headers)
2226 }
2227 None => Ok(Vec::new()),
2228 }
2229}
2230
2231pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2232 headers
2233 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2234 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2235 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2236}
2237
2238pub(crate) fn create_message_id() -> String {
2239 format!("{}{}", GENERATED_PREFIX, create_id())
2240}
2241
2242async fn prefetch_get_chat(
2244 context: &Context,
2245 headers: &[mailparse::MailHeader<'_>],
2246) -> Result<Option<chat::Chat>> {
2247 let parent = get_prefetch_parent_message(context, headers).await?;
2248 if let Some(parent) = &parent {
2249 return Ok(Some(
2250 chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
2251 ));
2252 }
2253
2254 Ok(None)
2255}
2256
2257pub(crate) async fn prefetch_should_download(
2259 context: &Context,
2260 headers: &[mailparse::MailHeader<'_>],
2261 message_id: &str,
2262 mut flags: impl Iterator<Item = Flag<'_>>,
2263) -> Result<bool> {
2264 if message::rfc724_mid_exists(context, message_id)
2265 .await?
2266 .is_some()
2267 {
2268 markseen_on_imap_table(context, message_id).await?;
2269 return Ok(false);
2270 }
2271
2272 if let Some(chat) = prefetch_get_chat(context, headers).await? {
2276 if chat.typ == Chattype::Group && !chat.id.is_special() {
2277 return Ok(true);
2280 }
2281 }
2282
2283 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2284 let from = from.to_ascii_lowercase();
2285 from.contains("mailer-daemon") || from.contains("mail-daemon")
2286 } else {
2287 false
2288 };
2289
2290 let is_autocrypt_setup_message = headers
2292 .get_header_value(HeaderDef::AutocryptSetupMessage)
2293 .is_some();
2294
2295 let from = match mimeparser::get_from(headers) {
2296 Some(f) => f,
2297 None => return Ok(false),
2298 };
2299 let (_from_id, blocked_contact, origin) =
2300 match from_field_to_contact_id(context, &from, None, true, true).await? {
2301 Some(res) => res,
2302 None => return Ok(false),
2303 };
2304 if flags.any(|f| f == Flag::Draft) {
2308 info!(context, "Ignoring draft message");
2309 return Ok(false);
2310 }
2311
2312 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2313 let accepted_contact = origin.is_known();
2314 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2315 .await?
2316 .map(|parent| match parent.is_dc_message {
2317 MessengerMessage::No => false,
2318 MessengerMessage::Yes | MessengerMessage::Reply => true,
2319 })
2320 .unwrap_or_default();
2321
2322 let show_emails =
2323 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2324
2325 let show = is_autocrypt_setup_message
2326 || match show_emails {
2327 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2328 ShowEmails::AcceptedContacts => {
2329 is_chat_message || is_reply_to_chat_message || accepted_contact
2330 }
2331 ShowEmails::All => true,
2332 };
2333
2334 let should_download = (show && !blocked_contact) || maybe_ndn;
2335 Ok(should_download)
2336}
2337
2338async fn mark_seen_by_uid(
2342 context: &Context,
2343 folder: &str,
2344 uid_validity: u32,
2345 uid: u32,
2346) -> Result<Option<ChatId>> {
2347 if let Some((msg_id, chat_id)) = context
2348 .sql
2349 .query_row_optional(
2350 "SELECT id, chat_id FROM msgs
2351 WHERE id > 9 AND rfc724_mid IN (
2352 SELECT rfc724_mid FROM imap
2353 WHERE folder=?1
2354 AND uidvalidity=?2
2355 AND uid=?3
2356 LIMIT 1
2357 )",
2358 (&folder, uid_validity, uid),
2359 |row| {
2360 let msg_id: MsgId = row.get(0)?;
2361 let chat_id: ChatId = row.get(1)?;
2362 Ok((msg_id, chat_id))
2363 },
2364 )
2365 .await
2366 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2367 {
2368 let updated = context
2369 .sql
2370 .execute(
2371 "UPDATE msgs SET state=?1
2372 WHERE (state=?2 OR state=?3)
2373 AND id=?4",
2374 (
2375 MessageState::InSeen,
2376 MessageState::InFresh,
2377 MessageState::InNoticed,
2378 msg_id,
2379 ),
2380 )
2381 .await
2382 .with_context(|| format!("failed to update msg {msg_id} state"))?
2383 > 0;
2384
2385 if updated {
2386 msg_id
2387 .start_ephemeral_timer(context)
2388 .await
2389 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2390 Ok(Some(chat_id))
2391 } else {
2392 Ok(None)
2394 }
2395 } else {
2396 Ok(None)
2398 }
2399}
2400
2401pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2404 context
2405 .sql
2406 .execute(
2407 "INSERT OR IGNORE INTO imap_markseen (id)
2408 SELECT id FROM imap WHERE rfc724_mid=?",
2409 (message_id,),
2410 )
2411 .await?;
2412 context.scheduler.interrupt_inbox().await;
2413
2414 Ok(())
2415}
2416
2417pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
2421 context
2422 .sql
2423 .execute(
2424 "INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
2425 ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
2426 (folder, uid_next),
2427 )
2428 .await?;
2429 Ok(())
2430}
2431
2432async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
2438 Ok(context
2439 .sql
2440 .query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
2441 .await?
2442 .unwrap_or(0))
2443}
2444
2445pub(crate) async fn set_uidvalidity(
2446 context: &Context,
2447 folder: &str,
2448 uidvalidity: u32,
2449) -> Result<()> {
2450 context
2451 .sql
2452 .execute(
2453 "INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
2454 ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2455 (folder, uidvalidity),
2456 )
2457 .await?;
2458 Ok(())
2459}
2460
2461async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
2462 Ok(context
2463 .sql
2464 .query_get_value(
2465 "SELECT uidvalidity FROM imap_sync WHERE folder=?;",
2466 (folder,),
2467 )
2468 .await?
2469 .unwrap_or(0))
2470}
2471
2472pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> Result<()> {
2473 context
2474 .sql
2475 .execute(
2476 "INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
2477 ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
2478 (folder, modseq),
2479 )
2480 .await?;
2481 Ok(())
2482}
2483
2484async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
2485 Ok(context
2486 .sql
2487 .query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
2488 .await?
2489 .unwrap_or(0))
2490}
2491
2492pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
2494 let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
2496
2497 for item in context.get_secondary_self_addrs().await? {
2498 search_command = format!("OR ({search_command}) (FROM \"{item}\")");
2499 }
2500
2501 Ok(search_command)
2502}
2503
2504pub async fn get_config_last_seen_uid(context: &Context, folder: &str) -> Result<(u32, u32)> {
2506 let key = format!("imap.mailbox.{folder}");
2507 if let Some(entry) = context.sql.get_raw_config(&key).await? {
2508 let mut parts = entry.split(':');
2510 Ok((
2511 parts.next().unwrap_or_default().parse().unwrap_or(0),
2512 parts.next().unwrap_or_default().parse().unwrap_or(0),
2513 ))
2514 } else {
2515 Ok((0, 0))
2516 }
2517}
2518
2519async fn should_ignore_folder(
2524 context: &Context,
2525 folder: &str,
2526 folder_meaning: FolderMeaning,
2527) -> Result<bool> {
2528 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2529 return Ok(false);
2530 }
2531 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2532}
2533
2534fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2538 let mut ranges: Vec<UidRange> = vec![];
2540
2541 for ¤t in uids {
2542 if let Some(last) = ranges.last_mut() {
2543 if last.end + 1 == current {
2544 last.end = current;
2545 continue;
2546 }
2547 }
2548
2549 ranges.push(UidRange {
2550 start: current,
2551 end: current,
2552 });
2553 }
2554
2555 let mut result = vec![];
2557 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2558 for range in ranges {
2559 last_uids.reserve((range.end - range.start + 1).try_into()?);
2560 (range.start..=range.end).for_each(|u| last_uids.push(u));
2561 if !last_str.is_empty() {
2562 last_str.push(',');
2563 }
2564 last_str.push_str(&range.to_string());
2565
2566 if last_str.len() > 990 {
2567 result.push((take(&mut last_uids), take(&mut last_str)));
2568 }
2569 }
2570 result.push((last_uids, last_str));
2571
2572 result.retain(|(_, s)| !s.is_empty());
2573 Ok(result)
2574}
2575
2576struct UidRange {
2577 start: u32,
2578 end: u32,
2579 }
2581
2582impl std::fmt::Display for UidRange {
2583 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2584 if self.start == self.end {
2585 write!(f, "{}", self.start)
2586 } else {
2587 write!(f, "{}:{}", self.start, self.end)
2588 }
2589 }
2590}
2591async fn add_all_recipients_as_contacts(
2592 context: &Context,
2593 session: &mut Session,
2594 folder: Config,
2595) -> Result<()> {
2596 let mailbox = if let Some(m) = context.get_config(folder).await? {
2597 m
2598 } else {
2599 info!(
2600 context,
2601 "Folder {} is not configured, skipping fetching contacts from it.", folder
2602 );
2603 return Ok(());
2604 };
2605 let create = false;
2606 let folder_exists = session
2607 .select_with_uidvalidity(context, &mailbox, create)
2608 .await
2609 .with_context(|| format!("could not select {mailbox}"))?;
2610 if !folder_exists {
2611 return Ok(());
2612 }
2613
2614 let recipients = session
2615 .get_all_recipients(context)
2616 .await
2617 .context("could not get recipients")?;
2618
2619 let mut any_modified = false;
2620 for recipient in recipients {
2621 let recipient_addr = match ContactAddress::new(&recipient.addr) {
2622 Err(err) => {
2623 warn!(
2624 context,
2625 "Could not add contact for recipient with address {:?}: {:#}",
2626 recipient.addr,
2627 err
2628 );
2629 continue;
2630 }
2631 Ok(recipient_addr) => recipient_addr,
2632 };
2633
2634 let (_, modified) = Contact::add_or_lookup(
2635 context,
2636 &recipient.display_name.unwrap_or_default(),
2637 &recipient_addr,
2638 Origin::OutgoingTo,
2639 )
2640 .await?;
2641 if modified != Modifier::None {
2642 any_modified = true;
2643 }
2644 }
2645 if any_modified {
2646 context.emit_event(EventType::ContactsChanged(None));
2647 }
2648
2649 Ok(())
2650}
2651
2652#[cfg(test)]
2653mod imap_tests;