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 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 return Ok(());
1219 }
1220
1221 let create = false;
1222 let folder_exists = self
1223 .select_with_uidvalidity(context, folder, create)
1224 .await
1225 .context("Failed to select folder")?;
1226 if !folder_exists {
1227 return Ok(());
1228 }
1229
1230 let mailbox = self
1231 .selected_mailbox
1232 .as_ref()
1233 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1234
1235 if mailbox.highest_modseq.is_none() {
1238 info!(
1239 context,
1240 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1241 );
1242 return Ok(());
1243 }
1244
1245 let transport_id = self.transport_id();
1246 let mut updated_chat_ids = BTreeSet::new();
1247 let uid_validity = get_uidvalidity(context, transport_id, folder)
1248 .await
1249 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1250 let mut highest_modseq = get_modseq(context, transport_id, folder)
1251 .await
1252 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1253 let mut list = self
1254 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1255 .await
1256 .context("failed to fetch flags")?;
1257
1258 let mut got_unsolicited_fetch = false;
1259
1260 while let Some(fetch) = list
1261 .try_next()
1262 .await
1263 .context("failed to get FETCH result")?
1264 {
1265 let uid = if let Some(uid) = fetch.uid {
1266 uid
1267 } else {
1268 info!(context, "FETCH result contains no UID, skipping");
1269 got_unsolicited_fetch = true;
1270 continue;
1271 };
1272 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1273 if is_seen
1274 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1275 .await
1276 .with_context(|| {
1277 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1278 })?
1279 {
1280 updated_chat_ids.insert(chat_id);
1281 }
1282
1283 if let Some(modseq) = fetch.modseq {
1284 if modseq > highest_modseq {
1285 highest_modseq = modseq;
1286 }
1287 } else {
1288 warn!(context, "FETCH result contains no MODSEQ");
1289 }
1290 }
1291 drop(list);
1292
1293 if got_unsolicited_fetch {
1294 self.new_mail = true;
1299 }
1300
1301 set_modseq(context, transport_id, folder, highest_modseq)
1302 .await
1303 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1304 if !updated_chat_ids.is_empty() {
1305 context.on_archived_chats_maybe_noticed();
1306 }
1307 for updated_chat_id in updated_chat_ids {
1308 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1309 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1310 }
1311
1312 Ok(())
1313 }
1314
1315 pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1317 let mut uids: Vec<_> = self
1318 .uid_search(get_imap_self_sent_search_command(context).await?)
1319 .await?
1320 .into_iter()
1321 .collect();
1322 uids.sort_unstable();
1323
1324 let mut result = Vec::new();
1325 for (_, uid_set) in build_sequence_sets(&uids)? {
1326 let mut list = self
1327 .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1328 .await
1329 .context("IMAP Could not fetch")?;
1330
1331 while let Some(msg) = list.try_next().await? {
1332 match get_fetch_headers(&msg) {
1333 Ok(headers) => {
1334 if let Some(from) = mimeparser::get_from(&headers)
1335 && context.is_self_addr(&from.addr).await?
1336 {
1337 result.extend(mimeparser::get_recipients(&headers));
1338 }
1339 }
1340 Err(err) => {
1341 warn!(context, "{}", err);
1342 continue;
1343 }
1344 };
1345 }
1346 }
1347 Ok(result)
1348 }
1349
1350 pub(crate) async fn fetch_many_msgs(
1365 &mut self,
1366 context: &Context,
1367 folder: &str,
1368 request_uids: Vec<u32>,
1369 uid_message_ids: &BTreeMap<u32, String>,
1370 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1371 ) -> Result<()> {
1372 if request_uids.is_empty() {
1373 return Ok(());
1374 }
1375
1376 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1377 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1378 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1379 format!("fetching messages {} from folder \"{}\"", &set, folder)
1380 })?;
1381
1382 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1385
1386 let mut count = 0;
1387 for &request_uid in &request_uids {
1388 let mut fetch_response = uid_msgs.remove(&request_uid);
1390
1391 while fetch_response.is_none() {
1393 let Some(next_fetch_response) = fetch_responses
1394 .try_next()
1395 .await
1396 .context("Failed to process IMAP FETCH result")?
1397 else {
1398 break;
1400 };
1401
1402 if let Some(next_uid) = next_fetch_response.uid {
1403 if next_uid == request_uid {
1404 fetch_response = Some(next_fetch_response);
1405 } else if !request_uids.contains(&next_uid) {
1406 info!(
1413 context,
1414 "Skipping not requested FETCH response for UID {}.", next_uid
1415 );
1416 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1417 warn!(context, "Got duplicated UID {}.", next_uid);
1418 }
1419 } else {
1420 info!(context, "Skipping FETCH response without UID.");
1421 }
1422 }
1423
1424 let fetch_response = match fetch_response {
1425 Some(fetch) => fetch,
1426 None => {
1427 warn!(
1428 context,
1429 "Missed UID {} in the server response.", request_uid
1430 );
1431 continue;
1432 }
1433 };
1434 count += 1;
1435
1436 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1437 let body = fetch_response.body();
1438
1439 if is_deleted {
1440 info!(context, "Not processing deleted msg {}.", request_uid);
1441 received_msgs_channel.send((request_uid, None)).await?;
1442 continue;
1443 }
1444
1445 let body = if let Some(body) = body {
1446 body
1447 } else {
1448 info!(
1449 context,
1450 "Not processing message {} without a BODY.", request_uid
1451 );
1452 received_msgs_channel.send((request_uid, None)).await?;
1453 continue;
1454 };
1455
1456 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1457
1458 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1459 error!(
1460 context,
1461 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1462 request_uid
1463 );
1464 continue;
1465 };
1466
1467 info!(
1468 context,
1469 "Passing message UID {} to receive_imf().", request_uid
1470 );
1471 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1472 let received_msg = match res {
1473 Err(err) => {
1474 warn!(context, "receive_imf error: {err:#}.");
1475
1476 let text = format!(
1477 "❌ 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/.",
1478 );
1479 let mut msg = Message::new_text(text);
1480 add_device_msg(context, None, Some(&mut msg)).await?;
1481 None
1482 }
1483 Ok(msg) => msg,
1484 };
1485 received_msgs_channel
1486 .send((request_uid, received_msg))
1487 .await?;
1488 }
1489
1490 while fetch_responses
1497 .try_next()
1498 .await
1499 .context("Failed to drain FETCH responses")?
1500 .is_some()
1501 {}
1502
1503 if count != request_uids.len() {
1504 warn!(
1505 context,
1506 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1507 count,
1508 request_uids.len(),
1509 request_uids,
1510 );
1511 } else {
1512 info!(
1513 context,
1514 "Successfully received {} UIDs.",
1515 request_uids.len()
1516 );
1517 }
1518 }
1519
1520 Ok(())
1521 }
1522
1523 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1529 let mut lock = context.metadata.write().await;
1530
1531 if !self.can_metadata() {
1532 *lock = Some(Default::default());
1533 }
1534 if let Some(ref mut old_metadata) = *lock {
1535 let now = time();
1536
1537 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1539 return Ok(());
1540 }
1541
1542 let mut got_turn_server = false;
1543 if self.can_metadata() {
1544 info!(context, "ICE servers expired, requesting new credentials.");
1545 let mailbox = "";
1546 let options = "";
1547 let metadata = self
1548 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1549 .await?;
1550 for m in metadata {
1551 if m.entry == "/shared/vendor/deltachat/turn"
1552 && let Some(value) = m.value
1553 {
1554 match create_ice_servers_from_metadata(&value).await {
1555 Ok((parsed_timestamp, parsed_ice_servers)) => {
1556 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1557 old_metadata.ice_servers = parsed_ice_servers;
1558 got_turn_server = true;
1559 }
1560 Err(err) => {
1561 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1562 }
1563 }
1564 }
1565 }
1566 }
1567 if !got_turn_server {
1568 info!(context, "Will use fallback ICE servers.");
1569 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1571 old_metadata.ice_servers = create_fallback_ice_servers();
1572 }
1573 return Ok(());
1574 }
1575
1576 info!(
1577 context,
1578 "Server supports metadata, retrieving server comment and admin contact."
1579 );
1580
1581 let mut comment = None;
1582 let mut admin = None;
1583 let mut iroh_relay = None;
1584 let mut ice_servers = None;
1585 let mut ice_servers_expiration_timestamp = 0;
1586
1587 let mailbox = "";
1588 let options = "";
1589 let metadata = self
1590 .get_metadata(
1591 mailbox,
1592 options,
1593 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1594 )
1595 .await?;
1596 for m in metadata {
1597 match m.entry.as_ref() {
1598 "/shared/comment" => {
1599 comment = m.value;
1600 }
1601 "/shared/admin" => {
1602 admin = m.value;
1603 }
1604 "/shared/vendor/deltachat/irohrelay" => {
1605 if let Some(value) = m.value {
1606 if let Ok(url) = Url::parse(&value) {
1607 iroh_relay = Some(url);
1608 } else {
1609 warn!(
1610 context,
1611 "Got invalid URL from iroh relay metadata: {:?}.", value
1612 );
1613 }
1614 }
1615 }
1616 "/shared/vendor/deltachat/turn" => {
1617 if let Some(value) = m.value {
1618 match create_ice_servers_from_metadata(&value).await {
1619 Ok((parsed_timestamp, parsed_ice_servers)) => {
1620 ice_servers_expiration_timestamp = parsed_timestamp;
1621 ice_servers = Some(parsed_ice_servers);
1622 }
1623 Err(err) => {
1624 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1625 }
1626 }
1627 }
1628 }
1629 _ => {}
1630 }
1631 }
1632 let ice_servers = if let Some(ice_servers) = ice_servers {
1633 ice_servers
1634 } else {
1635 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1637 create_fallback_ice_servers()
1638 };
1639
1640 *lock = Some(ServerMetadata {
1641 comment,
1642 admin,
1643 iroh_relay,
1644 ice_servers,
1645 ice_servers_expiration_timestamp,
1646 });
1647 Ok(())
1648 }
1649
1650 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1652 if context.push_subscribed.load(Ordering::Relaxed) {
1653 return Ok(());
1654 }
1655
1656 let Some(device_token) = context.push_subscriber.device_token().await else {
1657 return Ok(());
1658 };
1659
1660 if self.can_metadata() && self.can_push() {
1661 let old_encrypted_device_token =
1662 context.get_config(Config::EncryptedDeviceToken).await?;
1663
1664 let device_token_changed = old_encrypted_device_token.is_none()
1666 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1667
1668 let new_encrypted_device_token;
1669 if device_token_changed {
1670 let encrypted_device_token = encrypt_device_token(&device_token)
1671 .context("Failed to encrypt device token")?;
1672
1673 let encrypted_device_token_len = encrypted_device_token.len();
1677
1678 context
1684 .set_config_internal(Config::DeviceToken, Some(&device_token))
1685 .await?;
1686 context
1687 .set_config_internal(
1688 Config::EncryptedDeviceToken,
1689 Some(&encrypted_device_token),
1690 )
1691 .await?;
1692
1693 if encrypted_device_token_len <= 4096 {
1694 new_encrypted_device_token = Some(encrypted_device_token);
1695 } else {
1696 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1706 new_encrypted_device_token = None;
1707 }
1708 } else {
1709 new_encrypted_device_token = old_encrypted_device_token;
1710 }
1711
1712 if let Some(encrypted_device_token) = new_encrypted_device_token {
1715 let folder = context
1716 .get_config(Config::ConfiguredInboxFolder)
1717 .await?
1718 .context("INBOX is not configured")?;
1719
1720 self.run_command_and_check_ok(&format_setmetadata(
1721 &folder,
1722 &encrypted_device_token,
1723 ))
1724 .await
1725 .context("SETMETADATA command failed")?;
1726
1727 context.push_subscribed.store(true, Ordering::Relaxed);
1728 }
1729 } else if !context.push_subscriber.heartbeat_subscribed().await {
1730 let context = context.clone();
1731 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1733 }
1734
1735 Ok(())
1736 }
1737}
1738
1739fn format_setmetadata(folder: &str, device_token: &str) -> String {
1740 let device_token_len = device_token.len();
1741 format!(
1742 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1743 )
1744}
1745
1746impl Session {
1747 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1753 if flag == "\\Deleted" {
1754 self.selected_folder_needs_expunge = true;
1755 }
1756 let query = format!("+FLAGS ({flag})");
1757 let mut responses = self
1758 .uid_store(uid_set, &query)
1759 .await
1760 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1761 while let Some(_response) = responses.try_next().await? {
1762 }
1764 Ok(())
1765 }
1766
1767 async fn configure_mvbox<'a>(
1776 &mut self,
1777 context: &Context,
1778 folders: &[&'a str],
1779 create_mvbox: bool,
1780 ) -> Result<Option<&'a str>> {
1781 self.maybe_close_folder(context).await?;
1784
1785 for folder in folders {
1786 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1787 let res = self.examine(&folder).await;
1788 if res.is_ok() {
1789 info!(
1790 context,
1791 "MVBOX-folder {:?} successfully selected, using it.", &folder
1792 );
1793 self.close().await?;
1794 let create = false;
1797 let folder_exists = self
1798 .select_with_uidvalidity(context, folder, create)
1799 .await?;
1800 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1801 return Ok(Some(folder));
1802 }
1803 }
1804
1805 if !create_mvbox {
1806 return Ok(None);
1807 }
1808 for folder in folders {
1811 match self
1812 .select_with_uidvalidity(context, folder, create_mvbox)
1813 .await
1814 {
1815 Ok(_) => {
1816 info!(context, "MVBOX-folder {} created.", folder);
1817 return Ok(Some(folder));
1818 }
1819 Err(err) => {
1820 warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1821 }
1822 }
1823 }
1824 Ok(None)
1825 }
1826}
1827
1828impl Imap {
1829 pub(crate) async fn configure_folders(
1830 &mut self,
1831 context: &Context,
1832 session: &mut Session,
1833 create_mvbox: bool,
1834 ) -> Result<()> {
1835 let mut folders = session
1836 .list(Some(""), Some("*"))
1837 .await
1838 .context("list_folders failed")?;
1839 let mut delimiter = ".".to_string();
1840 let mut delimiter_is_default = true;
1841 let mut folder_configs = BTreeMap::new();
1842
1843 while let Some(folder) = folders.try_next().await? {
1844 info!(context, "Scanning folder: {:?}", folder);
1845
1846 if let Some(d) = folder.delimiter()
1848 && delimiter_is_default
1849 && !d.is_empty()
1850 && delimiter != d
1851 {
1852 delimiter = d.to_string();
1853 delimiter_is_default = false;
1854 }
1855
1856 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1857 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1858 if let Some(config) = folder_meaning.to_config() {
1859 folder_configs.insert(config, folder.name().to_string());
1861 } else if let Some(config) = folder_name_meaning.to_config() {
1862 folder_configs
1864 .entry(config)
1865 .or_insert_with(|| folder.name().to_string());
1866 }
1867 }
1868 drop(folders);
1869
1870 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1871
1872 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1873 let mvbox_folder = session
1874 .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1875 .await
1876 .context("failed to configure mvbox")?;
1877
1878 context
1879 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1880 .await?;
1881 if let Some(mvbox_folder) = mvbox_folder {
1882 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1883 context
1884 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1885 .await?;
1886 }
1887 for (config, name) in folder_configs {
1888 context.set_config_internal(config, Some(&name)).await?;
1889 }
1890 context
1891 .sql
1892 .set_raw_config_int(
1893 constants::DC_FOLDERS_CONFIGURED_KEY,
1894 constants::DC_FOLDERS_CONFIGURED_VERSION,
1895 )
1896 .await?;
1897
1898 info!(context, "FINISHED configuring IMAP-folders.");
1899 Ok(())
1900 }
1901}
1902
1903impl Session {
1904 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1913 use UnsolicitedResponse::*;
1914 use async_imap::imap_proto::Response;
1915 use async_imap::imap_proto::ResponseCode;
1916
1917 let folder = self.selected_folder.as_deref().unwrap_or_default();
1918 let mut should_refetch = false;
1919 while let Ok(response) = self.unsolicited_responses.try_recv() {
1920 match response {
1921 Exists(_) => {
1922 info!(
1923 context,
1924 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1925 );
1926 should_refetch = true;
1927 }
1928
1929 Expunge(_) | Recent(_) => {}
1930 Other(ref response_data) => {
1931 match response_data.parsed() {
1932 Response::Fetch { .. } => {
1933 info!(
1934 context,
1935 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1936 );
1937 should_refetch = true;
1938 }
1939
1940 Response::Done {
1943 code: Some(ResponseCode::CopyUid(_, _, _)),
1944 ..
1945 } => {}
1946
1947 _ => {
1948 info!(context, "{folder:?}: got unsolicited response {response:?}")
1949 }
1950 }
1951 }
1952 _ => {
1953 info!(context, "{folder:?}: got unsolicited response {response:?}")
1954 }
1955 }
1956 }
1957 Ok(should_refetch)
1958 }
1959}
1960
1961async fn should_move_out_of_spam(
1962 context: &Context,
1963 headers: &[mailparse::MailHeader<'_>],
1964) -> Result<bool> {
1965 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1966 return Ok(true);
1977 }
1978
1979 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1980 if msg.chat_blocked != Blocked::Not {
1981 return Ok(false);
1983 }
1984 } else {
1985 let from = match mimeparser::get_from(headers) {
1986 Some(f) => f,
1987 None => return Ok(false),
1988 };
1989 let (from_id, blocked_contact, _origin) =
1991 match from_field_to_contact_id(context, &from, None, true, true)
1992 .await
1993 .context("from_field_to_contact_id")?
1994 {
1995 Some(res) => res,
1996 None => {
1997 warn!(
1998 context,
1999 "Contact with From address {:?} cannot exist, not moving out of spam", from
2000 );
2001 return Ok(false);
2002 }
2003 };
2004 if blocked_contact {
2005 return Ok(false);
2007 }
2008
2009 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2010 if chat_id_blocked.blocked != Blocked::Not {
2011 return Ok(false);
2012 }
2013 } else if from_id != ContactId::SELF {
2014 return Ok(false);
2016 }
2017 }
2018
2019 Ok(true)
2020}
2021
2022async fn spam_target_folder_cfg(
2027 context: &Context,
2028 headers: &[mailparse::MailHeader<'_>],
2029) -> Result<Option<Config>> {
2030 if !should_move_out_of_spam(context, headers).await? {
2031 return Ok(None);
2032 }
2033
2034 if needs_move_to_mvbox(context, headers).await?
2035 || context.get_config_bool(Config::OnlyFetchMvbox).await?
2038 {
2039 Ok(Some(Config::ConfiguredMvboxFolder))
2040 } else {
2041 Ok(Some(Config::ConfiguredInboxFolder))
2042 }
2043}
2044
2045pub async fn target_folder_cfg(
2048 context: &Context,
2049 folder: &str,
2050 folder_meaning: FolderMeaning,
2051 headers: &[mailparse::MailHeader<'_>],
2052) -> Result<Option<Config>> {
2053 if context.is_mvbox(folder).await? {
2054 return Ok(None);
2055 }
2056
2057 if folder_meaning == FolderMeaning::Spam {
2058 spam_target_folder_cfg(context, headers).await
2059 } else if folder_meaning == FolderMeaning::Inbox
2060 && needs_move_to_mvbox(context, headers).await?
2061 {
2062 Ok(Some(Config::ConfiguredMvboxFolder))
2063 } else {
2064 Ok(None)
2065 }
2066}
2067
2068pub async fn target_folder(
2069 context: &Context,
2070 folder: &str,
2071 folder_meaning: FolderMeaning,
2072 headers: &[mailparse::MailHeader<'_>],
2073) -> Result<String> {
2074 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2075 Some(config) => match context.get_config(config).await? {
2076 Some(target) => Ok(target),
2077 None => Ok(folder.to_string()),
2078 },
2079 None => Ok(folder.to_string()),
2080 }
2081}
2082
2083async fn needs_move_to_mvbox(
2084 context: &Context,
2085 headers: &[mailparse::MailHeader<'_>],
2086) -> Result<bool> {
2087 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2088 if !context.get_config_bool(Config::MvboxMove).await? {
2089 return Ok(false);
2090 }
2091
2092 if headers
2093 .get_header_value(HeaderDef::AutocryptSetupMessage)
2094 .is_some()
2095 {
2096 return Ok(false);
2099 }
2100
2101 if has_chat_version {
2102 Ok(true)
2103 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2104 match parent.is_dc_message {
2105 MessengerMessage::No => Ok(false),
2106 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2107 }
2108 } else {
2109 Ok(false)
2110 }
2111}
2112
2113fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2120 const SPAM_NAMES: &[&str] = &[
2122 "spam",
2123 "junk",
2124 "Correio electrónico não solicitado",
2125 "Correo basura",
2126 "Lixo",
2127 "Nettsøppel",
2128 "Nevyžádaná pošta",
2129 "No solicitado",
2130 "Ongewenst",
2131 "Posta indesiderata",
2132 "Skräp",
2133 "Wiadomości-śmieci",
2134 "Önemsiz",
2135 "Ανεπιθύμητα",
2136 "Спам",
2137 "垃圾邮件",
2138 "垃圾郵件",
2139 "迷惑メール",
2140 "스팸",
2141 ];
2142 const TRASH_NAMES: &[&str] = &[
2143 "Trash",
2144 "Bin",
2145 "Caixote do lixo",
2146 "Cestino",
2147 "Corbeille",
2148 "Papelera",
2149 "Papierkorb",
2150 "Papirkurv",
2151 "Papperskorgen",
2152 "Prullenbak",
2153 "Rubujo",
2154 "Κάδος απορριμμάτων",
2155 "Корзина",
2156 "Кошик",
2157 "ゴミ箱",
2158 "垃圾桶",
2159 "已删除邮件",
2160 "휴지통",
2161 ];
2162 let lower = folder_name.to_lowercase();
2163
2164 if lower == "inbox" {
2165 FolderMeaning::Inbox
2166 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2167 FolderMeaning::Spam
2168 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2169 FolderMeaning::Trash
2170 } else {
2171 FolderMeaning::Unknown
2172 }
2173}
2174
2175fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2176 for attr in folder_attrs {
2177 match attr {
2178 NameAttribute::Trash => return FolderMeaning::Trash,
2179 NameAttribute::Junk => return FolderMeaning::Spam,
2180 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2181 NameAttribute::Extension(label) => {
2182 match label.as_ref() {
2183 "\\Spam" => return FolderMeaning::Spam,
2184 "\\Important" => return FolderMeaning::Virtual,
2185 _ => {}
2186 };
2187 }
2188 _ => {}
2189 }
2190 }
2191 FolderMeaning::Unknown
2192}
2193
2194pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2195 match get_folder_meaning_by_attrs(folder.attributes()) {
2196 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2197 meaning => meaning,
2198 }
2199}
2200
2201fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2203 match prefetch_msg.header() {
2204 Some(header_bytes) => {
2205 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2206 Ok(headers)
2207 }
2208 None => Ok(Vec::new()),
2209 }
2210}
2211
2212pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2213 headers
2214 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2215 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2216 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2217}
2218
2219pub(crate) fn create_message_id() -> String {
2220 format!("{}{}", GENERATED_PREFIX, create_id())
2221}
2222
2223pub(crate) async fn prefetch_should_download(
2225 context: &Context,
2226 headers: &[mailparse::MailHeader<'_>],
2227 message_id: &str,
2228 mut flags: impl Iterator<Item = Flag<'_>>,
2229) -> Result<bool> {
2230 if message::rfc724_mid_download_tried(context, message_id).await? {
2231 if let Some(from) = mimeparser::get_from(headers)
2232 && context.is_self_addr(&from.addr).await?
2233 {
2234 markseen_on_imap_table(context, message_id).await?;
2235 }
2236 return Ok(false);
2237 }
2238
2239 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2243 let from = from.to_ascii_lowercase();
2244 from.contains("mailer-daemon") || from.contains("mail-daemon")
2245 } else {
2246 false
2247 };
2248
2249 let is_autocrypt_setup_message = headers
2251 .get_header_value(HeaderDef::AutocryptSetupMessage)
2252 .is_some();
2253
2254 let from = match mimeparser::get_from(headers) {
2255 Some(f) => f,
2256 None => return Ok(false),
2257 };
2258 let (_from_id, blocked_contact, origin) =
2259 match from_field_to_contact_id(context, &from, None, true, true).await? {
2260 Some(res) => res,
2261 None => return Ok(false),
2262 };
2263 if flags.any(|f| f == Flag::Draft) {
2267 info!(context, "Ignoring draft message");
2268 return Ok(false);
2269 }
2270
2271 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2272 let accepted_contact = origin.is_known();
2273 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2274 .await?
2275 .map(|parent| match parent.is_dc_message {
2276 MessengerMessage::No => false,
2277 MessengerMessage::Yes | MessengerMessage::Reply => true,
2278 })
2279 .unwrap_or_default();
2280
2281 let show_emails =
2282 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2283
2284 let show = is_autocrypt_setup_message
2285 || match show_emails {
2286 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2287 ShowEmails::AcceptedContacts => {
2288 is_chat_message || is_reply_to_chat_message || accepted_contact
2289 }
2290 ShowEmails::All => true,
2291 };
2292
2293 let should_download = (show && !blocked_contact) || maybe_ndn;
2294 Ok(should_download)
2295}
2296
2297async fn mark_seen_by_uid(
2301 context: &Context,
2302 transport_id: u32,
2303 folder: &str,
2304 uid_validity: u32,
2305 uid: u32,
2306) -> Result<Option<ChatId>> {
2307 if let Some((msg_id, chat_id)) = context
2308 .sql
2309 .query_row_optional(
2310 "SELECT id, chat_id FROM msgs
2311 WHERE id > 9 AND rfc724_mid IN (
2312 SELECT rfc724_mid FROM imap
2313 WHERE transport_id=?
2314 AND folder=?
2315 AND uidvalidity=?
2316 AND uid=?
2317 LIMIT 1
2318 )",
2319 (transport_id, &folder, uid_validity, uid),
2320 |row| {
2321 let msg_id: MsgId = row.get(0)?;
2322 let chat_id: ChatId = row.get(1)?;
2323 Ok((msg_id, chat_id))
2324 },
2325 )
2326 .await
2327 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2328 {
2329 let updated = context
2330 .sql
2331 .execute(
2332 "UPDATE msgs SET state=?1
2333 WHERE (state=?2 OR state=?3)
2334 AND id=?4",
2335 (
2336 MessageState::InSeen,
2337 MessageState::InFresh,
2338 MessageState::InNoticed,
2339 msg_id,
2340 ),
2341 )
2342 .await
2343 .with_context(|| format!("failed to update msg {msg_id} state"))?
2344 > 0;
2345
2346 if updated {
2347 msg_id
2348 .start_ephemeral_timer(context)
2349 .await
2350 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2351 Ok(Some(chat_id))
2352 } else {
2353 Ok(None)
2355 }
2356 } else {
2357 Ok(None)
2359 }
2360}
2361
2362pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2365 context
2366 .sql
2367 .execute(
2368 "INSERT OR IGNORE INTO imap_markseen (id)
2369 SELECT id FROM imap WHERE rfc724_mid=?",
2370 (message_id,),
2371 )
2372 .await?;
2373 context.scheduler.interrupt_inbox().await;
2374
2375 Ok(())
2376}
2377
2378pub(crate) async fn set_uid_next(
2382 context: &Context,
2383 transport_id: u32,
2384 folder: &str,
2385 uid_next: u32,
2386) -> Result<()> {
2387 context
2388 .sql
2389 .execute(
2390 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2391 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2392 (transport_id, folder, uid_next),
2393 )
2394 .await?;
2395 Ok(())
2396}
2397
2398async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2404 Ok(context
2405 .sql
2406 .query_get_value(
2407 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2408 (transport_id, folder),
2409 )
2410 .await?
2411 .unwrap_or(0))
2412}
2413
2414pub(crate) async fn set_uidvalidity(
2415 context: &Context,
2416 transport_id: u32,
2417 folder: &str,
2418 uidvalidity: u32,
2419) -> Result<()> {
2420 context
2421 .sql
2422 .execute(
2423 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2424 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2425 (transport_id, folder, uidvalidity),
2426 )
2427 .await?;
2428 Ok(())
2429}
2430
2431async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2432 Ok(context
2433 .sql
2434 .query_get_value(
2435 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2436 (transport_id, folder),
2437 )
2438 .await?
2439 .unwrap_or(0))
2440}
2441
2442pub(crate) async fn set_modseq(
2443 context: &Context,
2444 transport_id: u32,
2445 folder: &str,
2446 modseq: u64,
2447) -> Result<()> {
2448 context
2449 .sql
2450 .execute(
2451 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2452 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2453 (transport_id, folder, modseq),
2454 )
2455 .await?;
2456 Ok(())
2457}
2458
2459async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2460 Ok(context
2461 .sql
2462 .query_get_value(
2463 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2464 (transport_id, folder),
2465 )
2466 .await?
2467 .unwrap_or(0))
2468}
2469
2470pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
2472 let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
2474
2475 for item in context.get_secondary_self_addrs().await? {
2476 search_command = format!("OR ({search_command}) (FROM \"{item}\")");
2477 }
2478
2479 Ok(search_command)
2480}
2481
2482async fn should_ignore_folder(
2487 context: &Context,
2488 folder: &str,
2489 folder_meaning: FolderMeaning,
2490) -> Result<bool> {
2491 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2492 return Ok(false);
2493 }
2494 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2495}
2496
2497fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2501 let mut ranges: Vec<UidRange> = vec![];
2503
2504 for ¤t in uids {
2505 if let Some(last) = ranges.last_mut()
2506 && last.end + 1 == current
2507 {
2508 last.end = current;
2509 continue;
2510 }
2511
2512 ranges.push(UidRange {
2513 start: current,
2514 end: current,
2515 });
2516 }
2517
2518 let mut result = vec![];
2520 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2521 for range in ranges {
2522 last_uids.reserve((range.end - range.start + 1).try_into()?);
2523 (range.start..=range.end).for_each(|u| last_uids.push(u));
2524 if !last_str.is_empty() {
2525 last_str.push(',');
2526 }
2527 last_str.push_str(&range.to_string());
2528
2529 if last_str.len() > 990 {
2530 result.push((take(&mut last_uids), take(&mut last_str)));
2531 }
2532 }
2533 result.push((last_uids, last_str));
2534
2535 result.retain(|(_, s)| !s.is_empty());
2536 Ok(result)
2537}
2538
2539struct UidRange {
2540 start: u32,
2541 end: u32,
2542 }
2544
2545impl std::fmt::Display for UidRange {
2546 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2547 if self.start == self.end {
2548 write!(f, "{}", self.start)
2549 } else {
2550 write!(f, "{}:{}", self.start, self.end)
2551 }
2552 }
2553}
2554async fn add_all_recipients_as_contacts(
2555 context: &Context,
2556 session: &mut Session,
2557 folder: Config,
2558) -> Result<()> {
2559 let mailbox = if let Some(m) = context.get_config(folder).await? {
2560 m
2561 } else {
2562 info!(
2563 context,
2564 "Folder {} is not configured, skipping fetching contacts from it.", folder
2565 );
2566 return Ok(());
2567 };
2568 let create = false;
2569 let folder_exists = session
2570 .select_with_uidvalidity(context, &mailbox, create)
2571 .await
2572 .with_context(|| format!("could not select {mailbox}"))?;
2573 if !folder_exists {
2574 return Ok(());
2575 }
2576
2577 let recipients = session
2578 .get_all_recipients(context)
2579 .await
2580 .context("could not get recipients")?;
2581
2582 let mut any_modified = false;
2583 for recipient in recipients {
2584 let recipient_addr = match ContactAddress::new(&recipient.addr) {
2585 Err(err) => {
2586 warn!(
2587 context,
2588 "Could not add contact for recipient with address {:?}: {:#}",
2589 recipient.addr,
2590 err
2591 );
2592 continue;
2593 }
2594 Ok(recipient_addr) => recipient_addr,
2595 };
2596
2597 let (_, modified) = Contact::add_or_lookup(
2598 context,
2599 &recipient.display_name.unwrap_or_default(),
2600 &recipient_addr,
2601 Origin::OutgoingTo,
2602 )
2603 .await?;
2604 if modified != Modifier::None {
2605 any_modified = true;
2606 }
2607 }
2608 if any_modified {
2609 context.emit_event(EventType::ContactsChanged(None));
2610 }
2611
2612 Ok(())
2613}
2614
2615#[cfg(test)]
2616mod imap_tests;