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, add_device_msg};
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, warn};
36use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
37use crate::mimeparser;
38use crate::net::proxy::ProxyConfig;
39use crate::net::session::SessionStream;
40use crate::oauth2::get_oauth2_access_token;
41use crate::push::encrypt_device_token;
42use crate::receive_imf::{
43 ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
44};
45use crate::scheduler::connectivity::ConnectivityStore;
46use crate::stock_str;
47use crate::tools::{self, create_id, duration_to_str, time};
48use crate::transport::{
49 ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
50};
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 pub(crate) resync_request_sender: async_channel::Sender<()>,
110
111 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
113}
114
115#[derive(Debug)]
116struct OAuth2 {
117 user: String,
118 access_token: String,
119}
120
121#[derive(Debug)]
122pub(crate) struct ServerMetadata {
123 pub comment: Option<String>,
126
127 pub admin: Option<String>,
130
131 pub iroh_relay: Option<Url>,
132
133 pub ice_servers: String,
140
141 pub ice_servers_expiration_timestamp: i64,
144}
145
146impl async_imap::Authenticator for OAuth2 {
147 type Response = String;
148
149 fn process(&mut self, _data: &[u8]) -> Self::Response {
150 format!(
151 "user={}\x01auth=Bearer {}\x01\x01",
152 self.user, self.access_token
153 )
154 }
155}
156
157#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
158pub enum FolderMeaning {
159 Unknown,
160
161 Spam,
163 Inbox,
164 Mvbox,
165 Trash,
166
167 Virtual,
174}
175
176impl FolderMeaning {
177 pub fn to_config(self) -> Option<Config> {
178 match self {
179 FolderMeaning::Unknown => None,
180 FolderMeaning::Spam => None,
181 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
182 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
183 FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
184 FolderMeaning::Virtual => None,
185 }
186 }
187}
188
189struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
190 inner: Peekable<T>,
191}
192
193impl<T, I> From<I> for UidGrouper<T>
194where
195 T: Iterator<Item = (i64, u32, String)>,
196 I: IntoIterator<IntoIter = T>,
197{
198 fn from(inner: I) -> Self {
199 Self {
200 inner: inner.into_iter().peekable(),
201 }
202 }
203}
204
205impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
206 type Item = (String, Vec<i64>, String);
208
209 fn next(&mut self) -> Option<Self::Item> {
210 let (_, _, folder) = self.inner.peek().cloned()?;
211
212 let mut uid_set = String::new();
213 let mut rowid_set = Vec::new();
214
215 while uid_set.len() < 1000 {
216 if let Some((start_rowid, start_uid, _)) = self
218 .inner
219 .next_if(|(_, _, start_folder)| start_folder == &folder)
220 {
221 rowid_set.push(start_rowid);
222 let mut end_uid = start_uid;
223
224 while let Some((next_rowid, next_uid, _)) =
225 self.inner.next_if(|(_, next_uid, next_folder)| {
226 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
227 })
228 {
229 end_uid = next_uid;
230 rowid_set.push(next_rowid);
231 }
232
233 let uid_range = UidRange {
234 start: start_uid,
235 end: end_uid,
236 };
237 if !uid_set.is_empty() {
238 uid_set.push(',');
239 }
240 uid_set.push_str(&uid_range.to_string());
241 } else {
242 break;
243 }
244 }
245
246 Some((folder, rowid_set, uid_set))
247 }
248}
249
250impl Imap {
251 pub fn new(
255 lp: Vec<ConfiguredServerLoginParam>,
256 password: String,
257 proxy_config: Option<ProxyConfig>,
258 addr: &str,
259 strict_tls: bool,
260 oauth2: bool,
261 idle_interrupt_receiver: Receiver<()>,
262 ) -> Self {
263 let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
264 Imap {
265 idle_interrupt_receiver,
266 addr: addr.to_string(),
267 lp,
268 password,
269 proxy_config,
270 strict_tls,
271 oauth2,
272 authentication_failed_once: false,
273 connectivity: Default::default(),
274 conn_last_try: UNIX_EPOCH,
275 conn_backoff_ms: 0,
276 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
278 resync_request_sender,
279 resync_request_receiver,
280 }
281 }
282
283 pub async fn new_configured(
285 context: &Context,
286 idle_interrupt_receiver: Receiver<()>,
287 ) -> Result<Self> {
288 let param = ConfiguredLoginParam::load(context)
289 .await?
290 .context("Not configured")?;
291 let proxy_config = ProxyConfig::load(context).await?;
292 let strict_tls = param.strict_tls(proxy_config.is_some());
293 let imap = Self::new(
294 param.imap.clone(),
295 param.imap_password.clone(),
296 proxy_config,
297 ¶m.addr,
298 strict_tls,
299 param.oauth2,
300 idle_interrupt_receiver,
301 );
302 Ok(imap)
303 }
304
305 pub(crate) async fn connect(
311 &mut self,
312 context: &Context,
313 configuring: bool,
314 ) -> Result<Session> {
315 let now = tools::Time::now();
316 let until_can_send = max(
317 min(self.conn_last_try, now)
318 .checked_add(Duration::from_millis(self.conn_backoff_ms))
319 .unwrap_or(now),
320 now,
321 )
322 .duration_since(now)?;
323 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
324 if !ratelimit_duration.is_zero() {
325 warn!(
326 context,
327 "IMAP got rate limited, waiting for {} until can connect.",
328 duration_to_str(ratelimit_duration),
329 );
330 let interrupted = async {
331 tokio::time::sleep(ratelimit_duration).await;
332 false
333 }
334 .race(self.idle_interrupt_receiver.recv().map(|_| true))
335 .await;
336 if interrupted {
337 info!(
338 context,
339 "Connecting to IMAP without waiting for ratelimit due to interrupt."
340 );
341 }
342 }
343
344 info!(context, "Connecting to IMAP server.");
345 self.connectivity.set_connecting(context);
346
347 self.conn_last_try = tools::Time::now();
348 const BACKOFF_MIN_MS: u64 = 2000;
349 const BACKOFF_MAX_MS: u64 = 80_000;
350 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
351 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
352 (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
353 ));
354 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
355
356 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
357 let mut first_error = None;
358 for lp in login_params {
359 info!(context, "IMAP trying to connect to {}.", &lp.connection);
360 let connection_candidate = lp.connection.clone();
361 let client = match Client::connect(
362 context,
363 self.proxy_config.clone(),
364 self.strict_tls,
365 connection_candidate,
366 )
367 .await
368 .context("IMAP failed to connect")
369 {
370 Ok(client) => client,
371 Err(err) => {
372 warn!(context, "{err:#}.");
373 first_error.get_or_insert(err);
374 continue;
375 }
376 };
377
378 self.conn_backoff_ms = BACKOFF_MIN_MS;
379 self.ratelimit.send();
380
381 let imap_user: &str = lp.user.as_ref();
382 let imap_pw: &str = &self.password;
383
384 let login_res = if self.oauth2 {
385 info!(context, "Logging into IMAP server with OAuth 2.");
386 let addr: &str = self.addr.as_ref();
387
388 let token = get_oauth2_access_token(context, addr, imap_pw, true)
389 .await?
390 .context("IMAP could not get OAUTH token")?;
391 let auth = OAuth2 {
392 user: imap_user.into(),
393 access_token: token,
394 };
395 client.authenticate("XOAUTH2", auth).await
396 } else {
397 info!(context, "Logging into IMAP server with LOGIN.");
398 client.login(imap_user, imap_pw).await
399 };
400
401 match login_res {
402 Ok(mut session) => {
403 let capabilities = determine_capabilities(&mut session).await?;
404 let resync_request_sender = self.resync_request_sender.clone();
405
406 let session = if capabilities.can_compress {
407 info!(context, "Enabling IMAP compression.");
408 let compressed_session = session
409 .compress(|s| {
410 let session_stream: Box<dyn SessionStream> = Box::new(s);
411 session_stream
412 })
413 .await
414 .context("Failed to enable IMAP compression")?;
415 Session::new(compressed_session, capabilities, resync_request_sender)
416 } else {
417 Session::new(session, capabilities, resync_request_sender)
418 };
419
420 let mut lock = context.server_id.write().await;
422 lock.clone_from(&session.capabilities.server_id);
423
424 self.authentication_failed_once = false;
425 context.emit_event(EventType::ImapConnected(format!(
426 "IMAP-LOGIN as {}",
427 lp.user
428 )));
429 self.connectivity.set_preparing(context);
430 info!(context, "Successfully logged into IMAP server.");
431 return Ok(session);
432 }
433
434 Err(err) => {
435 let imap_user = lp.user.to_owned();
436 let message = stock_str::cannot_login(context, &imap_user).await;
437
438 warn!(context, "IMAP failed to login: {err:#}.");
439 first_error.get_or_insert(format_err!("{message} ({err:#})"));
440
441 let _lock = context.wrong_pw_warning_mutex.lock().await;
443 if err.to_string().to_lowercase().contains("authentication") {
444 if self.authentication_failed_once
445 && !configuring
446 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
447 {
448 let mut msg = Message::new_text(message);
449 if let Err(e) = chat::add_device_msg_with_importance(
450 context,
451 None,
452 Some(&mut msg),
453 true,
454 )
455 .await
456 {
457 warn!(context, "Failed to add device message: {e:#}.");
458 } else {
459 context
460 .set_config_internal(Config::NotifyAboutWrongPw, None)
461 .await
462 .log_err(context)
463 .ok();
464 }
465 } else {
466 self.authentication_failed_once = true;
467 }
468 } else {
469 self.authentication_failed_once = false;
470 }
471 }
472 }
473 }
474
475 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
476 }
477
478 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
483 let configuring = false;
484 let mut session = match self.connect(context, configuring).await {
485 Ok(session) => session,
486 Err(err) => {
487 self.connectivity.set_err(context, &err);
488 return Err(err);
489 }
490 };
491
492 let folders_configured = context
493 .sql
494 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
495 .await?;
496 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
497 let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
498 false => session.is_chatmail(),
499 true => context.get_config_bool(Config::IsChatmail).await?,
500 };
501 let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
502 self.configure_folders(context, &mut session, create_mvbox)
503 .await?;
504 }
505
506 Ok(session)
507 }
508
509 pub async fn fetch_move_delete(
514 &mut self,
515 context: &Context,
516 session: &mut Session,
517 watch_folder: &str,
518 folder_meaning: FolderMeaning,
519 ) -> Result<()> {
520 if !context.sql.is_open().await {
521 bail!("IMAP operation attempted while it is torn down");
523 }
524
525 let msgs_fetched = self
526 .fetch_new_messages(context, session, watch_folder, folder_meaning)
527 .await
528 .context("fetch_new_messages")?;
529 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
530 context.scheduler.interrupt_ephemeral_task().await;
535 }
536
537 session
538 .move_delete_messages(context, watch_folder)
539 .await
540 .context("move_delete_messages")?;
541
542 Ok(())
543 }
544
545 pub(crate) async fn fetch_new_messages(
549 &mut self,
550 context: &Context,
551 session: &mut Session,
552 folder: &str,
553 folder_meaning: FolderMeaning,
554 ) -> Result<bool> {
555 if should_ignore_folder(context, folder, folder_meaning).await? {
556 info!(context, "Not fetching from {folder:?}.");
557 session.new_mail = false;
558 return Ok(false);
559 }
560
561 let create = false;
562 let folder_exists = session
563 .select_with_uidvalidity(context, folder, create)
564 .await
565 .with_context(|| format!("Failed to select folder {folder:?}"))?;
566 if !folder_exists {
567 return Ok(false);
568 }
569
570 if !session.new_mail {
571 info!(context, "No new emails in folder {folder:?}.");
572 return Ok(false);
573 }
574 session.new_mail = false;
575
576 let mut read_cnt = 0;
577 loop {
578 let (n, fetch_more) = self
579 .fetch_new_msg_batch(context, session, folder, folder_meaning)
580 .await?;
581 read_cnt += n;
582 if !fetch_more {
583 return Ok(read_cnt > 0);
584 }
585 }
586 }
587
588 async fn fetch_new_msg_batch(
590 &mut self,
591 context: &Context,
592 session: &mut Session,
593 folder: &str,
594 folder_meaning: FolderMeaning,
595 ) -> Result<(usize, bool)> {
596 let uid_validity = get_uidvalidity(context, folder).await?;
597 let old_uid_next = get_uid_next(context, folder).await?;
598 info!(
599 context,
600 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
601 );
602
603 let uids_to_prefetch = 500;
604 let msgs = session
605 .prefetch(old_uid_next, uids_to_prefetch)
606 .await
607 .context("prefetch")?;
608 let read_cnt = msgs.len();
609
610 let download_limit = context.download_limit().await?;
611 let mut uids_fetch = Vec::<(u32, bool )>::with_capacity(msgs.len() + 1);
612 let mut uid_message_ids = BTreeMap::new();
613 let mut largest_uid_skipped = None;
614 let delete_target = context.get_delete_msgs_target().await?;
615
616 for (uid, ref fetch_response) in msgs {
618 let headers = match get_fetch_headers(fetch_response) {
619 Ok(headers) => headers,
620 Err(err) => {
621 warn!(context, "Failed to parse FETCH headers: {err:#}.");
622 continue;
623 }
624 };
625
626 let message_id = prefetch_get_message_id(&headers);
627
628 let delete = if let Some(message_id) = &message_id {
639 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
640 .await?
641 .is_some_and(|(_msg_id, deleted)| deleted)
642 } else {
643 false
644 };
645
646 let message_id = message_id.unwrap_or_else(create_message_id);
649
650 if delete {
651 info!(context, "Deleting locally deleted message {message_id}.");
652 }
653
654 let _target;
655 let target = if delete {
656 &delete_target
657 } else {
658 _target = target_folder(context, folder, folder_meaning, &headers).await?;
659 &_target
660 };
661
662 context
663 .sql
664 .execute(
665 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
666 VALUES (?1, ?2, ?3, ?4, ?5)
667 ON CONFLICT(folder, uid, uidvalidity)
668 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
669 target=excluded.target",
670 (&message_id, &folder, uid, uid_validity, target),
671 )
672 .await?;
673
674 if folder == target
681 && folder_meaning != FolderMeaning::Spam
686 && prefetch_should_download(
687 context,
688 &headers,
689 &message_id,
690 fetch_response.flags(),
691 )
692 .await.context("prefetch_should_download")?
693 {
694 match download_limit {
695 Some(download_limit) => uids_fetch.push((
696 uid,
697 fetch_response.size.unwrap_or_default() > download_limit,
698 )),
699 None => uids_fetch.push((uid, false)),
700 }
701 uid_message_ids.insert(uid, message_id);
702 } else {
703 largest_uid_skipped = Some(uid);
704 }
705 }
706
707 if !uids_fetch.is_empty() {
708 self.connectivity.set_working(context);
709 }
710
711 let (sender, receiver) = async_channel::unbounded();
712
713 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
714 let mailbox_uid_next = session
715 .selected_mailbox
716 .as_ref()
717 .with_context(|| format!("Expected {folder:?} to be selected"))?
718 .uid_next
719 .unwrap_or_default();
720
721 let update_uids_future = async {
722 let mut largest_uid_fetched: u32 = 0;
723
724 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
725 largest_uid_fetched = max(largest_uid_fetched, uid);
726 if let Some(received_msg) = received_msg_opt {
727 received_msgs.push(received_msg)
728 }
729 }
730
731 largest_uid_fetched
732 };
733
734 let actually_download_messages_future = async {
735 let sender = sender;
736 let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
737 let mut fetch_partially = false;
738 uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
739 for (uid, fp) in uids_fetch {
740 if fp != fetch_partially {
741 session
742 .fetch_many_msgs(
743 context,
744 folder,
745 uids_fetch_in_batch.split_off(0),
746 &uid_message_ids,
747 fetch_partially,
748 sender.clone(),
749 )
750 .await
751 .context("fetch_many_msgs")?;
752 fetch_partially = fp;
753 }
754 uids_fetch_in_batch.push(uid);
755 }
756
757 anyhow::Ok(())
758 };
759
760 let (largest_uid_fetched, fetch_res) =
761 tokio::join!(update_uids_future, actually_download_messages_future);
762
763 let mut new_uid_next = largest_uid_fetched + 1;
769 let fetch_more = fetch_res.is_ok() && {
770 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
771 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
775
776 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
777
778 prefetch_uid_next < mailbox_uid_next
779 };
780 if new_uid_next > old_uid_next {
781 set_uid_next(context, folder, new_uid_next).await?;
782 }
783
784 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
785
786 if !received_msgs.is_empty() {
787 context.emit_event(EventType::IncomingMsgBunch);
788 }
789
790 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
791
792 fetch_res?;
795
796 Ok((read_cnt, fetch_more))
797 }
798
799 pub(crate) async fn fetch_existing_msgs(
805 &mut self,
806 context: &Context,
807 session: &mut Session,
808 ) -> Result<()> {
809 add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
810 .await
811 .context("failed to get recipients from the movebox")?;
812 add_all_recipients_as_contacts(context, session, Config::ConfiguredInboxFolder)
813 .await
814 .context("failed to get recipients from the inbox")?;
815
816 info!(context, "Done fetching existing messages.");
817 Ok(())
818 }
819}
820
821impl Session {
822 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
824 let all_folders = self
825 .list_folders()
826 .await
827 .context("listing folders for resync")?;
828 for folder in all_folders {
829 let folder_meaning = get_folder_meaning(&folder);
830 if !matches!(
831 folder_meaning,
832 FolderMeaning::Virtual | FolderMeaning::Unknown
833 ) {
834 self.resync_folder_uids(context, folder.name(), folder_meaning)
835 .await?;
836 }
837 }
838 Ok(())
839 }
840
841 pub(crate) async fn resync_folder_uids(
848 &mut self,
849 context: &Context,
850 folder: &str,
851 folder_meaning: FolderMeaning,
852 ) -> Result<()> {
853 let uid_validity;
854 let mut msgs = BTreeMap::new();
856
857 let create = false;
858 let folder_exists = self
859 .select_with_uidvalidity(context, folder, create)
860 .await?;
861 if folder_exists {
862 let mut list = self
863 .uid_fetch("1:*", RFC724MID_UID)
864 .await
865 .with_context(|| format!("Can't resync folder {folder}"))?;
866 while let Some(fetch) = list.try_next().await? {
867 let headers = match get_fetch_headers(&fetch) {
868 Ok(headers) => headers,
869 Err(err) => {
870 warn!(context, "Failed to parse FETCH headers: {}", err);
871 continue;
872 }
873 };
874 let message_id = prefetch_get_message_id(&headers);
875
876 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
877 msgs.insert(
878 uid,
879 (
880 rfc724_mid,
881 target_folder(context, folder, folder_meaning, &headers).await?,
882 ),
883 );
884 }
885 }
886
887 info!(
888 context,
889 "resync_folder_uids: Collected {} message IDs in {folder}.",
890 msgs.len(),
891 );
892
893 uid_validity = get_uidvalidity(context, folder).await?;
894 } else {
895 warn!(context, "resync_folder_uids: No folder {folder}.");
896 uid_validity = 0;
897 }
898
899 context
901 .sql
902 .transaction(move |transaction| {
903 transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
904 for (uid, (rfc724_mid, target)) in &msgs {
905 transaction.execute(
908 "INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
909 VALUES (?1, ?2, ?3, ?4, ?5)
910 ON CONFLICT(folder, uid, uidvalidity)
911 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
912 target=excluded.target",
913 (rfc724_mid, folder, uid, uid_validity, target),
914 )?;
915 }
916 Ok(())
917 })
918 .await?;
919 Ok(())
920 }
921
922 async fn delete_message_batch(
925 &mut self,
926 context: &Context,
927 uid_set: &str,
928 row_ids: Vec<i64>,
929 ) -> Result<()> {
930 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
932 .await?;
933 context
934 .sql
935 .transaction(|transaction| {
936 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
937 for row_id in row_ids {
938 stmt.execute((row_id,))?;
939 }
940 Ok(())
941 })
942 .await
943 .context("Cannot remove deleted messages from imap table")?;
944
945 context.emit_event(EventType::ImapMessageDeleted(format!(
946 "IMAP messages {uid_set} marked as deleted"
947 )));
948 Ok(())
949 }
950
951 async fn move_message_batch(
954 &mut self,
955 context: &Context,
956 set: &str,
957 row_ids: Vec<i64>,
958 target: &str,
959 ) -> Result<()> {
960 if self.can_move() {
961 match self.uid_mv(set, &target).await {
962 Ok(()) => {
963 context
965 .sql
966 .transaction(|transaction| {
967 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
968 for row_id in row_ids {
969 stmt.execute((row_id,))?;
970 }
971 Ok(())
972 })
973 .await
974 .context("Cannot delete moved messages from imap table")?;
975 context.emit_event(EventType::ImapMessageMoved(format!(
976 "IMAP messages {set} moved to {target}"
977 )));
978 return Ok(());
979 }
980 Err(err) => {
981 if context.should_delete_to_trash().await? {
982 error!(
983 context,
984 "Cannot move messages {} to {}, no fallback to COPY/DELETE because \
985 delete_to_trash is set. Error: {:#}",
986 set,
987 target,
988 err,
989 );
990 return Err(err.into());
991 }
992 warn!(
993 context,
994 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
995 set,
996 target,
997 err
998 );
999 }
1000 }
1001 }
1002
1003 let copy = !context.is_trash(target).await?;
1006 if copy {
1007 info!(
1008 context,
1009 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1010 );
1011 self.uid_copy(&set, &target).await?;
1012 } else {
1013 error!(
1014 context,
1015 "Server does not support MOVE, fallback to DELETE {} to {}", set, target,
1016 );
1017 }
1018 context
1019 .sql
1020 .transaction(|transaction| {
1021 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1022 for row_id in row_ids {
1023 stmt.execute((row_id,))?;
1024 }
1025 Ok(())
1026 })
1027 .await
1028 .context("Cannot plan deletion of messages")?;
1029 if copy {
1030 context.emit_event(EventType::ImapMessageMoved(format!(
1031 "IMAP messages {set} copied to {target}"
1032 )));
1033 }
1034 Ok(())
1035 }
1036
1037 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1041 let rows = context
1042 .sql
1043 .query_map_vec(
1044 "SELECT id, uid, target FROM imap
1045 WHERE folder = ?
1046 AND target != folder
1047 ORDER BY target, uid",
1048 (folder,),
1049 |row| {
1050 let rowid: i64 = row.get(0)?;
1051 let uid: u32 = row.get(1)?;
1052 let target: String = row.get(2)?;
1053 Ok((rowid, uid, target))
1054 },
1055 )
1056 .await?;
1057
1058 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1059 let create = false;
1064 let folder_exists = self
1065 .select_with_uidvalidity(context, folder, create)
1066 .await?;
1067 ensure!(folder_exists, "No folder {folder}");
1068
1069 if target.is_empty() {
1071 self.delete_message_batch(context, &uid_set, rowid_set)
1072 .await
1073 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1074 } else {
1075 self.move_message_batch(context, &uid_set, rowid_set, &target)
1076 .await
1077 .with_context(|| {
1078 format!(
1079 "cannot move batch of messages {:?} to folder {:?}",
1080 &uid_set, target
1081 )
1082 })?;
1083 }
1084 }
1085
1086 if let Err(err) = self.maybe_close_folder(context).await {
1089 warn!(context, "Failed to close folder: {err:#}.");
1090 }
1091
1092 Ok(())
1093 }
1094
1095 pub(crate) async fn send_sync_msgs(&mut self, context: &Context, folder: &str) -> Result<()> {
1097 context.send_sync_msg().await?;
1098 while let Some((id, mime, msg_id, attempts)) = context
1099 .sql
1100 .query_row_optional(
1101 "SELECT id, mime, msg_id, attempts FROM imap_send ORDER BY id LIMIT 1",
1102 (),
1103 |row| {
1104 let id: i64 = row.get(0)?;
1105 let mime: String = row.get(1)?;
1106 let msg_id: MsgId = row.get(2)?;
1107 let attempts: i64 = row.get(3)?;
1108 Ok((id, mime, msg_id, attempts))
1109 },
1110 )
1111 .await
1112 .context("Failed to SELECT from imap_send")?
1113 {
1114 let res = self
1115 .append(folder, Some("(\\Seen)"), None, mime)
1116 .await
1117 .with_context(|| format!("IMAP APPEND to {folder} failed for {msg_id}"))
1118 .log_err(context);
1119 if res.is_ok() {
1120 msg_id.set_delivered(context).await?;
1121 }
1122 const MAX_ATTEMPTS: i64 = 2;
1123 if res.is_ok() || attempts >= MAX_ATTEMPTS - 1 {
1124 context
1125 .sql
1126 .execute("DELETE FROM imap_send WHERE id=?", (id,))
1127 .await
1128 .context("Failed to delete from imap_send")?;
1129 } else {
1130 context
1131 .sql
1132 .execute("UPDATE imap_send SET attempts=attempts+1 WHERE id=?", (id,))
1133 .await
1134 .context("Failed to update imap_send.attempts")?;
1135 res?;
1136 }
1137 }
1138 Ok(())
1139 }
1140
1141 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1143 let rows = context
1144 .sql
1145 .query_map_vec(
1146 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1147 WHERE imap.id = imap_markseen.id AND target = folder
1148 ORDER BY folder, uid",
1149 [],
1150 |row| {
1151 let rowid: i64 = row.get(0)?;
1152 let uid: u32 = row.get(1)?;
1153 let folder: String = row.get(2)?;
1154 Ok((rowid, uid, folder))
1155 },
1156 )
1157 .await?;
1158
1159 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1160 let create = false;
1161 let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
1162 Err(err) => {
1163 warn!(
1164 context,
1165 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1166 );
1167 continue;
1168 }
1169 Ok(folder_exists) => folder_exists,
1170 };
1171 if !folder_exists {
1172 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1173 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1174 warn!(
1175 context,
1176 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1177 );
1178 continue;
1179 } else {
1180 info!(
1181 context,
1182 "Marked messages {} in folder {} as seen.", uid_set, folder
1183 );
1184 }
1185 context
1186 .sql
1187 .transaction(|transaction| {
1188 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1189 for rowid in rowid_set {
1190 stmt.execute((rowid,))?;
1191 }
1192 Ok(())
1193 })
1194 .await
1195 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1196 }
1197
1198 Ok(())
1199 }
1200
1201 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1203 if !self.can_condstore() {
1204 info!(
1205 context,
1206 "Server does not support CONDSTORE, skipping flag synchronization."
1207 );
1208 return Ok(());
1209 }
1210
1211 let create = false;
1212 let folder_exists = self
1213 .select_with_uidvalidity(context, folder, create)
1214 .await
1215 .context("Failed to select folder")?;
1216 if !folder_exists {
1217 return Ok(());
1218 }
1219
1220 let mailbox = self
1221 .selected_mailbox
1222 .as_ref()
1223 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1224
1225 if mailbox.highest_modseq.is_none() {
1228 info!(
1229 context,
1230 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1231 );
1232 return Ok(());
1233 }
1234
1235 let mut updated_chat_ids = BTreeSet::new();
1236 let uid_validity = get_uidvalidity(context, folder)
1237 .await
1238 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1239 let mut highest_modseq = get_modseq(context, folder)
1240 .await
1241 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1242 let mut list = self
1243 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1244 .await
1245 .context("failed to fetch flags")?;
1246
1247 let mut got_unsolicited_fetch = false;
1248
1249 while let Some(fetch) = list
1250 .try_next()
1251 .await
1252 .context("failed to get FETCH result")?
1253 {
1254 let uid = if let Some(uid) = fetch.uid {
1255 uid
1256 } else {
1257 info!(context, "FETCH result contains no UID, skipping");
1258 got_unsolicited_fetch = true;
1259 continue;
1260 };
1261 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1262 if is_seen {
1263 if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
1264 .await
1265 .with_context(|| {
1266 format!("failed to update seen status for msg {folder}/{uid}")
1267 })?
1268 {
1269 updated_chat_ids.insert(chat_id);
1270 }
1271 }
1272
1273 if let Some(modseq) = fetch.modseq {
1274 if modseq > highest_modseq {
1275 highest_modseq = modseq;
1276 }
1277 } else {
1278 warn!(context, "FETCH result contains no MODSEQ");
1279 }
1280 }
1281 drop(list);
1282
1283 if got_unsolicited_fetch {
1284 self.new_mail = true;
1289 }
1290
1291 set_modseq(context, folder, highest_modseq)
1292 .await
1293 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1294 if !updated_chat_ids.is_empty() {
1295 context.on_archived_chats_maybe_noticed();
1296 }
1297 for updated_chat_id in updated_chat_ids {
1298 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1299 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1300 }
1301
1302 Ok(())
1303 }
1304
1305 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1307 let mut uids: Vec<_> = self
1308 .uid_search(get_imap_self_sent_search_command(context).await?)
1309 .await?
1310 .into_iter()
1311 .collect();
1312 uids.sort_unstable();
1313
1314 let mut result = Vec::new();
1315 for (_, uid_set) in build_sequence_sets(&uids)? {
1316 let mut list = self
1317 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1318 .await
1319 .context("IMAP Could not fetch")?;
1320
1321 while let Some(msg) = list.try_next().await? {
1322 match get_fetch_headers(&msg) {
1323 Ok(headers) => {
1324 if let Some(from) = mimeparser::get_from(&headers) {
1325 if context.is_self_addr(&from.addr).await? {
1326 result.extend(mimeparser::get_recipients(&headers));
1327 }
1328 }
1329 }
1330 Err(err) => {
1331 warn!(context, "{}", err);
1332 continue;
1333 }
1334 };
1335 }
1336 }
1337 Ok(result)
1338 }
1339
1340 pub(crate) async fn fetch_many_msgs(
1355 &mut self,
1356 context: &Context,
1357 folder: &str,
1358 request_uids: Vec<u32>,
1359 uid_message_ids: &BTreeMap<u32, String>,
1360 fetch_partially: bool,
1361 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1362 ) -> Result<()> {
1363 if request_uids.is_empty() {
1364 return Ok(());
1365 }
1366
1367 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1368 info!(
1369 context,
1370 "Starting a {} FETCH of message set \"{}\".",
1371 if fetch_partially { "partial" } else { "full" },
1372 set
1373 );
1374 let mut fetch_responses = self
1375 .uid_fetch(
1376 &set,
1377 if fetch_partially {
1378 BODY_PARTIAL
1379 } else {
1380 BODY_FULL
1381 },
1382 )
1383 .await
1384 .with_context(|| {
1385 format!("fetching messages {} from folder \"{}\"", &set, folder)
1386 })?;
1387
1388 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1391
1392 let mut count = 0;
1393 for &request_uid in &request_uids {
1394 let mut fetch_response = uid_msgs.remove(&request_uid);
1396
1397 while fetch_response.is_none() {
1399 let Some(next_fetch_response) = fetch_responses
1400 .try_next()
1401 .await
1402 .context("Failed to process IMAP FETCH result")?
1403 else {
1404 break;
1406 };
1407
1408 if let Some(next_uid) = next_fetch_response.uid {
1409 if next_uid == request_uid {
1410 fetch_response = Some(next_fetch_response);
1411 } else if !request_uids.contains(&next_uid) {
1412 info!(
1419 context,
1420 "Skipping not requested FETCH response for UID {}.", next_uid
1421 );
1422 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1423 warn!(context, "Got duplicated UID {}.", next_uid);
1424 }
1425 } else {
1426 info!(context, "Skipping FETCH response without UID.");
1427 }
1428 }
1429
1430 let fetch_response = match fetch_response {
1431 Some(fetch) => fetch,
1432 None => {
1433 warn!(
1434 context,
1435 "Missed UID {} in the server response.", request_uid
1436 );
1437 continue;
1438 }
1439 };
1440 count += 1;
1441
1442 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1443 let (body, partial) = if fetch_partially {
1444 (fetch_response.header(), fetch_response.size) } else {
1446 (fetch_response.body(), None) };
1448
1449 if is_deleted {
1450 info!(context, "Not processing deleted msg {}.", request_uid);
1451 received_msgs_channel.send((request_uid, None)).await?;
1452 continue;
1453 }
1454
1455 let body = if let Some(body) = body {
1456 body
1457 } else {
1458 info!(
1459 context,
1460 "Not processing message {} without a BODY.", request_uid
1461 );
1462 received_msgs_channel.send((request_uid, None)).await?;
1463 continue;
1464 };
1465
1466 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1467
1468 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1469 error!(
1470 context,
1471 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1472 request_uid
1473 );
1474 continue;
1475 };
1476
1477 info!(
1478 context,
1479 "Passing message UID {} to receive_imf().", request_uid
1480 );
1481 let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await;
1482 let received_msg = match res {
1483 Err(err) => {
1484 warn!(context, "receive_imf error: {err:#}.");
1485
1486 let text = format!(
1487 "❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
1488 );
1489 let mut msg = Message::new_text(text);
1490 add_device_msg(context, None, Some(&mut msg)).await?;
1491 None
1492 }
1493 Ok(msg) => msg,
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
2504async fn should_ignore_folder(
2509 context: &Context,
2510 folder: &str,
2511 folder_meaning: FolderMeaning,
2512) -> Result<bool> {
2513 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2514 return Ok(false);
2515 }
2516 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2517}
2518
2519fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2523 let mut ranges: Vec<UidRange> = vec![];
2525
2526 for ¤t in uids {
2527 if let Some(last) = ranges.last_mut() {
2528 if last.end + 1 == current {
2529 last.end = current;
2530 continue;
2531 }
2532 }
2533
2534 ranges.push(UidRange {
2535 start: current,
2536 end: current,
2537 });
2538 }
2539
2540 let mut result = vec![];
2542 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2543 for range in ranges {
2544 last_uids.reserve((range.end - range.start + 1).try_into()?);
2545 (range.start..=range.end).for_each(|u| last_uids.push(u));
2546 if !last_str.is_empty() {
2547 last_str.push(',');
2548 }
2549 last_str.push_str(&range.to_string());
2550
2551 if last_str.len() > 990 {
2552 result.push((take(&mut last_uids), take(&mut last_str)));
2553 }
2554 }
2555 result.push((last_uids, last_str));
2556
2557 result.retain(|(_, s)| !s.is_empty());
2558 Ok(result)
2559}
2560
2561struct UidRange {
2562 start: u32,
2563 end: u32,
2564 }
2566
2567impl std::fmt::Display for UidRange {
2568 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2569 if self.start == self.end {
2570 write!(f, "{}", self.start)
2571 } else {
2572 write!(f, "{}:{}", self.start, self.end)
2573 }
2574 }
2575}
2576async fn add_all_recipients_as_contacts(
2577 context: &Context,
2578 session: &mut Session,
2579 folder: Config,
2580) -> Result<()> {
2581 let mailbox = if let Some(m) = context.get_config(folder).await? {
2582 m
2583 } else {
2584 info!(
2585 context,
2586 "Folder {} is not configured, skipping fetching contacts from it.", folder
2587 );
2588 return Ok(());
2589 };
2590 let create = false;
2591 let folder_exists = session
2592 .select_with_uidvalidity(context, &mailbox, create)
2593 .await
2594 .with_context(|| format!("could not select {mailbox}"))?;
2595 if !folder_exists {
2596 return Ok(());
2597 }
2598
2599 let recipients = session
2600 .get_all_recipients(context)
2601 .await
2602 .context("could not get recipients")?;
2603
2604 let mut any_modified = false;
2605 for recipient in recipients {
2606 let recipient_addr = match ContactAddress::new(&recipient.addr) {
2607 Err(err) => {
2608 warn!(
2609 context,
2610 "Could not add contact for recipient with address {:?}: {:#}",
2611 recipient.addr,
2612 err
2613 );
2614 continue;
2615 }
2616 Ok(recipient_addr) => recipient_addr,
2617 };
2618
2619 let (_, modified) = Contact::add_or_lookup(
2620 context,
2621 &recipient.display_name.unwrap_or_default(),
2622 &recipient_addr,
2623 Origin::OutgoingTo,
2624 )
2625 .await?;
2626 if modified != Modifier::None {
2627 any_modified = true;
2628 }
2629 }
2630 if any_modified {
2631 context.emit_event(EventType::ContactsChanged(None));
2632 }
2633
2634 Ok(())
2635}
2636
2637#[cfg(test)]
2638mod imap_tests;