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