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 && 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 if let Some(modseq) = fetch.modseq {
1273 if modseq > highest_modseq {
1274 highest_modseq = modseq;
1275 }
1276 } else {
1277 warn!(context, "FETCH result contains no MODSEQ");
1278 }
1279 }
1280 drop(list);
1281
1282 if got_unsolicited_fetch {
1283 self.new_mail = true;
1288 }
1289
1290 set_modseq(context, folder, highest_modseq)
1291 .await
1292 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1293 if !updated_chat_ids.is_empty() {
1294 context.on_archived_chats_maybe_noticed();
1295 }
1296 for updated_chat_id in updated_chat_ids {
1297 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1298 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1299 }
1300
1301 Ok(())
1302 }
1303
1304 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1306 let mut uids: Vec<_> = self
1307 .uid_search(get_imap_self_sent_search_command(context).await?)
1308 .await?
1309 .into_iter()
1310 .collect();
1311 uids.sort_unstable();
1312
1313 let mut result = Vec::new();
1314 for (_, uid_set) in build_sequence_sets(&uids)? {
1315 let mut list = self
1316 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1317 .await
1318 .context("IMAP Could not fetch")?;
1319
1320 while let Some(msg) = list.try_next().await? {
1321 match get_fetch_headers(&msg) {
1322 Ok(headers) => {
1323 if let Some(from) = mimeparser::get_from(&headers)
1324 && context.is_self_addr(&from.addr).await?
1325 {
1326 result.extend(mimeparser::get_recipients(&headers));
1327 }
1328 }
1329 Err(err) => {
1330 warn!(context, "{}", err);
1331 continue;
1332 }
1333 };
1334 }
1335 }
1336 Ok(result)
1337 }
1338
1339 pub(crate) async fn fetch_many_msgs(
1354 &mut self,
1355 context: &Context,
1356 folder: &str,
1357 request_uids: Vec<u32>,
1358 uid_message_ids: &BTreeMap<u32, String>,
1359 fetch_partially: bool,
1360 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1361 ) -> Result<()> {
1362 if request_uids.is_empty() {
1363 return Ok(());
1364 }
1365
1366 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1367 info!(
1368 context,
1369 "Starting a {} FETCH of message set \"{}\".",
1370 if fetch_partially { "partial" } else { "full" },
1371 set
1372 );
1373 let mut fetch_responses = self
1374 .uid_fetch(
1375 &set,
1376 if fetch_partially {
1377 BODY_PARTIAL
1378 } else {
1379 BODY_FULL
1380 },
1381 )
1382 .await
1383 .with_context(|| {
1384 format!("fetching messages {} from folder \"{}\"", &set, folder)
1385 })?;
1386
1387 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1390
1391 let mut count = 0;
1392 for &request_uid in &request_uids {
1393 let mut fetch_response = uid_msgs.remove(&request_uid);
1395
1396 while fetch_response.is_none() {
1398 let Some(next_fetch_response) = fetch_responses
1399 .try_next()
1400 .await
1401 .context("Failed to process IMAP FETCH result")?
1402 else {
1403 break;
1405 };
1406
1407 if let Some(next_uid) = next_fetch_response.uid {
1408 if next_uid == request_uid {
1409 fetch_response = Some(next_fetch_response);
1410 } else if !request_uids.contains(&next_uid) {
1411 info!(
1418 context,
1419 "Skipping not requested FETCH response for UID {}.", next_uid
1420 );
1421 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1422 warn!(context, "Got duplicated UID {}.", next_uid);
1423 }
1424 } else {
1425 info!(context, "Skipping FETCH response without UID.");
1426 }
1427 }
1428
1429 let fetch_response = match fetch_response {
1430 Some(fetch) => fetch,
1431 None => {
1432 warn!(
1433 context,
1434 "Missed UID {} in the server response.", request_uid
1435 );
1436 continue;
1437 }
1438 };
1439 count += 1;
1440
1441 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1442 let (body, partial) = if fetch_partially {
1443 (fetch_response.header(), fetch_response.size) } else {
1445 (fetch_response.body(), None) };
1447
1448 if is_deleted {
1449 info!(context, "Not processing deleted msg {}.", request_uid);
1450 received_msgs_channel.send((request_uid, None)).await?;
1451 continue;
1452 }
1453
1454 let body = if let Some(body) = body {
1455 body
1456 } else {
1457 info!(
1458 context,
1459 "Not processing message {} without a BODY.", request_uid
1460 );
1461 received_msgs_channel.send((request_uid, None)).await?;
1462 continue;
1463 };
1464
1465 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1466
1467 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1468 error!(
1469 context,
1470 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1471 request_uid
1472 );
1473 continue;
1474 };
1475
1476 info!(
1477 context,
1478 "Passing message UID {} to receive_imf().", request_uid
1479 );
1480 let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await;
1481 let received_msg = match res {
1482 Err(err) => {
1483 warn!(context, "receive_imf error: {err:#}.");
1484
1485 let text = format!(
1486 "❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
1487 );
1488 let mut msg = Message::new_text(text);
1489 add_device_msg(context, None, Some(&mut msg)).await?;
1490 None
1491 }
1492 Ok(msg) => msg,
1493 };
1494 received_msgs_channel
1495 .send((request_uid, received_msg))
1496 .await?;
1497 }
1498
1499 while fetch_responses
1506 .try_next()
1507 .await
1508 .context("Failed to drain FETCH responses")?
1509 .is_some()
1510 {}
1511
1512 if count != request_uids.len() {
1513 warn!(
1514 context,
1515 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1516 count,
1517 request_uids.len(),
1518 request_uids,
1519 );
1520 } else {
1521 info!(
1522 context,
1523 "Successfully received {} UIDs.",
1524 request_uids.len()
1525 );
1526 }
1527 }
1528
1529 Ok(())
1530 }
1531
1532 pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
1538 if !self.can_metadata() {
1539 return Ok(());
1540 }
1541
1542 let mut lock = context.metadata.write().await;
1543 if let Some(ref mut old_metadata) = *lock {
1544 let now = time();
1545
1546 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1548 return Ok(());
1549 }
1550
1551 info!(context, "ICE servers expired, requesting new credentials.");
1552 let mailbox = "";
1553 let options = "";
1554 let metadata = self
1555 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1556 .await?;
1557 let mut got_turn_server = false;
1558 for m in metadata {
1559 if m.entry == "/shared/vendor/deltachat/turn"
1560 && let Some(value) = m.value
1561 {
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 if !got_turn_server {
1576 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1578 old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
1579 }
1580 return Ok(());
1581 }
1582
1583 info!(
1584 context,
1585 "Server supports metadata, retrieving server comment and admin contact."
1586 );
1587
1588 let mut comment = None;
1589 let mut admin = None;
1590 let mut iroh_relay = None;
1591 let mut ice_servers = None;
1592 let mut ice_servers_expiration_timestamp = 0;
1593
1594 let mailbox = "";
1595 let options = "";
1596 let metadata = self
1597 .get_metadata(
1598 mailbox,
1599 options,
1600 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1601 )
1602 .await?;
1603 for m in metadata {
1604 match m.entry.as_ref() {
1605 "/shared/comment" => {
1606 comment = m.value;
1607 }
1608 "/shared/admin" => {
1609 admin = m.value;
1610 }
1611 "/shared/vendor/deltachat/irohrelay" => {
1612 if let Some(value) = m.value {
1613 if let Ok(url) = Url::parse(&value) {
1614 iroh_relay = Some(url);
1615 } else {
1616 warn!(
1617 context,
1618 "Got invalid URL from iroh relay metadata: {:?}.", value
1619 );
1620 }
1621 }
1622 }
1623 "/shared/vendor/deltachat/turn" => {
1624 if let Some(value) = m.value {
1625 match create_ice_servers_from_metadata(context, &value).await {
1626 Ok((parsed_timestamp, parsed_ice_servers)) => {
1627 ice_servers_expiration_timestamp = parsed_timestamp;
1628 ice_servers = Some(parsed_ice_servers);
1629 }
1630 Err(err) => {
1631 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1632 }
1633 }
1634 }
1635 }
1636 _ => {}
1637 }
1638 }
1639 let ice_servers = if let Some(ice_servers) = ice_servers {
1640 ice_servers
1641 } else {
1642 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1644 create_fallback_ice_servers(context).await?
1645 };
1646
1647 *lock = Some(ServerMetadata {
1648 comment,
1649 admin,
1650 iroh_relay,
1651 ice_servers,
1652 ice_servers_expiration_timestamp,
1653 });
1654 Ok(())
1655 }
1656
1657 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1659 if context.push_subscribed.load(Ordering::Relaxed) {
1660 return Ok(());
1661 }
1662
1663 let Some(device_token) = context.push_subscriber.device_token().await else {
1664 return Ok(());
1665 };
1666
1667 if self.can_metadata() && self.can_push() {
1668 let old_encrypted_device_token =
1669 context.get_config(Config::EncryptedDeviceToken).await?;
1670
1671 let device_token_changed = old_encrypted_device_token.is_none()
1673 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1674
1675 let new_encrypted_device_token;
1676 if device_token_changed {
1677 let encrypted_device_token = encrypt_device_token(&device_token)
1678 .context("Failed to encrypt device token")?;
1679
1680 let encrypted_device_token_len = encrypted_device_token.len();
1684
1685 context
1691 .set_config_internal(Config::DeviceToken, Some(&device_token))
1692 .await?;
1693 context
1694 .set_config_internal(
1695 Config::EncryptedDeviceToken,
1696 Some(&encrypted_device_token),
1697 )
1698 .await?;
1699
1700 if encrypted_device_token_len <= 4096 {
1701 new_encrypted_device_token = Some(encrypted_device_token);
1702 } else {
1703 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1713 new_encrypted_device_token = None;
1714 }
1715 } else {
1716 new_encrypted_device_token = old_encrypted_device_token;
1717 }
1718
1719 if let Some(encrypted_device_token) = new_encrypted_device_token {
1722 let folder = context
1723 .get_config(Config::ConfiguredInboxFolder)
1724 .await?
1725 .context("INBOX is not configured")?;
1726
1727 self.run_command_and_check_ok(&format_setmetadata(
1728 &folder,
1729 &encrypted_device_token,
1730 ))
1731 .await
1732 .context("SETMETADATA command failed")?;
1733
1734 context.push_subscribed.store(true, Ordering::Relaxed);
1735 }
1736 } else if !context.push_subscriber.heartbeat_subscribed().await {
1737 let context = context.clone();
1738 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1740 }
1741
1742 Ok(())
1743 }
1744}
1745
1746fn format_setmetadata(folder: &str, device_token: &str) -> String {
1747 let device_token_len = device_token.len();
1748 format!(
1749 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1750 )
1751}
1752
1753impl Session {
1754 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1760 if flag == "\\Deleted" {
1761 self.selected_folder_needs_expunge = true;
1762 }
1763 let query = format!("+FLAGS ({flag})");
1764 let mut responses = self
1765 .uid_store(uid_set, &query)
1766 .await
1767 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1768 while let Some(_response) = responses.try_next().await? {
1769 }
1771 Ok(())
1772 }
1773
1774 async fn configure_mvbox<'a>(
1783 &mut self,
1784 context: &Context,
1785 folders: &[&'a str],
1786 create_mvbox: bool,
1787 ) -> Result<Option<&'a str>> {
1788 self.maybe_close_folder(context).await?;
1791
1792 for folder in folders {
1793 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1794 let res = self.examine(&folder).await;
1795 if res.is_ok() {
1796 info!(
1797 context,
1798 "MVBOX-folder {:?} successfully selected, using it.", &folder
1799 );
1800 self.close().await?;
1801 let create = false;
1804 let folder_exists = self
1805 .select_with_uidvalidity(context, folder, create)
1806 .await?;
1807 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1808 return Ok(Some(folder));
1809 }
1810 }
1811
1812 if !create_mvbox {
1813 return Ok(None);
1814 }
1815 for folder in folders {
1818 match self
1819 .select_with_uidvalidity(context, folder, create_mvbox)
1820 .await
1821 {
1822 Ok(_) => {
1823 info!(context, "MVBOX-folder {} created.", folder);
1824 return Ok(Some(folder));
1825 }
1826 Err(err) => {
1827 warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1828 }
1829 }
1830 }
1831 Ok(None)
1832 }
1833}
1834
1835impl Imap {
1836 pub(crate) async fn configure_folders(
1837 &mut self,
1838 context: &Context,
1839 session: &mut Session,
1840 create_mvbox: bool,
1841 ) -> Result<()> {
1842 let mut folders = session
1843 .list(Some(""), Some("*"))
1844 .await
1845 .context("list_folders failed")?;
1846 let mut delimiter = ".".to_string();
1847 let mut delimiter_is_default = true;
1848 let mut folder_configs = BTreeMap::new();
1849
1850 while let Some(folder) = folders.try_next().await? {
1851 info!(context, "Scanning folder: {:?}", folder);
1852
1853 if let Some(d) = folder.delimiter()
1855 && delimiter_is_default
1856 && !d.is_empty()
1857 && delimiter != d
1858 {
1859 delimiter = d.to_string();
1860 delimiter_is_default = false;
1861 }
1862
1863 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1864 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1865 if let Some(config) = folder_meaning.to_config() {
1866 folder_configs.insert(config, folder.name().to_string());
1868 } else if let Some(config) = folder_name_meaning.to_config() {
1869 folder_configs
1871 .entry(config)
1872 .or_insert_with(|| folder.name().to_string());
1873 }
1874 }
1875 drop(folders);
1876
1877 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1878
1879 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1880 let mvbox_folder = session
1881 .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1882 .await
1883 .context("failed to configure mvbox")?;
1884
1885 context
1886 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1887 .await?;
1888 if let Some(mvbox_folder) = mvbox_folder {
1889 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1890 context
1891 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1892 .await?;
1893 }
1894 for (config, name) in folder_configs {
1895 context.set_config_internal(config, Some(&name)).await?;
1896 }
1897 context
1898 .sql
1899 .set_raw_config_int(
1900 constants::DC_FOLDERS_CONFIGURED_KEY,
1901 constants::DC_FOLDERS_CONFIGURED_VERSION,
1902 )
1903 .await?;
1904
1905 info!(context, "FINISHED configuring IMAP-folders.");
1906 Ok(())
1907 }
1908}
1909
1910impl Session {
1911 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1920 use UnsolicitedResponse::*;
1921 use async_imap::imap_proto::Response;
1922 use async_imap::imap_proto::ResponseCode;
1923
1924 let folder = self.selected_folder.as_deref().unwrap_or_default();
1925 let mut should_refetch = false;
1926 while let Ok(response) = self.unsolicited_responses.try_recv() {
1927 match response {
1928 Exists(_) => {
1929 info!(
1930 context,
1931 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1932 );
1933 should_refetch = true;
1934 }
1935
1936 Expunge(_) | Recent(_) => {}
1937 Other(ref response_data) => {
1938 match response_data.parsed() {
1939 Response::Fetch { .. } => {
1940 info!(
1941 context,
1942 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1943 );
1944 should_refetch = true;
1945 }
1946
1947 Response::Done {
1950 code: Some(ResponseCode::CopyUid(_, _, _)),
1951 ..
1952 } => {}
1953
1954 _ => {
1955 info!(context, "{folder:?}: got unsolicited response {response:?}")
1956 }
1957 }
1958 }
1959 _ => {
1960 info!(context, "{folder:?}: got unsolicited response {response:?}")
1961 }
1962 }
1963 }
1964 Ok(should_refetch)
1965 }
1966}
1967
1968async fn should_move_out_of_spam(
1969 context: &Context,
1970 headers: &[mailparse::MailHeader<'_>],
1971) -> Result<bool> {
1972 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1973 return Ok(true);
1984 }
1985
1986 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1987 if msg.chat_blocked != Blocked::Not {
1988 return Ok(false);
1990 }
1991 } else {
1992 let from = match mimeparser::get_from(headers) {
1993 Some(f) => f,
1994 None => return Ok(false),
1995 };
1996 let (from_id, blocked_contact, _origin) =
1998 match from_field_to_contact_id(context, &from, None, true, true)
1999 .await
2000 .context("from_field_to_contact_id")?
2001 {
2002 Some(res) => res,
2003 None => {
2004 warn!(
2005 context,
2006 "Contact with From address {:?} cannot exist, not moving out of spam", from
2007 );
2008 return Ok(false);
2009 }
2010 };
2011 if blocked_contact {
2012 return Ok(false);
2014 }
2015
2016 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2017 if chat_id_blocked.blocked != Blocked::Not {
2018 return Ok(false);
2019 }
2020 } else if from_id != ContactId::SELF {
2021 return Ok(false);
2023 }
2024 }
2025
2026 Ok(true)
2027}
2028
2029async fn spam_target_folder_cfg(
2034 context: &Context,
2035 headers: &[mailparse::MailHeader<'_>],
2036) -> Result<Option<Config>> {
2037 if !should_move_out_of_spam(context, headers).await? {
2038 return Ok(None);
2039 }
2040
2041 if needs_move_to_mvbox(context, headers).await?
2042 || context.get_config_bool(Config::OnlyFetchMvbox).await?
2045 {
2046 Ok(Some(Config::ConfiguredMvboxFolder))
2047 } else {
2048 Ok(Some(Config::ConfiguredInboxFolder))
2049 }
2050}
2051
2052pub async fn target_folder_cfg(
2055 context: &Context,
2056 folder: &str,
2057 folder_meaning: FolderMeaning,
2058 headers: &[mailparse::MailHeader<'_>],
2059) -> Result<Option<Config>> {
2060 if context.is_mvbox(folder).await? {
2061 return Ok(None);
2062 }
2063
2064 if folder_meaning == FolderMeaning::Spam {
2065 spam_target_folder_cfg(context, headers).await
2066 } else if folder_meaning == FolderMeaning::Inbox
2067 && needs_move_to_mvbox(context, headers).await?
2068 {
2069 Ok(Some(Config::ConfiguredMvboxFolder))
2070 } else {
2071 Ok(None)
2072 }
2073}
2074
2075pub async fn target_folder(
2076 context: &Context,
2077 folder: &str,
2078 folder_meaning: FolderMeaning,
2079 headers: &[mailparse::MailHeader<'_>],
2080) -> Result<String> {
2081 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2082 Some(config) => match context.get_config(config).await? {
2083 Some(target) => Ok(target),
2084 None => Ok(folder.to_string()),
2085 },
2086 None => Ok(folder.to_string()),
2087 }
2088}
2089
2090async fn needs_move_to_mvbox(
2091 context: &Context,
2092 headers: &[mailparse::MailHeader<'_>],
2093) -> Result<bool> {
2094 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2095 if !context.get_config_bool(Config::IsChatmail).await?
2096 && has_chat_version
2097 && headers
2098 .get_header_value(HeaderDef::AutoSubmitted)
2099 .filter(|val| val.eq_ignore_ascii_case("auto-generated"))
2100 .is_some()
2101 && let Some(from) = mimeparser::get_from(headers)
2102 && context.is_self_addr(&from.addr).await?
2103 {
2104 return Ok(true);
2105 }
2106 if !context.get_config_bool(Config::MvboxMove).await? {
2107 return Ok(false);
2108 }
2109
2110 if headers
2111 .get_header_value(HeaderDef::AutocryptSetupMessage)
2112 .is_some()
2113 {
2114 return Ok(false);
2117 }
2118
2119 if has_chat_version {
2120 Ok(true)
2121 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2122 match parent.is_dc_message {
2123 MessengerMessage::No => Ok(false),
2124 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2125 }
2126 } else {
2127 Ok(false)
2128 }
2129}
2130
2131fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2138 const SPAM_NAMES: &[&str] = &[
2140 "spam",
2141 "junk",
2142 "Correio electrónico não solicitado",
2143 "Correo basura",
2144 "Lixo",
2145 "Nettsøppel",
2146 "Nevyžádaná pošta",
2147 "No solicitado",
2148 "Ongewenst",
2149 "Posta indesiderata",
2150 "Skräp",
2151 "Wiadomości-śmieci",
2152 "Önemsiz",
2153 "Ανεπιθύμητα",
2154 "Спам",
2155 "垃圾邮件",
2156 "垃圾郵件",
2157 "迷惑メール",
2158 "스팸",
2159 ];
2160 const TRASH_NAMES: &[&str] = &[
2161 "Trash",
2162 "Bin",
2163 "Caixote do lixo",
2164 "Cestino",
2165 "Corbeille",
2166 "Papelera",
2167 "Papierkorb",
2168 "Papirkurv",
2169 "Papperskorgen",
2170 "Prullenbak",
2171 "Rubujo",
2172 "Κάδος απορριμμάτων",
2173 "Корзина",
2174 "Кошик",
2175 "ゴミ箱",
2176 "垃圾桶",
2177 "已删除邮件",
2178 "휴지통",
2179 ];
2180 let lower = folder_name.to_lowercase();
2181
2182 if lower == "inbox" {
2183 FolderMeaning::Inbox
2184 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2185 FolderMeaning::Spam
2186 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2187 FolderMeaning::Trash
2188 } else {
2189 FolderMeaning::Unknown
2190 }
2191}
2192
2193fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2194 for attr in folder_attrs {
2195 match attr {
2196 NameAttribute::Trash => return FolderMeaning::Trash,
2197 NameAttribute::Junk => return FolderMeaning::Spam,
2198 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2199 NameAttribute::Extension(label) => {
2200 match label.as_ref() {
2201 "\\Spam" => return FolderMeaning::Spam,
2202 "\\Important" => return FolderMeaning::Virtual,
2203 _ => {}
2204 };
2205 }
2206 _ => {}
2207 }
2208 }
2209 FolderMeaning::Unknown
2210}
2211
2212pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2213 match get_folder_meaning_by_attrs(folder.attributes()) {
2214 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2215 meaning => meaning,
2216 }
2217}
2218
2219fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2221 match prefetch_msg.header() {
2222 Some(header_bytes) => {
2223 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2224 Ok(headers)
2225 }
2226 None => Ok(Vec::new()),
2227 }
2228}
2229
2230pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2231 headers
2232 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2233 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2234 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2235}
2236
2237pub(crate) fn create_message_id() -> String {
2238 format!("{}{}", GENERATED_PREFIX, create_id())
2239}
2240
2241async fn prefetch_get_chat(
2243 context: &Context,
2244 headers: &[mailparse::MailHeader<'_>],
2245) -> Result<Option<chat::Chat>> {
2246 let parent = get_prefetch_parent_message(context, headers).await?;
2247 if let Some(parent) = &parent {
2248 return Ok(Some(
2249 chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
2250 ));
2251 }
2252
2253 Ok(None)
2254}
2255
2256pub(crate) async fn prefetch_should_download(
2258 context: &Context,
2259 headers: &[mailparse::MailHeader<'_>],
2260 message_id: &str,
2261 mut flags: impl Iterator<Item = Flag<'_>>,
2262) -> Result<bool> {
2263 if message::rfc724_mid_exists(context, message_id)
2264 .await?
2265 .is_some()
2266 {
2267 markseen_on_imap_table(context, message_id).await?;
2268 return Ok(false);
2269 }
2270
2271 if let Some(chat) = prefetch_get_chat(context, headers).await?
2275 && chat.typ == Chattype::Group
2276 && !chat.id.is_special()
2277 {
2278 return Ok(true);
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 && last.end + 1 == current
2529 {
2530 last.end = current;
2531 continue;
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;