1use std::{
7 cmp::max,
8 cmp::min,
9 collections::{BTreeMap, BTreeSet, HashMap},
10 iter::Peekable,
11 mem::take,
12 sync::atomic::Ordering,
13 time::{Duration, UNIX_EPOCH},
14};
15
16use anyhow::{Context as _, Result, bail, ensure, format_err};
17use async_channel::{self, Receiver, Sender};
18use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
19use futures::{FutureExt as _, TryStreamExt};
20use futures_lite::FutureExt;
21use num_traits::FromPrimitive;
22use ratelimit::Ratelimit;
23use url::Url;
24
25use crate::calls::{
26 UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
27};
28use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
29use crate::chatlist_events;
30use crate::config::Config;
31use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
32use crate::contact::ContactId;
33use crate::context::Context;
34use crate::events::EventType;
35use crate::headerdef::{HeaderDef, HeaderDefMap};
36use crate::log::{LogExt, warn};
37use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
38use crate::mimeparser;
39use crate::net::proxy::ProxyConfig;
40use crate::net::session::SessionStream;
41use crate::oauth2::get_oauth2_access_token;
42use crate::push::encrypt_device_token;
43use crate::receive_imf::{
44 ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
45};
46use crate::scheduler::connectivity::ConnectivityStore;
47use crate::stock_str;
48use crate::tools::{self, create_id, duration_to_str, time};
49use crate::transport::{
50 ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
51};
52
53pub(crate) mod capabilities;
54mod client;
55mod idle;
56pub mod select_folder;
57pub(crate) mod session;
58
59use client::{Client, determine_capabilities};
60use session::Session;
61
62pub(crate) const GENERATED_PREFIX: &str = "GEN_";
63
64const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
65 MESSAGE-ID \
66 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
67 )])";
68const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
69
70#[derive(Debug)]
71pub(crate) struct Imap {
72 transport_id: u32,
76
77 pub(crate) idle_interrupt_receiver: Receiver<()>,
78
79 pub(crate) addr: String,
81
82 lp: Vec<ConfiguredServerLoginParam>,
84
85 password: String,
87
88 proxy_config: Option<ProxyConfig>,
90
91 strict_tls: bool,
92
93 oauth2: bool,
94
95 authentication_failed_once: bool,
96
97 pub(crate) connectivity: ConnectivityStore,
98
99 conn_last_try: tools::Time,
100 conn_backoff_ms: u64,
101
102 ratelimit: Ratelimit,
110
111 pub(crate) resync_request_sender: async_channel::Sender<()>,
113
114 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
116}
117
118#[derive(Debug)]
119struct OAuth2 {
120 user: String,
121 access_token: String,
122}
123
124#[derive(Debug, Default)]
125pub(crate) struct ServerMetadata {
126 pub comment: Option<String>,
129
130 pub admin: Option<String>,
133
134 pub iroh_relay: Option<Url>,
135
136 pub ice_servers: Vec<UnresolvedIceServer>,
138
139 pub ice_servers_expiration_timestamp: i64,
146}
147
148impl async_imap::Authenticator for OAuth2 {
149 type Response = String;
150
151 fn process(&mut self, _data: &[u8]) -> Self::Response {
152 format!(
153 "user={}\x01auth=Bearer {}\x01\x01",
154 self.user, self.access_token
155 )
156 }
157}
158
159#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
160pub enum FolderMeaning {
161 Unknown,
162
163 Spam,
165 Inbox,
166 Mvbox,
167 Trash,
168
169 Virtual,
176}
177
178impl FolderMeaning {
179 pub fn to_config(self) -> Option<Config> {
180 match self {
181 FolderMeaning::Unknown => None,
182 FolderMeaning::Spam => None,
183 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
184 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
185 FolderMeaning::Trash => None,
186 FolderMeaning::Virtual => None,
187 }
188 }
189}
190
191struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
192 inner: Peekable<T>,
193}
194
195impl<T, I> From<I> for UidGrouper<T>
196where
197 T: Iterator<Item = (i64, u32, String)>,
198 I: IntoIterator<IntoIter = T>,
199{
200 fn from(inner: I) -> Self {
201 Self {
202 inner: inner.into_iter().peekable(),
203 }
204 }
205}
206
207impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
208 type Item = (String, Vec<i64>, String);
210
211 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 .context("IMAP failed to connect")
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 pub(crate) async fn fetch_new_messages(
547 &mut self,
548 context: &Context,
549 session: &mut Session,
550 folder: &str,
551 folder_meaning: FolderMeaning,
552 ) -> Result<bool> {
553 if should_ignore_folder(context, folder, folder_meaning).await? {
554 info!(context, "Not fetching from {folder:?}.");
555 session.new_mail = false;
556 return Ok(false);
557 }
558
559 let folder_exists = session
560 .select_with_uidvalidity(context, folder)
561 .await
562 .with_context(|| format!("Failed to select folder {folder:?}"))?;
563 if !folder_exists {
564 return Ok(false);
565 }
566
567 if !session.new_mail {
568 info!(context, "No new emails in folder {folder:?}.");
569 return Ok(false);
570 }
571 session.new_mail = false;
572
573 let mut read_cnt = 0;
574 loop {
575 let (n, fetch_more) = self
576 .fetch_new_msg_batch(context, session, folder, folder_meaning)
577 .await?;
578 read_cnt += n;
579 if !fetch_more {
580 return Ok(read_cnt > 0);
581 }
582 }
583 }
584
585 async fn fetch_new_msg_batch(
587 &mut self,
588 context: &Context,
589 session: &mut Session,
590 folder: &str,
591 folder_meaning: FolderMeaning,
592 ) -> Result<(usize, bool)> {
593 let transport_id = self.transport_id;
594 let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
595 let old_uid_next = get_uid_next(context, transport_id, folder).await?;
596 info!(
597 context,
598 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
599 );
600
601 let uids_to_prefetch = 500;
602 let msgs = session
603 .prefetch(old_uid_next, uids_to_prefetch)
604 .await
605 .context("prefetch")?;
606 let read_cnt = msgs.len();
607
608 let mut uids_fetch: Vec<u32> = Vec::new();
609 let mut available_post_msgs: Vec<String> = Vec::new();
610 let mut download_later: Vec<String> = Vec::new();
611 let mut uid_message_ids = BTreeMap::new();
612 let mut largest_uid_skipped = None;
613
614 let download_limit: Option<u32> = context
615 .get_config_parsed(Config::DownloadLimit)
616 .await?
617 .filter(|&l| 0 < l);
618
619 for (uid, ref fetch_response) in msgs {
621 let headers = match get_fetch_headers(fetch_response) {
622 Ok(headers) => headers,
623 Err(err) => {
624 warn!(context, "Failed to parse FETCH headers: {err:#}.");
625 continue;
626 }
627 };
628
629 let message_id = prefetch_get_message_id(&headers);
630 let size = fetch_response
631 .size
632 .context("imap fetch response does not contain size")?;
633
634 let delete = if let Some(message_id) = &message_id {
645 message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
646 .await?
647 .is_some_and(|(_msg_id, deleted)| deleted)
648 } else {
649 false
650 };
651
652 let message_id = message_id.unwrap_or_else(create_message_id);
655
656 if delete {
657 info!(context, "Deleting locally deleted message {message_id}.");
658 }
659
660 let _target;
661 let target = if delete {
662 ""
663 } else {
664 _target = target_folder(context, folder, folder_meaning, &headers).await?;
665 &_target
666 };
667
668 context
669 .sql
670 .execute(
671 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
672 VALUES (?, ?, ?, ?, ?, ?)
673 ON CONFLICT(transport_id, folder, uid, uidvalidity)
674 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
675 target=excluded.target",
676 (
677 self.transport_id,
678 &message_id,
679 &folder,
680 uid,
681 uid_validity,
682 target,
683 ),
684 )
685 .await?;
686
687 if folder == target
694 && folder_meaning != FolderMeaning::Spam
699 && prefetch_should_download(
700 context,
701 &headers,
702 &message_id,
703 fetch_response.flags(),
704 )
705 .await.context("prefetch_should_download")?
706 {
707 if headers
708 .get_header_value(HeaderDef::ChatIsPostMessage)
709 .is_some()
710 {
711 info!(context, "{message_id:?} is a post-message.");
712 available_post_msgs.push(message_id.clone());
713
714 if download_limit.is_none_or(|download_limit| size <= download_limit) {
715 download_later.push(message_id.clone());
716 }
717 largest_uid_skipped = Some(uid);
718 } else {
719 info!(context, "{message_id:?} is not a post-message.");
720 if download_limit.is_none_or(|download_limit| size <= download_limit) {
721 uids_fetch.push(uid);
722 uid_message_ids.insert(uid, message_id);
723 } else {
724 download_later.push(message_id.clone());
725 largest_uid_skipped = Some(uid);
726 }
727 };
728 } else {
729 largest_uid_skipped = Some(uid);
730 }
731 }
732
733 if !uids_fetch.is_empty() {
734 self.connectivity.set_working(context);
735 }
736
737 let (sender, receiver) = async_channel::unbounded();
738
739 let mut received_msgs = Vec::with_capacity(uids_fetch.len());
740 let mailbox_uid_next = session
741 .selected_mailbox
742 .as_ref()
743 .with_context(|| format!("Expected {folder:?} to be selected"))?
744 .uid_next
745 .unwrap_or_default();
746
747 let update_uids_future = async {
748 let mut largest_uid_fetched: u32 = 0;
749
750 while let Ok((uid, received_msg_opt)) = receiver.recv().await {
751 largest_uid_fetched = max(largest_uid_fetched, uid);
752 if let Some(received_msg) = received_msg_opt {
753 received_msgs.push(received_msg)
754 }
755 }
756
757 largest_uid_fetched
758 };
759
760 let actually_download_messages_future = async {
761 session
762 .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
763 .await
764 .context("fetch_many_msgs")
765 };
766
767 let (largest_uid_fetched, fetch_res) =
768 tokio::join!(update_uids_future, actually_download_messages_future);
769
770 let mut new_uid_next = largest_uid_fetched + 1;
776 let fetch_more = fetch_res.is_ok() && {
777 let prefetch_uid_next = old_uid_next + uids_to_prefetch;
778 new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
782
783 new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
784
785 prefetch_uid_next < mailbox_uid_next
786 };
787 if new_uid_next > old_uid_next {
788 set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
789 }
790
791 info!(context, "{} mails read from \"{}\".", read_cnt, folder);
792
793 if !received_msgs.is_empty() {
794 context.emit_event(EventType::IncomingMsgBunch);
795 }
796
797 chat::mark_old_messages_as_noticed(context, received_msgs).await?;
798
799 if fetch_res.is_ok() {
800 info!(
801 context,
802 "available_post_msgs: {}, download_later: {}.",
803 available_post_msgs.len(),
804 download_later.len(),
805 );
806 let trans_fn = |t: &mut rusqlite::Transaction| {
807 let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
808 for rfc724_mid in available_post_msgs {
809 stmt.execute((rfc724_mid,))
810 .context("INSERT OR IGNORE INTO available_post_msgs")?;
811 }
812 let mut stmt =
813 t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
814 for rfc724_mid in download_later {
815 stmt.execute((rfc724_mid,))
816 .context("INSERT OR IGNORE INTO download")?;
817 }
818 Ok(())
819 };
820 context.sql.transaction(trans_fn).await?;
821 }
822
823 fetch_res?;
826
827 Ok((read_cnt, fetch_more))
828 }
829}
830
831impl Session {
832 pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
834 let all_folders = self
835 .list_folders()
836 .await
837 .context("listing folders for resync")?;
838 for folder in all_folders {
839 let folder_meaning = get_folder_meaning(&folder);
840 if !matches!(
841 folder_meaning,
842 FolderMeaning::Virtual | FolderMeaning::Unknown
843 ) {
844 self.resync_folder_uids(context, folder.name(), folder_meaning)
845 .await?;
846 }
847 }
848 Ok(())
849 }
850
851 pub(crate) async fn resync_folder_uids(
858 &mut self,
859 context: &Context,
860 folder: &str,
861 folder_meaning: FolderMeaning,
862 ) -> Result<()> {
863 let uid_validity;
864 let mut msgs = BTreeMap::new();
866
867 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
868 let transport_id = self.transport_id();
869 if folder_exists {
870 let mut list = self
871 .uid_fetch("1:*", RFC724MID_UID)
872 .await
873 .with_context(|| format!("Can't resync folder {folder}"))?;
874 while let Some(fetch) = list.try_next().await? {
875 let headers = match get_fetch_headers(&fetch) {
876 Ok(headers) => headers,
877 Err(err) => {
878 warn!(context, "Failed to parse FETCH headers: {}", err);
879 continue;
880 }
881 };
882 let message_id = prefetch_get_message_id(&headers);
883
884 if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
885 msgs.insert(
886 uid,
887 (
888 rfc724_mid,
889 target_folder(context, folder, folder_meaning, &headers).await?,
890 ),
891 );
892 }
893 }
894
895 info!(
896 context,
897 "resync_folder_uids: Collected {} message IDs in {folder}.",
898 msgs.len(),
899 );
900
901 uid_validity = get_uidvalidity(context, transport_id, folder).await?;
902 } else {
903 warn!(context, "resync_folder_uids: No folder {folder}.");
904 uid_validity = 0;
905 }
906
907 context
909 .sql
910 .transaction(move |transaction| {
911 transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
912 for (uid, (rfc724_mid, target)) in &msgs {
913 transaction.execute(
916 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
917 VALUES (?, ?, ?, ?, ?, ?)
918 ON CONFLICT(transport_id, folder, uid, uidvalidity)
919 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
920 target=excluded.target",
921 (transport_id, rfc724_mid, folder, uid, uid_validity, target),
922 )?;
923 }
924 Ok(())
925 })
926 .await?;
927 Ok(())
928 }
929
930 async fn delete_message_batch(
933 &mut self,
934 context: &Context,
935 uid_set: &str,
936 row_ids: Vec<i64>,
937 ) -> Result<()> {
938 self.add_flag_finalized_with_set(uid_set, "\\Deleted")
940 .await?;
941 context
942 .sql
943 .transaction(|transaction| {
944 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
945 for row_id in row_ids {
946 stmt.execute((row_id,))?;
947 }
948 Ok(())
949 })
950 .await
951 .context("Cannot remove deleted messages from imap table")?;
952
953 context.emit_event(EventType::ImapMessageDeleted(format!(
954 "IMAP messages {uid_set} marked as deleted"
955 )));
956 Ok(())
957 }
958
959 async fn move_message_batch(
962 &mut self,
963 context: &Context,
964 set: &str,
965 row_ids: Vec<i64>,
966 target: &str,
967 ) -> Result<()> {
968 if self.can_move() {
969 match self.uid_mv(set, &target).await {
970 Ok(()) => {
971 context
973 .sql
974 .transaction(|transaction| {
975 let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
976 for row_id in row_ids {
977 stmt.execute((row_id,))?;
978 }
979 Ok(())
980 })
981 .await
982 .context("Cannot delete moved messages from imap table")?;
983 context.emit_event(EventType::ImapMessageMoved(format!(
984 "IMAP messages {set} moved to {target}"
985 )));
986 return Ok(());
987 }
988 Err(err) => {
989 warn!(
990 context,
991 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
992 set,
993 target,
994 err
995 );
996 }
997 }
998 }
999
1000 info!(
1003 context,
1004 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1005 );
1006 self.uid_copy(&set, &target).await?;
1007 context
1008 .sql
1009 .transaction(|transaction| {
1010 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1011 for row_id in row_ids {
1012 stmt.execute((row_id,))?;
1013 }
1014 Ok(())
1015 })
1016 .await
1017 .context("Cannot plan deletion of messages")?;
1018 context.emit_event(EventType::ImapMessageMoved(format!(
1019 "IMAP messages {set} copied to {target}"
1020 )));
1021 Ok(())
1022 }
1023
1024 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1028 let transport_id = self.transport_id();
1029 let rows = context
1030 .sql
1031 .query_map_vec(
1032 "SELECT id, uid, target FROM imap
1033 WHERE folder = ?
1034 AND transport_id = ?
1035 AND target != folder
1036 ORDER BY target, uid",
1037 (folder, transport_id),
1038 |row| {
1039 let rowid: i64 = row.get(0)?;
1040 let uid: u32 = row.get(1)?;
1041 let target: String = row.get(2)?;
1042 Ok((rowid, uid, target))
1043 },
1044 )
1045 .await?;
1046
1047 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1048 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1053 ensure!(folder_exists, "No folder {folder}");
1054
1055 if target.is_empty() {
1057 self.delete_message_batch(context, &uid_set, rowid_set)
1058 .await
1059 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1060 } else {
1061 self.move_message_batch(context, &uid_set, rowid_set, &target)
1062 .await
1063 .with_context(|| {
1064 format!(
1065 "cannot move batch of messages {:?} to folder {:?}",
1066 &uid_set, target
1067 )
1068 })?;
1069 }
1070 }
1071
1072 if let Err(err) = self.maybe_close_folder(context).await {
1075 warn!(context, "Failed to close folder: {err:#}.");
1076 }
1077
1078 Ok(())
1079 }
1080
1081 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1083 if context.get_config_bool(Config::TeamProfile).await? {
1084 return Ok(());
1085 }
1086
1087 let transport_id = self.transport_id();
1088 let rows = context
1089 .sql
1090 .query_map_vec(
1091 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1092 WHERE imap.id = imap_markseen.id
1093 AND imap.transport_id=?
1094 AND target = folder
1095 ORDER BY folder, uid",
1096 (transport_id,),
1097 |row| {
1098 let rowid: i64 = row.get(0)?;
1099 let uid: u32 = row.get(1)?;
1100 let folder: String = row.get(2)?;
1101 Ok((rowid, uid, folder))
1102 },
1103 )
1104 .await?;
1105
1106 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1107 let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
1108 Err(err) => {
1109 warn!(
1110 context,
1111 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1112 );
1113 continue;
1114 }
1115 Ok(folder_exists) => folder_exists,
1116 };
1117 if !folder_exists {
1118 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1119 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1120 warn!(
1121 context,
1122 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1123 );
1124 continue;
1125 } else {
1126 info!(
1127 context,
1128 "Marked messages {} in folder {} as seen.", uid_set, folder
1129 );
1130 }
1131 context
1132 .sql
1133 .transaction(|transaction| {
1134 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1135 for rowid in rowid_set {
1136 stmt.execute((rowid,))?;
1137 }
1138 Ok(())
1139 })
1140 .await
1141 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1142 }
1143
1144 Ok(())
1145 }
1146
1147 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1149 if !self.can_condstore() {
1150 info!(
1151 context,
1152 "Server does not support CONDSTORE, skipping flag synchronization."
1153 );
1154 return Ok(());
1155 }
1156
1157 if context.get_config_bool(Config::TeamProfile).await? {
1158 return Ok(());
1159 }
1160
1161 let folder_exists = self
1162 .select_with_uidvalidity(context, folder)
1163 .await
1164 .context("Failed to select folder")?;
1165 if !folder_exists {
1166 return Ok(());
1167 }
1168
1169 let mailbox = self
1170 .selected_mailbox
1171 .as_ref()
1172 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1173
1174 if mailbox.highest_modseq.is_none() {
1177 info!(
1178 context,
1179 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1180 );
1181 return Ok(());
1182 }
1183
1184 let transport_id = self.transport_id();
1185 let mut updated_chat_ids = BTreeSet::new();
1186 let uid_validity = get_uidvalidity(context, transport_id, folder)
1187 .await
1188 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1189 let mut highest_modseq = get_modseq(context, transport_id, folder)
1190 .await
1191 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1192 let mut list = self
1193 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1194 .await
1195 .context("failed to fetch flags")?;
1196
1197 let mut got_unsolicited_fetch = false;
1198
1199 while let Some(fetch) = list
1200 .try_next()
1201 .await
1202 .context("failed to get FETCH result")?
1203 {
1204 let uid = if let Some(uid) = fetch.uid {
1205 uid
1206 } else {
1207 info!(context, "FETCH result contains no UID, skipping");
1208 got_unsolicited_fetch = true;
1209 continue;
1210 };
1211 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1212 if is_seen
1213 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1214 .await
1215 .with_context(|| {
1216 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1217 })?
1218 {
1219 updated_chat_ids.insert(chat_id);
1220 }
1221
1222 if let Some(modseq) = fetch.modseq {
1223 if modseq > highest_modseq {
1224 highest_modseq = modseq;
1225 }
1226 } else {
1227 warn!(context, "FETCH result contains no MODSEQ");
1228 }
1229 }
1230 drop(list);
1231
1232 if got_unsolicited_fetch {
1233 self.new_mail = true;
1238 }
1239
1240 set_modseq(context, transport_id, folder, highest_modseq)
1241 .await
1242 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1243 if !updated_chat_ids.is_empty() {
1244 context.on_archived_chats_maybe_noticed();
1245 }
1246 for updated_chat_id in updated_chat_ids {
1247 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1248 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1249 }
1250
1251 Ok(())
1252 }
1253
1254 pub(crate) async fn fetch_many_msgs(
1269 &mut self,
1270 context: &Context,
1271 folder: &str,
1272 request_uids: Vec<u32>,
1273 uid_message_ids: &BTreeMap<u32, String>,
1274 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1275 ) -> Result<()> {
1276 if request_uids.is_empty() {
1277 return Ok(());
1278 }
1279
1280 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1281 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1282 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1283 format!("fetching messages {} from folder \"{}\"", &set, folder)
1284 })?;
1285
1286 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1289
1290 let mut count = 0;
1291 for &request_uid in &request_uids {
1292 let mut fetch_response = uid_msgs.remove(&request_uid);
1294
1295 while fetch_response.is_none() {
1297 let Some(next_fetch_response) = fetch_responses
1298 .try_next()
1299 .await
1300 .context("Failed to process IMAP FETCH result")?
1301 else {
1302 break;
1304 };
1305
1306 if let Some(next_uid) = next_fetch_response.uid {
1307 if next_uid == request_uid {
1308 fetch_response = Some(next_fetch_response);
1309 } else if !request_uids.contains(&next_uid) {
1310 info!(
1317 context,
1318 "Skipping not requested FETCH response for UID {}.", next_uid
1319 );
1320 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1321 warn!(context, "Got duplicated UID {}.", next_uid);
1322 }
1323 } else {
1324 info!(context, "Skipping FETCH response without UID.");
1325 }
1326 }
1327
1328 let fetch_response = match fetch_response {
1329 Some(fetch) => fetch,
1330 None => {
1331 warn!(
1332 context,
1333 "Missed UID {} in the server response.", request_uid
1334 );
1335 continue;
1336 }
1337 };
1338 count += 1;
1339
1340 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1341 let body = fetch_response.body();
1342
1343 if is_deleted {
1344 info!(context, "Not processing deleted msg {}.", request_uid);
1345 received_msgs_channel.send((request_uid, None)).await?;
1346 continue;
1347 }
1348
1349 let body = if let Some(body) = body {
1350 body
1351 } else {
1352 info!(
1353 context,
1354 "Not processing message {} without a BODY.", request_uid
1355 );
1356 received_msgs_channel.send((request_uid, None)).await?;
1357 continue;
1358 };
1359
1360 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1361
1362 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1363 error!(
1364 context,
1365 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1366 request_uid
1367 );
1368 continue;
1369 };
1370
1371 info!(
1372 context,
1373 "Passing message UID {} to receive_imf().", request_uid
1374 );
1375 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1376 let received_msg = match res {
1377 Err(err) => {
1378 warn!(context, "receive_imf error: {err:#}.");
1379
1380 let text = format!(
1381 "❌ 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/.",
1382 );
1383 let mut msg = Message::new_text(text);
1384 add_device_msg(context, None, Some(&mut msg)).await?;
1385 None
1386 }
1387 Ok(msg) => msg,
1388 };
1389 received_msgs_channel
1390 .send((request_uid, received_msg))
1391 .await?;
1392 }
1393
1394 while fetch_responses
1401 .try_next()
1402 .await
1403 .context("Failed to drain FETCH responses")?
1404 .is_some()
1405 {}
1406
1407 if count != request_uids.len() {
1408 warn!(
1409 context,
1410 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1411 count,
1412 request_uids.len(),
1413 request_uids,
1414 );
1415 } else {
1416 info!(
1417 context,
1418 "Successfully received {} UIDs.",
1419 request_uids.len()
1420 );
1421 }
1422 }
1423
1424 Ok(())
1425 }
1426
1427 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1433 let mut lock = context.metadata.write().await;
1434
1435 if !self.can_metadata() {
1436 *lock = Some(Default::default());
1437 }
1438 if let Some(ref mut old_metadata) = *lock {
1439 let now = time();
1440
1441 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1443 return Ok(());
1444 }
1445
1446 let mut got_turn_server = false;
1447 if self.can_metadata() {
1448 info!(context, "ICE servers expired, requesting new credentials.");
1449 let mailbox = "";
1450 let options = "";
1451 let metadata = self
1452 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1453 .await?;
1454 for m in metadata {
1455 if m.entry == "/shared/vendor/deltachat/turn"
1456 && let Some(value) = m.value
1457 {
1458 match create_ice_servers_from_metadata(&value).await {
1459 Ok((parsed_timestamp, parsed_ice_servers)) => {
1460 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1461 old_metadata.ice_servers = parsed_ice_servers;
1462 got_turn_server = true;
1463 }
1464 Err(err) => {
1465 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1466 }
1467 }
1468 }
1469 }
1470 }
1471 if !got_turn_server {
1472 info!(context, "Will use fallback ICE servers.");
1473 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1475 old_metadata.ice_servers = create_fallback_ice_servers();
1476 }
1477 return Ok(());
1478 }
1479
1480 info!(
1481 context,
1482 "Server supports metadata, retrieving server comment and admin contact."
1483 );
1484
1485 let mut comment = None;
1486 let mut admin = None;
1487 let mut iroh_relay = None;
1488 let mut ice_servers = None;
1489 let mut ice_servers_expiration_timestamp = 0;
1490
1491 let mailbox = "";
1492 let options = "";
1493 let metadata = self
1494 .get_metadata(
1495 mailbox,
1496 options,
1497 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1498 )
1499 .await?;
1500 for m in metadata {
1501 match m.entry.as_ref() {
1502 "/shared/comment" => {
1503 comment = m.value;
1504 }
1505 "/shared/admin" => {
1506 admin = m.value;
1507 }
1508 "/shared/vendor/deltachat/irohrelay" => {
1509 if let Some(value) = m.value {
1510 if let Ok(url) = Url::parse(&value) {
1511 iroh_relay = Some(url);
1512 } else {
1513 warn!(
1514 context,
1515 "Got invalid URL from iroh relay metadata: {:?}.", value
1516 );
1517 }
1518 }
1519 }
1520 "/shared/vendor/deltachat/turn" => {
1521 if let Some(value) = m.value {
1522 match create_ice_servers_from_metadata(&value).await {
1523 Ok((parsed_timestamp, parsed_ice_servers)) => {
1524 ice_servers_expiration_timestamp = parsed_timestamp;
1525 ice_servers = Some(parsed_ice_servers);
1526 }
1527 Err(err) => {
1528 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1529 }
1530 }
1531 }
1532 }
1533 _ => {}
1534 }
1535 }
1536 let ice_servers = if let Some(ice_servers) = ice_servers {
1537 ice_servers
1538 } else {
1539 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1541 create_fallback_ice_servers()
1542 };
1543
1544 *lock = Some(ServerMetadata {
1545 comment,
1546 admin,
1547 iroh_relay,
1548 ice_servers,
1549 ice_servers_expiration_timestamp,
1550 });
1551 Ok(())
1552 }
1553
1554 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1556 if context.push_subscribed.load(Ordering::Relaxed) {
1557 return Ok(());
1558 }
1559
1560 let Some(device_token) = context.push_subscriber.device_token().await else {
1561 return Ok(());
1562 };
1563
1564 if self.can_metadata() && self.can_push() {
1565 let old_encrypted_device_token =
1566 context.get_config(Config::EncryptedDeviceToken).await?;
1567
1568 let device_token_changed = old_encrypted_device_token.is_none()
1570 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1571
1572 let new_encrypted_device_token;
1573 if device_token_changed {
1574 let encrypted_device_token = encrypt_device_token(&device_token)
1575 .context("Failed to encrypt device token")?;
1576
1577 let encrypted_device_token_len = encrypted_device_token.len();
1581
1582 context
1588 .set_config_internal(Config::DeviceToken, Some(&device_token))
1589 .await?;
1590 context
1591 .set_config_internal(
1592 Config::EncryptedDeviceToken,
1593 Some(&encrypted_device_token),
1594 )
1595 .await?;
1596
1597 if encrypted_device_token_len <= 4096 {
1598 new_encrypted_device_token = Some(encrypted_device_token);
1599 } else {
1600 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1610 new_encrypted_device_token = None;
1611 }
1612 } else {
1613 new_encrypted_device_token = old_encrypted_device_token;
1614 }
1615
1616 if let Some(encrypted_device_token) = new_encrypted_device_token {
1619 let folder = context
1620 .get_config(Config::ConfiguredInboxFolder)
1621 .await?
1622 .context("INBOX is not configured")?;
1623
1624 self.run_command_and_check_ok(&format_setmetadata(
1625 &folder,
1626 &encrypted_device_token,
1627 ))
1628 .await
1629 .context("SETMETADATA command failed")?;
1630
1631 context.push_subscribed.store(true, Ordering::Relaxed);
1632 }
1633 } else if !context.push_subscriber.heartbeat_subscribed().await {
1634 let context = context.clone();
1635 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1637 }
1638
1639 Ok(())
1640 }
1641}
1642
1643fn format_setmetadata(folder: &str, device_token: &str) -> String {
1644 let device_token_len = device_token.len();
1645 format!(
1646 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1647 )
1648}
1649
1650impl Session {
1651 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1657 if flag == "\\Deleted" {
1658 self.selected_folder_needs_expunge = true;
1659 }
1660 let query = format!("+FLAGS ({flag})");
1661 let mut responses = self
1662 .uid_store(uid_set, &query)
1663 .await
1664 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1665 while let Some(_response) = responses.try_next().await? {
1666 }
1668 Ok(())
1669 }
1670
1671 async fn configure_mvbox<'a>(
1680 &mut self,
1681 context: &Context,
1682 folders: &[&'a str],
1683 ) -> Result<Option<&'a str>> {
1684 self.maybe_close_folder(context).await?;
1687
1688 for folder in folders {
1689 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1690 let res = self.examine(&folder).await;
1691 if res.is_ok() {
1692 info!(
1693 context,
1694 "MVBOX-folder {:?} successfully selected, using it.", &folder
1695 );
1696 self.close().await?;
1697 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1700 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1701 return Ok(Some(folder));
1702 }
1703 }
1704
1705 Ok(None)
1706 }
1707}
1708
1709impl Imap {
1710 pub(crate) async fn configure_folders(
1711 &mut self,
1712 context: &Context,
1713 session: &mut Session,
1714 ) -> Result<()> {
1715 let mut folders = session
1716 .list(Some(""), Some("*"))
1717 .await
1718 .context("list_folders failed")?;
1719 let mut delimiter = ".".to_string();
1720 let mut delimiter_is_default = true;
1721 let mut folder_configs = BTreeMap::new();
1722
1723 while let Some(folder) = folders.try_next().await? {
1724 info!(context, "Scanning folder: {:?}", folder);
1725
1726 if let Some(d) = folder.delimiter()
1728 && delimiter_is_default
1729 && !d.is_empty()
1730 && delimiter != d
1731 {
1732 delimiter = d.to_string();
1733 delimiter_is_default = false;
1734 }
1735
1736 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1737 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1738 if let Some(config) = folder_meaning.to_config() {
1739 folder_configs.insert(config, folder.name().to_string());
1741 } else if let Some(config) = folder_name_meaning.to_config() {
1742 folder_configs
1744 .entry(config)
1745 .or_insert_with(|| folder.name().to_string());
1746 }
1747 }
1748 drop(folders);
1749
1750 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1751
1752 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1753 let mvbox_folder = session
1754 .configure_mvbox(context, &["DeltaChat", &fallback_folder])
1755 .await
1756 .context("failed to configure mvbox")?;
1757
1758 context
1759 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1760 .await?;
1761 if let Some(mvbox_folder) = mvbox_folder {
1762 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1763 context
1764 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1765 .await?;
1766 }
1767 for (config, name) in folder_configs {
1768 context.set_config_internal(config, Some(&name)).await?;
1769 }
1770 context
1771 .sql
1772 .set_raw_config_int(
1773 constants::DC_FOLDERS_CONFIGURED_KEY,
1774 constants::DC_FOLDERS_CONFIGURED_VERSION,
1775 )
1776 .await?;
1777
1778 info!(context, "FINISHED configuring IMAP-folders.");
1779 Ok(())
1780 }
1781}
1782
1783impl Session {
1784 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1793 use UnsolicitedResponse::*;
1794 use async_imap::imap_proto::Response;
1795 use async_imap::imap_proto::ResponseCode;
1796
1797 let folder = self.selected_folder.as_deref().unwrap_or_default();
1798 let mut should_refetch = false;
1799 while let Ok(response) = self.unsolicited_responses.try_recv() {
1800 match response {
1801 Exists(_) => {
1802 info!(
1803 context,
1804 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1805 );
1806 should_refetch = true;
1807 }
1808
1809 Expunge(_) | Recent(_) => {}
1810 Other(ref response_data) => {
1811 match response_data.parsed() {
1812 Response::Fetch { .. } => {
1813 info!(
1814 context,
1815 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1816 );
1817 should_refetch = true;
1818 }
1819
1820 Response::Done {
1823 code: Some(ResponseCode::CopyUid(_, _, _)),
1824 ..
1825 } => {}
1826
1827 _ => {
1828 info!(context, "{folder:?}: got unsolicited response {response:?}")
1829 }
1830 }
1831 }
1832 _ => {
1833 info!(context, "{folder:?}: got unsolicited response {response:?}")
1834 }
1835 }
1836 }
1837 Ok(should_refetch)
1838 }
1839}
1840
1841async fn should_move_out_of_spam(
1842 context: &Context,
1843 headers: &[mailparse::MailHeader<'_>],
1844) -> Result<bool> {
1845 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1846 return Ok(true);
1857 }
1858
1859 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1860 if msg.chat_blocked != Blocked::Not {
1861 return Ok(false);
1863 }
1864 } else {
1865 let from = match mimeparser::get_from(headers) {
1866 Some(f) => f,
1867 None => return Ok(false),
1868 };
1869 let (from_id, blocked_contact, _origin) =
1871 match from_field_to_contact_id(context, &from, None, true, true)
1872 .await
1873 .context("from_field_to_contact_id")?
1874 {
1875 Some(res) => res,
1876 None => {
1877 warn!(
1878 context,
1879 "Contact with From address {:?} cannot exist, not moving out of spam", from
1880 );
1881 return Ok(false);
1882 }
1883 };
1884 if blocked_contact {
1885 return Ok(false);
1887 }
1888
1889 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1890 if chat_id_blocked.blocked != Blocked::Not {
1891 return Ok(false);
1892 }
1893 } else if from_id != ContactId::SELF {
1894 return Ok(false);
1896 }
1897 }
1898
1899 Ok(true)
1900}
1901
1902async fn spam_target_folder_cfg(
1907 context: &Context,
1908 headers: &[mailparse::MailHeader<'_>],
1909) -> Result<Option<Config>> {
1910 if !should_move_out_of_spam(context, headers).await? {
1911 return Ok(None);
1912 }
1913
1914 if needs_move_to_mvbox(context, headers).await?
1915 || context.get_config_bool(Config::OnlyFetchMvbox).await?
1918 {
1919 Ok(Some(Config::ConfiguredMvboxFolder))
1920 } else {
1921 Ok(Some(Config::ConfiguredInboxFolder))
1922 }
1923}
1924
1925pub async fn target_folder_cfg(
1928 context: &Context,
1929 folder: &str,
1930 folder_meaning: FolderMeaning,
1931 headers: &[mailparse::MailHeader<'_>],
1932) -> Result<Option<Config>> {
1933 if context.is_mvbox(folder).await? {
1934 return Ok(None);
1935 }
1936
1937 if folder_meaning == FolderMeaning::Spam {
1938 spam_target_folder_cfg(context, headers).await
1939 } else if folder_meaning == FolderMeaning::Inbox
1940 && needs_move_to_mvbox(context, headers).await?
1941 {
1942 Ok(Some(Config::ConfiguredMvboxFolder))
1943 } else {
1944 Ok(None)
1945 }
1946}
1947
1948pub async fn target_folder(
1949 context: &Context,
1950 folder: &str,
1951 folder_meaning: FolderMeaning,
1952 headers: &[mailparse::MailHeader<'_>],
1953) -> Result<String> {
1954 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1955 Some(config) => match context.get_config(config).await? {
1956 Some(target) => Ok(target),
1957 None => Ok(folder.to_string()),
1958 },
1959 None => Ok(folder.to_string()),
1960 }
1961}
1962
1963async fn needs_move_to_mvbox(
1964 context: &Context,
1965 headers: &[mailparse::MailHeader<'_>],
1966) -> Result<bool> {
1967 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1968 if !context.get_config_bool(Config::MvboxMove).await? {
1969 return Ok(false);
1970 }
1971
1972 if headers
1973 .get_header_value(HeaderDef::AutocryptSetupMessage)
1974 .is_some()
1975 {
1976 return Ok(false);
1979 }
1980
1981 if has_chat_version {
1982 Ok(true)
1983 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1984 match parent.is_dc_message {
1985 MessengerMessage::No => Ok(false),
1986 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1987 }
1988 } else {
1989 Ok(false)
1990 }
1991}
1992
1993fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2000 const SPAM_NAMES: &[&str] = &[
2002 "spam",
2003 "junk",
2004 "Correio electrónico não solicitado",
2005 "Correo basura",
2006 "Lixo",
2007 "Nettsøppel",
2008 "Nevyžádaná pošta",
2009 "No solicitado",
2010 "Ongewenst",
2011 "Posta indesiderata",
2012 "Skräp",
2013 "Wiadomości-śmieci",
2014 "Önemsiz",
2015 "Ανεπιθύμητα",
2016 "Спам",
2017 "垃圾邮件",
2018 "垃圾郵件",
2019 "迷惑メール",
2020 "스팸",
2021 ];
2022 const TRASH_NAMES: &[&str] = &[
2023 "Trash",
2024 "Bin",
2025 "Caixote do lixo",
2026 "Cestino",
2027 "Corbeille",
2028 "Papelera",
2029 "Papierkorb",
2030 "Papirkurv",
2031 "Papperskorgen",
2032 "Prullenbak",
2033 "Rubujo",
2034 "Κάδος απορριμμάτων",
2035 "Корзина",
2036 "Кошик",
2037 "ゴミ箱",
2038 "垃圾桶",
2039 "已删除邮件",
2040 "휴지통",
2041 ];
2042 let lower = folder_name.to_lowercase();
2043
2044 if lower == "inbox" {
2045 FolderMeaning::Inbox
2046 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2047 FolderMeaning::Spam
2048 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2049 FolderMeaning::Trash
2050 } else {
2051 FolderMeaning::Unknown
2052 }
2053}
2054
2055fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2056 for attr in folder_attrs {
2057 match attr {
2058 NameAttribute::Trash => return FolderMeaning::Trash,
2059 NameAttribute::Junk => return FolderMeaning::Spam,
2060 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2061 NameAttribute::Extension(label) => {
2062 match label.as_ref() {
2063 "\\Spam" => return FolderMeaning::Spam,
2064 "\\Important" => return FolderMeaning::Virtual,
2065 _ => {}
2066 };
2067 }
2068 _ => {}
2069 }
2070 }
2071 FolderMeaning::Unknown
2072}
2073
2074pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2075 match get_folder_meaning_by_attrs(folder.attributes()) {
2076 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2077 meaning => meaning,
2078 }
2079}
2080
2081fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2083 match prefetch_msg.header() {
2084 Some(header_bytes) => {
2085 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2086 Ok(headers)
2087 }
2088 None => Ok(Vec::new()),
2089 }
2090}
2091
2092pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2093 headers
2094 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2095 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2096 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2097}
2098
2099pub(crate) fn create_message_id() -> String {
2100 format!("{}{}", GENERATED_PREFIX, create_id())
2101}
2102
2103pub(crate) async fn prefetch_should_download(
2105 context: &Context,
2106 headers: &[mailparse::MailHeader<'_>],
2107 message_id: &str,
2108 mut flags: impl Iterator<Item = Flag<'_>>,
2109) -> Result<bool> {
2110 if message::rfc724_mid_download_tried(context, message_id).await? {
2111 if let Some(from) = mimeparser::get_from(headers)
2112 && context.is_self_addr(&from.addr).await?
2113 {
2114 markseen_on_imap_table(context, message_id).await?;
2115 }
2116 return Ok(false);
2117 }
2118
2119 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2123 let from = from.to_ascii_lowercase();
2124 from.contains("mailer-daemon") || from.contains("mail-daemon")
2125 } else {
2126 false
2127 };
2128
2129 let is_autocrypt_setup_message = headers
2131 .get_header_value(HeaderDef::AutocryptSetupMessage)
2132 .is_some();
2133
2134 let from = match mimeparser::get_from(headers) {
2135 Some(f) => f,
2136 None => return Ok(false),
2137 };
2138 let (_from_id, blocked_contact, origin) =
2139 match from_field_to_contact_id(context, &from, None, true, true).await? {
2140 Some(res) => res,
2141 None => return Ok(false),
2142 };
2143 if flags.any(|f| f == Flag::Draft) {
2147 info!(context, "Ignoring draft message");
2148 return Ok(false);
2149 }
2150
2151 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2152 let accepted_contact = origin.is_known();
2153 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2154 .await?
2155 .is_some_and(|parent| match parent.is_dc_message {
2156 MessengerMessage::No => false,
2157 MessengerMessage::Yes | MessengerMessage::Reply => true,
2158 });
2159
2160 let show_emails =
2161 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2162
2163 let show = is_autocrypt_setup_message
2164 || match show_emails {
2165 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2166 ShowEmails::AcceptedContacts => {
2167 is_chat_message || is_reply_to_chat_message || accepted_contact
2168 }
2169 ShowEmails::All => true,
2170 };
2171
2172 let should_download = (show && !blocked_contact) || maybe_ndn;
2173 Ok(should_download)
2174}
2175
2176async fn mark_seen_by_uid(
2180 context: &Context,
2181 transport_id: u32,
2182 folder: &str,
2183 uid_validity: u32,
2184 uid: u32,
2185) -> Result<Option<ChatId>> {
2186 if let Some((msg_id, chat_id)) = context
2187 .sql
2188 .query_row_optional(
2189 "SELECT id, chat_id FROM msgs
2190 WHERE id > 9 AND rfc724_mid IN (
2191 SELECT rfc724_mid FROM imap
2192 WHERE transport_id=?
2193 AND folder=?
2194 AND uidvalidity=?
2195 AND uid=?
2196 LIMIT 1
2197 )",
2198 (transport_id, &folder, uid_validity, uid),
2199 |row| {
2200 let msg_id: MsgId = row.get(0)?;
2201 let chat_id: ChatId = row.get(1)?;
2202 Ok((msg_id, chat_id))
2203 },
2204 )
2205 .await
2206 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2207 {
2208 let updated = context
2209 .sql
2210 .execute(
2211 "UPDATE msgs SET state=?1
2212 WHERE (state=?2 OR state=?3)
2213 AND id=?4",
2214 (
2215 MessageState::InSeen,
2216 MessageState::InFresh,
2217 MessageState::InNoticed,
2218 msg_id,
2219 ),
2220 )
2221 .await
2222 .with_context(|| format!("failed to update msg {msg_id} state"))?
2223 > 0;
2224
2225 if updated {
2226 msg_id
2227 .start_ephemeral_timer(context)
2228 .await
2229 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2230 Ok(Some(chat_id))
2231 } else {
2232 Ok(None)
2234 }
2235 } else {
2236 Ok(None)
2238 }
2239}
2240
2241pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2244 context
2245 .sql
2246 .execute(
2247 "INSERT OR IGNORE INTO imap_markseen (id)
2248 SELECT id FROM imap WHERE rfc724_mid=?",
2249 (message_id,),
2250 )
2251 .await?;
2252 context.scheduler.interrupt_inbox().await;
2253
2254 Ok(())
2255}
2256
2257pub(crate) async fn set_uid_next(
2261 context: &Context,
2262 transport_id: u32,
2263 folder: &str,
2264 uid_next: u32,
2265) -> Result<()> {
2266 context
2267 .sql
2268 .execute(
2269 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2270 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2271 (transport_id, folder, uid_next),
2272 )
2273 .await?;
2274 Ok(())
2275}
2276
2277async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2283 Ok(context
2284 .sql
2285 .query_get_value(
2286 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2287 (transport_id, folder),
2288 )
2289 .await?
2290 .unwrap_or(0))
2291}
2292
2293pub(crate) async fn set_uidvalidity(
2294 context: &Context,
2295 transport_id: u32,
2296 folder: &str,
2297 uidvalidity: u32,
2298) -> Result<()> {
2299 context
2300 .sql
2301 .execute(
2302 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2303 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2304 (transport_id, folder, uidvalidity),
2305 )
2306 .await?;
2307 Ok(())
2308}
2309
2310async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2311 Ok(context
2312 .sql
2313 .query_get_value(
2314 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2315 (transport_id, folder),
2316 )
2317 .await?
2318 .unwrap_or(0))
2319}
2320
2321pub(crate) async fn set_modseq(
2322 context: &Context,
2323 transport_id: u32,
2324 folder: &str,
2325 modseq: u64,
2326) -> Result<()> {
2327 context
2328 .sql
2329 .execute(
2330 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2331 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2332 (transport_id, folder, modseq),
2333 )
2334 .await?;
2335 Ok(())
2336}
2337
2338async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2339 Ok(context
2340 .sql
2341 .query_get_value(
2342 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2343 (transport_id, folder),
2344 )
2345 .await?
2346 .unwrap_or(0))
2347}
2348
2349async fn should_ignore_folder(
2354 context: &Context,
2355 folder: &str,
2356 folder_meaning: FolderMeaning,
2357) -> Result<bool> {
2358 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2359 return Ok(false);
2360 }
2361 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2362}
2363
2364fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2368 let mut ranges: Vec<UidRange> = vec![];
2370
2371 for ¤t in uids {
2372 if let Some(last) = ranges.last_mut()
2373 && last.end + 1 == current
2374 {
2375 last.end = current;
2376 continue;
2377 }
2378
2379 ranges.push(UidRange {
2380 start: current,
2381 end: current,
2382 });
2383 }
2384
2385 let mut result = vec![];
2387 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2388 for range in ranges {
2389 last_uids.reserve((range.end - range.start + 1).try_into()?);
2390 (range.start..=range.end).for_each(|u| last_uids.push(u));
2391 if !last_str.is_empty() {
2392 last_str.push(',');
2393 }
2394 last_str.push_str(&range.to_string());
2395
2396 if last_str.len() > 990 {
2397 result.push((take(&mut last_uids), take(&mut last_str)));
2398 }
2399 }
2400 result.push((last_uids, last_str));
2401
2402 result.retain(|(_, s)| !s.is_empty());
2403 Ok(result)
2404}
2405
2406struct UidRange {
2407 start: u32,
2408 end: u32,
2409 }
2411
2412impl std::fmt::Display for UidRange {
2413 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2414 if self.start == self.end {
2415 write!(f, "{}", self.start)
2416 } else {
2417 write!(f, "{}:{}", self.start, self.end)
2418 }
2419 }
2420}
2421
2422pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2423 let mut res = vec![Config::ConfiguredInboxFolder];
2424 if context.should_watch_mvbox().await? {
2425 res.push(Config::ConfiguredMvboxFolder);
2426 }
2427 Ok(res)
2428}
2429
2430pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2431 let mut res = Vec::new();
2432 for folder_config in get_watched_folder_configs(context).await? {
2433 if let Some(folder) = context.get_config(folder_config).await? {
2434 res.push(folder);
2435 }
2436 }
2437 Ok(res)
2438}
2439
2440#[cfg(test)]
2441mod imap_tests;