deltachat/
imap.rs

1//! # IMAP handling module.
2//!
3//! uses [async-email/async-imap](https://github.com/async-email/async-imap)
4//! to implement connect, fetch, delete functionality with standard IMAP servers.
5
6use std::{
7    cmp::max,
8    cmp::min,
9    collections::{BTreeMap, BTreeSet, HashMap},
10    iter::Peekable,
11    mem::take,
12    sync::atomic::Ordering,
13    time::{Duration, UNIX_EPOCH},
14};
15
16use anyhow::{Context as _, Result, bail, ensure, format_err};
17use async_channel::{self, Receiver, Sender};
18use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
19use futures::{FutureExt as _, TryStreamExt};
20use futures_lite::FutureExt;
21use num_traits::FromPrimitive;
22use ratelimit::Ratelimit;
23use url::Url;
24
25use crate::calls::{
26    UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
27};
28use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
29use crate::chatlist_events;
30use crate::config::Config;
31use crate::constants::{self, Blocked, DC_VERSION_STR, ShowEmails};
32use crate::contact::ContactId;
33use crate::context::Context;
34use crate::events::EventType;
35use crate::headerdef::{HeaderDef, HeaderDefMap};
36use crate::log::{LogExt, warn};
37use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
38use crate::mimeparser;
39use crate::net::proxy::ProxyConfig;
40use crate::net::session::SessionStream;
41use crate::oauth2::get_oauth2_access_token;
42use crate::push::encrypt_device_token;
43use crate::receive_imf::{
44    ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
45};
46use crate::scheduler::connectivity::ConnectivityStore;
47use crate::stock_str;
48use crate::tools::{self, create_id, duration_to_str, time};
49use crate::transport::{
50    ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
51};
52
53pub(crate) mod capabilities;
54mod client;
55mod idle;
56pub mod select_folder;
57pub(crate) mod session;
58
59use client::{Client, determine_capabilities};
60use session::Session;
61
62pub(crate) const GENERATED_PREFIX: &str = "GEN_";
63
64const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
65                             MESSAGE-ID \
66                             X-MICROSOFT-ORIGINAL-MESSAGE-ID\
67                             )])";
68const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
69
70#[derive(Debug)]
71pub(crate) struct Imap {
72    /// ID of the transport configuration in the `transports` table.
73    ///
74    /// This ID is used to namespace records in the `imap` table.
75    transport_id: u32,
76
77    pub(crate) idle_interrupt_receiver: Receiver<()>,
78
79    /// Email address.
80    pub(crate) addr: String,
81
82    /// Login parameters.
83    lp: Vec<ConfiguredServerLoginParam>,
84
85    /// Password.
86    password: String,
87
88    /// Proxy configuration.
89    proxy_config: Option<ProxyConfig>,
90
91    strict_tls: bool,
92
93    oauth2: bool,
94
95    authentication_failed_once: bool,
96
97    pub(crate) connectivity: ConnectivityStore,
98
99    conn_last_try: tools::Time,
100    conn_backoff_ms: u64,
101
102    /// Rate limit for successful IMAP connections.
103    ///
104    /// This rate limit prevents busy loop in case the server refuses logins
105    /// or in case connection gets dropped over and over due to IMAP bug,
106    /// e.g. the server returning invalid response to SELECT command
107    /// immediately after logging in or returning an error in response to LOGIN command
108    /// due to internal server error.
109    ratelimit: Ratelimit,
110
111    /// IMAP UID resync request sender.
112    pub(crate) resync_request_sender: async_channel::Sender<()>,
113
114    /// IMAP UID resync request receiver.
115    pub(crate) resync_request_receiver: async_channel::Receiver<()>,
116}
117
118#[derive(Debug)]
119struct OAuth2 {
120    user: String,
121    access_token: String,
122}
123
124#[derive(Debug, Default)]
125pub(crate) struct ServerMetadata {
126    /// IMAP METADATA `/shared/comment` as defined in
127    /// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
128    pub comment: Option<String>,
129
130    /// IMAP METADATA `/shared/admin` as defined in
131    /// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2>.
132    pub admin: Option<String>,
133
134    pub iroh_relay: Option<Url>,
135
136    /// ICE servers for WebRTC calls.
137    pub ice_servers: Vec<UnresolvedIceServer>,
138
139    /// Timestamp when ICE servers are considered
140    /// expired and should be updated.
141    ///
142    /// If ICE servers are about to expire, new TURN credentials
143    /// should be fetched from the server
144    /// to be ready for WebRTC calls.
145    pub ice_servers_expiration_timestamp: i64,
146}
147
148impl async_imap::Authenticator for OAuth2 {
149    type Response = String;
150
151    fn process(&mut self, _data: &[u8]) -> Self::Response {
152        format!(
153            "user={}\x01auth=Bearer {}\x01\x01",
154            self.user, self.access_token
155        )
156    }
157}
158
159#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
160pub enum FolderMeaning {
161    Unknown,
162
163    /// Spam folder.
164    Spam,
165    Inbox,
166    Mvbox,
167    Trash,
168
169    /// Virtual folders.
170    ///
171    /// On Gmail there are virtual folders marked as \\All, \\Important and \\Flagged.
172    /// Delta Chat ignores these folders because the same messages can be fetched
173    /// from the real folder and the result of moving and deleting messages via
174    /// virtual folder is unclear.
175    Virtual,
176}
177
178impl FolderMeaning {
179    pub fn to_config(self) -> Option<Config> {
180        match self {
181            FolderMeaning::Unknown => None,
182            FolderMeaning::Spam => None,
183            FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
184            FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
185            FolderMeaning::Trash => None,
186            FolderMeaning::Virtual => None,
187        }
188    }
189}
190
191struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
192    inner: Peekable<T>,
193}
194
195impl<T, I> From<I> for UidGrouper<T>
196where
197    T: Iterator<Item = (i64, u32, String)>,
198    I: IntoIterator<IntoIter = T>,
199{
200    fn from(inner: I) -> Self {
201        Self {
202            inner: inner.into_iter().peekable(),
203        }
204    }
205}
206
207impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
208    // Tuple of folder, row IDs, and UID range as a string.
209    type Item = (String, Vec<i64>, String);
210
211    fn next(&mut self) -> Option<Self::Item> {
212        let (_, _, folder) = self.inner.peek().cloned()?;
213
214        let mut uid_set = String::new();
215        let mut rowid_set = Vec::new();
216
217        while uid_set.len() < 1000 {
218            // Construct a new range.
219            if let Some((start_rowid, start_uid, _)) = self
220                .inner
221                .next_if(|(_, _, start_folder)| start_folder == &folder)
222            {
223                rowid_set.push(start_rowid);
224                let mut end_uid = start_uid;
225
226                while let Some((next_rowid, next_uid, _)) =
227                    self.inner.next_if(|(_, next_uid, next_folder)| {
228                        next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
229                    })
230                {
231                    end_uid = next_uid;
232                    rowid_set.push(next_rowid);
233                }
234
235                let uid_range = UidRange {
236                    start: start_uid,
237                    end: end_uid,
238                };
239                if !uid_set.is_empty() {
240                    uid_set.push(',');
241                }
242                uid_set.push_str(&uid_range.to_string());
243            } else {
244                break;
245            }
246        }
247
248        Some((folder, rowid_set, uid_set))
249    }
250}
251
252impl Imap {
253    /// Creates new disconnected IMAP client using the specific login parameters.
254    pub async fn new(
255        context: &Context,
256        transport_id: u32,
257        param: ConfiguredLoginParam,
258        idle_interrupt_receiver: Receiver<()>,
259    ) -> Result<Self> {
260        let lp = param.imap.clone();
261        let password = param.imap_password.clone();
262        let proxy_config = ProxyConfig::load(context).await?;
263        let addr = &param.addr;
264        let strict_tls = param.strict_tls(proxy_config.is_some());
265        let oauth2 = param.oauth2;
266        let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
267        Ok(Imap {
268            transport_id,
269            idle_interrupt_receiver,
270            addr: addr.to_string(),
271            lp,
272            password,
273            proxy_config,
274            strict_tls,
275            oauth2,
276            authentication_failed_once: false,
277            connectivity: Default::default(),
278            conn_last_try: UNIX_EPOCH,
279            conn_backoff_ms: 0,
280            // 1 connection per minute + a burst of 2.
281            ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
282            resync_request_sender,
283            resync_request_receiver,
284        })
285    }
286
287    /// Creates new disconnected IMAP client using configured parameters.
288    pub async fn new_configured(
289        context: &Context,
290        idle_interrupt_receiver: Receiver<()>,
291    ) -> Result<Self> {
292        let (transport_id, param) = ConfiguredLoginParam::load(context)
293            .await?
294            .context("Not configured")?;
295        let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
296        Ok(imap)
297    }
298
299    /// Connects to IMAP server and returns a new IMAP session.
300    ///
301    /// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
302    /// instead if you are going to actually use connection rather than trying connection
303    /// parameters.
304    pub(crate) async fn connect(
305        &mut self,
306        context: &Context,
307        configuring: bool,
308    ) -> Result<Session> {
309        let now = tools::Time::now();
310        let until_can_send = max(
311            min(self.conn_last_try, now)
312                .checked_add(Duration::from_millis(self.conn_backoff_ms))
313                .unwrap_or(now),
314            now,
315        )
316        .duration_since(now)?;
317        let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
318        if !ratelimit_duration.is_zero() {
319            warn!(
320                context,
321                "IMAP got rate limited, waiting for {} until can connect.",
322                duration_to_str(ratelimit_duration),
323            );
324            let interrupted = async {
325                tokio::time::sleep(ratelimit_duration).await;
326                false
327            }
328            .race(self.idle_interrupt_receiver.recv().map(|_| true))
329            .await;
330            if interrupted {
331                info!(
332                    context,
333                    "Connecting to IMAP without waiting for ratelimit due to interrupt."
334                );
335            }
336        }
337
338        info!(context, "Connecting to IMAP server.");
339        self.connectivity.set_connecting(context);
340
341        self.conn_last_try = tools::Time::now();
342        const BACKOFF_MIN_MS: u64 = 2000;
343        const BACKOFF_MAX_MS: u64 = 80_000;
344        self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
345        self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
346            (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
347        ));
348        self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
349
350        let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
351        let mut first_error = None;
352        for lp in login_params {
353            info!(context, "IMAP trying to connect to {}.", &lp.connection);
354            let connection_candidate = lp.connection.clone();
355            let client = match Client::connect(
356                context,
357                self.proxy_config.clone(),
358                self.strict_tls,
359                connection_candidate,
360            )
361            .await
362            .context("IMAP failed to connect")
363            {
364                Ok(client) => client,
365                Err(err) => {
366                    warn!(context, "{err:#}.");
367                    first_error.get_or_insert(err);
368                    continue;
369                }
370            };
371
372            self.conn_backoff_ms = BACKOFF_MIN_MS;
373            self.ratelimit.send();
374
375            let imap_user: &str = lp.user.as_ref();
376            let imap_pw: &str = &self.password;
377
378            let login_res = if self.oauth2 {
379                info!(context, "Logging into IMAP server with OAuth 2.");
380                let addr: &str = self.addr.as_ref();
381
382                let token = get_oauth2_access_token(context, addr, imap_pw, true)
383                    .await?
384                    .context("IMAP could not get OAUTH token")?;
385                let auth = OAuth2 {
386                    user: imap_user.into(),
387                    access_token: token,
388                };
389                client.authenticate("XOAUTH2", auth).await
390            } else {
391                info!(context, "Logging into IMAP server with LOGIN.");
392                client.login(imap_user, imap_pw).await
393            };
394
395            match login_res {
396                Ok(mut session) => {
397                    let capabilities = determine_capabilities(&mut session).await?;
398                    let resync_request_sender = self.resync_request_sender.clone();
399
400                    let session = if capabilities.can_compress {
401                        info!(context, "Enabling IMAP compression.");
402                        let compressed_session = session
403                            .compress(|s| {
404                                let session_stream: Box<dyn SessionStream> = Box::new(s);
405                                session_stream
406                            })
407                            .await
408                            .context("Failed to enable IMAP compression")?;
409                        Session::new(
410                            compressed_session,
411                            capabilities,
412                            resync_request_sender,
413                            self.transport_id,
414                        )
415                    } else {
416                        Session::new(
417                            session,
418                            capabilities,
419                            resync_request_sender,
420                            self.transport_id,
421                        )
422                    };
423
424                    // Store server ID in the context to display in account info.
425                    let mut lock = context.server_id.write().await;
426                    lock.clone_from(&session.capabilities.server_id);
427
428                    self.authentication_failed_once = false;
429                    context.emit_event(EventType::ImapConnected(format!(
430                        "IMAP-LOGIN as {}",
431                        lp.user
432                    )));
433                    self.connectivity.set_preparing(context);
434                    info!(context, "Successfully logged into IMAP server.");
435                    return Ok(session);
436                }
437
438                Err(err) => {
439                    let imap_user = lp.user.to_owned();
440                    let message = stock_str::cannot_login(context, &imap_user).await;
441
442                    warn!(context, "IMAP failed to login: {err:#}.");
443                    first_error.get_or_insert(format_err!("{message} ({err:#})"));
444
445                    // If it looks like the password is wrong, send a notification:
446                    let _lock = context.wrong_pw_warning_mutex.lock().await;
447                    if err.to_string().to_lowercase().contains("authentication") {
448                        if self.authentication_failed_once
449                            && !configuring
450                            && context.get_config_bool(Config::NotifyAboutWrongPw).await?
451                        {
452                            let mut msg = Message::new_text(message);
453                            if let Err(e) = chat::add_device_msg_with_importance(
454                                context,
455                                None,
456                                Some(&mut msg),
457                                true,
458                            )
459                            .await
460                            {
461                                warn!(context, "Failed to add device message: {e:#}.");
462                            } else {
463                                context
464                                    .set_config_internal(Config::NotifyAboutWrongPw, None)
465                                    .await
466                                    .log_err(context)
467                                    .ok();
468                            }
469                        } else {
470                            self.authentication_failed_once = true;
471                        }
472                    } else {
473                        self.authentication_failed_once = false;
474                    }
475                }
476            }
477        }
478
479        Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
480    }
481
482    /// Prepare a new IMAP session.
483    ///
484    /// This creates a new IMAP connection and ensures
485    /// that folders are created and IMAP capabilities are determined.
486    pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
487        let configuring = false;
488        let mut session = match self.connect(context, configuring).await {
489            Ok(session) => session,
490            Err(err) => {
491                self.connectivity.set_err(context, &err);
492                return Err(err);
493            }
494        };
495
496        let folders_configured = context
497            .sql
498            .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
499            .await?;
500        if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
501            self.configure_folders(context, &mut session).await?;
502        }
503
504        Ok(session)
505    }
506
507    /// FETCH-MOVE-DELETE iteration.
508    ///
509    /// Prefetches headers and downloads new message from the folder, moves messages away from the
510    /// folder and deletes messages in the folder.
511    pub async fn fetch_move_delete(
512        &mut self,
513        context: &Context,
514        session: &mut Session,
515        watch_folder: &str,
516        folder_meaning: FolderMeaning,
517    ) -> Result<()> {
518        if !context.sql.is_open().await {
519            // probably shutdown
520            bail!("IMAP operation attempted while it is torn down");
521        }
522
523        let msgs_fetched = self
524            .fetch_new_messages(context, session, watch_folder, folder_meaning)
525            .await
526            .context("fetch_new_messages")?;
527        if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
528            // New messages were fetched and shall be deleted later, restart ephemeral loop.
529            // Note that the `Config::DeleteDeviceAfter` timer starts as soon as the messages are
530            // fetched while the per-chat ephemeral timers start as soon as the messages are marked
531            // as noticed.
532            context.scheduler.interrupt_ephemeral_task().await;
533        }
534
535        session
536            .move_delete_messages(context, watch_folder)
537            .await
538            .context("move_delete_messages")?;
539
540        Ok(())
541    }
542
543    /// Fetches new messages.
544    ///
545    /// Returns true if at least one message was fetched.
546    pub(crate) async fn fetch_new_messages(
547        &mut self,
548        context: &Context,
549        session: &mut Session,
550        folder: &str,
551        folder_meaning: FolderMeaning,
552    ) -> Result<bool> {
553        if should_ignore_folder(context, folder, folder_meaning).await? {
554            info!(context, "Not fetching from {folder:?}.");
555            session.new_mail = false;
556            return Ok(false);
557        }
558
559        let folder_exists = session
560            .select_with_uidvalidity(context, folder)
561            .await
562            .with_context(|| format!("Failed to select folder {folder:?}"))?;
563        if !folder_exists {
564            return Ok(false);
565        }
566
567        if !session.new_mail {
568            info!(context, "No new emails in folder {folder:?}.");
569            return Ok(false);
570        }
571        session.new_mail = false;
572
573        let mut read_cnt = 0;
574        loop {
575            let (n, fetch_more) = self
576                .fetch_new_msg_batch(context, session, folder, folder_meaning)
577                .await?;
578            read_cnt += n;
579            if !fetch_more {
580                return Ok(read_cnt > 0);
581            }
582        }
583    }
584
585    /// Returns number of messages processed and whether the function should be called again.
586    async fn fetch_new_msg_batch(
587        &mut self,
588        context: &Context,
589        session: &mut Session,
590        folder: &str,
591        folder_meaning: FolderMeaning,
592    ) -> Result<(usize, bool)> {
593        let transport_id = self.transport_id;
594        let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
595        let old_uid_next = get_uid_next(context, transport_id, folder).await?;
596        info!(
597            context,
598            "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
599        );
600
601        let uids_to_prefetch = 500;
602        let msgs = session
603            .prefetch(old_uid_next, uids_to_prefetch)
604            .await
605            .context("prefetch")?;
606        let read_cnt = msgs.len();
607
608        let mut uids_fetch: Vec<u32> = Vec::new();
609        let mut available_post_msgs: Vec<String> = Vec::new();
610        let mut download_later: Vec<String> = Vec::new();
611        let mut uid_message_ids = BTreeMap::new();
612        let mut largest_uid_skipped = None;
613
614        let download_limit: Option<u32> = context
615            .get_config_parsed(Config::DownloadLimit)
616            .await?
617            .filter(|&l| 0 < l);
618
619        // Store the info about IMAP messages in the database.
620        for (uid, ref fetch_response) in msgs {
621            let headers = match get_fetch_headers(fetch_response) {
622                Ok(headers) => headers,
623                Err(err) => {
624                    warn!(context, "Failed to parse FETCH headers: {err:#}.");
625                    continue;
626                }
627            };
628
629            let message_id = prefetch_get_message_id(&headers);
630            let size = fetch_response
631                .size
632                .context("imap fetch response does not contain size")?;
633
634            // Determine the target folder where the message should be moved to.
635            //
636            // We only move the messages from the INBOX and Spam folders.
637            // This is required to avoid infinite MOVE loop on IMAP servers
638            // that alias `DeltaChat` folder to other names.
639            // For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
640            // In this case moving from `INBOX.DeltaChat` to `DeltaChat`
641            // results in the messages getting a new UID,
642            // so the messages will be detected as new
643            // in the `INBOX.DeltaChat` folder again.
644            let delete = if let Some(message_id) = &message_id {
645                message::rfc724_mid_exists_ex(context, message_id, "deleted=1")
646                    .await?
647                    .is_some_and(|(_msg_id, deleted)| deleted)
648            } else {
649                false
650            };
651
652            // Generate a fake Message-ID to identify the message in the database
653            // if the message has no real Message-ID.
654            let message_id = message_id.unwrap_or_else(create_message_id);
655
656            if delete {
657                info!(context, "Deleting locally deleted message {message_id}.");
658            }
659
660            let _target;
661            let target = if delete {
662                ""
663            } else {
664                _target = target_folder(context, folder, folder_meaning, &headers).await?;
665                &_target
666            };
667
668            context
669                .sql
670                .execute(
671                    "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
672                       VALUES         (?,            ?,          ?,      ?,   ?,           ?)
673                       ON CONFLICT(transport_id, folder, uid, uidvalidity)
674                       DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
675                                     target=excluded.target",
676                    (
677                        self.transport_id,
678                        &message_id,
679                        &folder,
680                        uid,
681                        uid_validity,
682                        target,
683                    ),
684                )
685                .await?;
686
687            // Download only the messages which have reached their target folder if there are
688            // multiple devices. This prevents race conditions in multidevice case, where one
689            // device tries to download the message while another device moves the message at the
690            // same time. Even in single device case it is possible to fail downloading the first
691            // message, move it to the movebox and then download the second message before
692            // downloading the first one, if downloading from inbox before moving is allowed.
693            if folder == target
694                // Never download messages directly from the spam folder.
695                // If the sender is known, the message will be moved to the Inbox or Mvbox
696                // and then we download the message from there.
697                // Also see `spam_target_folder_cfg()`.
698                && folder_meaning != FolderMeaning::Spam
699                && prefetch_should_download(
700                    context,
701                    &headers,
702                    &message_id,
703                    fetch_response.flags(),
704                )
705                .await.context("prefetch_should_download")?
706            {
707                if headers
708                    .get_header_value(HeaderDef::ChatIsPostMessage)
709                    .is_some()
710                {
711                    info!(context, "{message_id:?} is a post-message.");
712                    available_post_msgs.push(message_id.clone());
713
714                    if download_limit.is_none_or(|download_limit| size <= download_limit) {
715                        download_later.push(message_id.clone());
716                    }
717                    largest_uid_skipped = Some(uid);
718                } else {
719                    info!(context, "{message_id:?} is not a post-message.");
720                    if download_limit.is_none_or(|download_limit| size <= download_limit) {
721                        uids_fetch.push(uid);
722                        uid_message_ids.insert(uid, message_id);
723                    } else {
724                        download_later.push(message_id.clone());
725                        largest_uid_skipped = Some(uid);
726                    }
727                };
728            } else {
729                largest_uid_skipped = Some(uid);
730            }
731        }
732
733        if !uids_fetch.is_empty() {
734            self.connectivity.set_working(context);
735        }
736
737        let (sender, receiver) = async_channel::unbounded();
738
739        let mut received_msgs = Vec::with_capacity(uids_fetch.len());
740        let mailbox_uid_next = session
741            .selected_mailbox
742            .as_ref()
743            .with_context(|| format!("Expected {folder:?} to be selected"))?
744            .uid_next
745            .unwrap_or_default();
746
747        let update_uids_future = async {
748            let mut largest_uid_fetched: u32 = 0;
749
750            while let Ok((uid, received_msg_opt)) = receiver.recv().await {
751                largest_uid_fetched = max(largest_uid_fetched, uid);
752                if let Some(received_msg) = received_msg_opt {
753                    received_msgs.push(received_msg)
754                }
755            }
756
757            largest_uid_fetched
758        };
759
760        let actually_download_messages_future = async {
761            session
762                .fetch_many_msgs(context, folder, uids_fetch, &uid_message_ids, sender)
763                .await
764                .context("fetch_many_msgs")
765        };
766
767        let (largest_uid_fetched, fetch_res) =
768            tokio::join!(update_uids_future, actually_download_messages_future);
769
770        // Advance uid_next to the largest fetched UID plus 1.
771        //
772        // This may be larger than `mailbox_uid_next`
773        // if the message has arrived after selecting mailbox
774        // and determining its UIDNEXT and before prefetch.
775        let mut new_uid_next = largest_uid_fetched + 1;
776        let fetch_more = fetch_res.is_ok() && {
777            let prefetch_uid_next = old_uid_next + uids_to_prefetch;
778            // If we have successfully fetched all messages we planned during prefetch,
779            // then we have covered at least the range between old UIDNEXT
780            // and UIDNEXT of the mailbox at the time of selecting it.
781            new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
782
783            new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
784
785            prefetch_uid_next < mailbox_uid_next
786        };
787        if new_uid_next > old_uid_next {
788            set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
789        }
790
791        info!(context, "{} mails read from \"{}\".", read_cnt, folder);
792
793        if !received_msgs.is_empty() {
794            context.emit_event(EventType::IncomingMsgBunch);
795        }
796
797        chat::mark_old_messages_as_noticed(context, received_msgs).await?;
798
799        if fetch_res.is_ok() {
800            info!(
801                context,
802                "available_post_msgs: {}, download_later: {}.",
803                available_post_msgs.len(),
804                download_later.len(),
805            );
806            let trans_fn = |t: &mut rusqlite::Transaction| {
807                let mut stmt = t.prepare("INSERT OR IGNORE INTO available_post_msgs VALUES (?)")?;
808                for rfc724_mid in available_post_msgs {
809                    stmt.execute((rfc724_mid,))
810                        .context("INSERT OR IGNORE INTO available_post_msgs")?;
811                }
812                let mut stmt =
813                    t.prepare("INSERT OR IGNORE INTO download (rfc724_mid, msg_id) VALUES (?,0)")?;
814                for rfc724_mid in download_later {
815                    stmt.execute((rfc724_mid,))
816                        .context("INSERT OR IGNORE INTO download")?;
817                }
818                Ok(())
819            };
820            context.sql.transaction(trans_fn).await?;
821        }
822
823        // Now fail if fetching failed, so we will
824        // establish a new session if this one is broken.
825        fetch_res?;
826
827        Ok((read_cnt, fetch_more))
828    }
829}
830
831impl Session {
832    /// Synchronizes UIDs for all folders.
833    pub(crate) async fn resync_folders(&mut self, context: &Context) -> Result<()> {
834        let all_folders = self
835            .list_folders()
836            .await
837            .context("listing folders for resync")?;
838        for folder in all_folders {
839            let folder_meaning = get_folder_meaning(&folder);
840            if !matches!(
841                folder_meaning,
842                FolderMeaning::Virtual | FolderMeaning::Unknown
843            ) {
844                self.resync_folder_uids(context, folder.name(), folder_meaning)
845                    .await?;
846            }
847        }
848        Ok(())
849    }
850
851    /// Synchronizes UIDs in the database with UIDs on the server.
852    ///
853    /// It is assumed that no operations are taking place on the same
854    /// folder at the moment. Make sure to run it in the same
855    /// thread/task as other network operations on this folder to
856    /// avoid race conditions.
857    pub(crate) async fn resync_folder_uids(
858        &mut self,
859        context: &Context,
860        folder: &str,
861        folder_meaning: FolderMeaning,
862    ) -> Result<()> {
863        let uid_validity;
864        // Collect pairs of UID and Message-ID.
865        let mut msgs = BTreeMap::new();
866
867        let folder_exists = self.select_with_uidvalidity(context, folder).await?;
868        let transport_id = self.transport_id();
869        if folder_exists {
870            let mut list = self
871                .uid_fetch("1:*", RFC724MID_UID)
872                .await
873                .with_context(|| format!("Can't resync folder {folder}"))?;
874            while let Some(fetch) = list.try_next().await? {
875                let headers = match get_fetch_headers(&fetch) {
876                    Ok(headers) => headers,
877                    Err(err) => {
878                        warn!(context, "Failed to parse FETCH headers: {}", err);
879                        continue;
880                    }
881                };
882                let message_id = prefetch_get_message_id(&headers);
883
884                if let (Some(uid), Some(rfc724_mid)) = (fetch.uid, message_id) {
885                    msgs.insert(
886                        uid,
887                        (
888                            rfc724_mid,
889                            target_folder(context, folder, folder_meaning, &headers).await?,
890                        ),
891                    );
892                }
893            }
894
895            info!(
896                context,
897                "resync_folder_uids: Collected {} message IDs in {folder}.",
898                msgs.len(),
899            );
900
901            uid_validity = get_uidvalidity(context, transport_id, folder).await?;
902        } else {
903            warn!(context, "resync_folder_uids: No folder {folder}.");
904            uid_validity = 0;
905        }
906
907        // Write collected UIDs to SQLite database.
908        context
909            .sql
910            .transaction(move |transaction| {
911                transaction.execute("DELETE FROM imap WHERE transport_id=? AND folder=?", (transport_id, folder,))?;
912                for (uid, (rfc724_mid, target)) in &msgs {
913                    // This may detect previously undetected moved
914                    // messages, so we update server_folder too.
915                    transaction.execute(
916                        "INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
917                         VALUES           (?,            ?,          ?,      ?,   ?,           ?)
918                         ON CONFLICT(transport_id, folder, uid, uidvalidity)
919                         DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
920                                       target=excluded.target",
921                        (transport_id, rfc724_mid, folder, uid, uid_validity, target),
922                    )?;
923                }
924                Ok(())
925            })
926            .await?;
927        Ok(())
928    }
929
930    /// Deletes batch of messages identified by their UID from the currently
931    /// selected folder.
932    async fn delete_message_batch(
933        &mut self,
934        context: &Context,
935        uid_set: &str,
936        row_ids: Vec<i64>,
937    ) -> Result<()> {
938        // mark the message for deletion
939        self.add_flag_finalized_with_set(uid_set, "\\Deleted")
940            .await?;
941        context
942            .sql
943            .transaction(|transaction| {
944                let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
945                for row_id in row_ids {
946                    stmt.execute((row_id,))?;
947                }
948                Ok(())
949            })
950            .await
951            .context("Cannot remove deleted messages from imap table")?;
952
953        context.emit_event(EventType::ImapMessageDeleted(format!(
954            "IMAP messages {uid_set} marked as deleted"
955        )));
956        Ok(())
957    }
958
959    /// Moves batch of messages identified by their UID from the currently
960    /// selected folder to the target folder.
961    async fn move_message_batch(
962        &mut self,
963        context: &Context,
964        set: &str,
965        row_ids: Vec<i64>,
966        target: &str,
967    ) -> Result<()> {
968        if self.can_move() {
969            match self.uid_mv(set, &target).await {
970                Ok(()) => {
971                    // Messages are moved or don't exist, IMAP returns OK response in both cases.
972                    context
973                        .sql
974                        .transaction(|transaction| {
975                            let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
976                            for row_id in row_ids {
977                                stmt.execute((row_id,))?;
978                            }
979                            Ok(())
980                        })
981                        .await
982                        .context("Cannot delete moved messages from imap table")?;
983                    context.emit_event(EventType::ImapMessageMoved(format!(
984                        "IMAP messages {set} moved to {target}"
985                    )));
986                    return Ok(());
987                }
988                Err(err) => {
989                    warn!(
990                        context,
991                        "Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
992                        set,
993                        target,
994                        err
995                    );
996                }
997            }
998        }
999
1000        // Server does not support MOVE or MOVE failed.
1001        // Copy messages to the destination folder if needed and mark records for deletion.
1002        info!(
1003            context,
1004            "Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
1005        );
1006        self.uid_copy(&set, &target).await?;
1007        context
1008            .sql
1009            .transaction(|transaction| {
1010                let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
1011                for row_id in row_ids {
1012                    stmt.execute((row_id,))?;
1013                }
1014                Ok(())
1015            })
1016            .await
1017            .context("Cannot plan deletion of messages")?;
1018        context.emit_event(EventType::ImapMessageMoved(format!(
1019            "IMAP messages {set} copied to {target}"
1020        )));
1021        Ok(())
1022    }
1023
1024    /// Moves and deletes messages as planned in the `imap` table.
1025    ///
1026    /// This is the only place where messages are moved or deleted on the IMAP server.
1027    async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
1028        let transport_id = self.transport_id();
1029        let rows = context
1030            .sql
1031            .query_map_vec(
1032                "SELECT id, uid, target FROM imap
1033                 WHERE folder = ?
1034                 AND transport_id = ?
1035                 AND target != folder
1036                 ORDER BY target, uid",
1037                (folder, transport_id),
1038                |row| {
1039                    let rowid: i64 = row.get(0)?;
1040                    let uid: u32 = row.get(1)?;
1041                    let target: String = row.get(2)?;
1042                    Ok((rowid, uid, target))
1043                },
1044            )
1045            .await?;
1046
1047        for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
1048            // Select folder inside the loop to avoid selecting it if there are no pending
1049            // MOVE/DELETE operations. This does not result in multiple SELECT commands
1050            // being sent because `select_folder()` does nothing if the folder is already
1051            // selected.
1052            let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1053            ensure!(folder_exists, "No folder {folder}");
1054
1055            // Empty target folder name means messages should be deleted.
1056            if target.is_empty() {
1057                self.delete_message_batch(context, &uid_set, rowid_set)
1058                    .await
1059                    .with_context(|| format!("cannot delete batch of messages {:?}", &uid_set))?;
1060            } else {
1061                self.move_message_batch(context, &uid_set, rowid_set, &target)
1062                    .await
1063                    .with_context(|| {
1064                        format!(
1065                            "cannot move batch of messages {:?} to folder {:?}",
1066                            &uid_set, target
1067                        )
1068                    })?;
1069            }
1070        }
1071
1072        // Expunge folder if needed, e.g. if some jobs have
1073        // deleted messages on the server.
1074        if let Err(err) = self.maybe_close_folder(context).await {
1075            warn!(context, "Failed to close folder: {err:#}.");
1076        }
1077
1078        Ok(())
1079    }
1080
1081    /// Stores pending `\Seen` flags for messages in `imap_markseen` table.
1082    pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
1083        if context.get_config_bool(Config::TeamProfile).await? {
1084            return Ok(());
1085        }
1086
1087        let transport_id = self.transport_id();
1088        let rows = context
1089            .sql
1090            .query_map_vec(
1091                "SELECT imap.id, uid, folder FROM imap, imap_markseen
1092                 WHERE imap.id = imap_markseen.id
1093                 AND imap.transport_id=?
1094                 AND target = folder
1095                 ORDER BY folder, uid",
1096                (transport_id,),
1097                |row| {
1098                    let rowid: i64 = row.get(0)?;
1099                    let uid: u32 = row.get(1)?;
1100                    let folder: String = row.get(2)?;
1101                    Ok((rowid, uid, folder))
1102                },
1103            )
1104            .await?;
1105
1106        for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1107            let folder_exists = match self.select_with_uidvalidity(context, &folder).await {
1108                Err(err) => {
1109                    warn!(
1110                        context,
1111                        "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1112                    );
1113                    continue;
1114                }
1115                Ok(folder_exists) => folder_exists,
1116            };
1117            if !folder_exists {
1118                warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1119            } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1120                warn!(
1121                    context,
1122                    "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1123                );
1124                continue;
1125            } else {
1126                info!(
1127                    context,
1128                    "Marked messages {} in folder {} as seen.", uid_set, folder
1129                );
1130            }
1131            context
1132                .sql
1133                .transaction(|transaction| {
1134                    let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1135                    for rowid in rowid_set {
1136                        stmt.execute((rowid,))?;
1137                    }
1138                    Ok(())
1139                })
1140                .await
1141                .context("Cannot remove messages marked as seen from imap_markseen table")?;
1142        }
1143
1144        Ok(())
1145    }
1146
1147    /// Synchronizes `\Seen` flags using `CONDSTORE` extension.
1148    pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1149        if !self.can_condstore() {
1150            info!(
1151                context,
1152                "Server does not support CONDSTORE, skipping flag synchronization."
1153            );
1154            return Ok(());
1155        }
1156
1157        if context.get_config_bool(Config::TeamProfile).await? {
1158            return Ok(());
1159        }
1160
1161        let folder_exists = self
1162            .select_with_uidvalidity(context, folder)
1163            .await
1164            .context("Failed to select folder")?;
1165        if !folder_exists {
1166            return Ok(());
1167        }
1168
1169        let mailbox = self
1170            .selected_mailbox
1171            .as_ref()
1172            .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1173
1174        // Check if the mailbox supports MODSEQ.
1175        // We are not interested in actual value of HIGHESTMODSEQ.
1176        if mailbox.highest_modseq.is_none() {
1177            info!(
1178                context,
1179                "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1180            );
1181            return Ok(());
1182        }
1183
1184        let transport_id = self.transport_id();
1185        let mut updated_chat_ids = BTreeSet::new();
1186        let uid_validity = get_uidvalidity(context, transport_id, folder)
1187            .await
1188            .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1189        let mut highest_modseq = get_modseq(context, transport_id, folder)
1190            .await
1191            .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1192        let mut list = self
1193            .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1194            .await
1195            .context("failed to fetch flags")?;
1196
1197        let mut got_unsolicited_fetch = false;
1198
1199        while let Some(fetch) = list
1200            .try_next()
1201            .await
1202            .context("failed to get FETCH result")?
1203        {
1204            let uid = if let Some(uid) = fetch.uid {
1205                uid
1206            } else {
1207                info!(context, "FETCH result contains no UID, skipping");
1208                got_unsolicited_fetch = true;
1209                continue;
1210            };
1211            let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1212            if is_seen
1213                && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1214                    .await
1215                    .with_context(|| {
1216                        format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1217                    })?
1218            {
1219                updated_chat_ids.insert(chat_id);
1220            }
1221
1222            if let Some(modseq) = fetch.modseq {
1223                if modseq > highest_modseq {
1224                    highest_modseq = modseq;
1225                }
1226            } else {
1227                warn!(context, "FETCH result contains no MODSEQ");
1228            }
1229        }
1230        drop(list);
1231
1232        if got_unsolicited_fetch {
1233            // We got unsolicited FETCH, which means some flags
1234            // have been modified while our request was in progress.
1235            // We may or may not have these new flags as a part of the response,
1236            // so better skip next IDLE and do another round of flag synchronization.
1237            self.new_mail = true;
1238        }
1239
1240        set_modseq(context, transport_id, folder, highest_modseq)
1241            .await
1242            .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1243        if !updated_chat_ids.is_empty() {
1244            context.on_archived_chats_maybe_noticed();
1245        }
1246        for updated_chat_id in updated_chat_ids {
1247            context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1248            chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1249        }
1250
1251        Ok(())
1252    }
1253
1254    /// Fetches a list of messages by server UID.
1255    ///
1256    /// Sends pairs of UID and info about each downloaded message to the provided channel.
1257    /// Received message info is optional because UID may be ignored
1258    /// if the message has a `\Deleted` flag.
1259    ///
1260    /// The channel is used to return the results because the function may fail
1261    /// due to network errors before it finishes fetching all the messages.
1262    /// In this case caller still may want to process all the results
1263    /// received over the channel and persist last seen UID in the database
1264    /// before bubbling up the failure.
1265    ///
1266    /// If the message is incorrect or there is a failure to write a message to the database,
1267    /// it is skipped and the error is logged.
1268    pub(crate) async fn fetch_many_msgs(
1269        &mut self,
1270        context: &Context,
1271        folder: &str,
1272        request_uids: Vec<u32>,
1273        uid_message_ids: &BTreeMap<u32, String>,
1274        received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1275    ) -> Result<()> {
1276        if request_uids.is_empty() {
1277            return Ok(());
1278        }
1279
1280        for (request_uids, set) in build_sequence_sets(&request_uids)? {
1281            info!(context, "Starting UID FETCH of message set \"{}\".", set);
1282            let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1283                format!("fetching messages {} from folder \"{}\"", &set, folder)
1284            })?;
1285
1286            // Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
1287            // when we want to process other messages first.
1288            let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1289
1290            let mut count = 0;
1291            for &request_uid in &request_uids {
1292                // Check if FETCH response is already in `uid_msgs`.
1293                let mut fetch_response = uid_msgs.remove(&request_uid);
1294
1295                // Try to find a requested UID in returned FETCH responses.
1296                while fetch_response.is_none() {
1297                    let Some(next_fetch_response) = fetch_responses
1298                        .try_next()
1299                        .await
1300                        .context("Failed to process IMAP FETCH result")?
1301                    else {
1302                        // No more FETCH responses received from the server.
1303                        break;
1304                    };
1305
1306                    if let Some(next_uid) = next_fetch_response.uid {
1307                        if next_uid == request_uid {
1308                            fetch_response = Some(next_fetch_response);
1309                        } else if !request_uids.contains(&next_uid) {
1310                            // (size of `request_uids` is bounded by IMAP command length limit,
1311                            // search in this vector is always fast)
1312
1313                            // Unwanted UIDs are possible because of unsolicited responses, e.g. if
1314                            // another client changes \Seen flag on a message after we do a prefetch but
1315                            // before fetch. It's not an error if we receive such unsolicited response.
1316                            info!(
1317                                context,
1318                                "Skipping not requested FETCH response for UID {}.", next_uid
1319                            );
1320                        } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1321                            warn!(context, "Got duplicated UID {}.", next_uid);
1322                        }
1323                    } else {
1324                        info!(context, "Skipping FETCH response without UID.");
1325                    }
1326                }
1327
1328                let fetch_response = match fetch_response {
1329                    Some(fetch) => fetch,
1330                    None => {
1331                        warn!(
1332                            context,
1333                            "Missed UID {} in the server response.", request_uid
1334                        );
1335                        continue;
1336                    }
1337                };
1338                count += 1;
1339
1340                let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1341                let body = fetch_response.body();
1342
1343                if is_deleted {
1344                    info!(context, "Not processing deleted msg {}.", request_uid);
1345                    received_msgs_channel.send((request_uid, None)).await?;
1346                    continue;
1347                }
1348
1349                let body = if let Some(body) = body {
1350                    body
1351                } else {
1352                    info!(
1353                        context,
1354                        "Not processing message {} without a BODY.", request_uid
1355                    );
1356                    received_msgs_channel.send((request_uid, None)).await?;
1357                    continue;
1358                };
1359
1360                let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1361
1362                let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1363                    error!(
1364                        context,
1365                        "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1366                        request_uid
1367                    );
1368                    continue;
1369                };
1370
1371                info!(
1372                    context,
1373                    "Passing message UID {} to receive_imf().", request_uid
1374                );
1375                let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1376                let received_msg = match res {
1377                    Err(err) => {
1378                        warn!(context, "receive_imf error: {err:#}.");
1379
1380                        let text = format!(
1381                            "❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
1382                        );
1383                        let mut msg = Message::new_text(text);
1384                        add_device_msg(context, None, Some(&mut msg)).await?;
1385                        None
1386                    }
1387                    Ok(msg) => msg,
1388                };
1389                received_msgs_channel
1390                    .send((request_uid, received_msg))
1391                    .await?;
1392            }
1393
1394            // If we don't process the whole response, IMAP client is left in a broken state where
1395            // it will try to process the rest of response as the next response.
1396            //
1397            // Make sure to not ignore the errors, because
1398            // if connection times out, it will return
1399            // infinite stream of `Some(Err(_))` results.
1400            while fetch_responses
1401                .try_next()
1402                .await
1403                .context("Failed to drain FETCH responses")?
1404                .is_some()
1405            {}
1406
1407            if count != request_uids.len() {
1408                warn!(
1409                    context,
1410                    "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1411                    count,
1412                    request_uids.len(),
1413                    request_uids,
1414                );
1415            } else {
1416                info!(
1417                    context,
1418                    "Successfully received {} UIDs.",
1419                    request_uids.len()
1420                );
1421            }
1422        }
1423
1424        Ok(())
1425    }
1426
1427    /// Retrieves server metadata if it is supported, otherwise uses fallback one.
1428    ///
1429    /// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
1430    /// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
1431    /// metadata.
1432    pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1433        let mut lock = context.metadata.write().await;
1434
1435        if !self.can_metadata() {
1436            *lock = Some(Default::default());
1437        }
1438        if let Some(ref mut old_metadata) = *lock {
1439            let now = time();
1440
1441            // Refresh TURN server credentials if they expire in 12 hours.
1442            if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1443                return Ok(());
1444            }
1445
1446            let mut got_turn_server = false;
1447            if self.can_metadata() {
1448                info!(context, "ICE servers expired, requesting new credentials.");
1449                let mailbox = "";
1450                let options = "";
1451                let metadata = self
1452                    .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1453                    .await?;
1454                for m in metadata {
1455                    if m.entry == "/shared/vendor/deltachat/turn"
1456                        && let Some(value) = m.value
1457                    {
1458                        match create_ice_servers_from_metadata(&value).await {
1459                            Ok((parsed_timestamp, parsed_ice_servers)) => {
1460                                old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1461                                old_metadata.ice_servers = parsed_ice_servers;
1462                                got_turn_server = true;
1463                            }
1464                            Err(err) => {
1465                                warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1466                            }
1467                        }
1468                    }
1469                }
1470            }
1471            if !got_turn_server {
1472                info!(context, "Will use fallback ICE servers.");
1473                // Set expiration timestamp 7 days in the future so we don't request it again.
1474                old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1475                old_metadata.ice_servers = create_fallback_ice_servers();
1476            }
1477            return Ok(());
1478        }
1479
1480        info!(
1481            context,
1482            "Server supports metadata, retrieving server comment and admin contact."
1483        );
1484
1485        let mut comment = None;
1486        let mut admin = None;
1487        let mut iroh_relay = None;
1488        let mut ice_servers = None;
1489        let mut ice_servers_expiration_timestamp = 0;
1490
1491        let mailbox = "";
1492        let options = "";
1493        let metadata = self
1494            .get_metadata(
1495                mailbox,
1496                options,
1497                "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1498            )
1499            .await?;
1500        for m in metadata {
1501            match m.entry.as_ref() {
1502                "/shared/comment" => {
1503                    comment = m.value;
1504                }
1505                "/shared/admin" => {
1506                    admin = m.value;
1507                }
1508                "/shared/vendor/deltachat/irohrelay" => {
1509                    if let Some(value) = m.value {
1510                        if let Ok(url) = Url::parse(&value) {
1511                            iroh_relay = Some(url);
1512                        } else {
1513                            warn!(
1514                                context,
1515                                "Got invalid URL from iroh relay metadata: {:?}.", value
1516                            );
1517                        }
1518                    }
1519                }
1520                "/shared/vendor/deltachat/turn" => {
1521                    if let Some(value) = m.value {
1522                        match create_ice_servers_from_metadata(&value).await {
1523                            Ok((parsed_timestamp, parsed_ice_servers)) => {
1524                                ice_servers_expiration_timestamp = parsed_timestamp;
1525                                ice_servers = Some(parsed_ice_servers);
1526                            }
1527                            Err(err) => {
1528                                warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1529                            }
1530                        }
1531                    }
1532                }
1533                _ => {}
1534            }
1535        }
1536        let ice_servers = if let Some(ice_servers) = ice_servers {
1537            ice_servers
1538        } else {
1539            // Set expiration timestamp 7 days in the future so we don't request it again.
1540            ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1541            create_fallback_ice_servers()
1542        };
1543
1544        *lock = Some(ServerMetadata {
1545            comment,
1546            admin,
1547            iroh_relay,
1548            ice_servers,
1549            ice_servers_expiration_timestamp,
1550        });
1551        Ok(())
1552    }
1553
1554    /// Stores device token into /private/devicetoken IMAP METADATA of the Inbox.
1555    pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1556        if context.push_subscribed.load(Ordering::Relaxed) {
1557            return Ok(());
1558        }
1559
1560        let Some(device_token) = context.push_subscriber.device_token().await else {
1561            return Ok(());
1562        };
1563
1564        if self.can_metadata() && self.can_push() {
1565            let old_encrypted_device_token =
1566                context.get_config(Config::EncryptedDeviceToken).await?;
1567
1568            // Whether we need to update encrypted device token.
1569            let device_token_changed = old_encrypted_device_token.is_none()
1570                || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1571
1572            let new_encrypted_device_token;
1573            if device_token_changed {
1574                let encrypted_device_token = encrypt_device_token(&device_token)
1575                    .context("Failed to encrypt device token")?;
1576
1577                // We expect that the server supporting `XDELTAPUSH` capability
1578                // has non-synchronizing literals support as well:
1579                // <https://www.rfc-editor.org/rfc/rfc7888>.
1580                let encrypted_device_token_len = encrypted_device_token.len();
1581
1582                // Store device token saved on the server
1583                // to prevent storing duplicate tokens.
1584                // The server cannot deduplicate on its own
1585                // because encryption gives a different
1586                // result each time.
1587                context
1588                    .set_config_internal(Config::DeviceToken, Some(&device_token))
1589                    .await?;
1590                context
1591                    .set_config_internal(
1592                        Config::EncryptedDeviceToken,
1593                        Some(&encrypted_device_token),
1594                    )
1595                    .await?;
1596
1597                if encrypted_device_token_len <= 4096 {
1598                    new_encrypted_device_token = Some(encrypted_device_token);
1599                } else {
1600                    // If Apple or Google (FCM) gives us a very large token,
1601                    // do not even try to give it to IMAP servers.
1602                    //
1603                    // Limit of 4096 is arbitrarily selected
1604                    // to be the same as required by LITERAL- IMAP extension.
1605                    //
1606                    // Dovecot supports LITERAL+ and non-synchronizing literals
1607                    // of any length, but there is no reason for tokens
1608                    // to be that large even after OpenPGP encryption.
1609                    warn!(context, "Device token is too long for LITERAL-, ignoring.");
1610                    new_encrypted_device_token = None;
1611                }
1612            } else {
1613                new_encrypted_device_token = old_encrypted_device_token;
1614            }
1615
1616            // Store new encrypted device token on the server
1617            // even if it is the same as the old one.
1618            if let Some(encrypted_device_token) = new_encrypted_device_token {
1619                let folder = context
1620                    .get_config(Config::ConfiguredInboxFolder)
1621                    .await?
1622                    .context("INBOX is not configured")?;
1623
1624                self.run_command_and_check_ok(&format_setmetadata(
1625                    &folder,
1626                    &encrypted_device_token,
1627                ))
1628                .await
1629                .context("SETMETADATA command failed")?;
1630
1631                context.push_subscribed.store(true, Ordering::Relaxed);
1632            }
1633        } else if !context.push_subscriber.heartbeat_subscribed().await {
1634            let context = context.clone();
1635            // Subscribe for heartbeat notifications.
1636            tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1637        }
1638
1639        Ok(())
1640    }
1641}
1642
1643fn format_setmetadata(folder: &str, device_token: &str) -> String {
1644    let device_token_len = device_token.len();
1645    format!(
1646        "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1647    )
1648}
1649
1650impl Session {
1651    /// Returns success if we successfully set the flag or we otherwise
1652    /// think add_flag should not be retried: Disconnection during setting
1653    /// the flag, or other imap-errors, returns Ok as well.
1654    ///
1655    /// Returning error means that the operation can be retried.
1656    async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1657        if flag == "\\Deleted" {
1658            self.selected_folder_needs_expunge = true;
1659        }
1660        let query = format!("+FLAGS ({flag})");
1661        let mut responses = self
1662            .uid_store(uid_set, &query)
1663            .await
1664            .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1665        while let Some(_response) = responses.try_next().await? {
1666            // Read all the responses
1667        }
1668        Ok(())
1669    }
1670
1671    /// Attempts to configure mvbox.
1672    ///
1673    /// Tries to find any folder examining `folders` in the order they go.
1674    /// This method does not use LIST command to ensure that
1675    /// configuration works even if mailbox lookup is forbidden via Access Control List (see
1676    /// <https://datatracker.ietf.org/doc/html/rfc4314>).
1677    ///
1678    /// Returns first found folder name.
1679    async fn configure_mvbox<'a>(
1680        &mut self,
1681        context: &Context,
1682        folders: &[&'a str],
1683    ) -> Result<Option<&'a str>> {
1684        // Close currently selected folder if needed.
1685        // We are going to select folders using low-level EXAMINE operations below.
1686        self.maybe_close_folder(context).await?;
1687
1688        for folder in folders {
1689            info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1690            let res = self.examine(&folder).await;
1691            if res.is_ok() {
1692                info!(
1693                    context,
1694                    "MVBOX-folder {:?} successfully selected, using it.", &folder
1695                );
1696                self.close().await?;
1697                // Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
1698                // emails moved before that wouldn't be fetched but considered "old" instead.
1699                let folder_exists = self.select_with_uidvalidity(context, folder).await?;
1700                ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1701                return Ok(Some(folder));
1702            }
1703        }
1704
1705        Ok(None)
1706    }
1707}
1708
1709impl Imap {
1710    pub(crate) async fn configure_folders(
1711        &mut self,
1712        context: &Context,
1713        session: &mut Session,
1714    ) -> Result<()> {
1715        let mut folders = session
1716            .list(Some(""), Some("*"))
1717            .await
1718            .context("list_folders failed")?;
1719        let mut delimiter = ".".to_string();
1720        let mut delimiter_is_default = true;
1721        let mut folder_configs = BTreeMap::new();
1722
1723        while let Some(folder) = folders.try_next().await? {
1724            info!(context, "Scanning folder: {:?}", folder);
1725
1726            // Update the delimiter iff there is a different one, but only once.
1727            if let Some(d) = folder.delimiter()
1728                && delimiter_is_default
1729                && !d.is_empty()
1730                && delimiter != d
1731            {
1732                delimiter = d.to_string();
1733                delimiter_is_default = false;
1734            }
1735
1736            let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1737            let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1738            if let Some(config) = folder_meaning.to_config() {
1739                // Always takes precedence
1740                folder_configs.insert(config, folder.name().to_string());
1741            } else if let Some(config) = folder_name_meaning.to_config() {
1742                // only set if none has been already set
1743                folder_configs
1744                    .entry(config)
1745                    .or_insert_with(|| folder.name().to_string());
1746            }
1747        }
1748        drop(folders);
1749
1750        info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1751
1752        let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1753        let mvbox_folder = session
1754            .configure_mvbox(context, &["DeltaChat", &fallback_folder])
1755            .await
1756            .context("failed to configure mvbox")?;
1757
1758        context
1759            .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1760            .await?;
1761        if let Some(mvbox_folder) = mvbox_folder {
1762            info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1763            context
1764                .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1765                .await?;
1766        }
1767        for (config, name) in folder_configs {
1768            context.set_config_internal(config, Some(&name)).await?;
1769        }
1770        context
1771            .sql
1772            .set_raw_config_int(
1773                constants::DC_FOLDERS_CONFIGURED_KEY,
1774                constants::DC_FOLDERS_CONFIGURED_VERSION,
1775            )
1776            .await?;
1777
1778        info!(context, "FINISHED configuring IMAP-folders.");
1779        Ok(())
1780    }
1781}
1782
1783impl Session {
1784    /// Return whether the server sent an unsolicited EXISTS or FETCH response.
1785    ///
1786    /// Drains all responses from `session.unsolicited_responses` in the process.
1787    ///
1788    /// If this returns `true`, this means that new emails arrived
1789    /// or flags have been changed.
1790    /// In this case we may want to skip next IDLE and do a round
1791    /// of fetching new messages and synchronizing seen flags.
1792    fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1793        use UnsolicitedResponse::*;
1794        use async_imap::imap_proto::Response;
1795        use async_imap::imap_proto::ResponseCode;
1796
1797        let folder = self.selected_folder.as_deref().unwrap_or_default();
1798        let mut should_refetch = false;
1799        while let Ok(response) = self.unsolicited_responses.try_recv() {
1800            match response {
1801                Exists(_) => {
1802                    info!(
1803                        context,
1804                        "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1805                    );
1806                    should_refetch = true;
1807                }
1808
1809                Expunge(_) | Recent(_) => {}
1810                Other(ref response_data) => {
1811                    match response_data.parsed() {
1812                        Response::Fetch { .. } => {
1813                            info!(
1814                                context,
1815                                "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1816                            );
1817                            should_refetch = true;
1818                        }
1819
1820                        // We are not interested in the following responses and they are are
1821                        // sent quite frequently, so, we ignore them without logging them.
1822                        Response::Done {
1823                            code: Some(ResponseCode::CopyUid(_, _, _)),
1824                            ..
1825                        } => {}
1826
1827                        _ => {
1828                            info!(context, "{folder:?}: got unsolicited response {response:?}")
1829                        }
1830                    }
1831                }
1832                _ => {
1833                    info!(context, "{folder:?}: got unsolicited response {response:?}")
1834                }
1835            }
1836        }
1837        Ok(should_refetch)
1838    }
1839}
1840
1841async fn should_move_out_of_spam(
1842    context: &Context,
1843    headers: &[mailparse::MailHeader<'_>],
1844) -> Result<bool> {
1845    if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1846        // If this is a chat message (i.e. has a ChatVersion header), then this might be
1847        // a securejoin message. We can't find out at this point as we didn't prefetch
1848        // the SecureJoin header. So, we always move chat messages out of Spam.
1849        // Two possibilities to change this would be:
1850        // 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
1851        // `fetch_new_messages()`, and then let `receive_imf()` check
1852        // if it's a spam message and should be hidden.
1853        // 2. Or add a flag to the ChatVersion header that this is a securejoin
1854        // request, and return `true` here only if the message has this flag.
1855        // `receive_imf()` can then check if the securejoin request is valid.
1856        return Ok(true);
1857    }
1858
1859    if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1860        if msg.chat_blocked != Blocked::Not {
1861            // Blocked or contact request message in the spam folder, leave it there.
1862            return Ok(false);
1863        }
1864    } else {
1865        let from = match mimeparser::get_from(headers) {
1866            Some(f) => f,
1867            None => return Ok(false),
1868        };
1869        // No chat found.
1870        let (from_id, blocked_contact, _origin) =
1871            match from_field_to_contact_id(context, &from, None, true, true)
1872                .await
1873                .context("from_field_to_contact_id")?
1874            {
1875                Some(res) => res,
1876                None => {
1877                    warn!(
1878                        context,
1879                        "Contact with From address {:?} cannot exist, not moving out of spam", from
1880                    );
1881                    return Ok(false);
1882                }
1883            };
1884        if blocked_contact {
1885            // Contact is blocked, leave the message in spam.
1886            return Ok(false);
1887        }
1888
1889        if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
1890            if chat_id_blocked.blocked != Blocked::Not {
1891                return Ok(false);
1892            }
1893        } else if from_id != ContactId::SELF {
1894            // No chat with this contact found.
1895            return Ok(false);
1896        }
1897    }
1898
1899    Ok(true)
1900}
1901
1902/// Returns target folder for a message found in the Spam folder.
1903/// If this returns None, the message will not be moved out of the
1904/// Spam folder, and as `fetch_new_messages()` doesn't download
1905/// messages from the Spam folder, the message will be ignored.
1906async fn spam_target_folder_cfg(
1907    context: &Context,
1908    headers: &[mailparse::MailHeader<'_>],
1909) -> Result<Option<Config>> {
1910    if !should_move_out_of_spam(context, headers).await? {
1911        return Ok(None);
1912    }
1913
1914    if needs_move_to_mvbox(context, headers).await?
1915        // If OnlyFetchMvbox is set, we don't want to move the message to
1916        // the inbox where we wouldn't fetch it again:
1917        || context.get_config_bool(Config::OnlyFetchMvbox).await?
1918    {
1919        Ok(Some(Config::ConfiguredMvboxFolder))
1920    } else {
1921        Ok(Some(Config::ConfiguredInboxFolder))
1922    }
1923}
1924
1925/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
1926/// the message needs to be moved from `folder`. Otherwise returns `None`.
1927pub async fn target_folder_cfg(
1928    context: &Context,
1929    folder: &str,
1930    folder_meaning: FolderMeaning,
1931    headers: &[mailparse::MailHeader<'_>],
1932) -> Result<Option<Config>> {
1933    if context.is_mvbox(folder).await? {
1934        return Ok(None);
1935    }
1936
1937    if folder_meaning == FolderMeaning::Spam {
1938        spam_target_folder_cfg(context, headers).await
1939    } else if folder_meaning == FolderMeaning::Inbox
1940        && needs_move_to_mvbox(context, headers).await?
1941    {
1942        Ok(Some(Config::ConfiguredMvboxFolder))
1943    } else {
1944        Ok(None)
1945    }
1946}
1947
1948pub async fn target_folder(
1949    context: &Context,
1950    folder: &str,
1951    folder_meaning: FolderMeaning,
1952    headers: &[mailparse::MailHeader<'_>],
1953) -> Result<String> {
1954    match target_folder_cfg(context, folder, folder_meaning, headers).await? {
1955        Some(config) => match context.get_config(config).await? {
1956            Some(target) => Ok(target),
1957            None => Ok(folder.to_string()),
1958        },
1959        None => Ok(folder.to_string()),
1960    }
1961}
1962
1963async fn needs_move_to_mvbox(
1964    context: &Context,
1965    headers: &[mailparse::MailHeader<'_>],
1966) -> Result<bool> {
1967    let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
1968    if !context.get_config_bool(Config::MvboxMove).await? {
1969        return Ok(false);
1970    }
1971
1972    if headers
1973        .get_header_value(HeaderDef::AutocryptSetupMessage)
1974        .is_some()
1975    {
1976        // do not move setup messages;
1977        // there may be a non-delta device that wants to handle it
1978        return Ok(false);
1979    }
1980
1981    if has_chat_version {
1982        Ok(true)
1983    } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1984        match parent.is_dc_message {
1985            MessengerMessage::No => Ok(false),
1986            MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1987        }
1988    } else {
1989        Ok(false)
1990    }
1991}
1992
1993/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
1994// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
1995// however, if we fail to find out the sent-folder,
1996// only watching this folder is not working. at least, this is no show stopper.
1997// CAVE: if possible, take care not to add a name here that is "sent" in one language
1998// but sth. different in others - a hard job.
1999fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2000    // source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
2001    const SPAM_NAMES: &[&str] = &[
2002        "spam",
2003        "junk",
2004        "Correio electrónico não solicitado",
2005        "Correo basura",
2006        "Lixo",
2007        "Nettsøppel",
2008        "Nevyžádaná pošta",
2009        "No solicitado",
2010        "Ongewenst",
2011        "Posta indesiderata",
2012        "Skräp",
2013        "Wiadomości-śmieci",
2014        "Önemsiz",
2015        "Ανεπιθύμητα",
2016        "Спам",
2017        "垃圾邮件",
2018        "垃圾郵件",
2019        "迷惑メール",
2020        "스팸",
2021    ];
2022    const TRASH_NAMES: &[&str] = &[
2023        "Trash",
2024        "Bin",
2025        "Caixote do lixo",
2026        "Cestino",
2027        "Corbeille",
2028        "Papelera",
2029        "Papierkorb",
2030        "Papirkurv",
2031        "Papperskorgen",
2032        "Prullenbak",
2033        "Rubujo",
2034        "Κάδος απορριμμάτων",
2035        "Корзина",
2036        "Кошик",
2037        "ゴミ箱",
2038        "垃圾桶",
2039        "已删除邮件",
2040        "휴지통",
2041    ];
2042    let lower = folder_name.to_lowercase();
2043
2044    if lower == "inbox" {
2045        FolderMeaning::Inbox
2046    } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2047        FolderMeaning::Spam
2048    } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2049        FolderMeaning::Trash
2050    } else {
2051        FolderMeaning::Unknown
2052    }
2053}
2054
2055fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2056    for attr in folder_attrs {
2057        match attr {
2058            NameAttribute::Trash => return FolderMeaning::Trash,
2059            NameAttribute::Junk => return FolderMeaning::Spam,
2060            NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2061            NameAttribute::Extension(label) => {
2062                match label.as_ref() {
2063                    "\\Spam" => return FolderMeaning::Spam,
2064                    "\\Important" => return FolderMeaning::Virtual,
2065                    _ => {}
2066                };
2067            }
2068            _ => {}
2069        }
2070    }
2071    FolderMeaning::Unknown
2072}
2073
2074pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2075    match get_folder_meaning_by_attrs(folder.attributes()) {
2076        FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2077        meaning => meaning,
2078    }
2079}
2080
2081/// Parses the headers from the FETCH result.
2082fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2083    match prefetch_msg.header() {
2084        Some(header_bytes) => {
2085            let (headers, _) = mailparse::parse_headers(header_bytes)?;
2086            Ok(headers)
2087        }
2088        None => Ok(Vec::new()),
2089    }
2090}
2091
2092pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2093    headers
2094        .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2095        .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2096        .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2097}
2098
2099pub(crate) fn create_message_id() -> String {
2100    format!("{}{}", GENERATED_PREFIX, create_id())
2101}
2102
2103/// Determines whether the message should be downloaded based on prefetched headers.
2104pub(crate) async fn prefetch_should_download(
2105    context: &Context,
2106    headers: &[mailparse::MailHeader<'_>],
2107    message_id: &str,
2108    mut flags: impl Iterator<Item = Flag<'_>>,
2109) -> Result<bool> {
2110    if message::rfc724_mid_download_tried(context, message_id).await? {
2111        if let Some(from) = mimeparser::get_from(headers)
2112            && context.is_self_addr(&from.addr).await?
2113        {
2114            markseen_on_imap_table(context, message_id).await?;
2115        }
2116        return Ok(false);
2117    }
2118
2119    // We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
2120    // the further process).
2121
2122    let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2123        let from = from.to_ascii_lowercase();
2124        from.contains("mailer-daemon") || from.contains("mail-daemon")
2125    } else {
2126        false
2127    };
2128
2129    // Autocrypt Setup Message should be shown even if it is from non-chat client.
2130    let is_autocrypt_setup_message = headers
2131        .get_header_value(HeaderDef::AutocryptSetupMessage)
2132        .is_some();
2133
2134    let from = match mimeparser::get_from(headers) {
2135        Some(f) => f,
2136        None => return Ok(false),
2137    };
2138    let (_from_id, blocked_contact, origin) =
2139        match from_field_to_contact_id(context, &from, None, true, true).await? {
2140            Some(res) => res,
2141            None => return Ok(false),
2142        };
2143    // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
2144    // (prevent_rename is the last argument of from_field_to_contact_id())
2145
2146    if flags.any(|f| f == Flag::Draft) {
2147        info!(context, "Ignoring draft message");
2148        return Ok(false);
2149    }
2150
2151    let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2152    let accepted_contact = origin.is_known();
2153    let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2154        .await?
2155        .is_some_and(|parent| match parent.is_dc_message {
2156            MessengerMessage::No => false,
2157            MessengerMessage::Yes | MessengerMessage::Reply => true,
2158        });
2159
2160    let show_emails =
2161        ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2162
2163    let show = is_autocrypt_setup_message
2164        || match show_emails {
2165            ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2166            ShowEmails::AcceptedContacts => {
2167                is_chat_message || is_reply_to_chat_message || accepted_contact
2168            }
2169            ShowEmails::All => true,
2170        };
2171
2172    let should_download = (show && !blocked_contact) || maybe_ndn;
2173    Ok(should_download)
2174}
2175
2176/// Marks messages in `msgs` table as seen, searching for them by UID.
2177///
2178/// Returns updated chat ID if any message was marked as seen.
2179async fn mark_seen_by_uid(
2180    context: &Context,
2181    transport_id: u32,
2182    folder: &str,
2183    uid_validity: u32,
2184    uid: u32,
2185) -> Result<Option<ChatId>> {
2186    if let Some((msg_id, chat_id)) = context
2187        .sql
2188        .query_row_optional(
2189            "SELECT id, chat_id FROM msgs
2190                 WHERE id > 9 AND rfc724_mid IN (
2191                   SELECT rfc724_mid FROM imap
2192                   WHERE transport_id=?
2193                   AND folder=?
2194                   AND uidvalidity=?
2195                   AND uid=?
2196                   LIMIT 1
2197                 )",
2198            (transport_id, &folder, uid_validity, uid),
2199            |row| {
2200                let msg_id: MsgId = row.get(0)?;
2201                let chat_id: ChatId = row.get(1)?;
2202                Ok((msg_id, chat_id))
2203            },
2204        )
2205        .await
2206        .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2207    {
2208        let updated = context
2209            .sql
2210            .execute(
2211                "UPDATE msgs SET state=?1
2212                     WHERE (state=?2 OR state=?3)
2213                     AND id=?4",
2214                (
2215                    MessageState::InSeen,
2216                    MessageState::InFresh,
2217                    MessageState::InNoticed,
2218                    msg_id,
2219                ),
2220            )
2221            .await
2222            .with_context(|| format!("failed to update msg {msg_id} state"))?
2223            > 0;
2224
2225        if updated {
2226            msg_id
2227                .start_ephemeral_timer(context)
2228                .await
2229                .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2230            Ok(Some(chat_id))
2231        } else {
2232            // Message state has not changed.
2233            Ok(None)
2234        }
2235    } else {
2236        // There is no message is `msgs` table matching the given UID.
2237        Ok(None)
2238    }
2239}
2240
2241/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
2242/// the given Message-ID to `imap_markseen` table.
2243pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2244    context
2245        .sql
2246        .execute(
2247            "INSERT OR IGNORE INTO imap_markseen (id)
2248             SELECT id FROM imap WHERE rfc724_mid=?",
2249            (message_id,),
2250        )
2251        .await?;
2252    context.scheduler.interrupt_inbox().await;
2253
2254    Ok(())
2255}
2256
2257/// uid_next is the next unique identifier value from the last time we fetched a folder
2258/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2259/// This function is used to update our uid_next after fetching messages.
2260pub(crate) async fn set_uid_next(
2261    context: &Context,
2262    transport_id: u32,
2263    folder: &str,
2264    uid_next: u32,
2265) -> Result<()> {
2266    context
2267        .sql
2268        .execute(
2269            "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2270                ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2271            (transport_id, folder, uid_next),
2272        )
2273        .await?;
2274    Ok(())
2275}
2276
2277/// uid_next is the next unique identifier value from the last time we fetched a folder
2278/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2279/// This method returns the uid_next from the last time we fetched messages.
2280/// We can compare this to the current uid_next to find out whether there are new messages
2281/// and fetch from this value on to get all new messages.
2282async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2283    Ok(context
2284        .sql
2285        .query_get_value(
2286            "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2287            (transport_id, folder),
2288        )
2289        .await?
2290        .unwrap_or(0))
2291}
2292
2293pub(crate) async fn set_uidvalidity(
2294    context: &Context,
2295    transport_id: u32,
2296    folder: &str,
2297    uidvalidity: u32,
2298) -> Result<()> {
2299    context
2300        .sql
2301        .execute(
2302            "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2303                ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2304            (transport_id, folder, uidvalidity),
2305        )
2306        .await?;
2307    Ok(())
2308}
2309
2310async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2311    Ok(context
2312        .sql
2313        .query_get_value(
2314            "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2315            (transport_id, folder),
2316        )
2317        .await?
2318        .unwrap_or(0))
2319}
2320
2321pub(crate) async fn set_modseq(
2322    context: &Context,
2323    transport_id: u32,
2324    folder: &str,
2325    modseq: u64,
2326) -> Result<()> {
2327    context
2328        .sql
2329        .execute(
2330            "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2331                ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2332            (transport_id, folder, modseq),
2333        )
2334        .await?;
2335    Ok(())
2336}
2337
2338async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2339    Ok(context
2340        .sql
2341        .query_get_value(
2342            "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2343            (transport_id, folder),
2344        )
2345        .await?
2346        .unwrap_or(0))
2347}
2348
2349/// Whether to ignore fetching messages from a folder.
2350///
2351/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
2352/// not explicitly watched should not be fetched.
2353async fn should_ignore_folder(
2354    context: &Context,
2355    folder: &str,
2356    folder_meaning: FolderMeaning,
2357) -> Result<bool> {
2358    if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2359        return Ok(false);
2360    }
2361    Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2362}
2363
2364/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
2365/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
2366/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
2367fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2368    // first, try to find consecutive ranges:
2369    let mut ranges: Vec<UidRange> = vec![];
2370
2371    for &current in uids {
2372        if let Some(last) = ranges.last_mut()
2373            && last.end + 1 == current
2374        {
2375            last.end = current;
2376            continue;
2377        }
2378
2379        ranges.push(UidRange {
2380            start: current,
2381            end: current,
2382        });
2383    }
2384
2385    // Second, sort the uids into uid sets that are each below ~1000 characters
2386    let mut result = vec![];
2387    let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2388    for range in ranges {
2389        last_uids.reserve((range.end - range.start + 1).try_into()?);
2390        (range.start..=range.end).for_each(|u| last_uids.push(u));
2391        if !last_str.is_empty() {
2392            last_str.push(',');
2393        }
2394        last_str.push_str(&range.to_string());
2395
2396        if last_str.len() > 990 {
2397            result.push((take(&mut last_uids), take(&mut last_str)));
2398        }
2399    }
2400    result.push((last_uids, last_str));
2401
2402    result.retain(|(_, s)| !s.is_empty());
2403    Ok(result)
2404}
2405
2406struct UidRange {
2407    start: u32,
2408    end: u32,
2409    // If start == end, then this range represents a single number
2410}
2411
2412impl std::fmt::Display for UidRange {
2413    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2414        if self.start == self.end {
2415            write!(f, "{}", self.start)
2416        } else {
2417            write!(f, "{}:{}", self.start, self.end)
2418        }
2419    }
2420}
2421
2422pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2423    let mut res = vec![Config::ConfiguredInboxFolder];
2424    if context.should_watch_mvbox().await? {
2425        res.push(Config::ConfiguredMvboxFolder);
2426    }
2427    Ok(res)
2428}
2429
2430pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2431    let mut res = Vec::new();
2432    for folder_config in get_watched_folder_configs(context).await? {
2433        if let Some(folder) = context.get_config(folder_config).await? {
2434            res.push(folder);
2435        }
2436    }
2437    Ok(res)
2438}
2439
2440#[cfg(test)]
2441mod imap_tests;