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 ratelimit::Ratelimit;
22use url::Url;
23
24use crate::calls::{
25 UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
26};
27use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
28use crate::chatlist_events;
29use crate::config::Config;
30use crate::constants::{self, Blocked, DC_VERSION_STR};
31use crate::contact::ContactId;
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 select_folder;
56pub(crate) mod session;
57
58use client::{Client, determine_capabilities};
59use session::Session;
60
61pub(crate) const GENERATED_PREFIX: &str = "GEN_";
62
63const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
64 MESSAGE-ID \
65 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
66 )])";
67const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
68
69#[derive(Debug)]
70pub(crate) struct Imap {
71 transport_id: u32,
75
76 pub(crate) idle_interrupt_receiver: Receiver<()>,
77
78 pub(crate) addr: String,
80
81 lp: Vec<ConfiguredServerLoginParam>,
83
84 password: String,
86
87 proxy_config: Option<ProxyConfig>,
89
90 strict_tls: bool,
91
92 oauth2: bool,
93
94 authentication_failed_once: bool,
95
96 pub(crate) connectivity: ConnectivityStore,
97
98 conn_last_try: tools::Time,
99 conn_backoff_ms: u64,
100
101 ratelimit: Ratelimit,
109
110 pub(crate) resync_request_sender: async_channel::Sender<()>,
112
113 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
115}
116
117#[derive(Debug)]
118struct OAuth2 {
119 user: String,
120 access_token: String,
121}
122
123#[derive(Debug, Default)]
124pub(crate) struct ServerMetadata {
125 pub comment: Option<String>,
128
129 pub admin: Option<String>,
132
133 pub iroh_relay: Option<Url>,
134
135 pub ice_servers: Vec<UnresolvedIceServer>,
137
138 pub ice_servers_expiration_timestamp: i64,
145}
146
147impl async_imap::Authenticator for OAuth2 {
148 type Response = String;
149
150 fn process(&mut self, _data: &[u8]) -> Self::Response {
151 format!(
152 "user={}\x01auth=Bearer {}\x01\x01",
153 self.user, self.access_token
154 )
155 }
156}
157
158#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
159pub enum FolderMeaning {
160 Unknown,
161
162 Spam,
164 Inbox,
165 Mvbox,
166 Trash,
167
168 Virtual,
175}
176
177impl FolderMeaning {
178 pub fn to_config(self) -> Option<Config> {
179 match self {
180 FolderMeaning::Unknown => None,
181 FolderMeaning::Spam => None,
182 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
183 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
184 FolderMeaning::Trash => None,
185 FolderMeaning::Virtual => None,
186 }
187 }
188}
189
190struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
191 inner: Peekable<T>,
192}
193
194impl<T, I> From<I> for UidGrouper<T>
195where
196 T: Iterator<Item = (i64, u32, String)>,
197 I: IntoIterator<IntoIter = T>,
198{
199 fn from(inner: I) -> Self {
200 Self {
201 inner: inner.into_iter().peekable(),
202 }
203 }
204}
205
206impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
207 type Item = (String, Vec<i64>, String);
209
210 #[expect(clippy::arithmetic_side_effects)]
211 fn next(&mut self) -> Option<Self::Item> {
212 let (_, _, folder) = self.inner.peek().cloned()?;
213
214 let mut uid_set = String::new();
215 let mut rowid_set = Vec::new();
216
217 while uid_set.len() < 1000 {
218 if let Some((start_rowid, start_uid, _)) = self
220 .inner
221 .next_if(|(_, _, start_folder)| start_folder == &folder)
222 {
223 rowid_set.push(start_rowid);
224 let mut end_uid = start_uid;
225
226 while let Some((next_rowid, next_uid, _)) =
227 self.inner.next_if(|(_, next_uid, next_folder)| {
228 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
229 })
230 {
231 end_uid = next_uid;
232 rowid_set.push(next_rowid);
233 }
234
235 let uid_range = UidRange {
236 start: start_uid,
237 end: end_uid,
238 };
239 if !uid_set.is_empty() {
240 uid_set.push(',');
241 }
242 uid_set.push_str(&uid_range.to_string());
243 } else {
244 break;
245 }
246 }
247
248 Some((folder, rowid_set, uid_set))
249 }
250}
251
252impl Imap {
253 pub async fn new(
255 context: &Context,
256 transport_id: u32,
257 param: ConfiguredLoginParam,
258 idle_interrupt_receiver: Receiver<()>,
259 ) -> Result<Self> {
260 let lp = param.imap.clone();
261 let password = param.imap_password.clone();
262 let proxy_config = ProxyConfig::load(context).await?;
263 let addr = ¶m.addr;
264 let strict_tls = param.strict_tls(proxy_config.is_some());
265 let oauth2 = param.oauth2;
266 let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
267 Ok(Imap {
268 transport_id,
269 idle_interrupt_receiver,
270 addr: addr.to_string(),
271 lp,
272 password,
273 proxy_config,
274 strict_tls,
275 oauth2,
276 authentication_failed_once: false,
277 connectivity: Default::default(),
278 conn_last_try: UNIX_EPOCH,
279 conn_backoff_ms: 0,
280 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
282 resync_request_sender,
283 resync_request_receiver,
284 })
285 }
286
287 pub async fn new_configured(
289 context: &Context,
290 idle_interrupt_receiver: Receiver<()>,
291 ) -> Result<Self> {
292 let (transport_id, param) = ConfiguredLoginParam::load(context)
293 .await?
294 .context("Not configured")?;
295 let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
296 Ok(imap)
297 }
298
299 pub(crate) async fn connect(
305 &mut self,
306 context: &Context,
307 configuring: bool,
308 ) -> Result<Session> {
309 let now = tools::Time::now();
310 let until_can_send = max(
311 min(self.conn_last_try, now)
312 .checked_add(Duration::from_millis(self.conn_backoff_ms))
313 .unwrap_or(now),
314 now,
315 )
316 .duration_since(now)?;
317 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
318 if !ratelimit_duration.is_zero() {
319 warn!(
320 context,
321 "IMAP got rate limited, waiting for {} until can connect.",
322 duration_to_str(ratelimit_duration),
323 );
324 let interrupted = async {
325 tokio::time::sleep(ratelimit_duration).await;
326 false
327 }
328 .race(self.idle_interrupt_receiver.recv().map(|_| true))
329 .await;
330 if interrupted {
331 info!(
332 context,
333 "Connecting to IMAP without waiting for ratelimit due to interrupt."
334 );
335 }
336 }
337
338 info!(context, "Connecting to IMAP server.");
339 self.connectivity.set_connecting(context);
340
341 self.conn_last_try = tools::Time::now();
342 const BACKOFF_MIN_MS: u64 = 2000;
343 const BACKOFF_MAX_MS: u64 = 80_000;
344 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
345 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
346 (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
347 ));
348 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
349
350 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
351 let mut first_error = None;
352 for lp in login_params {
353 info!(context, "IMAP trying to connect to {}.", &lp.connection);
354 let connection_candidate = lp.connection.clone();
355 let client = match Client::connect(
356 context,
357 self.proxy_config.clone(),
358 self.strict_tls,
359 &connection_candidate,
360 )
361 .await
362 .with_context(|| format!("IMAP failed to connect to {connection_candidate}"))
363 {
364 Ok(client) => client,
365 Err(err) => {
366 warn!(context, "{err:#}.");
367 first_error.get_or_insert(err);
368 continue;
369 }
370 };
371
372 self.conn_backoff_ms = BACKOFF_MIN_MS;
373 self.ratelimit.send();
374
375 let imap_user: &str = lp.user.as_ref();
376 let imap_pw: &str = &self.password;
377
378 let login_res = if self.oauth2 {
379 info!(context, "Logging into IMAP server with OAuth 2.");
380 let addr: &str = self.addr.as_ref();
381
382 let token = get_oauth2_access_token(context, addr, imap_pw, true)
383 .await?
384 .context("IMAP could not get OAUTH token")?;
385 let auth = OAuth2 {
386 user: imap_user.into(),
387 access_token: token,
388 };
389 client.authenticate("XOAUTH2", auth).await
390 } else {
391 info!(context, "Logging into IMAP server with LOGIN.");
392 client.login(imap_user, imap_pw).await
393 };
394
395 match login_res {
396 Ok(mut session) => {
397 let capabilities = determine_capabilities(&mut session).await?;
398 let resync_request_sender = self.resync_request_sender.clone();
399
400 let session = if capabilities.can_compress {
401 info!(context, "Enabling IMAP compression.");
402 let compressed_session = session
403 .compress(|s| {
404 let session_stream: Box<dyn SessionStream> = Box::new(s);
405 session_stream
406 })
407 .await
408 .context("Failed to enable IMAP compression")?;
409 Session::new(
410 compressed_session,
411 capabilities,
412 resync_request_sender,
413 self.transport_id,
414 )
415 } else {
416 Session::new(
417 session,
418 capabilities,
419 resync_request_sender,
420 self.transport_id,
421 )
422 };
423
424 let mut lock = context.server_id.write().await;
426 lock.clone_from(&session.capabilities.server_id);
427
428 self.authentication_failed_once = false;
429 context.emit_event(EventType::ImapConnected(format!(
430 "IMAP-LOGIN as {}",
431 lp.user
432 )));
433 self.connectivity.set_preparing(context);
434 info!(context, "Successfully logged into IMAP server.");
435 return Ok(session);
436 }
437
438 Err(err) => {
439 let imap_user = lp.user.to_owned();
440 let message = stock_str::cannot_login(context, &imap_user).await;
441
442 warn!(context, "IMAP failed to login: {err:#}.");
443 first_error.get_or_insert(format_err!("{message} ({err:#})"));
444
445 let _lock = context.wrong_pw_warning_mutex.lock().await;
447 if err.to_string().to_lowercase().contains("authentication") {
448 if self.authentication_failed_once
449 && !configuring
450 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
451 {
452 let mut msg = Message::new_text(message);
453 if let Err(e) = chat::add_device_msg_with_importance(
454 context,
455 None,
456 Some(&mut msg),
457 true,
458 )
459 .await
460 {
461 warn!(context, "Failed to add device message: {e:#}.");
462 } else {
463 context
464 .set_config_internal(Config::NotifyAboutWrongPw, None)
465 .await
466 .log_err(context)
467 .ok();
468 }
469 } else {
470 self.authentication_failed_once = true;
471 }
472 } else {
473 self.authentication_failed_once = false;
474 }
475 }
476 }
477 }
478
479 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
480 }
481
482 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
487 let configuring = false;
488 let mut session = match self.connect(context, configuring).await {
489 Ok(session) => session,
490 Err(err) => {
491 self.connectivity.set_err(context, &err);
492 return Err(err);
493 }
494 };
495
496 let folders_configured = context
497 .sql
498 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
499 .await?;
500 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
501 self.configure_folders(context, &mut session).await?;
502 }
503
504 Ok(session)
505 }
506
507 pub async fn fetch_move_delete(
512 &mut self,
513 context: &Context,
514 session: &mut Session,
515 watch_folder: &str,
516 folder_meaning: FolderMeaning,
517 ) -> Result<()> {
518 if !context.sql.is_open().await {
519 bail!("IMAP operation attempted while it is torn down");
521 }
522
523 let msgs_fetched = self
524 .fetch_new_messages(context, session, watch_folder, folder_meaning)
525 .await
526 .context("fetch_new_messages")?;
527 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
528 context.scheduler.interrupt_ephemeral_task().await;
533 }
534
535 session
536 .move_delete_messages(context, watch_folder)
537 .await
538 .context("move_delete_messages")?;
539
540 Ok(())
541 }
542
543 #[expect(clippy::arithmetic_side_effects)]
547 pub(crate) async fn fetch_new_messages(
548 &mut self,
549 context: &Context,
550 session: &mut Session,
551 folder: &str,
552 folder_meaning: FolderMeaning,
553 ) -> Result<bool> {
554 if should_ignore_folder(context, folder, folder_meaning).await? {
555 info!(context, "Not fetching from {folder:?}.");
556 session.new_mail = false;
557 return Ok(false);
558 }
559
560 let folder_exists = session
561 .select_with_uidvalidity(context, folder)
562 .await
563 .with_context(|| format!("Failed to select folder {folder:?}"))?;
564 if !folder_exists {
565 return Ok(false);
566 }
567
568 if !session.new_mail {
569 info!(context, "No new emails in folder {folder:?}.");
570 return Ok(false);
571 }
572 session.new_mail = false;
573
574 let mut read_cnt = 0;
575 loop {
576 let (n, fetch_more) = self
577 .fetch_new_msg_batch(context, session, folder, folder_meaning)
578 .await?;
579 read_cnt += n;
580 if !fetch_more {
581 return Ok(read_cnt > 0);
582 }
583 }
584 }
585
586 #[expect(clippy::arithmetic_side_effects)]
588 async fn fetch_new_msg_batch(
589 &mut self,
590 context: &Context,
591 session: &mut Session,
592 folder: &str,
593 folder_meaning: FolderMeaning,
594 ) -> Result<(usize, bool)> {
595 let transport_id = self.transport_id;
596 let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
597 let old_uid_next = get_uid_next(context, transport_id, folder).await?;
598 info!(
599 context,
600 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
601 );
602
603 let uids_to_prefetch = 500;
604 let msgs = session
605 .prefetch(old_uid_next, uids_to_prefetch)
606 .await
607 .context("prefetch")?;
608 let read_cnt = msgs.len();
609 let _fetch_msgs_lock_guard = context.fetch_msgs_mutex.lock().await;
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 has_chat_version {
1978 Ok(true)
1979 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1980 match parent.is_dc_message {
1981 MessengerMessage::No => Ok(false),
1982 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1983 }
1984 } else {
1985 Ok(false)
1986 }
1987}
1988
1989fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
1996 const SPAM_NAMES: &[&str] = &[
1998 "spam",
1999 "junk",
2000 "Correio electrónico não solicitado",
2001 "Correo basura",
2002 "Lixo",
2003 "Nettsøppel",
2004 "Nevyžádaná pošta",
2005 "No solicitado",
2006 "Ongewenst",
2007 "Posta indesiderata",
2008 "Skräp",
2009 "Wiadomości-śmieci",
2010 "Önemsiz",
2011 "Ανεπιθύμητα",
2012 "Спам",
2013 "垃圾邮件",
2014 "垃圾郵件",
2015 "迷惑メール",
2016 "스팸",
2017 ];
2018 const TRASH_NAMES: &[&str] = &[
2019 "Trash",
2020 "Bin",
2021 "Caixote do lixo",
2022 "Cestino",
2023 "Corbeille",
2024 "Papelera",
2025 "Papierkorb",
2026 "Papirkurv",
2027 "Papperskorgen",
2028 "Prullenbak",
2029 "Rubujo",
2030 "Κάδος απορριμμάτων",
2031 "Корзина",
2032 "Кошик",
2033 "ゴミ箱",
2034 "垃圾桶",
2035 "已删除邮件",
2036 "휴지통",
2037 ];
2038 let lower = folder_name.to_lowercase();
2039
2040 if lower == "inbox" {
2041 FolderMeaning::Inbox
2042 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2043 FolderMeaning::Spam
2044 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2045 FolderMeaning::Trash
2046 } else {
2047 FolderMeaning::Unknown
2048 }
2049}
2050
2051fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2052 for attr in folder_attrs {
2053 match attr {
2054 NameAttribute::Trash => return FolderMeaning::Trash,
2055 NameAttribute::Junk => return FolderMeaning::Spam,
2056 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2057 NameAttribute::Extension(label) => {
2058 match label.as_ref() {
2059 "\\Spam" => return FolderMeaning::Spam,
2060 "\\Important" => return FolderMeaning::Virtual,
2061 _ => {}
2062 };
2063 }
2064 _ => {}
2065 }
2066 }
2067 FolderMeaning::Unknown
2068}
2069
2070pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2071 match get_folder_meaning_by_attrs(folder.attributes()) {
2072 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2073 meaning => meaning,
2074 }
2075}
2076
2077fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2079 match prefetch_msg.header() {
2080 Some(header_bytes) => {
2081 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2082 Ok(headers)
2083 }
2084 None => Ok(Vec::new()),
2085 }
2086}
2087
2088pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2089 headers
2090 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2091 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2092 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2093}
2094
2095pub(crate) fn create_message_id() -> String {
2096 format!("{}{}", GENERATED_PREFIX, create_id())
2097}
2098
2099pub(crate) async fn prefetch_should_download(
2101 context: &Context,
2102 headers: &[mailparse::MailHeader<'_>],
2103 message_id: &str,
2104 mut flags: impl Iterator<Item = Flag<'_>>,
2105) -> Result<bool> {
2106 if message::rfc724_mid_download_tried(context, message_id).await? {
2107 if let Some(from) = mimeparser::get_from(headers)
2108 && context.is_self_addr(&from.addr).await?
2109 {
2110 markseen_on_imap_table(context, message_id).await?;
2111 }
2112 return Ok(false);
2113 }
2114
2115 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2119 let from = from.to_ascii_lowercase();
2120 from.contains("mailer-daemon") || from.contains("mail-daemon")
2121 } else {
2122 false
2123 };
2124
2125 let from = match mimeparser::get_from(headers) {
2126 Some(f) => f,
2127 None => return Ok(false),
2128 };
2129 let (_from_id, blocked_contact, _origin) =
2130 match from_field_to_contact_id(context, &from, None, true, true).await? {
2131 Some(res) => res,
2132 None => return Ok(false),
2133 };
2134 if flags.any(|f| f == Flag::Draft) {
2138 info!(context, "Ignoring draft message");
2139 return Ok(false);
2140 }
2141
2142 let should_download = (!blocked_contact) || maybe_ndn;
2143 Ok(should_download)
2144}
2145
2146async fn mark_seen_by_uid(
2150 context: &Context,
2151 transport_id: u32,
2152 folder: &str,
2153 uid_validity: u32,
2154 uid: u32,
2155) -> Result<Option<ChatId>> {
2156 if let Some((msg_id, chat_id)) = context
2157 .sql
2158 .query_row_optional(
2159 "SELECT id, chat_id FROM msgs
2160 WHERE id > 9 AND rfc724_mid IN (
2161 SELECT rfc724_mid FROM imap
2162 WHERE transport_id=?
2163 AND folder=?
2164 AND uidvalidity=?
2165 AND uid=?
2166 LIMIT 1
2167 )",
2168 (transport_id, &folder, uid_validity, uid),
2169 |row| {
2170 let msg_id: MsgId = row.get(0)?;
2171 let chat_id: ChatId = row.get(1)?;
2172 Ok((msg_id, chat_id))
2173 },
2174 )
2175 .await
2176 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2177 {
2178 let updated = context
2179 .sql
2180 .execute(
2181 "UPDATE msgs SET state=?1
2182 WHERE (state=?2 OR state=?3)
2183 AND id=?4",
2184 (
2185 MessageState::InSeen,
2186 MessageState::InFresh,
2187 MessageState::InNoticed,
2188 msg_id,
2189 ),
2190 )
2191 .await
2192 .with_context(|| format!("failed to update msg {msg_id} state"))?
2193 > 0;
2194
2195 if updated {
2196 msg_id
2197 .start_ephemeral_timer(context)
2198 .await
2199 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2200 Ok(Some(chat_id))
2201 } else {
2202 Ok(None)
2204 }
2205 } else {
2206 Ok(None)
2208 }
2209}
2210
2211pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2214 context
2215 .sql
2216 .execute(
2217 "INSERT OR IGNORE INTO imap_markseen (id)
2218 SELECT id FROM imap WHERE rfc724_mid=?",
2219 (message_id,),
2220 )
2221 .await?;
2222 context.scheduler.interrupt_inbox().await;
2223
2224 Ok(())
2225}
2226
2227pub(crate) async fn set_uid_next(
2231 context: &Context,
2232 transport_id: u32,
2233 folder: &str,
2234 uid_next: u32,
2235) -> Result<()> {
2236 context
2237 .sql
2238 .execute(
2239 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2240 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2241 (transport_id, folder, uid_next),
2242 )
2243 .await?;
2244 Ok(())
2245}
2246
2247async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2253 Ok(context
2254 .sql
2255 .query_get_value(
2256 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2257 (transport_id, folder),
2258 )
2259 .await?
2260 .unwrap_or(0))
2261}
2262
2263pub(crate) async fn set_uidvalidity(
2264 context: &Context,
2265 transport_id: u32,
2266 folder: &str,
2267 uidvalidity: u32,
2268) -> Result<()> {
2269 context
2270 .sql
2271 .execute(
2272 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2273 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2274 (transport_id, folder, uidvalidity),
2275 )
2276 .await?;
2277 Ok(())
2278}
2279
2280async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2281 Ok(context
2282 .sql
2283 .query_get_value(
2284 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2285 (transport_id, folder),
2286 )
2287 .await?
2288 .unwrap_or(0))
2289}
2290
2291pub(crate) async fn set_modseq(
2292 context: &Context,
2293 transport_id: u32,
2294 folder: &str,
2295 modseq: u64,
2296) -> Result<()> {
2297 context
2298 .sql
2299 .execute(
2300 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2301 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2302 (transport_id, folder, modseq),
2303 )
2304 .await?;
2305 Ok(())
2306}
2307
2308async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2309 Ok(context
2310 .sql
2311 .query_get_value(
2312 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2313 (transport_id, folder),
2314 )
2315 .await?
2316 .unwrap_or(0))
2317}
2318
2319async fn should_ignore_folder(
2324 context: &Context,
2325 folder: &str,
2326 folder_meaning: FolderMeaning,
2327) -> Result<bool> {
2328 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2329 return Ok(false);
2330 }
2331 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2332}
2333
2334#[expect(clippy::arithmetic_side_effects)]
2338fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2339 let mut ranges: Vec<UidRange> = vec![];
2341
2342 for ¤t in uids {
2343 if let Some(last) = ranges.last_mut()
2344 && last.end + 1 == current
2345 {
2346 last.end = current;
2347 continue;
2348 }
2349
2350 ranges.push(UidRange {
2351 start: current,
2352 end: current,
2353 });
2354 }
2355
2356 let mut result = vec![];
2358 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2359 for range in ranges {
2360 last_uids.reserve((range.end - range.start + 1).try_into()?);
2361 (range.start..=range.end).for_each(|u| last_uids.push(u));
2362 if !last_str.is_empty() {
2363 last_str.push(',');
2364 }
2365 last_str.push_str(&range.to_string());
2366
2367 if last_str.len() > 990 {
2368 result.push((take(&mut last_uids), take(&mut last_str)));
2369 }
2370 }
2371 result.push((last_uids, last_str));
2372
2373 result.retain(|(_, s)| !s.is_empty());
2374 Ok(result)
2375}
2376
2377struct UidRange {
2378 start: u32,
2379 end: u32,
2380 }
2382
2383impl std::fmt::Display for UidRange {
2384 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2385 if self.start == self.end {
2386 write!(f, "{}", self.start)
2387 } else {
2388 write!(f, "{}:{}", self.start, self.end)
2389 }
2390 }
2391}
2392
2393pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2394 let mut res = vec![Config::ConfiguredInboxFolder];
2395 if context.should_watch_mvbox().await? {
2396 res.push(Config::ConfiguredMvboxFolder);
2397 }
2398 Ok(res)
2399}
2400
2401pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2402 let mut res = Vec::new();
2403 for folder_config in get_watched_folder_configs(context).await? {
2404 if let Some(folder) = context.get_config(folder_config).await? {
2405 res.push(folder);
2406 }
2407 }
2408 Ok(res)
2409}
2410
2411#[cfg(test)]
2412mod imap_tests;