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