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