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::{
27 UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
28};
29use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
30use crate::chatlist_events;
31use crate::config::Config;
32use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
33use crate::contact::{Contact, ContactId, Modifier, Origin};
34use crate::context::Context;
35use crate::events::EventType;
36use crate::headerdef::{HeaderDef, HeaderDefMap};
37use crate::log::{LogExt, warn};
38use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
39use crate::mimeparser;
40use crate::net::proxy::ProxyConfig;
41use crate::net::session::SessionStream;
42use crate::oauth2::get_oauth2_access_token;
43use crate::push::encrypt_device_token;
44use crate::receive_imf::{
45 ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
46};
47use crate::scheduler::connectivity::ConnectivityStore;
48use crate::stock_str;
49use crate::tools::{self, create_id, duration_to_str, time};
50use crate::transport::{
51 ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
52};
53
54pub(crate) mod capabilities;
55mod client;
56mod idle;
57pub mod scan_folders;
58pub mod select_folder;
59pub(crate) mod session;
60
61use client::{Client, determine_capabilities};
62use mailparse::SingleInfo;
63use session::Session;
64
65pub(crate) const GENERATED_PREFIX: &str = "GEN_";
66
67const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
68 MESSAGE-ID \
69 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
70 )])";
71const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
72
73#[derive(Debug)]
74pub(crate) struct Imap {
75 transport_id: u32,
79
80 pub(crate) idle_interrupt_receiver: Receiver<()>,
81
82 pub(crate) addr: String,
84
85 lp: Vec<ConfiguredServerLoginParam>,
87
88 password: String,
90
91 proxy_config: Option<ProxyConfig>,
93
94 strict_tls: bool,
95
96 oauth2: bool,
97
98 authentication_failed_once: bool,
99
100 pub(crate) connectivity: ConnectivityStore,
101
102 conn_last_try: tools::Time,
103 conn_backoff_ms: u64,
104
105 ratelimit: Ratelimit,
113
114 pub(crate) resync_request_sender: async_channel::Sender<()>,
116
117 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
119}
120
121#[derive(Debug)]
122struct OAuth2 {
123 user: String,
124 access_token: String,
125}
126
127#[derive(Debug, Default)]
128pub(crate) struct ServerMetadata {
129 pub comment: Option<String>,
132
133 pub admin: Option<String>,
136
137 pub iroh_relay: Option<Url>,
138
139 pub ice_servers: Vec<UnresolvedIceServer>,
141
142 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 mut uids_fetch: Vec<u32> = Vec::new();
619 let mut available_post_msgs: Vec<String> = Vec::new();
620 let mut download_later: Vec<String> = Vec::new();
621 let mut uid_message_ids = BTreeMap::new();
622 let mut largest_uid_skipped = None;
623 let delete_target = context.get_delete_msgs_target().await?;
624
625 let download_limit: Option<u32> = context
626 .get_config_parsed(Config::DownloadLimit)
627 .await?
628 .filter(|&l| 0 < l);
629
630 for (uid, ref fetch_response) in msgs {
632 let headers = match get_fetch_headers(fetch_response) {
633 Ok(headers) => headers,
634 Err(err) => {
635 warn!(context, "Failed to parse FETCH headers: {err:#}.");
636 continue;
637 }
638 };
639
640 let message_id = prefetch_get_message_id(&headers);
641 let size = fetch_response
642 .size
643 .context("imap fetch response does not contain size")?;
644
645 let delete = if let Some(message_id) = &message_id {
656 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
657 .await?
658 .is_some_and(|(_msg_id, deleted)| deleted)
659 } else {
660 false
661 };
662
663 let message_id = message_id.unwrap_or_else(create_message_id);
666
667 if delete {
668 info!(context, "Deleting locally deleted message {message_id}.");
669 }
670
671 let _target;
672 let target = if delete {
673 &delete_target
674 } else {
675 _target = target_folder(context, folder, folder_meaning, &headers).await?;
676 &_target
677 };
678
679 context
680 .sql
681 .execute(
682 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
683 VALUES (?, ?, ?, ?, ?, ?)
684 ON CONFLICT(transport_id, folder, uid, uidvalidity)
685 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
686 target=excluded.target",
687 (
688 self.transport_id,
689 &message_id,
690 &folder,
691 uid,
692 uid_validity,
693 target,
694 ),
695 )
696 .await?;
697
698 if folder == target
705 && folder_meaning != FolderMeaning::Spam
710 && prefetch_should_download(
711 context,
712 &headers,
713 &message_id,
714 fetch_response.flags(),
715 )
716 .await.context("prefetch_should_download")?
717 {
718 if headers
719 .get_header_value(HeaderDef::ChatIsPostMessage)
720 .is_some()
721 {
722 info!(context, "{message_id:?} is a post-message.");
723 available_post_msgs.push(message_id.clone());
724
725 if download_limit.is_none_or(|download_limit| size <= download_limit) {
726 download_later.push(message_id.clone());
727 }
728 largest_uid_skipped = Some(uid);
729 } else {
730 info!(context, "{message_id:?} is not a post-message.");
731 if download_limit.is_none_or(|download_limit| size <= download_limit) {
732 uids_fetch.push(uid);
733 uid_message_ids.insert(uid, message_id);
734 } else {
735 download_later.push(message_id.clone());
736 largest_uid_skipped = Some(uid);
737 }
738 };
739 } else {
740 largest_uid_skipped = Some(uid);
741 }
742 }
743
744 if !uids_fetch.is_empty() {
745 self.connectivity.set_working(context);
746 }
747
748 let (sender, receiver) = async_channel::unbounded();
749
750 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
751 let mailbox_uid_next = session
752 .selected_mailbox
753 .as_ref()
754 .with_context(|| format!("Expected {folder:?} to be selected"))?
755 .uid_next
756 .unwrap_or_default();
757
758 let update_uids_future = async {
759 let mut largest_uid_fetched: u32 = 0;
760
761 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
762 largest_uid_fetched = max(largest_uid_fetched, uid);
763 if let Some(received_msg) = received_msg_opt {
764 received_msgs.push(received_msg)
765 }
766 }
767
768 largest_uid_fetched
769 };
770
771 let actually_download_messages_future = async {
772 session
773 .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
774 .await
775 .context("fetch_many_msgs")
776 };
777
778 let (largest_uid_fetched, fetch_res) =
779 tokio::join!(update_uids_future, actually_download_messages_future);
780
781 let mut new_uid_next = largest_uid_fetched + 1;
787 let fetch_more = fetch_res.is_ok() && {
788 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
789 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
793
794 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
795
796 prefetch_uid_next < mailbox_uid_next
797 };
798 if new_uid_next > old_uid_next {
799 set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
800 }
801
802 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
803
804 if !received_msgs.is_empty() {
805 context.emit_event(EventType::IncomingMsgBunch);
806 }
807
808 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
809
810 if fetch_res.is_ok() {
811 info!(
812 context,
813 "available_post_msgs: {}, download_later: {}.",
814 available_post_msgs.len(),
815 download_later.len(),
816 );
817 let trans_fn = |t: &mut rusqlite::Transaction| {
818 let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
819 for rfc724_mid in available_post_msgs {
820 stmt.execute((rfc724_mid,))
821 .context("INSERT OR IGNORE INTO available_post_msgs")?;
822 }
823 let mut stmt =
824 t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
825 for rfc724_mid in download_later {
826 stmt.execute((rfc724_mid,))
827 .context("INSERT OR IGNORE INTO download")?;
828 }
829 Ok(())
830 };
831 context.sql.transaction(trans_fn).await?;
832 }
833
834 fetch_res?;
837
838 Ok((read_cnt, fetch_more))
839 }
840
841 pub(crate) async fn fetch_existing_msgs(
847 &mut self,
848 context: &Context,
849 session: &mut Session,
850 ) -> Result<()> {
851 add_all_recipients_as_contacts(context, session, Config::ConfiguredMvboxFolder)
852 .await
853 .context("failed to get recipients from the movebox")?;
854 add_all_recipients_as_contacts(context, session, Config::ConfiguredInboxFolder)
855 .await
856 .context("failed to get recipients from the inbox")?;
857
858 info!(context, "Done fetching existing messages.");
859 Ok(())
860 }
861}
862
863impl Session {
864 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
866 let all_folders = self
867 .list_folders()
868 .await
869 .context("listing folders for resync")?;
870 for folder in all_folders {
871 let folder_meaning = get_folder_meaning(&folder);
872 if !matches!(
873 folder_meaning,
874 FolderMeaning::Virtual | FolderMeaning::Unknown
875 ) {
876 self.resync_folder_uids(context, folder.name(), folder_meaning)
877 .await?;
878 }
879 }
880 Ok(())
881 }
882
883 pub(crate) async fn resync_folder_uids(
890 &mut self,
891 context: &Context,
892 folder: &str,
893 folder_meaning: FolderMeaning,
894 ) -> Result<()> {
895 let uid_validity;
896 let mut msgs = BTreeMap::new();
898
899 let create = false;
900 let folder_exists = self
901 .select_with_uidvalidity(context, folder, create)
902 .await?;
903 let transport_id = self.transport_id();
904 if folder_exists {
905 let mut list = self
906 .uid_fetch("1:*", RFC724MID_UID)
907 .await
908 .with_context(|| format!("Can't resync folder {folder}"))?;
909 while let Some(fetch) = list.try_next().await? {
910 let headers = match get_fetch_headers(&fetch) {
911 Ok(headers) => headers,
912 Err(err) => {
913 warn!(context, "Failed to parse FETCH headers: {}", err);
914 continue;
915 }
916 };
917 let message_id = prefetch_get_message_id(&headers);
918
919 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
920 msgs.insert(
921 uid,
922 (
923 rfc724_mid,
924 target_folder(context, folder, folder_meaning, &headers).await?,
925 ),
926 );
927 }
928 }
929
930 info!(
931 context,
932 "resync_folder_uids: Collected {} message IDs in {folder}.",
933 msgs.len(),
934 );
935
936 uid_validity = get_uidvalidity(context, transport_id, folder).await?;
937 } else {
938 warn!(context, "resync_folder_uids: No folder {folder}.");
939 uid_validity = 0;
940 }
941
942 context
944 .sql
945 .transaction(move |transaction| {
946 transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
947 for (uid, (rfc724_mid, target)) in &msgs {
948 transaction.execute(
951 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
952 VALUES (?, ?, ?, ?, ?, ?)
953 ON CONFLICT(transport_id, folder, uid, uidvalidity)
954 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
955 target=excluded.target",
956 (transport_id, rfc724_mid, folder, uid, uid_validity, target),
957 )?;
958 }
959 Ok(())
960 })
961 .await?;
962 Ok(())
963 }
964
965 async fn delete_message_batch(
968 &mut self,
969 context: &Context,
970 uid_set: &str,
971 row_ids: Vec<i64>,
972 ) -> Result<()> {
973 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
975 .await?;
976 context
977 .sql
978 .transaction(|transaction| {
979 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
980 for row_id in row_ids {
981 stmt.execute((row_id,))?;
982 }
983 Ok(())
984 })
985 .await
986 .context("Cannot remove deleted messages from imap table")?;
987
988 context.emit_event(EventType::ImapMessageDeleted(format!(
989 "IMAP messages {uid_set} marked as deleted"
990 )));
991 Ok(())
992 }
993
994 async fn move_message_batch(
997 &mut self,
998 context: &Context,
999 set: &str,
1000 row_ids: Vec<i64>,
1001 target: &str,
1002 ) -> Result<()> {
1003 if self.can_move() {
1004 match self.uid_mv(set, &target).await {
1005 Ok(()) => {
1006 context
1008 .sql
1009 .transaction(|transaction| {
1010 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
1011 for row_id in row_ids {
1012 stmt.execute((row_id,))?;
1013 }
1014 Ok(())
1015 })
1016 .await
1017 .context("Cannot delete moved messages from imap table")?;
1018 context.emit_event(EventType::ImapMessageMoved(format!(
1019 "IMAP messages {set} moved to {target}"
1020 )));
1021 return Ok(());
1022 }
1023 Err(err) => {
1024 if context.should_delete_to_trash().await? {
1025 error!(
1026 context,
1027 "Cannot move messages {} to {}, no fallback to COPY/DELETE because \
1028 delete_to_trash is set. Error: {:#}",
1029 set,
1030 target,
1031 err,
1032 );
1033 return Err(err.into());
1034 }
1035 warn!(
1036 context,
1037 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
1038 set,
1039 target,
1040 err
1041 );
1042 }
1043 }
1044 }
1045
1046 let copy = !context.is_trash(target).await?;
1049 if copy {
1050 info!(
1051 context,
1052 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1053 );
1054 self.uid_copy(&set, &target).await?;
1055 } else {
1056 error!(
1057 context,
1058 "Server does not support MOVE, fallback to DELETE {} to {}", set, target,
1059 );
1060 }
1061 context
1062 .sql
1063 .transaction(|transaction| {
1064 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1065 for row_id in row_ids {
1066 stmt.execute((row_id,))?;
1067 }
1068 Ok(())
1069 })
1070 .await
1071 .context("Cannot plan deletion of messages")?;
1072 if copy {
1073 context.emit_event(EventType::ImapMessageMoved(format!(
1074 "IMAP messages {set} copied to {target}"
1075 )));
1076 }
1077 Ok(())
1078 }
1079
1080 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1084 let transport_id = self.transport_id();
1085 let rows = context
1086 .sql
1087 .query_map_vec(
1088 "SELECT id, uid, target FROM imap
1089 WHERE folder = ?
1090 AND transport_id = ?
1091 AND target != folder
1092 ORDER BY target, uid",
1093 (folder, transport_id),
1094 |row| {
1095 let rowid: i64 = row.get(0)?;
1096 let uid: u32 = row.get(1)?;
1097 let target: String = row.get(2)?;
1098 Ok((rowid, uid, target))
1099 },
1100 )
1101 .await?;
1102
1103 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1104 let create = false;
1109 let folder_exists = self
1110 .select_with_uidvalidity(context, folder, create)
1111 .await?;
1112 ensure!(folder_exists, "No folder {folder}");
1113
1114 if target.is_empty() {
1116 self.delete_message_batch(context, &uid_set, rowid_set)
1117 .await
1118 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1119 } else {
1120 self.move_message_batch(context, &uid_set, rowid_set, &target)
1121 .await
1122 .with_context(|| {
1123 format!(
1124 "cannot move batch of messages {:?} to folder {:?}",
1125 &uid_set, target
1126 )
1127 })?;
1128 }
1129 }
1130
1131 if let Err(err) = self.maybe_close_folder(context).await {
1134 warn!(context, "Failed to close folder: {err:#}.");
1135 }
1136
1137 Ok(())
1138 }
1139
1140 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1142 if context.get_config_bool(Config::TeamProfile).await? {
1143 info!(context, "Team profile, skipping seen flag synchronization.");
1144 return Ok(());
1145 }
1146
1147 let transport_id = self.transport_id();
1148 let rows = context
1149 .sql
1150 .query_map_vec(
1151 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1152 WHERE imap.id = imap_markseen.id
1153 AND imap.transport_id=?
1154 AND target = folder
1155 ORDER BY folder, uid",
1156 (transport_id,),
1157 |row| {
1158 let rowid: i64 = row.get(0)?;
1159 let uid: u32 = row.get(1)?;
1160 let folder: String = row.get(2)?;
1161 Ok((rowid, uid, folder))
1162 },
1163 )
1164 .await?;
1165
1166 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1167 let create = false;
1168 let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
1169 Err(err) => {
1170 warn!(
1171 context,
1172 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1173 );
1174 continue;
1175 }
1176 Ok(folder_exists) => folder_exists,
1177 };
1178 if !folder_exists {
1179 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1180 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1181 warn!(
1182 context,
1183 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1184 );
1185 continue;
1186 } else {
1187 info!(
1188 context,
1189 "Marked messages {} in folder {} as seen.", uid_set, folder
1190 );
1191 }
1192 context
1193 .sql
1194 .transaction(|transaction| {
1195 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1196 for rowid in rowid_set {
1197 stmt.execute((rowid,))?;
1198 }
1199 Ok(())
1200 })
1201 .await
1202 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1203 }
1204
1205 Ok(())
1206 }
1207
1208 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1210 if !self.can_condstore() {
1211 info!(
1212 context,
1213 "Server does not support CONDSTORE, skipping flag synchronization."
1214 );
1215 return Ok(());
1216 }
1217
1218 if context.get_config_bool(Config::TeamProfile).await? {
1219 info!(context, "Team profile, skipping seen flag synchronization.");
1220 return Ok(());
1221 }
1222
1223 let create = false;
1224 let folder_exists = self
1225 .select_with_uidvalidity(context, folder, create)
1226 .await
1227 .context("Failed to select folder")?;
1228 if !folder_exists {
1229 return Ok(());
1230 }
1231
1232 let mailbox = self
1233 .selected_mailbox
1234 .as_ref()
1235 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1236
1237 if mailbox.highest_modseq.is_none() {
1240 info!(
1241 context,
1242 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1243 );
1244 return Ok(());
1245 }
1246
1247 let transport_id = self.transport_id();
1248 let mut updated_chat_ids = BTreeSet::new();
1249 let uid_validity = get_uidvalidity(context, transport_id, folder)
1250 .await
1251 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1252 let mut highest_modseq = get_modseq(context, transport_id, folder)
1253 .await
1254 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1255 let mut list = self
1256 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1257 .await
1258 .context("failed to fetch flags")?;
1259
1260 let mut got_unsolicited_fetch = false;
1261
1262 while let Some(fetch) = list
1263 .try_next()
1264 .await
1265 .context("failed to get FETCH result")?
1266 {
1267 let uid = if let Some(uid) = fetch.uid {
1268 uid
1269 } else {
1270 info!(context, "FETCH result contains no UID, skipping");
1271 got_unsolicited_fetch = true;
1272 continue;
1273 };
1274 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1275 if is_seen
1276 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1277 .await
1278 .with_context(|| {
1279 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1280 })?
1281 {
1282 updated_chat_ids.insert(chat_id);
1283 }
1284
1285 if let Some(modseq) = fetch.modseq {
1286 if modseq > highest_modseq {
1287 highest_modseq = modseq;
1288 }
1289 } else {
1290 warn!(context, "FETCH result contains no MODSEQ");
1291 }
1292 }
1293 drop(list);
1294
1295 if got_unsolicited_fetch {
1296 self.new_mail = true;
1301 }
1302
1303 set_modseq(context, transport_id, folder, highest_modseq)
1304 .await
1305 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1306 if !updated_chat_ids.is_empty() {
1307 context.on_archived_chats_maybe_noticed();
1308 }
1309 for updated_chat_id in updated_chat_ids {
1310 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1311 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1312 }
1313
1314 Ok(())
1315 }
1316
1317 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1319 let mut uids: Vec<_> = self
1320 .uid_search(get_imap_self_sent_search_command(context).await?)
1321 .await?
1322 .into_iter()
1323 .collect();
1324 uids.sort_unstable();
1325
1326 let mut result = Vec::new();
1327 for (_, uid_set) in build_sequence_sets(&uids)? {
1328 let mut list = self
1329 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1330 .await
1331 .context("IMAP Could not fetch")?;
1332
1333 while let Some(msg) = list.try_next().await? {
1334 match get_fetch_headers(&msg) {
1335 Ok(headers) => {
1336 if let Some(from) = mimeparser::get_from(&headers)
1337 && context.is_self_addr(&from.addr).await?
1338 {
1339 result.extend(mimeparser::get_recipients(&headers));
1340 }
1341 }
1342 Err(err) => {
1343 warn!(context, "{}", err);
1344 continue;
1345 }
1346 };
1347 }
1348 }
1349 Ok(result)
1350 }
1351
1352 pub(crate) async fn fetch_many_msgs(
1367 &mut self,
1368 context: &Context,
1369 folder: &str,
1370 request_uids: Vec<u32>,
1371 uid_message_ids: &BTreeMap<u32, String>,
1372 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1373 ) -> Result<()> {
1374 if request_uids.is_empty() {
1375 return Ok(());
1376 }
1377
1378 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1379 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1380 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1381 format!("fetching messages {} from folder \"{}\"", &set, folder)
1382 })?;
1383
1384 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1387
1388 let mut count = 0;
1389 for &request_uid in &request_uids {
1390 let mut fetch_response = uid_msgs.remove(&request_uid);
1392
1393 while fetch_response.is_none() {
1395 let Some(next_fetch_response) = fetch_responses
1396 .try_next()
1397 .await
1398 .context("Failed to process IMAP FETCH result")?
1399 else {
1400 break;
1402 };
1403
1404 if let Some(next_uid) = next_fetch_response.uid {
1405 if next_uid == request_uid {
1406 fetch_response = Some(next_fetch_response);
1407 } else if !request_uids.contains(&next_uid) {
1408 info!(
1415 context,
1416 "Skipping not requested FETCH response for UID {}.", next_uid
1417 );
1418 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1419 warn!(context, "Got duplicated UID {}.", next_uid);
1420 }
1421 } else {
1422 info!(context, "Skipping FETCH response without UID.");
1423 }
1424 }
1425
1426 let fetch_response = match fetch_response {
1427 Some(fetch) => fetch,
1428 None => {
1429 warn!(
1430 context,
1431 "Missed UID {} in the server response.", request_uid
1432 );
1433 continue;
1434 }
1435 };
1436 count += 1;
1437
1438 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1439 let body = fetch_response.body();
1440
1441 if is_deleted {
1442 info!(context, "Not processing deleted msg {}.", request_uid);
1443 received_msgs_channel.send((request_uid, None)).await?;
1444 continue;
1445 }
1446
1447 let body = if let Some(body) = body {
1448 body
1449 } else {
1450 info!(
1451 context,
1452 "Not processing message {} without a BODY.", request_uid
1453 );
1454 received_msgs_channel.send((request_uid, None)).await?;
1455 continue;
1456 };
1457
1458 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1459
1460 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1461 error!(
1462 context,
1463 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1464 request_uid
1465 );
1466 continue;
1467 };
1468
1469 info!(
1470 context,
1471 "Passing message UID {} to receive_imf().", request_uid
1472 );
1473 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1474 let received_msg = match res {
1475 Err(err) => {
1476 warn!(context, "receive_imf error: {err:#}.");
1477
1478 let text = format!(
1479 "❌ 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/.",
1480 );
1481 let mut msg = Message::new_text(text);
1482 add_device_msg(context, None, Some(&mut msg)).await?;
1483 None
1484 }
1485 Ok(msg) => msg,
1486 };
1487 received_msgs_channel
1488 .send((request_uid, received_msg))
1489 .await?;
1490 }
1491
1492 while fetch_responses
1499 .try_next()
1500 .await
1501 .context("Failed to drain FETCH responses")?
1502 .is_some()
1503 {}
1504
1505 if count != request_uids.len() {
1506 warn!(
1507 context,
1508 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1509 count,
1510 request_uids.len(),
1511 request_uids,
1512 );
1513 } else {
1514 info!(
1515 context,
1516 "Successfully received {} UIDs.",
1517 request_uids.len()
1518 );
1519 }
1520 }
1521
1522 Ok(())
1523 }
1524
1525 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1531 let mut lock = context.metadata.write().await;
1532
1533 if !self.can_metadata() {
1534 *lock = Some(Default::default());
1535 }
1536 if let Some(ref mut old_metadata) = *lock {
1537 let now = time();
1538
1539 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1541 return Ok(());
1542 }
1543
1544 let mut got_turn_server = false;
1545 if self.can_metadata() {
1546 info!(context, "ICE servers expired, requesting new credentials.");
1547 let mailbox = "";
1548 let options = "";
1549 let metadata = self
1550 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1551 .await?;
1552 for m in metadata {
1553 if m.entry == "/shared/vendor/deltachat/turn"
1554 && let Some(value) = m.value
1555 {
1556 match create_ice_servers_from_metadata(&value).await {
1557 Ok((parsed_timestamp, parsed_ice_servers)) => {
1558 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1559 old_metadata.ice_servers = parsed_ice_servers;
1560 got_turn_server = true;
1561 }
1562 Err(err) => {
1563 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1564 }
1565 }
1566 }
1567 }
1568 }
1569 if !got_turn_server {
1570 info!(context, "Will use fallback ICE servers.");
1571 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1573 old_metadata.ice_servers = create_fallback_ice_servers();
1574 }
1575 return Ok(());
1576 }
1577
1578 info!(
1579 context,
1580 "Server supports metadata, retrieving server comment and admin contact."
1581 );
1582
1583 let mut comment = None;
1584 let mut admin = None;
1585 let mut iroh_relay = None;
1586 let mut ice_servers = None;
1587 let mut ice_servers_expiration_timestamp = 0;
1588
1589 let mailbox = "";
1590 let options = "";
1591 let metadata = self
1592 .get_metadata(
1593 mailbox,
1594 options,
1595 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1596 )
1597 .await?;
1598 for m in metadata {
1599 match m.entry.as_ref() {
1600 "/shared/comment" => {
1601 comment = m.value;
1602 }
1603 "/shared/admin" => {
1604 admin = m.value;
1605 }
1606 "/shared/vendor/deltachat/irohrelay" => {
1607 if let Some(value) = m.value {
1608 if let Ok(url) = Url::parse(&value) {
1609 iroh_relay = Some(url);
1610 } else {
1611 warn!(
1612 context,
1613 "Got invalid URL from iroh relay metadata: {:?}.", value
1614 );
1615 }
1616 }
1617 }
1618 "/shared/vendor/deltachat/turn" => {
1619 if let Some(value) = m.value {
1620 match create_ice_servers_from_metadata(&value).await {
1621 Ok((parsed_timestamp, parsed_ice_servers)) => {
1622 ice_servers_expiration_timestamp = parsed_timestamp;
1623 ice_servers = Some(parsed_ice_servers);
1624 }
1625 Err(err) => {
1626 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1627 }
1628 }
1629 }
1630 }
1631 _ => {}
1632 }
1633 }
1634 let ice_servers = if let Some(ice_servers) = ice_servers {
1635 ice_servers
1636 } else {
1637 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1639 create_fallback_ice_servers()
1640 };
1641
1642 *lock = Some(ServerMetadata {
1643 comment,
1644 admin,
1645 iroh_relay,
1646 ice_servers,
1647 ice_servers_expiration_timestamp,
1648 });
1649 Ok(())
1650 }
1651
1652 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1654 if context.push_subscribed.load(Ordering::Relaxed) {
1655 return Ok(());
1656 }
1657
1658 let Some(device_token) = context.push_subscriber.device_token().await else {
1659 return Ok(());
1660 };
1661
1662 if self.can_metadata() && self.can_push() {
1663 let old_encrypted_device_token =
1664 context.get_config(Config::EncryptedDeviceToken).await?;
1665
1666 let device_token_changed = old_encrypted_device_token.is_none()
1668 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1669
1670 let new_encrypted_device_token;
1671 if device_token_changed {
1672 let encrypted_device_token = encrypt_device_token(&device_token)
1673 .context("Failed to encrypt device token")?;
1674
1675 let encrypted_device_token_len = encrypted_device_token.len();
1679
1680 context
1686 .set_config_internal(Config::DeviceToken, Some(&device_token))
1687 .await?;
1688 context
1689 .set_config_internal(
1690 Config::EncryptedDeviceToken,
1691 Some(&encrypted_device_token),
1692 )
1693 .await?;
1694
1695 if encrypted_device_token_len <= 4096 {
1696 new_encrypted_device_token = Some(encrypted_device_token);
1697 } else {
1698 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1708 new_encrypted_device_token = None;
1709 }
1710 } else {
1711 new_encrypted_device_token = old_encrypted_device_token;
1712 }
1713
1714 if let Some(encrypted_device_token) = new_encrypted_device_token {
1717 let folder = context
1718 .get_config(Config::ConfiguredInboxFolder)
1719 .await?
1720 .context("INBOX is not configured")?;
1721
1722 self.run_command_and_check_ok(&format_setmetadata(
1723 &folder,
1724 &encrypted_device_token,
1725 ))
1726 .await
1727 .context("SETMETADATA command failed")?;
1728
1729 context.push_subscribed.store(true, Ordering::Relaxed);
1730 }
1731 } else if !context.push_subscriber.heartbeat_subscribed().await {
1732 let context = context.clone();
1733 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1735 }
1736
1737 Ok(())
1738 }
1739}
1740
1741fn format_setmetadata(folder: &str, device_token: &str) -> String {
1742 let device_token_len = device_token.len();
1743 format!(
1744 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1745 )
1746}
1747
1748impl Session {
1749 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1755 if flag == "\\Deleted" {
1756 self.selected_folder_needs_expunge = true;
1757 }
1758 let query = format!("+FLAGS ({flag})");
1759 let mut responses = self
1760 .uid_store(uid_set, &query)
1761 .await
1762 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1763 while let Some(_response) = responses.try_next().await? {
1764 }
1766 Ok(())
1767 }
1768
1769 async fn configure_mvbox<'a>(
1778 &mut self,
1779 context: &Context,
1780 folders: &[&'a str],
1781 create_mvbox: bool,
1782 ) -> Result<Option<&'a str>> {
1783 self.maybe_close_folder(context).await?;
1786
1787 for folder in folders {
1788 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1789 let res = self.examine(&folder).await;
1790 if res.is_ok() {
1791 info!(
1792 context,
1793 "MVBOX-folder {:?} successfully selected, using it.", &folder
1794 );
1795 self.close().await?;
1796 let create = false;
1799 let folder_exists = self
1800 .select_with_uidvalidity(context, folder, create)
1801 .await?;
1802 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1803 return Ok(Some(folder));
1804 }
1805 }
1806
1807 if !create_mvbox {
1808 return Ok(None);
1809 }
1810 for folder in folders {
1813 match self
1814 .select_with_uidvalidity(context, folder, create_mvbox)
1815 .await
1816 {
1817 Ok(_) => {
1818 info!(context, "MVBOX-folder {} created.", folder);
1819 return Ok(Some(folder));
1820 }
1821 Err(err) => {
1822 warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1823 }
1824 }
1825 }
1826 Ok(None)
1827 }
1828}
1829
1830impl Imap {
1831 pub(crate) async fn configure_folders(
1832 &mut self,
1833 context: &Context,
1834 session: &mut Session,
1835 create_mvbox: bool,
1836 ) -> Result<()> {
1837 let mut folders = session
1838 .list(Some(""), Some("*"))
1839 .await
1840 .context("list_folders failed")?;
1841 let mut delimiter = ".".to_string();
1842 let mut delimiter_is_default = true;
1843 let mut folder_configs = BTreeMap::new();
1844
1845 while let Some(folder) = folders.try_next().await? {
1846 info!(context, "Scanning folder: {:?}", folder);
1847
1848 if let Some(d) = folder.delimiter()
1850 && delimiter_is_default
1851 && !d.is_empty()
1852 && delimiter != d
1853 {
1854 delimiter = d.to_string();
1855 delimiter_is_default = false;
1856 }
1857
1858 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1859 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1860 if let Some(config) = folder_meaning.to_config() {
1861 folder_configs.insert(config, folder.name().to_string());
1863 } else if let Some(config) = folder_name_meaning.to_config() {
1864 folder_configs
1866 .entry(config)
1867 .or_insert_with(|| folder.name().to_string());
1868 }
1869 }
1870 drop(folders);
1871
1872 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1873
1874 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1875 let mvbox_folder = session
1876 .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1877 .await
1878 .context("failed to configure mvbox")?;
1879
1880 context
1881 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1882 .await?;
1883 if let Some(mvbox_folder) = mvbox_folder {
1884 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1885 context
1886 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1887 .await?;
1888 }
1889 for (config, name) in folder_configs {
1890 context.set_config_internal(config, Some(&name)).await?;
1891 }
1892 context
1893 .sql
1894 .set_raw_config_int(
1895 constants::DC_FOLDERS_CONFIGURED_KEY,
1896 constants::DC_FOLDERS_CONFIGURED_VERSION,
1897 )
1898 .await?;
1899
1900 info!(context, "FINISHED configuring IMAP-folders.");
1901 Ok(())
1902 }
1903}
1904
1905impl Session {
1906 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1915 use UnsolicitedResponse::*;
1916 use async_imap::imap_proto::Response;
1917 use async_imap::imap_proto::ResponseCode;
1918
1919 let folder = self.selected_folder.as_deref().unwrap_or_default();
1920 let mut should_refetch = false;
1921 while let Ok(response) = self.unsolicited_responses.try_recv() {
1922 match response {
1923 Exists(_) => {
1924 info!(
1925 context,
1926 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1927 );
1928 should_refetch = true;
1929 }
1930
1931 Expunge(_) | Recent(_) => {}
1932 Other(ref response_data) => {
1933 match response_data.parsed() {
1934 Response::Fetch { .. } => {
1935 info!(
1936 context,
1937 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1938 );
1939 should_refetch = true;
1940 }
1941
1942 Response::Done {
1945 code: Some(ResponseCode::CopyUid(_, _, _)),
1946 ..
1947 } => {}
1948
1949 _ => {
1950 info!(context, "{folder:?}: got unsolicited response {response:?}")
1951 }
1952 }
1953 }
1954 _ => {
1955 info!(context, "{folder:?}: got unsolicited response {response:?}")
1956 }
1957 }
1958 }
1959 Ok(should_refetch)
1960 }
1961}
1962
1963async fn should_move_out_of_spam(
1964 context: &Context,
1965 headers: &[mailparse::MailHeader<'_>],
1966) -> Result<bool> {
1967 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1968 return Ok(true);
1979 }
1980
1981 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1982 if msg.chat_blocked != Blocked::Not {
1983 return Ok(false);
1985 }
1986 } else {
1987 let from = match mimeparser::get_from(headers) {
1988 Some(f) => f,
1989 None => return Ok(false),
1990 };
1991 let (from_id, blocked_contact, _origin) =
1993 match from_field_to_contact_id(context, &from, None, true, true)
1994 .await
1995 .context("from_field_to_contact_id")?
1996 {
1997 Some(res) => res,
1998 None => {
1999 warn!(
2000 context,
2001 "Contact with From address {:?} cannot exist, not moving out of spam", from
2002 );
2003 return Ok(false);
2004 }
2005 };
2006 if blocked_contact {
2007 return Ok(false);
2009 }
2010
2011 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2012 if chat_id_blocked.blocked != Blocked::Not {
2013 return Ok(false);
2014 }
2015 } else if from_id != ContactId::SELF {
2016 return Ok(false);
2018 }
2019 }
2020
2021 Ok(true)
2022}
2023
2024async fn spam_target_folder_cfg(
2029 context: &Context,
2030 headers: &[mailparse::MailHeader<'_>],
2031) -> Result<Option<Config>> {
2032 if !should_move_out_of_spam(context, headers).await? {
2033 return Ok(None);
2034 }
2035
2036 if needs_move_to_mvbox(context, headers).await?
2037 || context.get_config_bool(Config::OnlyFetchMvbox).await?
2040 {
2041 Ok(Some(Config::ConfiguredMvboxFolder))
2042 } else {
2043 Ok(Some(Config::ConfiguredInboxFolder))
2044 }
2045}
2046
2047pub async fn target_folder_cfg(
2050 context: &Context,
2051 folder: &str,
2052 folder_meaning: FolderMeaning,
2053 headers: &[mailparse::MailHeader<'_>],
2054) -> Result<Option<Config>> {
2055 if context.is_mvbox(folder).await? {
2056 return Ok(None);
2057 }
2058
2059 if folder_meaning == FolderMeaning::Spam {
2060 spam_target_folder_cfg(context, headers).await
2061 } else if folder_meaning == FolderMeaning::Inbox
2062 && needs_move_to_mvbox(context, headers).await?
2063 {
2064 Ok(Some(Config::ConfiguredMvboxFolder))
2065 } else {
2066 Ok(None)
2067 }
2068}
2069
2070pub async fn target_folder(
2071 context: &Context,
2072 folder: &str,
2073 folder_meaning: FolderMeaning,
2074 headers: &[mailparse::MailHeader<'_>],
2075) -> Result<String> {
2076 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2077 Some(config) => match context.get_config(config).await? {
2078 Some(target) => Ok(target),
2079 None => Ok(folder.to_string()),
2080 },
2081 None => Ok(folder.to_string()),
2082 }
2083}
2084
2085async fn needs_move_to_mvbox(
2086 context: &Context,
2087 headers: &[mailparse::MailHeader<'_>],
2088) -> Result<bool> {
2089 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2090 if !context.get_config_bool(Config::MvboxMove).await? {
2091 return Ok(false);
2092 }
2093
2094 if headers
2095 .get_header_value(HeaderDef::AutocryptSetupMessage)
2096 .is_some()
2097 {
2098 return Ok(false);
2101 }
2102
2103 if has_chat_version {
2104 Ok(true)
2105 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2106 match parent.is_dc_message {
2107 MessengerMessage::No => Ok(false),
2108 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2109 }
2110 } else {
2111 Ok(false)
2112 }
2113}
2114
2115fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2122 const SPAM_NAMES: &[&str] = &[
2124 "spam",
2125 "junk",
2126 "Correio electrónico não solicitado",
2127 "Correo basura",
2128 "Lixo",
2129 "Nettsøppel",
2130 "Nevyžádaná pošta",
2131 "No solicitado",
2132 "Ongewenst",
2133 "Posta indesiderata",
2134 "Skräp",
2135 "Wiadomości-śmieci",
2136 "Önemsiz",
2137 "Ανεπιθύμητα",
2138 "Спам",
2139 "垃圾邮件",
2140 "垃圾郵件",
2141 "迷惑メール",
2142 "스팸",
2143 ];
2144 const TRASH_NAMES: &[&str] = &[
2145 "Trash",
2146 "Bin",
2147 "Caixote do lixo",
2148 "Cestino",
2149 "Corbeille",
2150 "Papelera",
2151 "Papierkorb",
2152 "Papirkurv",
2153 "Papperskorgen",
2154 "Prullenbak",
2155 "Rubujo",
2156 "Κάδος απορριμμάτων",
2157 "Корзина",
2158 "Кошик",
2159 "ゴミ箱",
2160 "垃圾桶",
2161 "已删除邮件",
2162 "휴지통",
2163 ];
2164 let lower = folder_name.to_lowercase();
2165
2166 if lower == "inbox" {
2167 FolderMeaning::Inbox
2168 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2169 FolderMeaning::Spam
2170 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2171 FolderMeaning::Trash
2172 } else {
2173 FolderMeaning::Unknown
2174 }
2175}
2176
2177fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2178 for attr in folder_attrs {
2179 match attr {
2180 NameAttribute::Trash => return FolderMeaning::Trash,
2181 NameAttribute::Junk => return FolderMeaning::Spam,
2182 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2183 NameAttribute::Extension(label) => {
2184 match label.as_ref() {
2185 "\\Spam" => return FolderMeaning::Spam,
2186 "\\Important" => return FolderMeaning::Virtual,
2187 _ => {}
2188 };
2189 }
2190 _ => {}
2191 }
2192 }
2193 FolderMeaning::Unknown
2194}
2195
2196pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2197 match get_folder_meaning_by_attrs(folder.attributes()) {
2198 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2199 meaning => meaning,
2200 }
2201}
2202
2203fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2205 match prefetch_msg.header() {
2206 Some(header_bytes) => {
2207 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2208 Ok(headers)
2209 }
2210 None => Ok(Vec::new()),
2211 }
2212}
2213
2214pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2215 headers
2216 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2217 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2218 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2219}
2220
2221pub(crate) fn create_message_id() -> String {
2222 format!("{}{}", GENERATED_PREFIX, create_id())
2223}
2224
2225pub(crate) async fn prefetch_should_download(
2227 context: &Context,
2228 headers: &[mailparse::MailHeader<'_>],
2229 message_id: &str,
2230 mut flags: impl Iterator<Item = Flag<'_>>,
2231) -> Result<bool> {
2232 if message::rfc724_mid_download_tried(context, message_id).await? {
2233 if let Some(from) = mimeparser::get_from(headers)
2234 && context.is_self_addr(&from.addr).await?
2235 {
2236 markseen_on_imap_table(context, message_id).await?;
2237 }
2238 return Ok(false);
2239 }
2240
2241 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2245 let from = from.to_ascii_lowercase();
2246 from.contains("mailer-daemon") || from.contains("mail-daemon")
2247 } else {
2248 false
2249 };
2250
2251 let is_autocrypt_setup_message = headers
2253 .get_header_value(HeaderDef::AutocryptSetupMessage)
2254 .is_some();
2255
2256 let from = match mimeparser::get_from(headers) {
2257 Some(f) => f,
2258 None => return Ok(false),
2259 };
2260 let (_from_id, blocked_contact, origin) =
2261 match from_field_to_contact_id(context, &from, None, true, true).await? {
2262 Some(res) => res,
2263 None => return Ok(false),
2264 };
2265 if flags.any(|f| f == Flag::Draft) {
2269 info!(context, "Ignoring draft message");
2270 return Ok(false);
2271 }
2272
2273 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2274 let accepted_contact = origin.is_known();
2275 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2276 .await?
2277 .map(|parent| match parent.is_dc_message {
2278 MessengerMessage::No => false,
2279 MessengerMessage::Yes | MessengerMessage::Reply => true,
2280 })
2281 .unwrap_or_default();
2282
2283 let show_emails =
2284 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2285
2286 let show = is_autocrypt_setup_message
2287 || match show_emails {
2288 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2289 ShowEmails::AcceptedContacts => {
2290 is_chat_message || is_reply_to_chat_message || accepted_contact
2291 }
2292 ShowEmails::All => true,
2293 };
2294
2295 let should_download = (show && !blocked_contact) || maybe_ndn;
2296 Ok(should_download)
2297}
2298
2299async fn mark_seen_by_uid(
2303 context: &Context,
2304 transport_id: u32,
2305 folder: &str,
2306 uid_validity: u32,
2307 uid: u32,
2308) -> Result<Option<ChatId>> {
2309 if let Some((msg_id, chat_id)) = context
2310 .sql
2311 .query_row_optional(
2312 "SELECT id, chat_id FROM msgs
2313 WHERE id > 9 AND rfc724_mid IN (
2314 SELECT rfc724_mid FROM imap
2315 WHERE transport_id=?
2316 AND folder=?
2317 AND uidvalidity=?
2318 AND uid=?
2319 LIMIT 1
2320 )",
2321 (transport_id, &folder, uid_validity, uid),
2322 |row| {
2323 let msg_id: MsgId = row.get(0)?;
2324 let chat_id: ChatId = row.get(1)?;
2325 Ok((msg_id, chat_id))
2326 },
2327 )
2328 .await
2329 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2330 {
2331 let updated = context
2332 .sql
2333 .execute(
2334 "UPDATE msgs SET state=?1
2335 WHERE (state=?2 OR state=?3)
2336 AND id=?4",
2337 (
2338 MessageState::InSeen,
2339 MessageState::InFresh,
2340 MessageState::InNoticed,
2341 msg_id,
2342 ),
2343 )
2344 .await
2345 .with_context(|| format!("failed to update msg {msg_id} state"))?
2346 > 0;
2347
2348 if updated {
2349 msg_id
2350 .start_ephemeral_timer(context)
2351 .await
2352 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2353 Ok(Some(chat_id))
2354 } else {
2355 Ok(None)
2357 }
2358 } else {
2359 Ok(None)
2361 }
2362}
2363
2364pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2367 context
2368 .sql
2369 .execute(
2370 "INSERT OR IGNORE INTO imap_markseen (id)
2371 SELECT id FROM imap WHERE rfc724_mid=?",
2372 (message_id,),
2373 )
2374 .await?;
2375 context.scheduler.interrupt_inbox().await;
2376
2377 Ok(())
2378}
2379
2380pub(crate) async fn set_uid_next(
2384 context: &Context,
2385 transport_id: u32,
2386 folder: &str,
2387 uid_next: u32,
2388) -> Result<()> {
2389 context
2390 .sql
2391 .execute(
2392 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2393 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2394 (transport_id, folder, uid_next),
2395 )
2396 .await?;
2397 Ok(())
2398}
2399
2400async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2406 Ok(context
2407 .sql
2408 .query_get_value(
2409 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2410 (transport_id, folder),
2411 )
2412 .await?
2413 .unwrap_or(0))
2414}
2415
2416pub(crate) async fn set_uidvalidity(
2417 context: &Context,
2418 transport_id: u32,
2419 folder: &str,
2420 uidvalidity: u32,
2421) -> Result<()> {
2422 context
2423 .sql
2424 .execute(
2425 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2426 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2427 (transport_id, folder, uidvalidity),
2428 )
2429 .await?;
2430 Ok(())
2431}
2432
2433async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2434 Ok(context
2435 .sql
2436 .query_get_value(
2437 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2438 (transport_id, folder),
2439 )
2440 .await?
2441 .unwrap_or(0))
2442}
2443
2444pub(crate) async fn set_modseq(
2445 context: &Context,
2446 transport_id: u32,
2447 folder: &str,
2448 modseq: u64,
2449) -> Result<()> {
2450 context
2451 .sql
2452 .execute(
2453 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2454 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2455 (transport_id, folder, modseq),
2456 )
2457 .await?;
2458 Ok(())
2459}
2460
2461async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2462 Ok(context
2463 .sql
2464 .query_get_value(
2465 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2466 (transport_id, folder),
2467 )
2468 .await?
2469 .unwrap_or(0))
2470}
2471
2472pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
2474 let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
2476
2477 for item in context.get_secondary_self_addrs().await? {
2478 search_command = format!("OR ({search_command}) (FROM \"{item}\")");
2479 }
2480
2481 Ok(search_command)
2482}
2483
2484async fn should_ignore_folder(
2489 context: &Context,
2490 folder: &str,
2491 folder_meaning: FolderMeaning,
2492) -> Result<bool> {
2493 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2494 return Ok(false);
2495 }
2496 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2497}
2498
2499fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2503 let mut ranges: Vec<UidRange> = vec![];
2505
2506 for ¤t in uids {
2507 if let Some(last) = ranges.last_mut()
2508 && last.end + 1 == current
2509 {
2510 last.end = current;
2511 continue;
2512 }
2513
2514 ranges.push(UidRange {
2515 start: current,
2516 end: current,
2517 });
2518 }
2519
2520 let mut result = vec![];
2522 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2523 for range in ranges {
2524 last_uids.reserve((range.end - range.start + 1).try_into()?);
2525 (range.start..=range.end).for_each(|u| last_uids.push(u));
2526 if !last_str.is_empty() {
2527 last_str.push(',');
2528 }
2529 last_str.push_str(&range.to_string());
2530
2531 if last_str.len() > 990 {
2532 result.push((take(&mut last_uids), take(&mut last_str)));
2533 }
2534 }
2535 result.push((last_uids, last_str));
2536
2537 result.retain(|(_, s)| !s.is_empty());
2538 Ok(result)
2539}
2540
2541struct UidRange {
2542 start: u32,
2543 end: u32,
2544 }
2546
2547impl std::fmt::Display for UidRange {
2548 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2549 if self.start == self.end {
2550 write!(f, "{}", self.start)
2551 } else {
2552 write!(f, "{}:{}", self.start, self.end)
2553 }
2554 }
2555}
2556async fn add_all_recipients_as_contacts(
2557 context: &Context,
2558 session: &mut Session,
2559 folder: Config,
2560) -> Result<()> {
2561 let mailbox = if let Some(m) = context.get_config(folder).await? {
2562 m
2563 } else {
2564 info!(
2565 context,
2566 "Folder {} is not configured, skipping fetching contacts from it.", folder
2567 );
2568 return Ok(());
2569 };
2570 let create = false;
2571 let folder_exists = session
2572 .select_with_uidvalidity(context, &mailbox, create)
2573 .await
2574 .with_context(|| format!("could not select {mailbox}"))?;
2575 if !folder_exists {
2576 return Ok(());
2577 }
2578
2579 let recipients = session
2580 .get_all_recipients(context)
2581 .await
2582 .context("could not get recipients")?;
2583
2584 let mut any_modified = false;
2585 for recipient in recipients {
2586 let recipient_addr = match ContactAddress::new(&recipient.addr) {
2587 Err(err) => {
2588 warn!(
2589 context,
2590 "Could not add contact for recipient with address {:?}: {:#}",
2591 recipient.addr,
2592 err
2593 );
2594 continue;
2595 }
2596 Ok(recipient_addr) => recipient_addr,
2597 };
2598
2599 let (_, modified) = Contact::add_or_lookup(
2600 context,
2601 &recipient.display_name.unwrap_or_default(),
2602 &recipient_addr,
2603 Origin::OutgoingTo,
2604 )
2605 .await?;
2606 if modified != Modifier::None {
2607 any_modified = true;
2608 }
2609 }
2610 if any_modified {
2611 context.emit_event(EventType::ContactsChanged(None));
2612 }
2613
2614 Ok(())
2615}
2616
2617#[cfg(test)]
2618mod imap_tests;