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 scan_folders;
57pub mod select_folder;
58pub(crate) mod session;
59
60use client::{Client, determine_capabilities};
61use session::Session;
62
63pub(crate) const GENERATED_PREFIX: &str = "GEN_";
64
65const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
66 MESSAGE-ID \
67 X-MICROSOFT-ORIGINAL-MESSAGE-ID\
68 )])";
69const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
70
71#[derive(Debug)]
72pub(crate) struct Imap {
73 transport_id: u32,
77
78 pub(crate) idle_interrupt_receiver: Receiver<()>,
79
80 pub(crate) addr: String,
82
83 lp: Vec<ConfiguredServerLoginParam>,
85
86 password: String,
88
89 proxy_config: Option<ProxyConfig>,
91
92 strict_tls: bool,
93
94 oauth2: bool,
95
96 authentication_failed_once: bool,
97
98 pub(crate) connectivity: ConnectivityStore,
99
100 conn_last_try: tools::Time,
101 conn_backoff_ms: u64,
102
103 ratelimit: Ratelimit,
111
112 pub(crate) resync_request_sender: async_channel::Sender<()>,
114
115 pub(crate) resync_request_receiver: async_channel::Receiver<()>,
117}
118
119#[derive(Debug)]
120struct OAuth2 {
121 user: String,
122 access_token: String,
123}
124
125#[derive(Debug, Default)]
126pub(crate) struct ServerMetadata {
127 pub comment: Option<String>,
130
131 pub admin: Option<String>,
134
135 pub iroh_relay: Option<Url>,
136
137 pub ice_servers: Vec<UnresolvedIceServer>,
139
140 pub ice_servers_expiration_timestamp: i64,
147}
148
149impl async_imap::Authenticator for OAuth2 {
150 type Response = String;
151
152 fn process(&mut self, _data: &[u8]) -> Self::Response {
153 format!(
154 "user={}\x01auth=Bearer {}\x01\x01",
155 self.user, self.access_token
156 )
157 }
158}
159
160#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
161pub enum FolderMeaning {
162 Unknown,
163
164 Spam,
166 Inbox,
167 Mvbox,
168 Trash,
169
170 Virtual,
177}
178
179impl FolderMeaning {
180 pub fn to_config(self) -> Option<Config> {
181 match self {
182 FolderMeaning::Unknown => None,
183 FolderMeaning::Spam => None,
184 FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
185 FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
186 FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
187 FolderMeaning::Virtual => None,
188 }
189 }
190}
191
192struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
193 inner: Peekable<T>,
194}
195
196impl<T, I> From<I> for UidGrouper<T>
197where
198 T: Iterator<Item = (i64, u32, String)>,
199 I: IntoIterator<IntoIter = T>,
200{
201 fn from(inner: I) -> Self {
202 Self {
203 inner: inner.into_iter().peekable(),
204 }
205 }
206}
207
208impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
209 type Item = (String, Vec<i64>, String);
211
212 fn next(&mut self) -> Option<Self::Item> {
213 let (_, _, folder) = self.inner.peek().cloned()?;
214
215 let mut uid_set = String::new();
216 let mut rowid_set = Vec::new();
217
218 while uid_set.len() < 1000 {
219 if let Some((start_rowid, start_uid, _)) = self
221 .inner
222 .next_if(|(_, _, start_folder)| start_folder == &folder)
223 {
224 rowid_set.push(start_rowid);
225 let mut end_uid = start_uid;
226
227 while let Some((next_rowid, next_uid, _)) =
228 self.inner.next_if(|(_, next_uid, next_folder)| {
229 next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
230 })
231 {
232 end_uid = next_uid;
233 rowid_set.push(next_rowid);
234 }
235
236 let uid_range = UidRange {
237 start: start_uid,
238 end: end_uid,
239 };
240 if !uid_set.is_empty() {
241 uid_set.push(',');
242 }
243 uid_set.push_str(&uid_range.to_string());
244 } else {
245 break;
246 }
247 }
248
249 Some((folder, rowid_set, uid_set))
250 }
251}
252
253impl Imap {
254 pub async fn new(
256 context: &Context,
257 transport_id: u32,
258 param: ConfiguredLoginParam,
259 idle_interrupt_receiver: Receiver<()>,
260 ) -> Result<Self> {
261 let lp = param.imap.clone();
262 let password = param.imap_password.clone();
263 let proxy_config = ProxyConfig::load(context).await?;
264 let addr = ¶m.addr;
265 let strict_tls = param.strict_tls(proxy_config.is_some());
266 let oauth2 = param.oauth2;
267 let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
268 Ok(Imap {
269 transport_id,
270 idle_interrupt_receiver,
271 addr: addr.to_string(),
272 lp,
273 password,
274 proxy_config,
275 strict_tls,
276 oauth2,
277 authentication_failed_once: false,
278 connectivity: Default::default(),
279 conn_last_try: UNIX_EPOCH,
280 conn_backoff_ms: 0,
281 ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
283 resync_request_sender,
284 resync_request_receiver,
285 })
286 }
287
288 pub async fn new_configured(
290 context: &Context,
291 idle_interrupt_receiver: Receiver<()>,
292 ) -> Result<Self> {
293 let (transport_id, param) = ConfiguredLoginParam::load(context)
294 .await?
295 .context("Not configured")?;
296 let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
297 Ok(imap)
298 }
299
300 pub(crate) async fn connect(
306 &mut self,
307 context: &Context,
308 configuring: bool,
309 ) -> Result<Session> {
310 let now = tools::Time::now();
311 let until_can_send = max(
312 min(self.conn_last_try, now)
313 .checked_add(Duration::from_millis(self.conn_backoff_ms))
314 .unwrap_or(now),
315 now,
316 )
317 .duration_since(now)?;
318 let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
319 if !ratelimit_duration.is_zero() {
320 warn!(
321 context,
322 "IMAP got rate limited, waiting for {} until can connect.",
323 duration_to_str(ratelimit_duration),
324 );
325 let interrupted = async {
326 tokio::time::sleep(ratelimit_duration).await;
327 false
328 }
329 .race(self.idle_interrupt_receiver.recv().map(|_| true))
330 .await;
331 if interrupted {
332 info!(
333 context,
334 "Connecting to IMAP without waiting for ratelimit due to interrupt."
335 );
336 }
337 }
338
339 info!(context, "Connecting to IMAP server.");
340 self.connectivity.set_connecting(context);
341
342 self.conn_last_try = tools::Time::now();
343 const BACKOFF_MIN_MS: u64 = 2000;
344 const BACKOFF_MAX_MS: u64 = 80_000;
345 self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
346 self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
347 (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
348 ));
349 self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
350
351 let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
352 let mut first_error = None;
353 for lp in login_params {
354 info!(context, "IMAP trying to connect to {}.", &lp.connection);
355 let connection_candidate = lp.connection.clone();
356 let client = match Client::connect(
357 context,
358 self.proxy_config.clone(),
359 self.strict_tls,
360 connection_candidate,
361 )
362 .await
363 .context("IMAP failed to connect")
364 {
365 Ok(client) => client,
366 Err(err) => {
367 warn!(context, "{err:#}.");
368 first_error.get_or_insert(err);
369 continue;
370 }
371 };
372
373 self.conn_backoff_ms = BACKOFF_MIN_MS;
374 self.ratelimit.send();
375
376 let imap_user: &str = lp.user.as_ref();
377 let imap_pw: &str = &self.password;
378
379 let login_res = if self.oauth2 {
380 info!(context, "Logging into IMAP server with OAuth 2.");
381 let addr: &str = self.addr.as_ref();
382
383 let token = get_oauth2_access_token(context, addr, imap_pw, true)
384 .await?
385 .context("IMAP could not get OAUTH token")?;
386 let auth = OAuth2 {
387 user: imap_user.into(),
388 access_token: token,
389 };
390 client.authenticate("XOAUTH2", auth).await
391 } else {
392 info!(context, "Logging into IMAP server with LOGIN.");
393 client.login(imap_user, imap_pw).await
394 };
395
396 match login_res {
397 Ok(mut session) => {
398 let capabilities = determine_capabilities(&mut session).await?;
399 let resync_request_sender = self.resync_request_sender.clone();
400
401 let session = if capabilities.can_compress {
402 info!(context, "Enabling IMAP compression.");
403 let compressed_session = session
404 .compress(|s| {
405 let session_stream: Box<dyn SessionStream> = Box::new(s);
406 session_stream
407 })
408 .await
409 .context("Failed to enable IMAP compression")?;
410 Session::new(
411 compressed_session,
412 capabilities,
413 resync_request_sender,
414 self.transport_id,
415 )
416 } else {
417 Session::new(
418 session,
419 capabilities,
420 resync_request_sender,
421 self.transport_id,
422 )
423 };
424
425 let mut lock = context.server_id.write().await;
427 lock.clone_from(&session.capabilities.server_id);
428
429 self.authentication_failed_once = false;
430 context.emit_event(EventType::ImapConnected(format!(
431 "IMAP-LOGIN as {}",
432 lp.user
433 )));
434 self.connectivity.set_preparing(context);
435 info!(context, "Successfully logged into IMAP server.");
436 return Ok(session);
437 }
438
439 Err(err) => {
440 let imap_user = lp.user.to_owned();
441 let message = stock_str::cannot_login(context, &imap_user).await;
442
443 warn!(context, "IMAP failed to login: {err:#}.");
444 first_error.get_or_insert(format_err!("{message} ({err:#})"));
445
446 let _lock = context.wrong_pw_warning_mutex.lock().await;
448 if err.to_string().to_lowercase().contains("authentication") {
449 if self.authentication_failed_once
450 && !configuring
451 && context.get_config_bool(Config::NotifyAboutWrongPw).await?
452 {
453 let mut msg = Message::new_text(message);
454 if let Err(e) = chat::add_device_msg_with_importance(
455 context,
456 None,
457 Some(&mut msg),
458 true,
459 )
460 .await
461 {
462 warn!(context, "Failed to add device message: {e:#}.");
463 } else {
464 context
465 .set_config_internal(Config::NotifyAboutWrongPw, None)
466 .await
467 .log_err(context)
468 .ok();
469 }
470 } else {
471 self.authentication_failed_once = true;
472 }
473 } else {
474 self.authentication_failed_once = false;
475 }
476 }
477 }
478 }
479
480 Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
481 }
482
483 pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
488 let configuring = false;
489 let mut session = match self.connect(context, configuring).await {
490 Ok(session) => session,
491 Err(err) => {
492 self.connectivity.set_err(context, &err);
493 return Err(err);
494 }
495 };
496
497 let folders_configured = context
498 .sql
499 .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
500 .await?;
501 if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
502 self.configure_folders(context, &mut session).await?;
503 }
504
505 Ok(session)
506 }
507
508 pub async fn fetch_move_delete(
513 &mut self,
514 context: &Context,
515 session: &mut Session,
516 watch_folder: &str,
517 folder_meaning: FolderMeaning,
518 ) -> Result<()> {
519 if !context.sql.is_open().await {
520 bail!("IMAP operation attempted while it is torn down");
522 }
523
524 let msgs_fetched = self
525 .fetch_new_messages(context, session, watch_folder, folder_meaning)
526 .await
527 .context("fetch_new_messages")?;
528 if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
529 context.scheduler.interrupt_ephemeral_task().await;
534 }
535
536 session
537 .move_delete_messages(context, watch_folder)
538 .await
539 .context("move_delete_messages")?;
540
541 Ok(())
542 }
543
544 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 async fn fetch_new_msg_batch(
588 &mut self,
589 context: &Context,
590 session: &mut Session,
591 folder: &str,
592 folder_meaning: FolderMeaning,
593 ) -> Result<(usize, bool)> {
594 let transport_id = self.transport_id;
595 let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
596 let old_uid_next = get_uid_next(context, transport_id, folder).await?;
597 info!(
598 context,
599 "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
600 );
601
602 let uids_to_prefetch = 500;
603 let msgs = session
604 .prefetch(old_uid_next, uids_to_prefetch)
605 .await
606 .context("prefetch")?;
607 let read_cnt = msgs.len();
608
609 let mut uids_fetch: Vec<u32> = Vec::new();
610 let mut available_post_msgs: Vec<String> = Vec::new();
611 let mut download_later: Vec<String> = Vec::new();
612 let mut uid_message_ids = BTreeMap::new();
613 let mut largest_uid_skipped = None;
614 let delete_target = context.get_delete_msgs_target().await?;
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 &delete_target
665 } else {
666 _target = target_folder(context, folder, folder_meaning, &headers).await?;
667 &_target
668 };
669
670 context
671 .sql
672 .execute(
673 "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
674 VALUES (?, ?, ?, ?, ?, ?)
675 ON CONFLICT(transport_id, folder, uid, uidvalidity)
676 DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
677 target=excluded.target",
678 (
679 self.transport_id,
680 &message_id,
681 &folder,
682 uid,
683 uid_validity,
684 target,
685 ),
686 )
687 .await?;
688
689 if folder == target
696 && folder_meaning != FolderMeaning::Spam
701 && prefetch_should_download(
702 context,
703 &headers,
704 &message_id,
705 fetch_response.flags(),
706 )
707 .await.context("prefetch_should_download")?
708 {
709 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 if context.should_delete_to_trash().await? {
992 error!(
993 context,
994 "Cannot move messages {} to {}, no fallback to COPY/DELETE because \
995 delete_to_trash is set. Error: {:#}",
996 set,
997 target,
998 err,
999 );
1000 return Err(err.into());
1001 }
1002 warn!(
1003 context,
1004 "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
1005 set,
1006 target,
1007 err
1008 );
1009 }
1010 }
1011 }
1012
1013 let copy = !context.is_trash(target).await?;
1016 if copy {
1017 info!(
1018 context,
1019 "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1020 );
1021 self.uid_copy(&set, &target).await?;
1022 } else {
1023 error!(
1024 context,
1025 "Server does not support MOVE, fallback to DELETE {} to {}", set, target,
1026 );
1027 }
1028 context
1029 .sql
1030 .transaction(|transaction| {
1031 let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1032 for row_id in row_ids {
1033 stmt.execute((row_id,))?;
1034 }
1035 Ok(())
1036 })
1037 .await
1038 .context("Cannot plan deletion of messages")?;
1039 if copy {
1040 context.emit_event(EventType::ImapMessageMoved(format!(
1041 "IMAP messages {set} copied to {target}"
1042 )));
1043 }
1044 Ok(())
1045 }
1046
1047 async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1051 let transport_id = self.transport_id();
1052 let rows = context
1053 .sql
1054 .query_map_vec(
1055 "SELECT id, uid, target FROM imap
1056 WHERE folder = ?
1057 AND transport_id = ?
1058 AND target != folder
1059 ORDER BY target, uid",
1060 (folder, transport_id),
1061 |row| {
1062 let rowid: i64 = row.get(0)?;
1063 let uid: u32 = row.get(1)?;
1064 let target: String = row.get(2)?;
1065 Ok((rowid, uid, target))
1066 },
1067 )
1068 .await?;
1069
1070 for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1071 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1076 ensure!(folder_exists, "No folder {folder}");
1077
1078 if target.is_empty() {
1080 self.delete_message_batch(context, &uid_set, rowid_set)
1081 .await
1082 .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1083 } else {
1084 self.move_message_batch(context, &uid_set, rowid_set, &target)
1085 .await
1086 .with_context(|| {
1087 format!(
1088 "cannot move batch of messages {:?} to folder {:?}",
1089 &uid_set, target
1090 )
1091 })?;
1092 }
1093 }
1094
1095 if let Err(err) = self.maybe_close_folder(context).await {
1098 warn!(context, "Failed to close folder: {err:#}.");
1099 }
1100
1101 Ok(())
1102 }
1103
1104 pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1106 if context.get_config_bool(Config::TeamProfile).await? {
1107 return Ok(());
1108 }
1109
1110 let transport_id = self.transport_id();
1111 let rows = context
1112 .sql
1113 .query_map_vec(
1114 "SELECT imap.id, uid, folder FROM imap, imap_markseen
1115 WHERE imap.id = imap_markseen.id
1116 AND imap.transport_id=?
1117 AND target = folder
1118 ORDER BY folder, uid",
1119 (transport_id,),
1120 |row| {
1121 let rowid: i64 = row.get(0)?;
1122 let uid: u32 = row.get(1)?;
1123 let folder: String = row.get(2)?;
1124 Ok((rowid, uid, folder))
1125 },
1126 )
1127 .await?;
1128
1129 for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1130 let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
1131 Err(err) => {
1132 warn!(
1133 context,
1134 "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1135 );
1136 continue;
1137 }
1138 Ok(folder_exists) => folder_exists,
1139 };
1140 if !folder_exists {
1141 warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1142 } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1143 warn!(
1144 context,
1145 "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1146 );
1147 continue;
1148 } else {
1149 info!(
1150 context,
1151 "Marked messages {} in folder {} as seen.", uid_set, folder
1152 );
1153 }
1154 context
1155 .sql
1156 .transaction(|transaction| {
1157 let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1158 for rowid in rowid_set {
1159 stmt.execute((rowid,))?;
1160 }
1161 Ok(())
1162 })
1163 .await
1164 .context("Cannot remove messages marked as seen from imap_markseen table")?;
1165 }
1166
1167 Ok(())
1168 }
1169
1170 pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1172 if !self.can_condstore() {
1173 info!(
1174 context,
1175 "Server does not support CONDSTORE, skipping flag synchronization."
1176 );
1177 return Ok(());
1178 }
1179
1180 if context.get_config_bool(Config::TeamProfile).await? {
1181 return Ok(());
1182 }
1183
1184 let folder_exists = self
1185 .select_with_uidvalidity(context, folder)
1186 .await
1187 .context("Failed to select folder")?;
1188 if !folder_exists {
1189 return Ok(());
1190 }
1191
1192 let mailbox = self
1193 .selected_mailbox
1194 .as_ref()
1195 .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1196
1197 if mailbox.highest_modseq.is_none() {
1200 info!(
1201 context,
1202 "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1203 );
1204 return Ok(());
1205 }
1206
1207 let transport_id = self.transport_id();
1208 let mut updated_chat_ids = BTreeSet::new();
1209 let uid_validity = get_uidvalidity(context, transport_id, folder)
1210 .await
1211 .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1212 let mut highest_modseq = get_modseq(context, transport_id, folder)
1213 .await
1214 .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1215 let mut list = self
1216 .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1217 .await
1218 .context("failed to fetch flags")?;
1219
1220 let mut got_unsolicited_fetch = false;
1221
1222 while let Some(fetch) = list
1223 .try_next()
1224 .await
1225 .context("failed to get FETCH result")?
1226 {
1227 let uid = if let Some(uid) = fetch.uid {
1228 uid
1229 } else {
1230 info!(context, "FETCH result contains no UID, skipping");
1231 got_unsolicited_fetch = true;
1232 continue;
1233 };
1234 let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1235 if is_seen
1236 && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1237 .await
1238 .with_context(|| {
1239 format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1240 })?
1241 {
1242 updated_chat_ids.insert(chat_id);
1243 }
1244
1245 if let Some(modseq) = fetch.modseq {
1246 if modseq > highest_modseq {
1247 highest_modseq = modseq;
1248 }
1249 } else {
1250 warn!(context, "FETCH result contains no MODSEQ");
1251 }
1252 }
1253 drop(list);
1254
1255 if got_unsolicited_fetch {
1256 self.new_mail = true;
1261 }
1262
1263 set_modseq(context, transport_id, folder, highest_modseq)
1264 .await
1265 .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1266 if !updated_chat_ids.is_empty() {
1267 context.on_archived_chats_maybe_noticed();
1268 }
1269 for updated_chat_id in updated_chat_ids {
1270 context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1271 chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1272 }
1273
1274 Ok(())
1275 }
1276
1277 pub(crate) async fn fetch_many_msgs(
1292 &mut self,
1293 context: &Context,
1294 folder: &str,
1295 request_uids: Vec<u32>,
1296 uid_message_ids: &BTreeMap<u32, String>,
1297 received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1298 ) -> Result<()> {
1299 if request_uids.is_empty() {
1300 return Ok(());
1301 }
1302
1303 for (request_uids, set) in build_sequence_sets(&request_uids)? {
1304 info!(context, "Starting UID FETCH of message set \"{}\".", set);
1305 let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1306 format!("fetching messages {} from folder \"{}\"", &set, folder)
1307 })?;
1308
1309 let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1312
1313 let mut count = 0;
1314 for &request_uid in &request_uids {
1315 let mut fetch_response = uid_msgs.remove(&request_uid);
1317
1318 while fetch_response.is_none() {
1320 let Some(next_fetch_response) = fetch_responses
1321 .try_next()
1322 .await
1323 .context("Failed to process IMAP FETCH result")?
1324 else {
1325 break;
1327 };
1328
1329 if let Some(next_uid) = next_fetch_response.uid {
1330 if next_uid == request_uid {
1331 fetch_response = Some(next_fetch_response);
1332 } else if !request_uids.contains(&next_uid) {
1333 info!(
1340 context,
1341 "Skipping not requested FETCH response for UID {}.", next_uid
1342 );
1343 } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1344 warn!(context, "Got duplicated UID {}.", next_uid);
1345 }
1346 } else {
1347 info!(context, "Skipping FETCH response without UID.");
1348 }
1349 }
1350
1351 let fetch_response = match fetch_response {
1352 Some(fetch) => fetch,
1353 None => {
1354 warn!(
1355 context,
1356 "Missed UID {} in the server response.", request_uid
1357 );
1358 continue;
1359 }
1360 };
1361 count += 1;
1362
1363 let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1364 let body = fetch_response.body();
1365
1366 if is_deleted {
1367 info!(context, "Not processing deleted msg {}.", request_uid);
1368 received_msgs_channel.send((request_uid, None)).await?;
1369 continue;
1370 }
1371
1372 let body = if let Some(body) = body {
1373 body
1374 } else {
1375 info!(
1376 context,
1377 "Not processing message {} without a BODY.", request_uid
1378 );
1379 received_msgs_channel.send((request_uid, None)).await?;
1380 continue;
1381 };
1382
1383 let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1384
1385 let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1386 error!(
1387 context,
1388 "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1389 request_uid
1390 );
1391 continue;
1392 };
1393
1394 info!(
1395 context,
1396 "Passing message UID {} to receive_imf().", request_uid
1397 );
1398 let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1399 let received_msg = match res {
1400 Err(err) => {
1401 warn!(context, "receive_imf error: {err:#}.");
1402
1403 let text = format!(
1404 "❌ 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/.",
1405 );
1406 let mut msg = Message::new_text(text);
1407 add_device_msg(context, None, Some(&mut msg)).await?;
1408 None
1409 }
1410 Ok(msg) => msg,
1411 };
1412 received_msgs_channel
1413 .send((request_uid, received_msg))
1414 .await?;
1415 }
1416
1417 while fetch_responses
1424 .try_next()
1425 .await
1426 .context("Failed to drain FETCH responses")?
1427 .is_some()
1428 {}
1429
1430 if count != request_uids.len() {
1431 warn!(
1432 context,
1433 "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1434 count,
1435 request_uids.len(),
1436 request_uids,
1437 );
1438 } else {
1439 info!(
1440 context,
1441 "Successfully received {} UIDs.",
1442 request_uids.len()
1443 );
1444 }
1445 }
1446
1447 Ok(())
1448 }
1449
1450 pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1456 let mut lock = context.metadata.write().await;
1457
1458 if !self.can_metadata() {
1459 *lock = Some(Default::default());
1460 }
1461 if let Some(ref mut old_metadata) = *lock {
1462 let now = time();
1463
1464 if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1466 return Ok(());
1467 }
1468
1469 let mut got_turn_server = false;
1470 if self.can_metadata() {
1471 info!(context, "ICE servers expired, requesting new credentials.");
1472 let mailbox = "";
1473 let options = "";
1474 let metadata = self
1475 .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1476 .await?;
1477 for m in metadata {
1478 if m.entry == "/shared/vendor/deltachat/turn"
1479 && let Some(value) = m.value
1480 {
1481 match create_ice_servers_from_metadata(&value).await {
1482 Ok((parsed_timestamp, parsed_ice_servers)) => {
1483 old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1484 old_metadata.ice_servers = parsed_ice_servers;
1485 got_turn_server = true;
1486 }
1487 Err(err) => {
1488 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1489 }
1490 }
1491 }
1492 }
1493 }
1494 if !got_turn_server {
1495 info!(context, "Will use fallback ICE servers.");
1496 old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1498 old_metadata.ice_servers = create_fallback_ice_servers();
1499 }
1500 return Ok(());
1501 }
1502
1503 info!(
1504 context,
1505 "Server supports metadata, retrieving server comment and admin contact."
1506 );
1507
1508 let mut comment = None;
1509 let mut admin = None;
1510 let mut iroh_relay = None;
1511 let mut ice_servers = None;
1512 let mut ice_servers_expiration_timestamp = 0;
1513
1514 let mailbox = "";
1515 let options = "";
1516 let metadata = self
1517 .get_metadata(
1518 mailbox,
1519 options,
1520 "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1521 )
1522 .await?;
1523 for m in metadata {
1524 match m.entry.as_ref() {
1525 "/shared/comment" => {
1526 comment = m.value;
1527 }
1528 "/shared/admin" => {
1529 admin = m.value;
1530 }
1531 "/shared/vendor/deltachat/irohrelay" => {
1532 if let Some(value) = m.value {
1533 if let Ok(url) = Url::parse(&value) {
1534 iroh_relay = Some(url);
1535 } else {
1536 warn!(
1537 context,
1538 "Got invalid URL from iroh relay metadata: {:?}.", value
1539 );
1540 }
1541 }
1542 }
1543 "/shared/vendor/deltachat/turn" => {
1544 if let Some(value) = m.value {
1545 match create_ice_servers_from_metadata(&value).await {
1546 Ok((parsed_timestamp, parsed_ice_servers)) => {
1547 ice_servers_expiration_timestamp = parsed_timestamp;
1548 ice_servers = Some(parsed_ice_servers);
1549 }
1550 Err(err) => {
1551 warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1552 }
1553 }
1554 }
1555 }
1556 _ => {}
1557 }
1558 }
1559 let ice_servers = if let Some(ice_servers) = ice_servers {
1560 ice_servers
1561 } else {
1562 ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1564 create_fallback_ice_servers()
1565 };
1566
1567 *lock = Some(ServerMetadata {
1568 comment,
1569 admin,
1570 iroh_relay,
1571 ice_servers,
1572 ice_servers_expiration_timestamp,
1573 });
1574 Ok(())
1575 }
1576
1577 pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1579 if context.push_subscribed.load(Ordering::Relaxed) {
1580 return Ok(());
1581 }
1582
1583 let Some(device_token) = context.push_subscriber.device_token().await else {
1584 return Ok(());
1585 };
1586
1587 if self.can_metadata() && self.can_push() {
1588 let old_encrypted_device_token =
1589 context.get_config(Config::EncryptedDeviceToken).await?;
1590
1591 let device_token_changed = old_encrypted_device_token.is_none()
1593 || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1594
1595 let new_encrypted_device_token;
1596 if device_token_changed {
1597 let encrypted_device_token = encrypt_device_token(&device_token)
1598 .context("Failed to encrypt device token")?;
1599
1600 let encrypted_device_token_len = encrypted_device_token.len();
1604
1605 context
1611 .set_config_internal(Config::DeviceToken, Some(&device_token))
1612 .await?;
1613 context
1614 .set_config_internal(
1615 Config::EncryptedDeviceToken,
1616 Some(&encrypted_device_token),
1617 )
1618 .await?;
1619
1620 if encrypted_device_token_len <= 4096 {
1621 new_encrypted_device_token = Some(encrypted_device_token);
1622 } else {
1623 warn!(context, "Device token is too long for LITERAL-, ignoring.");
1633 new_encrypted_device_token = None;
1634 }
1635 } else {
1636 new_encrypted_device_token = old_encrypted_device_token;
1637 }
1638
1639 if let Some(encrypted_device_token) = new_encrypted_device_token {
1642 let folder = context
1643 .get_config(Config::ConfiguredInboxFolder)
1644 .await?
1645 .context("INBOX is not configured")?;
1646
1647 self.run_command_and_check_ok(&format_setmetadata(
1648 &folder,
1649 &encrypted_device_token,
1650 ))
1651 .await
1652 .context("SETMETADATA command failed")?;
1653
1654 context.push_subscribed.store(true, Ordering::Relaxed);
1655 }
1656 } else if !context.push_subscriber.heartbeat_subscribed().await {
1657 let context = context.clone();
1658 tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1660 }
1661
1662 Ok(())
1663 }
1664}
1665
1666fn format_setmetadata(folder: &str, device_token: &str) -> String {
1667 let device_token_len = device_token.len();
1668 format!(
1669 "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1670 )
1671}
1672
1673impl Session {
1674 async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1680 if flag == "\\Deleted" {
1681 self.selected_folder_needs_expunge = true;
1682 }
1683 let query = format!("+FLAGS ({flag})");
1684 let mut responses = self
1685 .uid_store(uid_set, &query)
1686 .await
1687 .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1688 while let Some(_response) = responses.try_next().await? {
1689 }
1691 Ok(())
1692 }
1693
1694 async fn configure_mvbox<'a>(
1703 &mut self,
1704 context: &Context,
1705 folders: &[&'a str],
1706 ) -> Result<Option<&'a str>> {
1707 self.maybe_close_folder(context).await?;
1710
1711 for folder in folders {
1712 info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1713 let res = self.examine(&folder).await;
1714 if res.is_ok() {
1715 info!(
1716 context,
1717 "MVBOX-folder {:?} successfully selected, using it.", &folder
1718 );
1719 self.close().await?;
1720 let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1723 ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1724 return Ok(Some(folder));
1725 }
1726 }
1727
1728 Ok(None)
1729 }
1730}
1731
1732impl Imap {
1733 pub(crate) async fn configure_folders(
1734 &mut self,
1735 context: &Context,
1736 session: &mut Session,
1737 ) -> Result<()> {
1738 let mut folders = session
1739 .list(Some(""), Some("*"))
1740 .await
1741 .context("list_folders failed")?;
1742 let mut delimiter = ".".to_string();
1743 let mut delimiter_is_default = true;
1744 let mut folder_configs = BTreeMap::new();
1745
1746 while let Some(folder) = folders.try_next().await? {
1747 info!(context, "Scanning folder: {:?}", folder);
1748
1749 if let Some(d) = folder.delimiter()
1751 && delimiter_is_default
1752 && !d.is_empty()
1753 && delimiter != d
1754 {
1755 delimiter = d.to_string();
1756 delimiter_is_default = false;
1757 }
1758
1759 let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1760 let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1761 if let Some(config) = folder_meaning.to_config() {
1762 folder_configs.insert(config, folder.name().to_string());
1764 } else if let Some(config) = folder_name_meaning.to_config() {
1765 folder_configs
1767 .entry(config)
1768 .or_insert_with(|| folder.name().to_string());
1769 }
1770 }
1771 drop(folders);
1772
1773 info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1774
1775 let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1776 let mvbox_folder = session
1777 .configure_mvbox(context, &["DeltaChat", &fallback_folder])
1778 .await
1779 .context("failed to configure mvbox")?;
1780
1781 context
1782 .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1783 .await?;
1784 if let Some(mvbox_folder) = mvbox_folder {
1785 info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1786 context
1787 .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1788 .await?;
1789 }
1790 for (config, name) in folder_configs {
1791 context.set_config_internal(config, Some(&name)).await?;
1792 }
1793 context
1794 .sql
1795 .set_raw_config_int(
1796 constants::DC_FOLDERS_CONFIGURED_KEY,
1797 constants::DC_FOLDERS_CONFIGURED_VERSION,
1798 )
1799 .await?;
1800
1801 info!(context, "FINISHED configuring IMAP-folders.");
1802 Ok(())
1803 }
1804}
1805
1806impl Session {
1807 fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1816 use UnsolicitedResponse::*;
1817 use async_imap::imap_proto::Response;
1818 use async_imap::imap_proto::ResponseCode;
1819
1820 let folder = self.selected_folder.as_deref().unwrap_or_default();
1821 let mut should_refetch = false;
1822 while let Ok(response) = self.unsolicited_responses.try_recv() {
1823 match response {
1824 Exists(_) => {
1825 info!(
1826 context,
1827 "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1828 );
1829 should_refetch = true;
1830 }
1831
1832 Expunge(_) | Recent(_) => {}
1833 Other(ref response_data) => {
1834 match response_data.parsed() {
1835 Response::Fetch { .. } => {
1836 info!(
1837 context,
1838 "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1839 );
1840 should_refetch = true;
1841 }
1842
1843 Response::Done {
1846 code: Some(ResponseCode::CopyUid(_, _, _)),
1847 ..
1848 } => {}
1849
1850 _ => {
1851 info!(context, "{folder:?}: got unsolicited response {response:?}")
1852 }
1853 }
1854 }
1855 _ => {
1856 info!(context, "{folder:?}: got unsolicited response {response:?}")
1857 }
1858 }
1859 }
1860 Ok(should_refetch)
1861 }
1862}
1863
1864async fn should_move_out_of_spam(
1865 context: &Context,
1866 headers: &[mailparse::MailHeader<'_>],
1867) -> Result<bool> {
1868 if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1869 return Ok(true);
1880 }
1881
1882 if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1883 if msg.chat_blocked != Blocked::Not {
1884 return Ok(false);
1886 }
1887 } else {
1888 let from = match mimeparser::get_from(headers) {
1889 Some(f) => f,
1890 None => return Ok(false),
1891 };
1892 let (from_id, blocked_contact, _origin) =
1894 match from_field_to_contact_id(context, &from, None, true, true)
1895 .await
1896 .context("from_field_to_contact_id")?
1897 {
1898 Some(res) => res,
1899 None => {
1900 warn!(
1901 context,
1902 "Contact with From address {:?} cannot exist, not moving out of spam", from
1903 );
1904 return Ok(false);
1905 }
1906 };
1907 if blocked_contact {
1908 return Ok(false);
1910 }
1911
1912 if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1913 if chat_id_blocked.blocked != Blocked::Not {
1914 return Ok(false);
1915 }
1916 } else if from_id != ContactId::SELF {
1917 return Ok(false);
1919 }
1920 }
1921
1922 Ok(true)
1923}
1924
1925async fn spam_target_folder_cfg(
1930 context: &Context,
1931 headers: &[mailparse::MailHeader<'_>],
1932) -> Result<Option<Config>> {
1933 if !should_move_out_of_spam(context, headers).await? {
1934 return Ok(None);
1935 }
1936
1937 if needs_move_to_mvbox(context, headers).await?
1938 || context.get_config_bool(Config::OnlyFetchMvbox).await?
1941 {
1942 Ok(Some(Config::ConfiguredMvboxFolder))
1943 } else {
1944 Ok(Some(Config::ConfiguredInboxFolder))
1945 }
1946}
1947
1948pub async fn target_folder_cfg(
1951 context: &Context,
1952 folder: &str,
1953 folder_meaning: FolderMeaning,
1954 headers: &[mailparse::MailHeader<'_>],
1955) -> Result<Option<Config>> {
1956 if context.is_mvbox(folder).await? {
1957 return Ok(None);
1958 }
1959
1960 if folder_meaning == FolderMeaning::Spam {
1961 spam_target_folder_cfg(context, headers).await
1962 } else if folder_meaning == FolderMeaning::Inbox
1963 && needs_move_to_mvbox(context, headers).await?
1964 {
1965 Ok(Some(Config::ConfiguredMvboxFolder))
1966 } else {
1967 Ok(None)
1968 }
1969}
1970
1971pub async fn target_folder(
1972 context: &Context,
1973 folder: &str,
1974 folder_meaning: FolderMeaning,
1975 headers: &[mailparse::MailHeader<'_>],
1976) -> Result<String> {
1977 match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1978 Some(config) => match context.get_config(config).await? {
1979 Some(target) => Ok(target),
1980 None => Ok(folder.to_string()),
1981 },
1982 None => Ok(folder.to_string()),
1983 }
1984}
1985
1986async fn needs_move_to_mvbox(
1987 context: &Context,
1988 headers: &[mailparse::MailHeader<'_>],
1989) -> Result<bool> {
1990 let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1991 if !context.get_config_bool(Config::MvboxMove).await? {
1992 return Ok(false);
1993 }
1994
1995 if headers
1996 .get_header_value(HeaderDef::AutocryptSetupMessage)
1997 .is_some()
1998 {
1999 return Ok(false);
2002 }
2003
2004 if has_chat_version {
2005 Ok(true)
2006 } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2007 match parent.is_dc_message {
2008 MessengerMessage::No => Ok(false),
2009 MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2010 }
2011 } else {
2012 Ok(false)
2013 }
2014}
2015
2016fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2023 const SPAM_NAMES: &[&str] = &[
2025 "spam",
2026 "junk",
2027 "Correio electrónico não solicitado",
2028 "Correo basura",
2029 "Lixo",
2030 "Nettsøppel",
2031 "Nevyžádaná pošta",
2032 "No solicitado",
2033 "Ongewenst",
2034 "Posta indesiderata",
2035 "Skräp",
2036 "Wiadomości-śmieci",
2037 "Önemsiz",
2038 "Ανεπιθύμητα",
2039 "Спам",
2040 "垃圾邮件",
2041 "垃圾郵件",
2042 "迷惑メール",
2043 "스팸",
2044 ];
2045 const TRASH_NAMES: &[&str] = &[
2046 "Trash",
2047 "Bin",
2048 "Caixote do lixo",
2049 "Cestino",
2050 "Corbeille",
2051 "Papelera",
2052 "Papierkorb",
2053 "Papirkurv",
2054 "Papperskorgen",
2055 "Prullenbak",
2056 "Rubujo",
2057 "Κάδος απορριμμάτων",
2058 "Корзина",
2059 "Кошик",
2060 "ゴミ箱",
2061 "垃圾桶",
2062 "已删除邮件",
2063 "휴지통",
2064 ];
2065 let lower = folder_name.to_lowercase();
2066
2067 if lower == "inbox" {
2068 FolderMeaning::Inbox
2069 } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2070 FolderMeaning::Spam
2071 } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2072 FolderMeaning::Trash
2073 } else {
2074 FolderMeaning::Unknown
2075 }
2076}
2077
2078fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2079 for attr in folder_attrs {
2080 match attr {
2081 NameAttribute::Trash => return FolderMeaning::Trash,
2082 NameAttribute::Junk => return FolderMeaning::Spam,
2083 NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2084 NameAttribute::Extension(label) => {
2085 match label.as_ref() {
2086 "\\Spam" => return FolderMeaning::Spam,
2087 "\\Important" => return FolderMeaning::Virtual,
2088 _ => {}
2089 };
2090 }
2091 _ => {}
2092 }
2093 }
2094 FolderMeaning::Unknown
2095}
2096
2097pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2098 match get_folder_meaning_by_attrs(folder.attributes()) {
2099 FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2100 meaning => meaning,
2101 }
2102}
2103
2104fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2106 match prefetch_msg.header() {
2107 Some(header_bytes) => {
2108 let (headers, _) = mailparse::parse_headers(header_bytes)?;
2109 Ok(headers)
2110 }
2111 None => Ok(Vec::new()),
2112 }
2113}
2114
2115pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2116 headers
2117 .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2118 .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2119 .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2120}
2121
2122pub(crate) fn create_message_id() -> String {
2123 format!("{}{}", GENERATED_PREFIX, create_id())
2124}
2125
2126pub(crate) async fn prefetch_should_download(
2128 context: &Context,
2129 headers: &[mailparse::MailHeader<'_>],
2130 message_id: &str,
2131 mut flags: impl Iterator<Item = Flag<'_>>,
2132) -> Result<bool> {
2133 if message::rfc724_mid_download_tried(context, message_id).await? {
2134 if let Some(from) = mimeparser::get_from(headers)
2135 && context.is_self_addr(&from.addr).await?
2136 {
2137 markseen_on_imap_table(context, message_id).await?;
2138 }
2139 return Ok(false);
2140 }
2141
2142 let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2146 let from = from.to_ascii_lowercase();
2147 from.contains("mailer-daemon") || from.contains("mail-daemon")
2148 } else {
2149 false
2150 };
2151
2152 let is_autocrypt_setup_message = headers
2154 .get_header_value(HeaderDef::AutocryptSetupMessage)
2155 .is_some();
2156
2157 let from = match mimeparser::get_from(headers) {
2158 Some(f) => f,
2159 None => return Ok(false),
2160 };
2161 let (_from_id, blocked_contact, origin) =
2162 match from_field_to_contact_id(context, &from, None, true, true).await? {
2163 Some(res) => res,
2164 None => return Ok(false),
2165 };
2166 if flags.any(|f| f == Flag::Draft) {
2170 info!(context, "Ignoring draft message");
2171 return Ok(false);
2172 }
2173
2174 let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2175 let accepted_contact = origin.is_known();
2176 let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2177 .await?
2178 .map(|parent| match parent.is_dc_message {
2179 MessengerMessage::No => false,
2180 MessengerMessage::Yes | MessengerMessage::Reply => true,
2181 })
2182 .unwrap_or_default();
2183
2184 let show_emails =
2185 ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2186
2187 let show = is_autocrypt_setup_message
2188 || match show_emails {
2189 ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2190 ShowEmails::AcceptedContacts => {
2191 is_chat_message || is_reply_to_chat_message || accepted_contact
2192 }
2193 ShowEmails::All => true,
2194 };
2195
2196 let should_download = (show && !blocked_contact) || maybe_ndn;
2197 Ok(should_download)
2198}
2199
2200async fn mark_seen_by_uid(
2204 context: &Context,
2205 transport_id: u32,
2206 folder: &str,
2207 uid_validity: u32,
2208 uid: u32,
2209) -> Result<Option<ChatId>> {
2210 if let Some((msg_id, chat_id)) = context
2211 .sql
2212 .query_row_optional(
2213 "SELECT id, chat_id FROM msgs
2214 WHERE id > 9 AND rfc724_mid IN (
2215 SELECT rfc724_mid FROM imap
2216 WHERE transport_id=?
2217 AND folder=?
2218 AND uidvalidity=?
2219 AND uid=?
2220 LIMIT 1
2221 )",
2222 (transport_id, &folder, uid_validity, uid),
2223 |row| {
2224 let msg_id: MsgId = row.get(0)?;
2225 let chat_id: ChatId = row.get(1)?;
2226 Ok((msg_id, chat_id))
2227 },
2228 )
2229 .await
2230 .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2231 {
2232 let updated = context
2233 .sql
2234 .execute(
2235 "UPDATE msgs SET state=?1
2236 WHERE (state=?2 OR state=?3)
2237 AND id=?4",
2238 (
2239 MessageState::InSeen,
2240 MessageState::InFresh,
2241 MessageState::InNoticed,
2242 msg_id,
2243 ),
2244 )
2245 .await
2246 .with_context(|| format!("failed to update msg {msg_id} state"))?
2247 > 0;
2248
2249 if updated {
2250 msg_id
2251 .start_ephemeral_timer(context)
2252 .await
2253 .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2254 Ok(Some(chat_id))
2255 } else {
2256 Ok(None)
2258 }
2259 } else {
2260 Ok(None)
2262 }
2263}
2264
2265pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2268 context
2269 .sql
2270 .execute(
2271 "INSERT OR IGNORE INTO imap_markseen (id)
2272 SELECT id FROM imap WHERE rfc724_mid=?",
2273 (message_id,),
2274 )
2275 .await?;
2276 context.scheduler.interrupt_inbox().await;
2277
2278 Ok(())
2279}
2280
2281pub(crate) async fn set_uid_next(
2285 context: &Context,
2286 transport_id: u32,
2287 folder: &str,
2288 uid_next: u32,
2289) -> Result<()> {
2290 context
2291 .sql
2292 .execute(
2293 "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2294 ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2295 (transport_id, folder, uid_next),
2296 )
2297 .await?;
2298 Ok(())
2299}
2300
2301async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2307 Ok(context
2308 .sql
2309 .query_get_value(
2310 "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2311 (transport_id, folder),
2312 )
2313 .await?
2314 .unwrap_or(0))
2315}
2316
2317pub(crate) async fn set_uidvalidity(
2318 context: &Context,
2319 transport_id: u32,
2320 folder: &str,
2321 uidvalidity: u32,
2322) -> Result<()> {
2323 context
2324 .sql
2325 .execute(
2326 "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2327 ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2328 (transport_id, folder, uidvalidity),
2329 )
2330 .await?;
2331 Ok(())
2332}
2333
2334async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2335 Ok(context
2336 .sql
2337 .query_get_value(
2338 "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2339 (transport_id, folder),
2340 )
2341 .await?
2342 .unwrap_or(0))
2343}
2344
2345pub(crate) async fn set_modseq(
2346 context: &Context,
2347 transport_id: u32,
2348 folder: &str,
2349 modseq: u64,
2350) -> Result<()> {
2351 context
2352 .sql
2353 .execute(
2354 "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2355 ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2356 (transport_id, folder, modseq),
2357 )
2358 .await?;
2359 Ok(())
2360}
2361
2362async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2363 Ok(context
2364 .sql
2365 .query_get_value(
2366 "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2367 (transport_id, folder),
2368 )
2369 .await?
2370 .unwrap_or(0))
2371}
2372
2373async fn should_ignore_folder(
2378 context: &Context,
2379 folder: &str,
2380 folder_meaning: FolderMeaning,
2381) -> Result<bool> {
2382 if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2383 return Ok(false);
2384 }
2385 Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2386}
2387
2388fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2392 let mut ranges: Vec<UidRange> = vec![];
2394
2395 for ¤t in uids {
2396 if let Some(last) = ranges.last_mut()
2397 && last.end + 1 == current
2398 {
2399 last.end = current;
2400 continue;
2401 }
2402
2403 ranges.push(UidRange {
2404 start: current,
2405 end: current,
2406 });
2407 }
2408
2409 let mut result = vec![];
2411 let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2412 for range in ranges {
2413 last_uids.reserve((range.end - range.start + 1).try_into()?);
2414 (range.start..=range.end).for_each(|u| last_uids.push(u));
2415 if !last_str.is_empty() {
2416 last_str.push(',');
2417 }
2418 last_str.push_str(&range.to_string());
2419
2420 if last_str.len() > 990 {
2421 result.push((take(&mut last_uids), take(&mut last_str)));
2422 }
2423 }
2424 result.push((last_uids, last_str));
2425
2426 result.retain(|(_, s)| !s.is_empty());
2427 Ok(result)
2428}
2429
2430struct UidRange {
2431 start: u32,
2432 end: u32,
2433 }
2435
2436impl std::fmt::Display for UidRange {
2437 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2438 if self.start == self.end {
2439 write!(f, "{}", self.start)
2440 } else {
2441 write!(f, "{}:{}", self.start, self.end)
2442 }
2443 }
2444}
2445
2446#[cfg(test)]
2447mod imap_tests;