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 futures::{FutureExt as _, TryStreamExt};
20use futures_lite::FutureExt;
21use num_traits::FromPrimitive;
22use ratelimit::Ratelimit;
23use url::Url;
24
25use crate::calls::{
26 UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
27};
28use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
29use crate::chatlist_events;
30use crate::config::Config;
31use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
32use crate::contact::ContactId;
33use crate::context::Context;
34use crate::events::EventType;
35use crate::headerdef::{HeaderDef, HeaderDefMap};
36use crate::log::{LogExt, warn};
37use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
38use crate::mimeparser;
39use crate::net::proxy::ProxyConfig;
40use crate::net::session::SessionStream;
41use crate::oauth2::get_oauth2_access_token;
42use crate::push::encrypt_device_token;
43use crate::receive_imf::{
44 ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
45};
46use crate::scheduler::connectivity::ConnectivityStore;
47use crate::stock_str;
48use crate::tools::{self, create_id, duration_to_str, time};
49use crate::transport::{
50 ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
51};
52
53pub(crate) mod capabilities;
54mod client;
55mod idle;
56pub mod select_folder;
57pub(crate) mod session;
58
59use client::{Client, determine_capabilities};
60use session::Session;
61
62pub(crate) const GENERATED_PREFIX: &str = "GEN_";
63
64const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
65 MESSAGE-ID \
66 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
67 )])";
68const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
69
70#[derive(Debug)]
71pub(crate) struct Imap {
72 transport_id: u32,
76
77 pub(crate) idle_interrupt_receiver: Receiver<()>,
78
79 pub(crate) addr: String,
81
82 lp: Vec<ConfiguredServerLoginParam>,
84
85 password: String,
87
88 proxy_config: Option<ProxyConfig>,
90
91 strict_tls: bool,
92
93 oauth2: bool,
94
95 authentication_failed_once: bool,
96
97 pub(crate) connectivity: ConnectivityStore,
98
99 conn_last_try: tools::Time,
100 conn_backoff_ms: u64,
101
102 ratelimit: Ratelimit,
110
111 pub(crate) resync_request_sender: async_channel::Sender<()>,
113
114 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
116}
117
118#[derive(Debug)]
119struct OAuth2 {
120 user: String,
121 access_token: String,
122}
123
124#[derive(Debug, Default)]
125pub(crate) struct ServerMetadata {
126 pub comment: Option<String>,
129
130 pub admin: Option<String>,
133
134 pub iroh_relay: Option<Url>,
135
136 pub ice_servers: Vec<UnresolvedIceServer>,
138
139 pub ice_servers_expiration_timestamp: i64,
146}
147
148impl async_imap::Authenticator for OAuth2 {
149 type Response = String;
150
151 fn process(&mut self, _data: &[u8]) -> Self::Response {
152 format!(
153 "user={}\x01auth=Bearer {}\x01\x01",
154 self.user, self.access_token
155 )
156 }
157}
158
159#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
160pub enum FolderMeaning {
161 Unknown,
162
163 Spam,
165 Inbox,
166 Mvbox,
167 Trash,
168
169 Virtual,
176}
177
178impl FolderMeaning {
179 pub fn to_config(self) -> Option<Config> {
180 match self {
181 FolderMeaning::Unknown => None,
182 FolderMeaning::Spam => None,
183 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
184 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
185 FolderMeaning::Trash => None,
186 FolderMeaning::Virtual => None,
187 }
188 }
189}
190
191struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
192 inner: Peekable<T>,
193}
194
195impl<T, I> From<I> for UidGrouper<T>
196where
197 T: Iterator<Item = (i64, u32, String)>,
198 I: IntoIterator<IntoIter = T>,
199{
200 fn from(inner: I) -> Self {
201 Self {
202 inner: inner.into_iter().peekable(),
203 }
204 }
205}
206
207impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
208 type Item = (String, Vec<i64>, String);
210
211 #[expect(clippy::arithmetic_side_effects)]
212 fn next(&mut self) -> Option<Self::Item> {
213 let (_, _, folder) = self.inner.peek().cloned()?;
214
215 let mut uid_set = String::new();
216 let mut rowid_set = Vec::new();
217
218 while uid_set.len() < 1000 {
219 if let Some((start_rowid, start_uid, _)) = self
221 .inner
222 .next_if(|(_, _, start_folder)| start_folder == &folder)
223 {
224 rowid_set.push(start_rowid);
225 let mut end_uid = start_uid;
226
227 while let Some((next_rowid, next_uid, _)) =
228 self.inner.next_if(|(_, next_uid, next_folder)| {
229 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
230 })
231 {
232 end_uid = next_uid;
233 rowid_set.push(next_rowid);
234 }
235
236 let uid_range = UidRange {
237 start: start_uid,
238 end: end_uid,
239 };
240 if !uid_set.is_empty() {
241 uid_set.push(',');
242 }
243 uid_set.push_str(&uid_range.to_string());
244 } else {
245 break;
246 }
247 }
248
249 Some((folder, rowid_set, uid_set))
250 }
251}
252
253impl Imap {
254 pub async fn new(
256 context: &Context,
257 transport_id: u32,
258 param: ConfiguredLoginParam,
259 idle_interrupt_receiver: Receiver<()>,
260 ) -> Result<Self> {
261 let lp = param.imap.clone();
262 let password = param.imap_password.clone();
263 let proxy_config = ProxyConfig::load(context).await?;
264 let addr = ¶m.addr;
265 let strict_tls = param.strict_tls(proxy_config.is_some());
266 let oauth2 = param.oauth2;
267 let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
268 Ok(Imap {
269 transport_id,
270 idle_interrupt_receiver,
271 addr: addr.to_string(),
272 lp,
273 password,
274 proxy_config,
275 strict_tls,
276 oauth2,
277 authentication_failed_once: false,
278 connectivity: Default::default(),
279 conn_last_try: UNIX_EPOCH,
280 conn_backoff_ms: 0,
281 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
283 resync_request_sender,
284 resync_request_receiver,
285 })
286 }
287
288 pub async fn new_configured(
290 context: &Context,
291 idle_interrupt_receiver: Receiver<()>,
292 ) -> Result<Self> {
293 let (transport_id, param) = ConfiguredLoginParam::load(context)
294 .await?
295 .context("Not configured")?;
296 let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
297 Ok(imap)
298 }
299
300 pub(crate) async fn connect(
306 &mut self,
307 context: &Context,
308 configuring: bool,
309 ) -> Result<Session> {
310 let now = tools::Time::now();
311 let until_can_send = max(
312 min(self.conn_last_try, now)
313 .checked_add(Duration::from_millis(self.conn_backoff_ms))
314 .unwrap_or(now),
315 now,
316 )
317 .duration_since(now)?;
318 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
319 if !ratelimit_duration.is_zero() {
320 warn!(
321 context,
322 "IMAP got rate limited, waiting for {} until can connect.",
323 duration_to_str(ratelimit_duration),
324 );
325 let interrupted = async {
326 tokio::time::sleep(ratelimit_duration).await;
327 false
328 }
329 .race(self.idle_interrupt_receiver.recv().map(|_| true))
330 .await;
331 if interrupted {
332 info!(
333 context,
334 "Connecting to IMAP without waiting for ratelimit due to interrupt."
335 );
336 }
337 }
338
339 info!(context, "Connecting to IMAP server.");
340 self.connectivity.set_connecting(context);
341
342 self.conn_last_try = tools::Time::now();
343 const BACKOFF_MIN_MS: u64 = 2000;
344 const BACKOFF_MAX_MS: u64 = 80_000;
345 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
346 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
347 (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
348 ));
349 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
350
351 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
352 let mut first_error = None;
353 for lp in login_params {
354 info!(context, "IMAP trying to connect to {}.", &lp.connection);
355 let connection_candidate = lp.connection.clone();
356 let client = match Client::connect(
357 context,
358 self.proxy_config.clone(),
359 self.strict_tls,
360 &connection_candidate,
361 )
362 .await
363 .with_context(|| format!("IMAP failed to connect to {connection_candidate}"))
364 {
365 Ok(client) => client,
366 Err(err) => {
367 warn!(context, "{err:#}.");
368 first_error.get_or_insert(err);
369 continue;
370 }
371 };
372
373 self.conn_backoff_ms = BACKOFF_MIN_MS;
374 self.ratelimit.send();
375
376 let imap_user: &str = lp.user.as_ref();
377 let imap_pw: &str = &self.password;
378
379 let login_res = if self.oauth2 {
380 info!(context, "Logging into IMAP server with OAuth 2.");
381 let addr: &str = self.addr.as_ref();
382
383 let token = get_oauth2_access_token(context, addr, imap_pw, true)
384 .await?
385 .context("IMAP could not get OAUTH token")?;
386 let auth = OAuth2 {
387 user: imap_user.into(),
388 access_token: token,
389 };
390 client.authenticate("XOAUTH2", auth).await
391 } else {
392 info!(context, "Logging into IMAP server with LOGIN.");
393 client.login(imap_user, imap_pw).await
394 };
395
396 match login_res {
397 Ok(mut session) => {
398 let capabilities = determine_capabilities(&mut session).await?;
399 let resync_request_sender = self.resync_request_sender.clone();
400
401 let session = if capabilities.can_compress {
402 info!(context, "Enabling IMAP compression.");
403 let compressed_session = session
404 .compress(|s| {
405 let session_stream: Box<dyn SessionStream> = Box::new(s);
406 session_stream
407 })
408 .await
409 .context("Failed to enable IMAP compression")?;
410 Session::new(
411 compressed_session,
412 capabilities,
413 resync_request_sender,
414 self.transport_id,
415 )
416 } else {
417 Session::new(
418 session,
419 capabilities,
420 resync_request_sender,
421 self.transport_id,
422 )
423 };
424
425 let mut lock = context.server_id.write().await;
427 lock.clone_from(&session.capabilities.server_id);
428
429 self.authentication_failed_once = false;
430 context.emit_event(EventType::ImapConnected(format!(
431 "IMAP-LOGIN as {}",
432 lp.user
433 )));
434 self.connectivity.set_preparing(context);
435 info!(context, "Successfully logged into IMAP server.");
436 return Ok(session);
437 }
438
439 Err(err) => {
440 let imap_user = lp.user.to_owned();
441 let message = stock_str::cannot_login(context, &imap_user).await;
442
443 warn!(context, "IMAP failed to login: {err:#}.");
444 first_error.get_or_insert(format_err!("{message} ({err:#})"));
445
446 let _lock = context.wrong_pw_warning_mutex.lock().await;
448 if err.to_string().to_lowercase().contains("authentication") {
449 if self.authentication_failed_once
450 && !configuring
451 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
452 {
453 let mut msg = Message::new_text(message);
454 if let Err(e) = chat::add_device_msg_with_importance(
455 context,
456 None,
457 Some(&mut msg),
458 true,
459 )
460 .await
461 {
462 warn!(context, "Failed to add device message: {e:#}.");
463 } else {
464 context
465 .set_config_internal(Config::NotifyAboutWrongPw, None)
466 .await
467 .log_err(context)
468 .ok();
469 }
470 } else {
471 self.authentication_failed_once = true;
472 }
473 } else {
474 self.authentication_failed_once = false;
475 }
476 }
477 }
478 }
479
480 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
481 }
482
483 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
488 let configuring = false;
489 let mut session = match self.connect(context, configuring).await {
490 Ok(session) => session,
491 Err(err) => {
492 self.connectivity.set_err(context, &err);
493 return Err(err);
494 }
495 };
496
497 let folders_configured = context
498 .sql
499 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
500 .await?;
501 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
502 self.configure_folders(context, &mut session).await?;
503 }
504
505 Ok(session)
506 }
507
508 pub async fn fetch_move_delete(
513 &mut self,
514 context: &Context,
515 session: &mut Session,
516 watch_folder: &str,
517 folder_meaning: FolderMeaning,
518 ) -> Result<()> {
519 if !context.sql.is_open().await {
520 bail!("IMAP operation attempted while it is torn down");
522 }
523
524 let msgs_fetched = self
525 .fetch_new_messages(context, session, watch_folder, folder_meaning)
526 .await
527 .context("fetch_new_messages")?;
528 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
529 context.scheduler.interrupt_ephemeral_task().await;
534 }
535
536 session
537 .move_delete_messages(context, watch_folder)
538 .await
539 .context("move_delete_messages")?;
540
541 Ok(())
542 }
543
544 #[expect(clippy::arithmetic_side_effects)]
548 pub(crate) async fn fetch_new_messages(
549 &mut self,
550 context: &Context,
551 session: &mut Session,
552 folder: &str,
553 folder_meaning: FolderMeaning,
554 ) -> Result<bool> {
555 if should_ignore_folder(context, folder, folder_meaning).await? {
556 info!(context, "Not fetching from {folder:?}.");
557 session.new_mail = false;
558 return Ok(false);
559 }
560
561 let folder_exists = session
562 .select_with_uidvalidity(context, folder)
563 .await
564 .with_context(|| format!("Failed to select folder {folder:?}"))?;
565 if !folder_exists {
566 return Ok(false);
567 }
568
569 if !session.new_mail {
570 info!(context, "No new emails in folder {folder:?}.");
571 return Ok(false);
572 }
573 session.new_mail = false;
574
575 let mut read_cnt = 0;
576 loop {
577 let (n, fetch_more) = self
578 .fetch_new_msg_batch(context, session, folder, folder_meaning)
579 .await?;
580 read_cnt += n;
581 if !fetch_more {
582 return Ok(read_cnt > 0);
583 }
584 }
585 }
586
587 #[expect(clippy::arithmetic_side_effects)]
589 async fn fetch_new_msg_batch(
590 &mut self,
591 context: &Context,
592 session: &mut Session,
593 folder: &str,
594 folder_meaning: FolderMeaning,
595 ) -> Result<(usize, bool)> {
596 let transport_id = self.transport_id;
597 let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
598 let old_uid_next = get_uid_next(context, transport_id, folder).await?;
599 info!(
600 context,
601 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
602 );
603
604 let uids_to_prefetch = 500;
605 let msgs = session
606 .prefetch(old_uid_next, uids_to_prefetch)
607 .await
608 .context("prefetch")?;
609 let read_cnt = msgs.len();
610
611 let mut uids_fetch: Vec<u32> = Vec::new();
612 let mut available_post_msgs: Vec<String> = Vec::new();
613 let mut download_later: Vec<String> = Vec::new();
614 let mut uid_message_ids = BTreeMap::new();
615 let mut largest_uid_skipped = None;
616
617 let download_limit: Option<u32> = context
618 .get_config_parsed(Config::DownloadLimit)
619 .await?
620 .filter(|&l| 0 < l);
621
622 for (uid, ref fetch_response) in msgs {
624 let headers = match get_fetch_headers(fetch_response) {
625 Ok(headers) => headers,
626 Err(err) => {
627 warn!(context, "Failed to parse FETCH headers: {err:#}.");
628 continue;
629 }
630 };
631
632 let message_id = prefetch_get_message_id(&headers);
633 let size = fetch_response
634 .size
635 .context("imap fetch response does not contain size")?;
636
637 let delete = if let Some(message_id) = &message_id {
648 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
649 .await?
650 .is_some_and(|(_msg_id, deleted)| deleted)
651 } else {
652 false
653 };
654
655 let message_id = message_id.unwrap_or_else(create_message_id);
658
659 if delete {
660 info!(context, "Deleting locally deleted message {message_id}.");
661 }
662
663 let _target;
664 let target = if delete {
665 ""
666 } else {
667 _target = target_folder(context, folder, folder_meaning, &headers).await?;
668 &_target
669 };
670
671 context
672 .sql
673 .execute(
674 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
675 VALUES (?, ?, ?, ?, ?, ?)
676 ON CONFLICT(transport_id, folder, uid, uidvalidity)
677 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
678 target=excluded.target",
679 (
680 self.transport_id,
681 &message_id,
682 &folder,
683 uid,
684 uid_validity,
685 target,
686 ),
687 )
688 .await?;
689
690 if folder == target
697 && folder_meaning != FolderMeaning::Spam
702 && prefetch_should_download(
703 context,
704 &headers,
705 &message_id,
706 fetch_response.flags(),
707 )
708 .await.context("prefetch_should_download")?
709 {
710 if headers
711 .get_header_value(HeaderDef::ChatIsPostMessage)
712 .is_some()
713 {
714 info!(context, "{message_id:?} is a post-message.");
715 available_post_msgs.push(message_id.clone());
716
717 if download_limit.is_none_or(|download_limit| size <= download_limit) {
718 download_later.push(message_id.clone());
719 }
720 largest_uid_skipped = Some(uid);
721 } else {
722 info!(context, "{message_id:?} is not a post-message.");
723 if download_limit.is_none_or(|download_limit| size <= download_limit) {
724 uids_fetch.push(uid);
725 uid_message_ids.insert(uid, message_id);
726 } else {
727 download_later.push(message_id.clone());
728 largest_uid_skipped = Some(uid);
729 }
730 };
731 } else {
732 largest_uid_skipped = Some(uid);
733 }
734 }
735
736 if !uids_fetch.is_empty() {
737 self.connectivity.set_working(context);
738 }
739
740 let (sender, receiver) = async_channel::unbounded();
741
742 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
743 let mailbox_uid_next = session
744 .selected_mailbox
745 .as_ref()
746 .with_context(|| format!("Expected {folder:?} to be selected"))?
747 .uid_next
748 .unwrap_or_default();
749
750 let update_uids_future = async {
751 let mut largest_uid_fetched: u32 = 0;
752
753 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
754 largest_uid_fetched = max(largest_uid_fetched, uid);
755 if let Some(received_msg) = received_msg_opt {
756 received_msgs.push(received_msg)
757 }
758 }
759
760 largest_uid_fetched
761 };
762
763 let actually_download_messages_future = async {
764 session
765 .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
766 .await
767 .context("fetch_many_msgs")
768 };
769
770 let (largest_uid_fetched, fetch_res) =
771 tokio::join!(update_uids_future, actually_download_messages_future);
772
773 let mut new_uid_next = largest_uid_fetched + 1;
779 let fetch_more = fetch_res.is_ok() && {
780 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
781 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
785
786 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
787
788 prefetch_uid_next < mailbox_uid_next
789 };
790 if new_uid_next > old_uid_next {
791 set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
792 }
793
794 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
795
796 if !received_msgs.is_empty() {
797 context.emit_event(EventType::IncomingMsgBunch);
798 }
799
800 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
801
802 if fetch_res.is_ok() {
803 info!(
804 context,
805 "available_post_msgs: {}, download_later: {}.",
806 available_post_msgs.len(),
807 download_later.len(),
808 );
809 let trans_fn = |t: &mut rusqlite::Transaction| {
810 let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
811 for rfc724_mid in available_post_msgs {
812 stmt.execute((rfc724_mid,))
813 .context("INSERT OR IGNORE INTO available_post_msgs")?;
814 }
815 let mut stmt =
816 t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
817 for rfc724_mid in download_later {
818 stmt.execute((rfc724_mid,))
819 .context("INSERT OR IGNORE INTO download")?;
820 }
821 Ok(())
822 };
823 context.sql.transaction(trans_fn).await?;
824 }
825
826 fetch_res?;
829
830 Ok((read_cnt, fetch_more))
831 }
832}
833
834impl Session {
835 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
837 let all_folders = self
838 .list_folders()
839 .await
840 .context("listing folders for resync")?;
841 for folder in all_folders {
842 let folder_meaning = get_folder_meaning(&folder);
843 if !matches!(
844 folder_meaning,
845 FolderMeaning::Virtual | FolderMeaning::Unknown
846 ) {
847 self.resync_folder_uids(context, folder.name(), folder_meaning)
848 .await?;
849 }
850 }
851 Ok(())
852 }
853
854 pub(crate) async fn resync_folder_uids(
861 &mut self,
862 context: &Context,
863 folder: &str,
864 folder_meaning: FolderMeaning,
865 ) -> Result<()> {
866 let uid_validity;
867 let mut msgs = BTreeMap::new();
869
870 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
871 let transport_id = self.transport_id();
872 if folder_exists {
873 let mut list = self
874 .uid_fetch("1:*", RFC724MID_UID)
875 .await
876 .with_context(|| format!("Can't resync folder {folder}"))?;
877 while let Some(fetch) = list.try_next().await? {
878 let headers = match get_fetch_headers(&fetch) {
879 Ok(headers) => headers,
880 Err(err) => {
881 warn!(context, "Failed to parse FETCH headers: {}", err);
882 continue;
883 }
884 };
885 let message_id = prefetch_get_message_id(&headers);
886
887 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
888 msgs.insert(
889 uid,
890 (
891 rfc724_mid,
892 target_folder(context, folder, folder_meaning, &headers).await?,
893 ),
894 );
895 }
896 }
897
898 info!(
899 context,
900 "resync_folder_uids: Collected {} message IDs in {folder}.",
901 msgs.len(),
902 );
903
904 uid_validity = get_uidvalidity(context, transport_id, folder).await?;
905 } else {
906 warn!(context, "resync_folder_uids: No folder {folder}.");
907 uid_validity = 0;
908 }
909
910 context
912 .sql
913 .transaction(move |transaction| {
914 transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
915 for (uid, (rfc724_mid, target)) in &msgs {
916 transaction.execute(
919 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
920 VALUES (?, ?, ?, ?, ?, ?)
921 ON CONFLICT(transport_id, folder, uid, uidvalidity)
922 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
923 target=excluded.target",
924 (transport_id, rfc724_mid, folder, uid, uid_validity, target),
925 )?;
926 }
927 Ok(())
928 })
929 .await?;
930 Ok(())
931 }
932
933 async fn delete_message_batch(
936 &mut self,
937 context: &Context,
938 uid_set: &str,
939 row_ids: Vec<i64>,
940 ) -> Result<()> {
941 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
943 .await?;
944 context
945 .sql
946 .transaction(|transaction| {
947 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
948 for row_id in row_ids {
949 stmt.execute((row_id,))?;
950 }
951 Ok(())
952 })
953 .await
954 .context("Cannot remove deleted messages from imap table")?;
955
956 context.emit_event(EventType::ImapMessageDeleted(format!(
957 "IMAP messages {uid_set} marked as deleted"
958 )));
959 Ok(())
960 }
961
962 async fn move_message_batch(
965 &mut self,
966 context: &Context,
967 set: &str,
968 row_ids: Vec<i64>,
969 target: &str,
970 ) -> Result<()> {
971 if self.can_move() {
972 match self.uid_mv(set, &target).await {
973 Ok(()) => {
974 context
976 .sql
977 .transaction(|transaction| {
978 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
979 for row_id in row_ids {
980 stmt.execute((row_id,))?;
981 }
982 Ok(())
983 })
984 .await
985 .context("Cannot delete moved messages from imap table")?;
986 context.emit_event(EventType::ImapMessageMoved(format!(
987 "IMAP messages {set} moved to {target}"
988 )));
989 return Ok(());
990 }
991 Err(err) => {
992 warn!(
993 context,
994 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
995 set,
996 target,
997 err
998 );
999 }
1000 }
1001 }
1002
1003 info!(
1006 context,
1007 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1008 );
1009 self.uid_copy(&set, &target).await?;
1010 context
1011 .sql
1012 .transaction(|transaction| {
1013 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1014 for row_id in row_ids {
1015 stmt.execute((row_id,))?;
1016 }
1017 Ok(())
1018 })
1019 .await
1020 .context("Cannot plan deletion of messages")?;
1021 context.emit_event(EventType::ImapMessageMoved(format!(
1022 "IMAP messages {set} copied to {target}"
1023 )));
1024 Ok(())
1025 }
1026
1027 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1031 let transport_id = self.transport_id();
1032 let rows = context
1033 .sql
1034 .query_map_vec(
1035 "SELECT id, uid, target FROM imap
1036 WHERE folder = ?
1037 AND transport_id = ?
1038 AND target != folder
1039 ORDER BY target, uid",
1040 (folder, transport_id),
1041 |row| {
1042 let rowid: i64 = row.get(0)?;
1043 let uid: u32 = row.get(1)?;
1044 let target: String = row.get(2)?;
1045 Ok((rowid, uid, target))
1046 },
1047 )
1048 .await?;
1049
1050 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1051 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1056 ensure!(folder_exists, "No folder {folder}");
1057
1058 if target.is_empty() {
1060 self.delete_message_batch(context, &uid_set, rowid_set)
1061 .await
1062 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1063 } else {
1064 self.move_message_batch(context, &uid_set, rowid_set, &target)
1065 .await
1066 .with_context(|| {
1067 format!(
1068 "cannot move batch of messages {:?} to folder {:?}",
1069 &uid_set, target
1070 )
1071 })?;
1072 }
1073 }
1074
1075 if let Err(err) = self.maybe_close_folder(context).await {
1078 warn!(context, "Failed to close folder: {err:#}.");
1079 }
1080
1081 Ok(())
1082 }
1083
1084 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1086 if context.get_config_bool(Config::TeamProfile).await? {
1087 return Ok(());
1088 }
1089
1090 let transport_id = self.transport_id();
1091 let rows = context
1092 .sql
1093 .query_map_vec(
1094 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1095 WHERE imap.id = imap_markseen.id
1096 AND imap.transport_id=?
1097 AND target = folder
1098 ORDER BY folder, uid",
1099 (transport_id,),
1100 |row| {
1101 let rowid: i64 = row.get(0)?;
1102 let uid: u32 = row.get(1)?;
1103 let folder: String = row.get(2)?;
1104 Ok((rowid, uid, folder))
1105 },
1106 )
1107 .await?;
1108
1109 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1110 let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
1111 Err(err) => {
1112 warn!(
1113 context,
1114 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1115 );
1116 continue;
1117 }
1118 Ok(folder_exists) => folder_exists,
1119 };
1120 if !folder_exists {
1121 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1122 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1123 warn!(
1124 context,
1125 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1126 );
1127 continue;
1128 } else {
1129 info!(
1130 context,
1131 "Marked messages {} in folder {} as seen.", uid_set, folder
1132 );
1133 }
1134 context
1135 .sql
1136 .transaction(|transaction| {
1137 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1138 for rowid in rowid_set {
1139 stmt.execute((rowid,))?;
1140 }
1141 Ok(())
1142 })
1143 .await
1144 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1145 }
1146
1147 Ok(())
1148 }
1149
1150 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1152 if !self.can_condstore() {
1153 info!(
1154 context,
1155 "Server does not support CONDSTORE, skipping flag synchronization."
1156 );
1157 return Ok(());
1158 }
1159
1160 if context.get_config_bool(Config::TeamProfile).await? {
1161 return Ok(());
1162 }
1163
1164 let folder_exists = self
1165 .select_with_uidvalidity(context, folder)
1166 .await
1167 .context("Failed to select folder")?;
1168 if !folder_exists {
1169 return Ok(());
1170 }
1171
1172 let mailbox = self
1173 .selected_mailbox
1174 .as_ref()
1175 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1176
1177 if mailbox.highest_modseq.is_none() {
1180 info!(
1181 context,
1182 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1183 );
1184 return Ok(());
1185 }
1186
1187 let transport_id = self.transport_id();
1188 let mut updated_chat_ids = BTreeSet::new();
1189 let uid_validity = get_uidvalidity(context, transport_id, folder)
1190 .await
1191 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1192 let mut highest_modseq = get_modseq(context, transport_id, folder)
1193 .await
1194 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1195 let mut list = self
1196 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1197 .await
1198 .context("failed to fetch flags")?;
1199
1200 let mut got_unsolicited_fetch = false;
1201
1202 while let Some(fetch) = list
1203 .try_next()
1204 .await
1205 .context("failed to get FETCH result")?
1206 {
1207 let uid = if let Some(uid) = fetch.uid {
1208 uid
1209 } else {
1210 info!(context, "FETCH result contains no UID, skipping");
1211 got_unsolicited_fetch = true;
1212 continue;
1213 };
1214 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1215 if is_seen
1216 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1217 .await
1218 .with_context(|| {
1219 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1220 })?
1221 {
1222 updated_chat_ids.insert(chat_id);
1223 }
1224
1225 if let Some(modseq) = fetch.modseq {
1226 if modseq > highest_modseq {
1227 highest_modseq = modseq;
1228 }
1229 } else {
1230 warn!(context, "FETCH result contains no MODSEQ");
1231 }
1232 }
1233 drop(list);
1234
1235 if got_unsolicited_fetch {
1236 self.new_mail = true;
1241 }
1242
1243 set_modseq(context, transport_id, folder, highest_modseq)
1244 .await
1245 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1246 if !updated_chat_ids.is_empty() {
1247 context.on_archived_chats_maybe_noticed();
1248 }
1249 for updated_chat_id in updated_chat_ids {
1250 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1251 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1252 }
1253
1254 Ok(())
1255 }
1256
1257 #[expect(clippy::arithmetic_side_effects)]
1272 pub(crate) async fn fetch_many_msgs(
1273 &mut self,
1274 context: &Context,
1275 folder: &str,
1276 request_uids: Vec<u32>,
1277 uid_message_ids: &BTreeMap<u32, String>,
1278 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1279 ) -> Result<()> {
1280 if request_uids.is_empty() {
1281 return Ok(());
1282 }
1283
1284 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1285 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1286 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1287 format!("fetching messages {} from folder \"{}\"", &set, folder)
1288 })?;
1289
1290 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1293
1294 let mut count = 0;
1295 for &request_uid in &request_uids {
1296 let mut fetch_response = uid_msgs.remove(&request_uid);
1298
1299 while fetch_response.is_none() {
1301 let Some(next_fetch_response) = fetch_responses
1302 .try_next()
1303 .await
1304 .context("Failed to process IMAP FETCH result")?
1305 else {
1306 break;
1308 };
1309
1310 if let Some(next_uid) = next_fetch_response.uid {
1311 if next_uid == request_uid {
1312 fetch_response = Some(next_fetch_response);
1313 } else if !request_uids.contains(&next_uid) {
1314 info!(
1321 context,
1322 "Skipping not requested FETCH response for UID {}.", next_uid
1323 );
1324 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1325 warn!(context, "Got duplicated UID {}.", next_uid);
1326 }
1327 } else {
1328 info!(context, "Skipping FETCH response without UID.");
1329 }
1330 }
1331
1332 let fetch_response = match fetch_response {
1333 Some(fetch) => fetch,
1334 None => {
1335 warn!(
1336 context,
1337 "Missed UID {} in the server response.", request_uid
1338 );
1339 continue;
1340 }
1341 };
1342 count += 1;
1343
1344 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1345 let body = fetch_response.body();
1346
1347 if is_deleted {
1348 info!(context, "Not processing deleted msg {}.", request_uid);
1349 received_msgs_channel.send((request_uid, None)).await?;
1350 continue;
1351 }
1352
1353 let body = if let Some(body) = body {
1354 body
1355 } else {
1356 info!(
1357 context,
1358 "Not processing message {} without a BODY.", request_uid
1359 );
1360 received_msgs_channel.send((request_uid, None)).await?;
1361 continue;
1362 };
1363
1364 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1365
1366 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1367 error!(
1368 context,
1369 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1370 request_uid
1371 );
1372 continue;
1373 };
1374
1375 info!(
1376 context,
1377 "Passing message UID {} to receive_imf().", request_uid
1378 );
1379 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1380 let received_msg = match res {
1381 Err(err) => {
1382 warn!(context, "receive_imf error: {err:#}.");
1383
1384 let text = format!(
1385 "❌ 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/.",
1386 );
1387 let mut msg = Message::new_text(text);
1388 add_device_msg(context, None, Some(&mut msg)).await?;
1389 None
1390 }
1391 Ok(msg) => msg,
1392 };
1393 received_msgs_channel
1394 .send((request_uid, received_msg))
1395 .await?;
1396 }
1397
1398 while fetch_responses
1405 .try_next()
1406 .await
1407 .context("Failed to drain FETCH responses")?
1408 .is_some()
1409 {}
1410
1411 if count != request_uids.len() {
1412 warn!(
1413 context,
1414 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1415 count,
1416 request_uids.len(),
1417 request_uids,
1418 );
1419 } else {
1420 info!(
1421 context,
1422 "Successfully received {} UIDs.",
1423 request_uids.len()
1424 );
1425 }
1426 }
1427
1428 Ok(())
1429 }
1430
1431 #[expect(clippy::arithmetic_side_effects)]
1437 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1438 let mut lock = context.metadata.write().await;
1439
1440 if !self.can_metadata() {
1441 *lock = Some(Default::default());
1442 }
1443 if let Some(ref mut old_metadata) = *lock {
1444 let now = time();
1445
1446 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1448 return Ok(());
1449 }
1450
1451 let mut got_turn_server = false;
1452 if self.can_metadata() {
1453 info!(context, "ICE servers expired, requesting new credentials.");
1454 let mailbox = "";
1455 let options = "";
1456 let metadata = self
1457 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1458 .await?;
1459 for m in metadata {
1460 if m.entry == "/shared/vendor/deltachat/turn"
1461 && let Some(value) = m.value
1462 {
1463 match create_ice_servers_from_metadata(&value).await {
1464 Ok((parsed_timestamp, parsed_ice_servers)) => {
1465 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1466 old_metadata.ice_servers = parsed_ice_servers;
1467 got_turn_server = true;
1468 }
1469 Err(err) => {
1470 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1471 }
1472 }
1473 }
1474 }
1475 }
1476 if !got_turn_server {
1477 info!(context, "Will use fallback ICE servers.");
1478 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1480 old_metadata.ice_servers = create_fallback_ice_servers();
1481 }
1482 return Ok(());
1483 }
1484
1485 info!(
1486 context,
1487 "Server supports metadata, retrieving server comment and admin contact."
1488 );
1489
1490 let mut comment = None;
1491 let mut admin = None;
1492 let mut iroh_relay = None;
1493 let mut ice_servers = None;
1494 let mut ice_servers_expiration_timestamp = 0;
1495
1496 let mailbox = "";
1497 let options = "";
1498 let metadata = self
1499 .get_metadata(
1500 mailbox,
1501 options,
1502 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1503 )
1504 .await?;
1505 for m in metadata {
1506 match m.entry.as_ref() {
1507 "/shared/comment" => {
1508 comment = m.value;
1509 }
1510 "/shared/admin" => {
1511 admin = m.value;
1512 }
1513 "/shared/vendor/deltachat/irohrelay" => {
1514 if let Some(value) = m.value {
1515 if let Ok(url) = Url::parse(&value) {
1516 iroh_relay = Some(url);
1517 } else {
1518 warn!(
1519 context,
1520 "Got invalid URL from iroh relay metadata: {:?}.", value
1521 );
1522 }
1523 }
1524 }
1525 "/shared/vendor/deltachat/turn" => {
1526 if let Some(value) = m.value {
1527 match create_ice_servers_from_metadata(&value).await {
1528 Ok((parsed_timestamp, parsed_ice_servers)) => {
1529 ice_servers_expiration_timestamp = parsed_timestamp;
1530 ice_servers = Some(parsed_ice_servers);
1531 }
1532 Err(err) => {
1533 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1534 }
1535 }
1536 }
1537 }
1538 _ => {}
1539 }
1540 }
1541 let ice_servers = if let Some(ice_servers) = ice_servers {
1542 ice_servers
1543 } else {
1544 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1546 create_fallback_ice_servers()
1547 };
1548
1549 *lock = Some(ServerMetadata {
1550 comment,
1551 admin,
1552 iroh_relay,
1553 ice_servers,
1554 ice_servers_expiration_timestamp,
1555 });
1556 Ok(())
1557 }
1558
1559 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1561 if context.push_subscribed.load(Ordering::Relaxed) {
1562 return Ok(());
1563 }
1564
1565 let Some(device_token) = context.push_subscriber.device_token().await else {
1566 return Ok(());
1567 };
1568
1569 if self.can_metadata() && self.can_push() {
1570 let old_encrypted_device_token =
1571 context.get_config(Config::EncryptedDeviceToken).await?;
1572
1573 let device_token_changed = old_encrypted_device_token.is_none()
1575 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1576
1577 let new_encrypted_device_token;
1578 if device_token_changed {
1579 let encrypted_device_token = encrypt_device_token(&device_token)
1580 .context("Failed to encrypt device token")?;
1581
1582 let encrypted_device_token_len = encrypted_device_token.len();
1586
1587 context
1593 .set_config_internal(Config::DeviceToken, Some(&device_token))
1594 .await?;
1595 context
1596 .set_config_internal(
1597 Config::EncryptedDeviceToken,
1598 Some(&encrypted_device_token),
1599 )
1600 .await?;
1601
1602 if encrypted_device_token_len <= 4096 {
1603 new_encrypted_device_token = Some(encrypted_device_token);
1604 } else {
1605 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1615 new_encrypted_device_token = None;
1616 }
1617 } else {
1618 new_encrypted_device_token = old_encrypted_device_token;
1619 }
1620
1621 if let Some(encrypted_device_token) = new_encrypted_device_token {
1624 let folder = context
1625 .get_config(Config::ConfiguredInboxFolder)
1626 .await?
1627 .context("INBOX is not configured")?;
1628
1629 self.run_command_and_check_ok(&format_setmetadata(
1630 &folder,
1631 &encrypted_device_token,
1632 ))
1633 .await
1634 .context("SETMETADATA command failed")?;
1635
1636 context.push_subscribed.store(true, Ordering::Relaxed);
1637 }
1638 } else if !context.push_subscriber.heartbeat_subscribed().await {
1639 let context = context.clone();
1640 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1642 }
1643
1644 Ok(())
1645 }
1646}
1647
1648fn format_setmetadata(folder: &str, device_token: &str) -> String {
1649 let device_token_len = device_token.len();
1650 format!(
1651 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1652 )
1653}
1654
1655impl Session {
1656 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1662 if flag == "\\Deleted" {
1663 self.selected_folder_needs_expunge = true;
1664 }
1665 let query = format!("+FLAGS ({flag})");
1666 let mut responses = self
1667 .uid_store(uid_set, &query)
1668 .await
1669 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1670 while let Some(_response) = responses.try_next().await? {
1671 }
1673 Ok(())
1674 }
1675
1676 async fn configure_mvbox<'a>(
1685 &mut self,
1686 context: &Context,
1687 folders: &[&'a str],
1688 ) -> Result<Option<&'a str>> {
1689 self.maybe_close_folder(context).await?;
1692
1693 for folder in folders {
1694 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1695 let res = self.examine(&folder).await;
1696 if res.is_ok() {
1697 info!(
1698 context,
1699 "MVBOX-folder {:?} successfully selected, using it.", &folder
1700 );
1701 self.close().await?;
1702 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1705 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1706 return Ok(Some(folder));
1707 }
1708 }
1709
1710 Ok(None)
1711 }
1712}
1713
1714impl Imap {
1715 pub(crate) async fn configure_folders(
1716 &mut self,
1717 context: &Context,
1718 session: &mut Session,
1719 ) -> Result<()> {
1720 let mut folders = session
1721 .list(Some(""), Some("*"))
1722 .await
1723 .context("list_folders failed")?;
1724 let mut delimiter = ".".to_string();
1725 let mut delimiter_is_default = true;
1726 let mut folder_configs = BTreeMap::new();
1727
1728 while let Some(folder) = folders.try_next().await? {
1729 info!(context, "Scanning folder: {:?}", folder);
1730
1731 if let Some(d) = folder.delimiter()
1733 && delimiter_is_default
1734 && !d.is_empty()
1735 && delimiter != d
1736 {
1737 delimiter = d.to_string();
1738 delimiter_is_default = false;
1739 }
1740
1741 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1742 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1743 if let Some(config) = folder_meaning.to_config() {
1744 folder_configs.insert(config, folder.name().to_string());
1746 } else if let Some(config) = folder_name_meaning.to_config() {
1747 folder_configs
1749 .entry(config)
1750 .or_insert_with(|| folder.name().to_string());
1751 }
1752 }
1753 drop(folders);
1754
1755 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1756
1757 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1758 let mvbox_folder = session
1759 .configure_mvbox(context, &["DeltaChat", &fallback_folder])
1760 .await
1761 .context("failed to configure mvbox")?;
1762
1763 context
1764 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1765 .await?;
1766 if let Some(mvbox_folder) = mvbox_folder {
1767 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1768 context
1769 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1770 .await?;
1771 }
1772 for (config, name) in folder_configs {
1773 context.set_config_internal(config, Some(&name)).await?;
1774 }
1775 context
1776 .sql
1777 .set_raw_config_int(
1778 constants::DC_FOLDERS_CONFIGURED_KEY,
1779 constants::DC_FOLDERS_CONFIGURED_VERSION,
1780 )
1781 .await?;
1782
1783 info!(context, "FINISHED configuring IMAP-folders.");
1784 Ok(())
1785 }
1786}
1787
1788impl Session {
1789 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1798 use UnsolicitedResponse::*;
1799 use async_imap::imap_proto::Response;
1800 use async_imap::imap_proto::ResponseCode;
1801
1802 let folder = self.selected_folder.as_deref().unwrap_or_default();
1803 let mut should_refetch = false;
1804 while let Ok(response) = self.unsolicited_responses.try_recv() {
1805 match response {
1806 Exists(_) => {
1807 info!(
1808 context,
1809 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1810 );
1811 should_refetch = true;
1812 }
1813
1814 Expunge(_) | Recent(_) => {}
1815 Other(ref response_data) => {
1816 match response_data.parsed() {
1817 Response::Fetch { .. } => {
1818 info!(
1819 context,
1820 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1821 );
1822 should_refetch = true;
1823 }
1824
1825 Response::Done {
1828 code: Some(ResponseCode::CopyUid(_, _, _)),
1829 ..
1830 } => {}
1831
1832 _ => {
1833 info!(context, "{folder:?}: got unsolicited response {response:?}")
1834 }
1835 }
1836 }
1837 _ => {
1838 info!(context, "{folder:?}: got unsolicited response {response:?}")
1839 }
1840 }
1841 }
1842 Ok(should_refetch)
1843 }
1844}
1845
1846async fn should_move_out_of_spam(
1847 context: &Context,
1848 headers: &[mailparse::MailHeader<'_>],
1849) -> Result<bool> {
1850 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1851 return Ok(true);
1862 }
1863
1864 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1865 if msg.chat_blocked != Blocked::Not {
1866 return Ok(false);
1868 }
1869 } else {
1870 let from = match mimeparser::get_from(headers) {
1871 Some(f) => f,
1872 None => return Ok(false),
1873 };
1874 let (from_id, blocked_contact, _origin) =
1876 match from_field_to_contact_id(context, &from, None, true, true)
1877 .await
1878 .context("from_field_to_contact_id")?
1879 {
1880 Some(res) => res,
1881 None => {
1882 warn!(
1883 context,
1884 "Contact with From address {:?} cannot exist, not moving out of spam", from
1885 );
1886 return Ok(false);
1887 }
1888 };
1889 if blocked_contact {
1890 return Ok(false);
1892 }
1893
1894 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1895 if chat_id_blocked.blocked != Blocked::Not {
1896 return Ok(false);
1897 }
1898 } else if from_id != ContactId::SELF {
1899 return Ok(false);
1901 }
1902 }
1903
1904 Ok(true)
1905}
1906
1907async fn spam_target_folder_cfg(
1912 context: &Context,
1913 headers: &[mailparse::MailHeader<'_>],
1914) -> Result<Option<Config>> {
1915 if !should_move_out_of_spam(context, headers).await? {
1916 return Ok(None);
1917 }
1918
1919 if needs_move_to_mvbox(context, headers).await?
1920 || context.get_config_bool(Config::OnlyFetchMvbox).await?
1923 {
1924 Ok(Some(Config::ConfiguredMvboxFolder))
1925 } else {
1926 Ok(Some(Config::ConfiguredInboxFolder))
1927 }
1928}
1929
1930pub async fn target_folder_cfg(
1933 context: &Context,
1934 folder: &str,
1935 folder_meaning: FolderMeaning,
1936 headers: &[mailparse::MailHeader<'_>],
1937) -> Result<Option<Config>> {
1938 if context.is_mvbox(folder).await? {
1939 return Ok(None);
1940 }
1941
1942 if folder_meaning == FolderMeaning::Spam {
1943 spam_target_folder_cfg(context, headers).await
1944 } else if folder_meaning == FolderMeaning::Inbox
1945 && needs_move_to_mvbox(context, headers).await?
1946 {
1947 Ok(Some(Config::ConfiguredMvboxFolder))
1948 } else {
1949 Ok(None)
1950 }
1951}
1952
1953pub async fn target_folder(
1954 context: &Context,
1955 folder: &str,
1956 folder_meaning: FolderMeaning,
1957 headers: &[mailparse::MailHeader<'_>],
1958) -> Result<String> {
1959 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1960 Some(config) => match context.get_config(config).await? {
1961 Some(target) => Ok(target),
1962 None => Ok(folder.to_string()),
1963 },
1964 None => Ok(folder.to_string()),
1965 }
1966}
1967
1968async fn needs_move_to_mvbox(
1969 context: &Context,
1970 headers: &[mailparse::MailHeader<'_>],
1971) -> Result<bool> {
1972 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1973 if !context.get_config_bool(Config::MvboxMove).await? {
1974 return Ok(false);
1975 }
1976
1977 if headers
1978 .get_header_value(HeaderDef::AutocryptSetupMessage)
1979 .is_some()
1980 {
1981 return Ok(false);
1984 }
1985
1986 if has_chat_version {
1987 Ok(true)
1988 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1989 match parent.is_dc_message {
1990 MessengerMessage::No => Ok(false),
1991 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1992 }
1993 } else {
1994 Ok(false)
1995 }
1996}
1997
1998fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2005 const SPAM_NAMES: &[&str] = &[
2007 "spam",
2008 "junk",
2009 "Correio electrónico não solicitado",
2010 "Correo basura",
2011 "Lixo",
2012 "Nettsøppel",
2013 "Nevyžádaná pošta",
2014 "No solicitado",
2015 "Ongewenst",
2016 "Posta indesiderata",
2017 "Skräp",
2018 "Wiadomości-śmieci",
2019 "Önemsiz",
2020 "Ανεπιθύμητα",
2021 "Спам",
2022 "垃圾邮件",
2023 "垃圾郵件",
2024 "迷惑メール",
2025 "스팸",
2026 ];
2027 const TRASH_NAMES: &[&str] = &[
2028 "Trash",
2029 "Bin",
2030 "Caixote do lixo",
2031 "Cestino",
2032 "Corbeille",
2033 "Papelera",
2034 "Papierkorb",
2035 "Papirkurv",
2036 "Papperskorgen",
2037 "Prullenbak",
2038 "Rubujo",
2039 "Κάδος απορριμμάτων",
2040 "Корзина",
2041 "Кошик",
2042 "ゴミ箱",
2043 "垃圾桶",
2044 "已删除邮件",
2045 "휴지통",
2046 ];
2047 let lower = folder_name.to_lowercase();
2048
2049 if lower == "inbox" {
2050 FolderMeaning::Inbox
2051 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2052 FolderMeaning::Spam
2053 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2054 FolderMeaning::Trash
2055 } else {
2056 FolderMeaning::Unknown
2057 }
2058}
2059
2060fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2061 for attr in folder_attrs {
2062 match attr {
2063 NameAttribute::Trash => return FolderMeaning::Trash,
2064 NameAttribute::Junk => return FolderMeaning::Spam,
2065 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2066 NameAttribute::Extension(label) => {
2067 match label.as_ref() {
2068 "\\Spam" => return FolderMeaning::Spam,
2069 "\\Important" => return FolderMeaning::Virtual,
2070 _ => {}
2071 };
2072 }
2073 _ => {}
2074 }
2075 }
2076 FolderMeaning::Unknown
2077}
2078
2079pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2080 match get_folder_meaning_by_attrs(folder.attributes()) {
2081 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2082 meaning => meaning,
2083 }
2084}
2085
2086fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2088 match prefetch_msg.header() {
2089 Some(header_bytes) => {
2090 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2091 Ok(headers)
2092 }
2093 None => Ok(Vec::new()),
2094 }
2095}
2096
2097pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2098 headers
2099 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2100 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2101 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2102}
2103
2104pub(crate) fn create_message_id() -> String {
2105 format!("{}{}", GENERATED_PREFIX, create_id())
2106}
2107
2108pub(crate) async fn prefetch_should_download(
2110 context: &Context,
2111 headers: &[mailparse::MailHeader<'_>],
2112 message_id: &str,
2113 mut flags: impl Iterator<Item = Flag<'_>>,
2114) -> Result<bool> {
2115 if message::rfc724_mid_download_tried(context, message_id).await? {
2116 if let Some(from) = mimeparser::get_from(headers)
2117 && context.is_self_addr(&from.addr).await?
2118 {
2119 markseen_on_imap_table(context, message_id).await?;
2120 }
2121 return Ok(false);
2122 }
2123
2124 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2128 let from = from.to_ascii_lowercase();
2129 from.contains("mailer-daemon") || from.contains("mail-daemon")
2130 } else {
2131 false
2132 };
2133
2134 let is_autocrypt_setup_message = headers
2136 .get_header_value(HeaderDef::AutocryptSetupMessage)
2137 .is_some();
2138
2139 let from = match mimeparser::get_from(headers) {
2140 Some(f) => f,
2141 None => return Ok(false),
2142 };
2143 let (_from_id, blocked_contact, origin) =
2144 match from_field_to_contact_id(context, &from, None, true, true).await? {
2145 Some(res) => res,
2146 None => return Ok(false),
2147 };
2148 if flags.any(|f| f == Flag::Draft) {
2152 info!(context, "Ignoring draft message");
2153 return Ok(false);
2154 }
2155
2156 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2157 let accepted_contact = origin.is_known();
2158 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2159 .await?
2160 .is_some_and(|parent| match parent.is_dc_message {
2161 MessengerMessage::No => false,
2162 MessengerMessage::Yes | MessengerMessage::Reply => true,
2163 });
2164
2165 let show_emails =
2166 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2167
2168 let show = is_autocrypt_setup_message
2169 || match show_emails {
2170 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2171 ShowEmails::AcceptedContacts => {
2172 is_chat_message || is_reply_to_chat_message || accepted_contact
2173 }
2174 ShowEmails::All => true,
2175 };
2176
2177 let should_download = (show && !blocked_contact) || maybe_ndn;
2178 Ok(should_download)
2179}
2180
2181async fn mark_seen_by_uid(
2185 context: &Context,
2186 transport_id: u32,
2187 folder: &str,
2188 uid_validity: u32,
2189 uid: u32,
2190) -> Result<Option<ChatId>> {
2191 if let Some((msg_id, chat_id)) = context
2192 .sql
2193 .query_row_optional(
2194 "SELECT id, chat_id FROM msgs
2195 WHERE id > 9 AND rfc724_mid IN (
2196 SELECT rfc724_mid FROM imap
2197 WHERE transport_id=?
2198 AND folder=?
2199 AND uidvalidity=?
2200 AND uid=?
2201 LIMIT 1
2202 )",
2203 (transport_id, &folder, uid_validity, uid),
2204 |row| {
2205 let msg_id: MsgId = row.get(0)?;
2206 let chat_id: ChatId = row.get(1)?;
2207 Ok((msg_id, chat_id))
2208 },
2209 )
2210 .await
2211 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2212 {
2213 let updated = context
2214 .sql
2215 .execute(
2216 "UPDATE msgs SET state=?1
2217 WHERE (state=?2 OR state=?3)
2218 AND id=?4",
2219 (
2220 MessageState::InSeen,
2221 MessageState::InFresh,
2222 MessageState::InNoticed,
2223 msg_id,
2224 ),
2225 )
2226 .await
2227 .with_context(|| format!("failed to update msg {msg_id} state"))?
2228 > 0;
2229
2230 if updated {
2231 msg_id
2232 .start_ephemeral_timer(context)
2233 .await
2234 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2235 Ok(Some(chat_id))
2236 } else {
2237 Ok(None)
2239 }
2240 } else {
2241 Ok(None)
2243 }
2244}
2245
2246pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2249 context
2250 .sql
2251 .execute(
2252 "INSERT OR IGNORE INTO imap_markseen (id)
2253 SELECT id FROM imap WHERE rfc724_mid=?",
2254 (message_id,),
2255 )
2256 .await?;
2257 context.scheduler.interrupt_inbox().await;
2258
2259 Ok(())
2260}
2261
2262pub(crate) async fn set_uid_next(
2266 context: &Context,
2267 transport_id: u32,
2268 folder: &str,
2269 uid_next: u32,
2270) -> Result<()> {
2271 context
2272 .sql
2273 .execute(
2274 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2275 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2276 (transport_id, folder, uid_next),
2277 )
2278 .await?;
2279 Ok(())
2280}
2281
2282async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2288 Ok(context
2289 .sql
2290 .query_get_value(
2291 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2292 (transport_id, folder),
2293 )
2294 .await?
2295 .unwrap_or(0))
2296}
2297
2298pub(crate) async fn set_uidvalidity(
2299 context: &Context,
2300 transport_id: u32,
2301 folder: &str,
2302 uidvalidity: u32,
2303) -> Result<()> {
2304 context
2305 .sql
2306 .execute(
2307 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2308 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2309 (transport_id, folder, uidvalidity),
2310 )
2311 .await?;
2312 Ok(())
2313}
2314
2315async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2316 Ok(context
2317 .sql
2318 .query_get_value(
2319 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2320 (transport_id, folder),
2321 )
2322 .await?
2323 .unwrap_or(0))
2324}
2325
2326pub(crate) async fn set_modseq(
2327 context: &Context,
2328 transport_id: u32,
2329 folder: &str,
2330 modseq: u64,
2331) -> Result<()> {
2332 context
2333 .sql
2334 .execute(
2335 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2336 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2337 (transport_id, folder, modseq),
2338 )
2339 .await?;
2340 Ok(())
2341}
2342
2343async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2344 Ok(context
2345 .sql
2346 .query_get_value(
2347 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2348 (transport_id, folder),
2349 )
2350 .await?
2351 .unwrap_or(0))
2352}
2353
2354async fn should_ignore_folder(
2359 context: &Context,
2360 folder: &str,
2361 folder_meaning: FolderMeaning,
2362) -> Result<bool> {
2363 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2364 return Ok(false);
2365 }
2366 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2367}
2368
2369#[expect(clippy::arithmetic_side_effects)]
2373fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2374 let mut ranges: Vec<UidRange> = vec![];
2376
2377 for ¤t in uids {
2378 if let Some(last) = ranges.last_mut()
2379 && last.end + 1 == current
2380 {
2381 last.end = current;
2382 continue;
2383 }
2384
2385 ranges.push(UidRange {
2386 start: current,
2387 end: current,
2388 });
2389 }
2390
2391 let mut result = vec![];
2393 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2394 for range in ranges {
2395 last_uids.reserve((range.end - range.start + 1).try_into()?);
2396 (range.start..=range.end).for_each(|u| last_uids.push(u));
2397 if !last_str.is_empty() {
2398 last_str.push(',');
2399 }
2400 last_str.push_str(&range.to_string());
2401
2402 if last_str.len() > 990 {
2403 result.push((take(&mut last_uids), take(&mut last_str)));
2404 }
2405 }
2406 result.push((last_uids, last_str));
2407
2408 result.retain(|(_, s)| !s.is_empty());
2409 Ok(result)
2410}
2411
2412struct UidRange {
2413 start: u32,
2414 end: u32,
2415 }
2417
2418impl std::fmt::Display for UidRange {
2419 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2420 if self.start == self.end {
2421 write!(f, "{}", self.start)
2422 } else {
2423 write!(f, "{}:{}", self.start, self.end)
2424 }
2425 }
2426}
2427
2428pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2429 let mut res = vec![Config::ConfiguredInboxFolder];
2430 if context.should_watch_mvbox().await? {
2431 res.push(Config::ConfiguredMvboxFolder);
2432 }
2433 Ok(res)
2434}
2435
2436pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2437 let mut res = Vec::new();
2438 for folder_config in get_watched_folder_configs(context).await? {
2439 if let Some(folder) = context.get_config(folder_config).await? {
2440 res.push(folder);
2441 }
2442 }
2443 Ok(res)
2444}
2445
2446#[cfg(test)]
2447mod imap_tests;