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