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