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