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
610 let mut uids_fetch: Vec<u32> = Vec::new();
611 let mut available_post_msgs: Vec<String> = Vec::new();
612 let mut download_later: Vec<String> = Vec::new();
613 let mut uid_message_ids = BTreeMap::new();
614 let mut largest_uid_skipped = None;
615
616 let download_limit: Option<u32> = context
617 .get_config_parsed(Config::DownloadLimit)
618 .await?
619 .filter(|&l| 0 < l);
620
621 for (uid, ref fetch_response) in msgs {
623 let headers = match get_fetch_headers(fetch_response) {
624 Ok(headers) => headers,
625 Err(err) => {
626 warn!(context, "Failed to parse FETCH headers: {err:#}.");
627 continue;
628 }
629 };
630
631 let message_id = prefetch_get_message_id(&headers);
632 let size = fetch_response
633 .size
634 .context("imap fetch response does not contain size")?;
635
636 let delete = if let Some(message_id) = &message_id {
647 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
648 .await?
649 .is_some_and(|(_msg_id, deleted)| deleted)
650 } else {
651 false
652 };
653
654 let message_id = message_id.unwrap_or_else(create_message_id);
657
658 if delete {
659 info!(context, "Deleting locally deleted message {message_id}.");
660 }
661
662 let _target;
663 let target = if delete {
664 ""
665 } else {
666 _target = target_folder(context, folder, folder_meaning, &headers).await?;
667 &_target
668 };
669
670 context
671 .sql
672 .execute(
673 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
674 VALUES (?, ?, ?, ?, ?, ?)
675 ON CONFLICT(transport_id, folder, uid, uidvalidity)
676 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
677 target=excluded.target",
678 (
679 self.transport_id,
680 &message_id,
681 &folder,
682 uid,
683 uid_validity,
684 target,
685 ),
686 )
687 .await?;
688
689 if folder == target
696 && folder_meaning != FolderMeaning::Spam
701 && prefetch_should_download(
702 context,
703 &headers,
704 &message_id,
705 fetch_response.flags(),
706 )
707 .await.context("prefetch_should_download")?
708 {
709 if headers
710 .get_header_value(HeaderDef::ChatIsPostMessage)
711 .is_some()
712 {
713 info!(context, "{message_id:?} is a post-message.");
714 available_post_msgs.push(message_id.clone());
715
716 if download_limit.is_none_or(|download_limit| size <= download_limit) {
717 download_later.push(message_id.clone());
718 }
719 largest_uid_skipped = Some(uid);
720 } else {
721 info!(context, "{message_id:?} is not a post-message.");
722 if download_limit.is_none_or(|download_limit| size <= download_limit) {
723 uids_fetch.push(uid);
724 uid_message_ids.insert(uid, message_id);
725 } else {
726 download_later.push(message_id.clone());
727 largest_uid_skipped = Some(uid);
728 }
729 };
730 } else {
731 largest_uid_skipped = Some(uid);
732 }
733 }
734
735 if !uids_fetch.is_empty() {
736 self.connectivity.set_working(context);
737 }
738
739 let (sender, receiver) = async_channel::unbounded();
740
741 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
742 let mailbox_uid_next = session
743 .selected_mailbox
744 .as_ref()
745 .with_context(|| format!("Expected {folder:?} to be selected"))?
746 .uid_next
747 .unwrap_or_default();
748
749 let update_uids_future = async {
750 let mut largest_uid_fetched: u32 = 0;
751
752 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
753 largest_uid_fetched = max(largest_uid_fetched, uid);
754 if let Some(received_msg) = received_msg_opt {
755 received_msgs.push(received_msg)
756 }
757 }
758
759 largest_uid_fetched
760 };
761
762 let actually_download_messages_future = async {
763 session
764 .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
765 .await
766 .context("fetch_many_msgs")
767 };
768
769 let (largest_uid_fetched, fetch_res) =
770 tokio::join!(update_uids_future, actually_download_messages_future);
771
772 let mut new_uid_next = largest_uid_fetched + 1;
778 let fetch_more = fetch_res.is_ok() && {
779 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
780 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
784
785 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
786
787 prefetch_uid_next < mailbox_uid_next
788 };
789 if new_uid_next > old_uid_next {
790 set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
791 }
792
793 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
794
795 if !received_msgs.is_empty() {
796 context.emit_event(EventType::IncomingMsgBunch);
797 }
798
799 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
800
801 if fetch_res.is_ok() {
802 info!(
803 context,
804 "available_post_msgs: {}, download_later: {}.",
805 available_post_msgs.len(),
806 download_later.len(),
807 );
808 let trans_fn = |t: &mut rusqlite::Transaction| {
809 let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
810 for rfc724_mid in available_post_msgs {
811 stmt.execute((rfc724_mid,))
812 .context("INSERT OR IGNORE INTO available_post_msgs")?;
813 }
814 let mut stmt =
815 t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
816 for rfc724_mid in download_later {
817 stmt.execute((rfc724_mid,))
818 .context("INSERT OR IGNORE INTO download")?;
819 }
820 Ok(())
821 };
822 context.sql.transaction(trans_fn).await?;
823 }
824
825 fetch_res?;
828
829 Ok((read_cnt, fetch_more))
830 }
831}
832
833impl Session {
834 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
836 let all_folders = self
837 .list_folders()
838 .await
839 .context("listing folders for resync")?;
840 for folder in all_folders {
841 let folder_meaning = get_folder_meaning(&folder);
842 if !matches!(
843 folder_meaning,
844 FolderMeaning::Virtual | FolderMeaning::Unknown
845 ) {
846 self.resync_folder_uids(context, folder.name(), folder_meaning)
847 .await?;
848 }
849 }
850 Ok(())
851 }
852
853 pub(crate) async fn resync_folder_uids(
860 &mut self,
861 context: &Context,
862 folder: &str,
863 folder_meaning: FolderMeaning,
864 ) -> Result<()> {
865 let uid_validity;
866 let mut msgs = BTreeMap::new();
868
869 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
870 let transport_id = self.transport_id();
871 if folder_exists {
872 let mut list = self
873 .uid_fetch("1:*", RFC724MID_UID)
874 .await
875 .with_context(|| format!("Can't resync folder {folder}"))?;
876 while let Some(fetch) = list.try_next().await? {
877 let headers = match get_fetch_headers(&fetch) {
878 Ok(headers) => headers,
879 Err(err) => {
880 warn!(context, "Failed to parse FETCH headers: {}", err);
881 continue;
882 }
883 };
884 let message_id = prefetch_get_message_id(&headers);
885
886 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
887 msgs.insert(
888 uid,
889 (
890 rfc724_mid,
891 target_folder(context, folder, folder_meaning, &headers).await?,
892 ),
893 );
894 }
895 }
896
897 info!(
898 context,
899 "resync_folder_uids: Collected {} message IDs in {folder}.",
900 msgs.len(),
901 );
902
903 uid_validity = get_uidvalidity(context, transport_id, folder).await?;
904 } else {
905 warn!(context, "resync_folder_uids: No folder {folder}.");
906 uid_validity = 0;
907 }
908
909 context
911 .sql
912 .transaction(move |transaction| {
913 transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
914 for (uid, (rfc724_mid, target)) in &msgs {
915 transaction.execute(
918 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
919 VALUES (?, ?, ?, ?, ?, ?)
920 ON CONFLICT(transport_id, folder, uid, uidvalidity)
921 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
922 target=excluded.target",
923 (transport_id, rfc724_mid, folder, uid, uid_validity, target),
924 )?;
925 }
926 Ok(())
927 })
928 .await?;
929 Ok(())
930 }
931
932 async fn delete_message_batch(
935 &mut self,
936 context: &Context,
937 uid_set: &str,
938 row_ids: Vec<i64>,
939 ) -> Result<()> {
940 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
942 .await?;
943 context
944 .sql
945 .transaction(|transaction| {
946 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
947 for row_id in row_ids {
948 stmt.execute((row_id,))?;
949 }
950 Ok(())
951 })
952 .await
953 .context("Cannot remove deleted messages from imap table")?;
954
955 context.emit_event(EventType::ImapMessageDeleted(format!(
956 "IMAP messages {uid_set} marked as deleted"
957 )));
958 Ok(())
959 }
960
961 async fn move_message_batch(
964 &mut self,
965 context: &Context,
966 set: &str,
967 row_ids: Vec<i64>,
968 target: &str,
969 ) -> Result<()> {
970 if self.can_move() {
971 match self.uid_mv(set, &target).await {
972 Ok(()) => {
973 context
975 .sql
976 .transaction(|transaction| {
977 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
978 for row_id in row_ids {
979 stmt.execute((row_id,))?;
980 }
981 Ok(())
982 })
983 .await
984 .context("Cannot delete moved messages from imap table")?;
985 context.emit_event(EventType::ImapMessageMoved(format!(
986 "IMAP messages {set} moved to {target}"
987 )));
988 return Ok(());
989 }
990 Err(err) => {
991 warn!(
992 context,
993 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
994 set,
995 target,
996 err
997 );
998 }
999 }
1000 }
1001
1002 info!(
1005 context,
1006 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1007 );
1008 self.uid_copy(&set, &target).await?;
1009 context
1010 .sql
1011 .transaction(|transaction| {
1012 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1013 for row_id in row_ids {
1014 stmt.execute((row_id,))?;
1015 }
1016 Ok(())
1017 })
1018 .await
1019 .context("Cannot plan deletion of messages")?;
1020 context.emit_event(EventType::ImapMessageMoved(format!(
1021 "IMAP messages {set} copied to {target}"
1022 )));
1023 Ok(())
1024 }
1025
1026 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1030 let transport_id = self.transport_id();
1031 let rows = context
1032 .sql
1033 .query_map_vec(
1034 "SELECT id, uid, target FROM imap
1035 WHERE folder = ?
1036 AND transport_id = ?
1037 AND target != folder
1038 ORDER BY target, uid",
1039 (folder, transport_id),
1040 |row| {
1041 let rowid: i64 = row.get(0)?;
1042 let uid: u32 = row.get(1)?;
1043 let target: String = row.get(2)?;
1044 Ok((rowid, uid, target))
1045 },
1046 )
1047 .await?;
1048
1049 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1050 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1055 ensure!(folder_exists, "No folder {folder}");
1056
1057 if target.is_empty() {
1059 self.delete_message_batch(context, &uid_set, rowid_set)
1060 .await
1061 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1062 } else {
1063 self.move_message_batch(context, &uid_set, rowid_set, &target)
1064 .await
1065 .with_context(|| {
1066 format!(
1067 "cannot move batch of messages {:?} to folder {:?}",
1068 &uid_set, target
1069 )
1070 })?;
1071 }
1072 }
1073
1074 if let Err(err) = self.maybe_close_folder(context).await {
1077 warn!(context, "Failed to close folder: {err:#}.");
1078 }
1079
1080 Ok(())
1081 }
1082
1083 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1085 if context.get_config_bool(Config::TeamProfile).await? {
1086 return Ok(());
1087 }
1088
1089 let transport_id = self.transport_id();
1090 let rows = context
1091 .sql
1092 .query_map_vec(
1093 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1094 WHERE imap.id = imap_markseen.id
1095 AND imap.transport_id=?
1096 AND target = folder
1097 ORDER BY folder, uid",
1098 (transport_id,),
1099 |row| {
1100 let rowid: i64 = row.get(0)?;
1101 let uid: u32 = row.get(1)?;
1102 let folder: String = row.get(2)?;
1103 Ok((rowid, uid, folder))
1104 },
1105 )
1106 .await?;
1107
1108 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1109 let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
1110 Err(err) => {
1111 warn!(
1112 context,
1113 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1114 );
1115 continue;
1116 }
1117 Ok(folder_exists) => folder_exists,
1118 };
1119 if !folder_exists {
1120 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1121 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1122 warn!(
1123 context,
1124 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1125 );
1126 continue;
1127 } else {
1128 info!(
1129 context,
1130 "Marked messages {} in folder {} as seen.", uid_set, folder
1131 );
1132 }
1133 context
1134 .sql
1135 .transaction(|transaction| {
1136 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1137 for rowid in rowid_set {
1138 stmt.execute((rowid,))?;
1139 }
1140 Ok(())
1141 })
1142 .await
1143 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1144 }
1145
1146 Ok(())
1147 }
1148
1149 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1151 if !self.can_condstore() {
1152 info!(
1153 context,
1154 "Server does not support CONDSTORE, skipping flag synchronization."
1155 );
1156 return Ok(());
1157 }
1158
1159 if context.get_config_bool(Config::TeamProfile).await? {
1160 return Ok(());
1161 }
1162
1163 let folder_exists = self
1164 .select_with_uidvalidity(context, folder)
1165 .await
1166 .context("Failed to select folder")?;
1167 if !folder_exists {
1168 return Ok(());
1169 }
1170
1171 let mailbox = self
1172 .selected_mailbox
1173 .as_ref()
1174 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1175
1176 if mailbox.highest_modseq.is_none() {
1179 info!(
1180 context,
1181 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1182 );
1183 return Ok(());
1184 }
1185
1186 let transport_id = self.transport_id();
1187 let mut updated_chat_ids = BTreeSet::new();
1188 let uid_validity = get_uidvalidity(context, transport_id, folder)
1189 .await
1190 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1191 let mut highest_modseq = get_modseq(context, transport_id, folder)
1192 .await
1193 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1194 let mut list = self
1195 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1196 .await
1197 .context("failed to fetch flags")?;
1198
1199 let mut got_unsolicited_fetch = false;
1200
1201 while let Some(fetch) = list
1202 .try_next()
1203 .await
1204 .context("failed to get FETCH result")?
1205 {
1206 let uid = if let Some(uid) = fetch.uid {
1207 uid
1208 } else {
1209 info!(context, "FETCH result contains no UID, skipping");
1210 got_unsolicited_fetch = true;
1211 continue;
1212 };
1213 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1214 if is_seen
1215 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1216 .await
1217 .with_context(|| {
1218 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1219 })?
1220 {
1221 updated_chat_ids.insert(chat_id);
1222 }
1223
1224 if let Some(modseq) = fetch.modseq {
1225 if modseq > highest_modseq {
1226 highest_modseq = modseq;
1227 }
1228 } else {
1229 warn!(context, "FETCH result contains no MODSEQ");
1230 }
1231 }
1232 drop(list);
1233
1234 if got_unsolicited_fetch {
1235 self.new_mail = true;
1240 }
1241
1242 set_modseq(context, transport_id, folder, highest_modseq)
1243 .await
1244 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1245 if !updated_chat_ids.is_empty() {
1246 context.on_archived_chats_maybe_noticed();
1247 }
1248 for updated_chat_id in updated_chat_ids {
1249 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1250 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1251 }
1252
1253 Ok(())
1254 }
1255
1256 #[expect(clippy::arithmetic_side_effects)]
1271 pub(crate) async fn fetch_many_msgs(
1272 &mut self,
1273 context: &Context,
1274 folder: &str,
1275 request_uids: Vec<u32>,
1276 uid_message_ids: &BTreeMap<u32, String>,
1277 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1278 ) -> Result<()> {
1279 if request_uids.is_empty() {
1280 return Ok(());
1281 }
1282
1283 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1284 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1285 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1286 format!("fetching messages {} from folder \"{}\"", &set, folder)
1287 })?;
1288
1289 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1292
1293 let mut count = 0;
1294 for &request_uid in &request_uids {
1295 let mut fetch_response = uid_msgs.remove(&request_uid);
1297
1298 while fetch_response.is_none() {
1300 let Some(next_fetch_response) = fetch_responses
1301 .try_next()
1302 .await
1303 .context("Failed to process IMAP FETCH result")?
1304 else {
1305 break;
1307 };
1308
1309 if let Some(next_uid) = next_fetch_response.uid {
1310 if next_uid == request_uid {
1311 fetch_response = Some(next_fetch_response);
1312 } else if !request_uids.contains(&next_uid) {
1313 info!(
1320 context,
1321 "Skipping not requested FETCH response for UID {}.", next_uid
1322 );
1323 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1324 warn!(context, "Got duplicated UID {}.", next_uid);
1325 }
1326 } else {
1327 info!(context, "Skipping FETCH response without UID.");
1328 }
1329 }
1330
1331 let fetch_response = match fetch_response {
1332 Some(fetch) => fetch,
1333 None => {
1334 warn!(
1335 context,
1336 "Missed UID {} in the server response.", request_uid
1337 );
1338 continue;
1339 }
1340 };
1341 count += 1;
1342
1343 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1344 let body = fetch_response.body();
1345
1346 if is_deleted {
1347 info!(context, "Not processing deleted msg {}.", request_uid);
1348 received_msgs_channel.send((request_uid, None)).await?;
1349 continue;
1350 }
1351
1352 let body = if let Some(body) = body {
1353 body
1354 } else {
1355 info!(
1356 context,
1357 "Not processing message {} without a BODY.", request_uid
1358 );
1359 received_msgs_channel.send((request_uid, None)).await?;
1360 continue;
1361 };
1362
1363 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1364
1365 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1366 error!(
1367 context,
1368 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1369 request_uid
1370 );
1371 continue;
1372 };
1373
1374 info!(
1375 context,
1376 "Passing message UID {} to receive_imf().", request_uid
1377 );
1378 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1379 let received_msg = match res {
1380 Err(err) => {
1381 warn!(context, "receive_imf error: {err:#}.");
1382
1383 let text = format!(
1384 "❌ 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/.",
1385 );
1386 let mut msg = Message::new_text(text);
1387 add_device_msg(context, None, Some(&mut msg)).await?;
1388 None
1389 }
1390 Ok(msg) => msg,
1391 };
1392 received_msgs_channel
1393 .send((request_uid, received_msg))
1394 .await?;
1395 }
1396
1397 while fetch_responses
1404 .try_next()
1405 .await
1406 .context("Failed to drain FETCH responses")?
1407 .is_some()
1408 {}
1409
1410 if count != request_uids.len() {
1411 warn!(
1412 context,
1413 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1414 count,
1415 request_uids.len(),
1416 request_uids,
1417 );
1418 } else {
1419 info!(
1420 context,
1421 "Successfully received {} UIDs.",
1422 request_uids.len()
1423 );
1424 }
1425 }
1426
1427 Ok(())
1428 }
1429
1430 #[expect(clippy::arithmetic_side_effects)]
1436 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1437 let mut lock = context.metadata.write().await;
1438
1439 if !self.can_metadata() {
1440 *lock = Some(Default::default());
1441 }
1442 if let Some(ref mut old_metadata) = *lock {
1443 let now = time();
1444
1445 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1447 return Ok(());
1448 }
1449
1450 let mut got_turn_server = false;
1451 if self.can_metadata() {
1452 info!(context, "ICE servers expired, requesting new credentials.");
1453 let mailbox = "";
1454 let options = "";
1455 let metadata = self
1456 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1457 .await?;
1458 for m in metadata {
1459 if m.entry == "/shared/vendor/deltachat/turn"
1460 && let Some(value) = m.value
1461 {
1462 match create_ice_servers_from_metadata(&value).await {
1463 Ok((parsed_timestamp, parsed_ice_servers)) => {
1464 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1465 old_metadata.ice_servers = parsed_ice_servers;
1466 got_turn_server = true;
1467 }
1468 Err(err) => {
1469 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1470 }
1471 }
1472 }
1473 }
1474 }
1475 if !got_turn_server {
1476 info!(context, "Will use fallback ICE servers.");
1477 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1479 old_metadata.ice_servers = create_fallback_ice_servers();
1480 }
1481 return Ok(());
1482 }
1483
1484 info!(
1485 context,
1486 "Server supports metadata, retrieving server comment and admin contact."
1487 );
1488
1489 let mut comment = None;
1490 let mut admin = None;
1491 let mut iroh_relay = None;
1492 let mut ice_servers = None;
1493 let mut ice_servers_expiration_timestamp = 0;
1494
1495 let mailbox = "";
1496 let options = "";
1497 let metadata = self
1498 .get_metadata(
1499 mailbox,
1500 options,
1501 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1502 )
1503 .await?;
1504 for m in metadata {
1505 match m.entry.as_ref() {
1506 "/shared/comment" => {
1507 comment = m.value;
1508 }
1509 "/shared/admin" => {
1510 admin = m.value;
1511 }
1512 "/shared/vendor/deltachat/irohrelay" => {
1513 if let Some(value) = m.value {
1514 if let Ok(url) = Url::parse(&value) {
1515 iroh_relay = Some(url);
1516 } else {
1517 warn!(
1518 context,
1519 "Got invalid URL from iroh relay metadata: {:?}.", value
1520 );
1521 }
1522 }
1523 }
1524 "/shared/vendor/deltachat/turn" => {
1525 if let Some(value) = m.value {
1526 match create_ice_servers_from_metadata(&value).await {
1527 Ok((parsed_timestamp, parsed_ice_servers)) => {
1528 ice_servers_expiration_timestamp = parsed_timestamp;
1529 ice_servers = Some(parsed_ice_servers);
1530 }
1531 Err(err) => {
1532 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1533 }
1534 }
1535 }
1536 }
1537 _ => {}
1538 }
1539 }
1540 let ice_servers = if let Some(ice_servers) = ice_servers {
1541 ice_servers
1542 } else {
1543 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1545 create_fallback_ice_servers()
1546 };
1547
1548 *lock = Some(ServerMetadata {
1549 comment,
1550 admin,
1551 iroh_relay,
1552 ice_servers,
1553 ice_servers_expiration_timestamp,
1554 });
1555 Ok(())
1556 }
1557
1558 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1560 if context.push_subscribed.load(Ordering::Relaxed) {
1561 return Ok(());
1562 }
1563
1564 let Some(device_token) = context.push_subscriber.device_token().await else {
1565 return Ok(());
1566 };
1567
1568 if self.can_metadata() && self.can_push() {
1569 let old_encrypted_device_token =
1570 context.get_config(Config::EncryptedDeviceToken).await?;
1571
1572 let device_token_changed = old_encrypted_device_token.is_none()
1574 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1575
1576 let new_encrypted_device_token;
1577 if device_token_changed {
1578 let encrypted_device_token = encrypt_device_token(&device_token)
1579 .context("Failed to encrypt device token")?;
1580
1581 let encrypted_device_token_len = encrypted_device_token.len();
1585
1586 context
1592 .set_config_internal(Config::DeviceToken, Some(&device_token))
1593 .await?;
1594 context
1595 .set_config_internal(
1596 Config::EncryptedDeviceToken,
1597 Some(&encrypted_device_token),
1598 )
1599 .await?;
1600
1601 if encrypted_device_token_len <= 4096 {
1602 new_encrypted_device_token = Some(encrypted_device_token);
1603 } else {
1604 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1614 new_encrypted_device_token = None;
1615 }
1616 } else {
1617 new_encrypted_device_token = old_encrypted_device_token;
1618 }
1619
1620 if let Some(encrypted_device_token) = new_encrypted_device_token {
1623 let folder = context
1624 .get_config(Config::ConfiguredInboxFolder)
1625 .await?
1626 .context("INBOX is not configured")?;
1627
1628 self.run_command_and_check_ok(&format_setmetadata(
1629 &folder,
1630 &encrypted_device_token,
1631 ))
1632 .await
1633 .context("SETMETADATA command failed")?;
1634
1635 context.push_subscribed.store(true, Ordering::Relaxed);
1636 }
1637 } else if !context.push_subscriber.heartbeat_subscribed().await {
1638 let context = context.clone();
1639 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1641 }
1642
1643 Ok(())
1644 }
1645}
1646
1647fn format_setmetadata(folder: &str, device_token: &str) -> String {
1648 let device_token_len = device_token.len();
1649 format!(
1650 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1651 )
1652}
1653
1654impl Session {
1655 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1661 if flag == "\\Deleted" {
1662 self.selected_folder_needs_expunge = true;
1663 }
1664 let query = format!("+FLAGS ({flag})");
1665 let mut responses = self
1666 .uid_store(uid_set, &query)
1667 .await
1668 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1669 while let Some(_response) = responses.try_next().await? {
1670 }
1672 Ok(())
1673 }
1674
1675 async fn configure_mvbox<'a>(
1684 &mut self,
1685 context: &Context,
1686 folders: &[&'a str],
1687 ) -> Result<Option<&'a str>> {
1688 self.maybe_close_folder(context).await?;
1691
1692 for folder in folders {
1693 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1694 let res = self.examine(&folder).await;
1695 if res.is_ok() {
1696 info!(
1697 context,
1698 "MVBOX-folder {:?} successfully selected, using it.", &folder
1699 );
1700 self.close().await?;
1701 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1704 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1705 return Ok(Some(folder));
1706 }
1707 }
1708
1709 Ok(None)
1710 }
1711}
1712
1713impl Imap {
1714 pub(crate) async fn configure_folders(
1715 &mut self,
1716 context: &Context,
1717 session: &mut Session,
1718 ) -> Result<()> {
1719 let mut folders = session
1720 .list(Some(""), Some("*"))
1721 .await
1722 .context("list_folders failed")?;
1723 let mut delimiter = ".".to_string();
1724 let mut delimiter_is_default = true;
1725 let mut folder_configs = BTreeMap::new();
1726
1727 while let Some(folder) = folders.try_next().await? {
1728 info!(context, "Scanning folder: {:?}", folder);
1729
1730 if let Some(d) = folder.delimiter()
1732 && delimiter_is_default
1733 && !d.is_empty()
1734 && delimiter != d
1735 {
1736 delimiter = d.to_string();
1737 delimiter_is_default = false;
1738 }
1739
1740 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1741 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1742 if let Some(config) = folder_meaning.to_config() {
1743 folder_configs.insert(config, folder.name().to_string());
1745 } else if let Some(config) = folder_name_meaning.to_config() {
1746 folder_configs
1748 .entry(config)
1749 .or_insert_with(|| folder.name().to_string());
1750 }
1751 }
1752 drop(folders);
1753
1754 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1755
1756 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1757 let mvbox_folder = session
1758 .configure_mvbox(context, &["DeltaChat", &fallback_folder])
1759 .await
1760 .context("failed to configure mvbox")?;
1761
1762 context
1763 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1764 .await?;
1765 if let Some(mvbox_folder) = mvbox_folder {
1766 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1767 context
1768 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1769 .await?;
1770 }
1771 for (config, name) in folder_configs {
1772 context.set_config_internal(config, Some(&name)).await?;
1773 }
1774 context
1775 .sql
1776 .set_raw_config_int(
1777 constants::DC_FOLDERS_CONFIGURED_KEY,
1778 constants::DC_FOLDERS_CONFIGURED_VERSION,
1779 )
1780 .await?;
1781
1782 info!(context, "FINISHED configuring IMAP-folders.");
1783 Ok(())
1784 }
1785}
1786
1787impl Session {
1788 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1797 use UnsolicitedResponse::*;
1798 use async_imap::imap_proto::Response;
1799 use async_imap::imap_proto::ResponseCode;
1800
1801 let folder = self.selected_folder.as_deref().unwrap_or_default();
1802 let mut should_refetch = false;
1803 while let Ok(response) = self.unsolicited_responses.try_recv() {
1804 match response {
1805 Exists(_) => {
1806 info!(
1807 context,
1808 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1809 );
1810 should_refetch = true;
1811 }
1812
1813 Expunge(_) | Recent(_) => {}
1814 Other(ref response_data) => {
1815 match response_data.parsed() {
1816 Response::Fetch { .. } => {
1817 info!(
1818 context,
1819 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1820 );
1821 should_refetch = true;
1822 }
1823
1824 Response::Done {
1827 code: Some(ResponseCode::CopyUid(_, _, _)),
1828 ..
1829 } => {}
1830
1831 _ => {
1832 info!(context, "{folder:?}: got unsolicited response {response:?}")
1833 }
1834 }
1835 }
1836 _ => {
1837 info!(context, "{folder:?}: got unsolicited response {response:?}")
1838 }
1839 }
1840 }
1841 Ok(should_refetch)
1842 }
1843}
1844
1845async fn should_move_out_of_spam(
1846 context: &Context,
1847 headers: &[mailparse::MailHeader<'_>],
1848) -> Result<bool> {
1849 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1850 return Ok(true);
1861 }
1862
1863 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1864 if msg.chat_blocked != Blocked::Not {
1865 return Ok(false);
1867 }
1868 } else {
1869 let from = match mimeparser::get_from(headers) {
1870 Some(f) => f,
1871 None => return Ok(false),
1872 };
1873 let (from_id, blocked_contact, _origin) =
1875 match from_field_to_contact_id(context, &from, None, true, true)
1876 .await
1877 .context("from_field_to_contact_id")?
1878 {
1879 Some(res) => res,
1880 None => {
1881 warn!(
1882 context,
1883 "Contact with From address {:?} cannot exist, not moving out of spam", from
1884 );
1885 return Ok(false);
1886 }
1887 };
1888 if blocked_contact {
1889 return Ok(false);
1891 }
1892
1893 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1894 if chat_id_blocked.blocked != Blocked::Not {
1895 return Ok(false);
1896 }
1897 } else if from_id != ContactId::SELF {
1898 return Ok(false);
1900 }
1901 }
1902
1903 Ok(true)
1904}
1905
1906async fn spam_target_folder_cfg(
1911 context: &Context,
1912 headers: &[mailparse::MailHeader<'_>],
1913) -> Result<Option<Config>> {
1914 if !should_move_out_of_spam(context, headers).await? {
1915 return Ok(None);
1916 }
1917
1918 if needs_move_to_mvbox(context, headers).await?
1919 || context.get_config_bool(Config::OnlyFetchMvbox).await?
1922 {
1923 Ok(Some(Config::ConfiguredMvboxFolder))
1924 } else {
1925 Ok(Some(Config::ConfiguredInboxFolder))
1926 }
1927}
1928
1929pub async fn target_folder_cfg(
1932 context: &Context,
1933 folder: &str,
1934 folder_meaning: FolderMeaning,
1935 headers: &[mailparse::MailHeader<'_>],
1936) -> Result<Option<Config>> {
1937 if context.is_mvbox(folder).await? {
1938 return Ok(None);
1939 }
1940
1941 if folder_meaning == FolderMeaning::Spam {
1942 spam_target_folder_cfg(context, headers).await
1943 } else if folder_meaning == FolderMeaning::Inbox
1944 && needs_move_to_mvbox(context, headers).await?
1945 {
1946 Ok(Some(Config::ConfiguredMvboxFolder))
1947 } else {
1948 Ok(None)
1949 }
1950}
1951
1952pub async fn target_folder(
1953 context: &Context,
1954 folder: &str,
1955 folder_meaning: FolderMeaning,
1956 headers: &[mailparse::MailHeader<'_>],
1957) -> Result<String> {
1958 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1959 Some(config) => match context.get_config(config).await? {
1960 Some(target) => Ok(target),
1961 None => Ok(folder.to_string()),
1962 },
1963 None => Ok(folder.to_string()),
1964 }
1965}
1966
1967async fn needs_move_to_mvbox(
1968 context: &Context,
1969 headers: &[mailparse::MailHeader<'_>],
1970) -> Result<bool> {
1971 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1972 if !context.get_config_bool(Config::MvboxMove).await? {
1973 return Ok(false);
1974 }
1975
1976 if headers
1977 .get_header_value(HeaderDef::AutocryptSetupMessage)
1978 .is_some()
1979 {
1980 return Ok(false);
1983 }
1984
1985 if has_chat_version {
1986 Ok(true)
1987 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1988 match parent.is_dc_message {
1989 MessengerMessage::No => Ok(false),
1990 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1991 }
1992 } else {
1993 Ok(false)
1994 }
1995}
1996
1997fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2004 const SPAM_NAMES: &[&str] = &[
2006 "spam",
2007 "junk",
2008 "Correio electrónico não solicitado",
2009 "Correo basura",
2010 "Lixo",
2011 "Nettsøppel",
2012 "Nevyžádaná pošta",
2013 "No solicitado",
2014 "Ongewenst",
2015 "Posta indesiderata",
2016 "Skräp",
2017 "Wiadomości-śmieci",
2018 "Önemsiz",
2019 "Ανεπιθύμητα",
2020 "Спам",
2021 "垃圾邮件",
2022 "垃圾郵件",
2023 "迷惑メール",
2024 "스팸",
2025 ];
2026 const TRASH_NAMES: &[&str] = &[
2027 "Trash",
2028 "Bin",
2029 "Caixote do lixo",
2030 "Cestino",
2031 "Corbeille",
2032 "Papelera",
2033 "Papierkorb",
2034 "Papirkurv",
2035 "Papperskorgen",
2036 "Prullenbak",
2037 "Rubujo",
2038 "Κάδος απορριμμάτων",
2039 "Корзина",
2040 "Кошик",
2041 "ゴミ箱",
2042 "垃圾桶",
2043 "已删除邮件",
2044 "휴지통",
2045 ];
2046 let lower = folder_name.to_lowercase();
2047
2048 if lower == "inbox" {
2049 FolderMeaning::Inbox
2050 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2051 FolderMeaning::Spam
2052 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2053 FolderMeaning::Trash
2054 } else {
2055 FolderMeaning::Unknown
2056 }
2057}
2058
2059fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2060 for attr in folder_attrs {
2061 match attr {
2062 NameAttribute::Trash => return FolderMeaning::Trash,
2063 NameAttribute::Junk => return FolderMeaning::Spam,
2064 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2065 NameAttribute::Extension(label) => {
2066 match label.as_ref() {
2067 "\\Spam" => return FolderMeaning::Spam,
2068 "\\Important" => return FolderMeaning::Virtual,
2069 _ => {}
2070 };
2071 }
2072 _ => {}
2073 }
2074 }
2075 FolderMeaning::Unknown
2076}
2077
2078pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2079 match get_folder_meaning_by_attrs(folder.attributes()) {
2080 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2081 meaning => meaning,
2082 }
2083}
2084
2085fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2087 match prefetch_msg.header() {
2088 Some(header_bytes) => {
2089 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2090 Ok(headers)
2091 }
2092 None => Ok(Vec::new()),
2093 }
2094}
2095
2096pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2097 headers
2098 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2099 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2100 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2101}
2102
2103pub(crate) fn create_message_id() -> String {
2104 format!("{}{}", GENERATED_PREFIX, create_id())
2105}
2106
2107pub(crate) async fn prefetch_should_download(
2109 context: &Context,
2110 headers: &[mailparse::MailHeader<'_>],
2111 message_id: &str,
2112 mut flags: impl Iterator<Item = Flag<'_>>,
2113) -> Result<bool> {
2114 if message::rfc724_mid_download_tried(context, message_id).await? {
2115 if let Some(from) = mimeparser::get_from(headers)
2116 && context.is_self_addr(&from.addr).await?
2117 {
2118 markseen_on_imap_table(context, message_id).await?;
2119 }
2120 return Ok(false);
2121 }
2122
2123 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2127 let from = from.to_ascii_lowercase();
2128 from.contains("mailer-daemon") || from.contains("mail-daemon")
2129 } else {
2130 false
2131 };
2132
2133 let from = match mimeparser::get_from(headers) {
2134 Some(f) => f,
2135 None => return Ok(false),
2136 };
2137 let (_from_id, blocked_contact, _origin) =
2138 match from_field_to_contact_id(context, &from, None, true, true).await? {
2139 Some(res) => res,
2140 None => return Ok(false),
2141 };
2142 if flags.any(|f| f == Flag::Draft) {
2146 info!(context, "Ignoring draft message");
2147 return Ok(false);
2148 }
2149
2150 let should_download = (!blocked_contact) || maybe_ndn;
2151 Ok(should_download)
2152}
2153
2154async fn mark_seen_by_uid(
2158 context: &Context,
2159 transport_id: u32,
2160 folder: &str,
2161 uid_validity: u32,
2162 uid: u32,
2163) -> Result<Option<ChatId>> {
2164 if let Some((msg_id, chat_id)) = context
2165 .sql
2166 .query_row_optional(
2167 "SELECT id, chat_id FROM msgs
2168 WHERE id > 9 AND rfc724_mid IN (
2169 SELECT rfc724_mid FROM imap
2170 WHERE transport_id=?
2171 AND folder=?
2172 AND uidvalidity=?
2173 AND uid=?
2174 LIMIT 1
2175 )",
2176 (transport_id, &folder, uid_validity, uid),
2177 |row| {
2178 let msg_id: MsgId = row.get(0)?;
2179 let chat_id: ChatId = row.get(1)?;
2180 Ok((msg_id, chat_id))
2181 },
2182 )
2183 .await
2184 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2185 {
2186 let updated = context
2187 .sql
2188 .execute(
2189 "UPDATE msgs SET state=?1
2190 WHERE (state=?2 OR state=?3)
2191 AND id=?4",
2192 (
2193 MessageState::InSeen,
2194 MessageState::InFresh,
2195 MessageState::InNoticed,
2196 msg_id,
2197 ),
2198 )
2199 .await
2200 .with_context(|| format!("failed to update msg {msg_id} state"))?
2201 > 0;
2202
2203 if updated {
2204 msg_id
2205 .start_ephemeral_timer(context)
2206 .await
2207 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2208 Ok(Some(chat_id))
2209 } else {
2210 Ok(None)
2212 }
2213 } else {
2214 Ok(None)
2216 }
2217}
2218
2219pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2222 context
2223 .sql
2224 .execute(
2225 "INSERT OR IGNORE INTO imap_markseen (id)
2226 SELECT id FROM imap WHERE rfc724_mid=?",
2227 (message_id,),
2228 )
2229 .await?;
2230 context.scheduler.interrupt_inbox().await;
2231
2232 Ok(())
2233}
2234
2235pub(crate) async fn set_uid_next(
2239 context: &Context,
2240 transport_id: u32,
2241 folder: &str,
2242 uid_next: u32,
2243) -> Result<()> {
2244 context
2245 .sql
2246 .execute(
2247 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2248 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2249 (transport_id, folder, uid_next),
2250 )
2251 .await?;
2252 Ok(())
2253}
2254
2255async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2261 Ok(context
2262 .sql
2263 .query_get_value(
2264 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2265 (transport_id, folder),
2266 )
2267 .await?
2268 .unwrap_or(0))
2269}
2270
2271pub(crate) async fn set_uidvalidity(
2272 context: &Context,
2273 transport_id: u32,
2274 folder: &str,
2275 uidvalidity: u32,
2276) -> Result<()> {
2277 context
2278 .sql
2279 .execute(
2280 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2281 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2282 (transport_id, folder, uidvalidity),
2283 )
2284 .await?;
2285 Ok(())
2286}
2287
2288async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2289 Ok(context
2290 .sql
2291 .query_get_value(
2292 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2293 (transport_id, folder),
2294 )
2295 .await?
2296 .unwrap_or(0))
2297}
2298
2299pub(crate) async fn set_modseq(
2300 context: &Context,
2301 transport_id: u32,
2302 folder: &str,
2303 modseq: u64,
2304) -> Result<()> {
2305 context
2306 .sql
2307 .execute(
2308 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2309 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2310 (transport_id, folder, modseq),
2311 )
2312 .await?;
2313 Ok(())
2314}
2315
2316async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2317 Ok(context
2318 .sql
2319 .query_get_value(
2320 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2321 (transport_id, folder),
2322 )
2323 .await?
2324 .unwrap_or(0))
2325}
2326
2327async fn should_ignore_folder(
2332 context: &Context,
2333 folder: &str,
2334 folder_meaning: FolderMeaning,
2335) -> Result<bool> {
2336 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2337 return Ok(false);
2338 }
2339 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2340}
2341
2342#[expect(clippy::arithmetic_side_effects)]
2346fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2347 let mut ranges: Vec<UidRange> = vec![];
2349
2350 for ¤t in uids {
2351 if let Some(last) = ranges.last_mut()
2352 && last.end + 1 == current
2353 {
2354 last.end = current;
2355 continue;
2356 }
2357
2358 ranges.push(UidRange {
2359 start: current,
2360 end: current,
2361 });
2362 }
2363
2364 let mut result = vec![];
2366 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2367 for range in ranges {
2368 last_uids.reserve((range.end - range.start + 1).try_into()?);
2369 (range.start..=range.end).for_each(|u| last_uids.push(u));
2370 if !last_str.is_empty() {
2371 last_str.push(',');
2372 }
2373 last_str.push_str(&range.to_string());
2374
2375 if last_str.len() > 990 {
2376 result.push((take(&mut last_uids), take(&mut last_str)));
2377 }
2378 }
2379 result.push((last_uids, last_str));
2380
2381 result.retain(|(_, s)| !s.is_empty());
2382 Ok(result)
2383}
2384
2385struct UidRange {
2386 start: u32,
2387 end: u32,
2388 }
2390
2391impl std::fmt::Display for UidRange {
2392 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2393 if self.start == self.end {
2394 write!(f, "{}", self.start)
2395 } else {
2396 write!(f, "{}:{}", self.start, self.end)
2397 }
2398 }
2399}
2400
2401pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2402 let mut res = vec![Config::ConfiguredInboxFolder];
2403 if context.should_watch_mvbox().await? {
2404 res.push(Config::ConfiguredMvboxFolder);
2405 }
2406 Ok(res)
2407}
2408
2409pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2410 let mut res = Vec::new();
2411 for folder_config in get_watched_folder_configs(context).await? {
2412 if let Some(folder) = context.get_config(folder_config).await? {
2413 res.push(folder);
2414 }
2415 }
2416 Ok(res)
2417}
2418
2419#[cfg(test)]
2420mod imap_tests;