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 ratelimit::Ratelimit;
22use url::Url;
23
24use crate::calls::{
25    UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
26};
27use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
28use crate::chatlist_events;
29use crate::config::Config;
30use crate::constants::{self, Blocked, DC_VERSION_STR};
31use crate::contact::ContactId;
32use crate::context::Context;
33use crate::events::EventType;
34use crate::headerdef::{HeaderDef, HeaderDefMap};
35use crate::log::{LogExt, warn};
36use crate::message::{self, Message, MessageState, MessengerMessage, MsgId};
37use crate::mimeparser;
38use crate::net::proxy::ProxyConfig;
39use crate::net::session::SessionStream;
40use crate::oauth2::get_oauth2_access_token;
41use crate::push::encrypt_device_token;
42use crate::receive_imf::{
43    ReceivedMsg, from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner,
44};
45use crate::scheduler::connectivity::ConnectivityStore;
46use crate::stock_str;
47use crate::tools::{self, create_id, duration_to_str, time};
48use crate::transport::{
49    ConfiguredLoginParam, ConfiguredServerLoginParam, prioritize_server_login_params,
50};
51
52pub(crate) mod capabilities;
53mod client;
54mod idle;
55pub mod select_folder;
56pub(crate) mod session;
57
58use client::{Client, determine_capabilities};
59use session::Session;
60
61pub(crate) const GENERATED_PREFIX: &str = "GEN_";
62
63const RFC724MID_UID: &str = "(UID BODY.PEEK[HEADER.FIELDS (\
64                             MESSAGE-ID \
65                             X-MICROSOFT-ORIGINAL-MESSAGE-ID\
66                             )])";
67const BODY_FULL: &str = "(FLAGS BODY.PEEK[])";
68
69#[derive(Debug)]
70pub(crate) struct Imap {
71    /// ID of the transport configuration in the `transports` table.
72    ///
73    /// This ID is used to namespace records in the `imap` table.
74    transport_id: u32,
75
76    pub(crate) idle_interrupt_receiver: Receiver<()>,
77
78    /// Email address.
79    pub(crate) addr: String,
80
81    /// Login parameters.
82    lp: Vec<ConfiguredServerLoginParam>,
83
84    /// Password.
85    password: String,
86
87    /// Proxy configuration.
88    proxy_config: Option<ProxyConfig>,
89
90    strict_tls: bool,
91
92    oauth2: bool,
93
94    authentication_failed_once: bool,
95
96    pub(crate) connectivity: ConnectivityStore,
97
98    conn_last_try: tools::Time,
99    conn_backoff_ms: u64,
100
101    /// Rate limit for successful IMAP connections.
102    ///
103    /// This rate limit prevents busy loop in case the server refuses logins
104    /// or in case connection gets dropped over and over due to IMAP bug,
105    /// e.g. the server returning invalid response to SELECT command
106    /// immediately after logging in or returning an error in response to LOGIN command
107    /// due to internal server error.
108    ratelimit: Ratelimit,
109
110    /// IMAP UID resync request sender.
111    pub(crate) resync_request_sender: async_channel::Sender<()>,
112
113    /// IMAP UID resync request receiver.
114    pub(crate) resync_request_receiver: async_channel::Receiver<()>,
115}
116
117#[derive(Debug)]
118struct OAuth2 {
119    user: String,
120    access_token: String,
121}
122
123#[derive(Debug, Default)]
124pub(crate) struct ServerMetadata {
125    /// IMAP METADATA `/shared/comment` as defined in
126    /// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1>.
127    pub comment: Option<String>,
128
129    /// IMAP METADATA `/shared/admin` as defined in
130    /// <https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2>.
131    pub admin: Option<String>,
132
133    pub iroh_relay: Option<Url>,
134
135    /// ICE servers for WebRTC calls.
136    pub ice_servers: Vec<UnresolvedIceServer>,
137
138    /// Timestamp when ICE servers are considered
139    /// expired and should be updated.
140    ///
141    /// If ICE servers are about to expire, new TURN credentials
142    /// should be fetched from the server
143    /// to be ready for WebRTC calls.
144    pub ice_servers_expiration_timestamp: i64,
145}
146
147impl async_imap::Authenticator for OAuth2 {
148    type Response = String;
149
150    fn process(&mut self, _data: &[u8]) -> Self::Response {
151        format!(
152            "user={}\x01auth=Bearer {}\x01\x01",
153            self.user, self.access_token
154        )
155    }
156}
157
158#[derive(Debug, Display, PartialEq, Eq, Clone, Copy)]
159pub enum FolderMeaning {
160    Unknown,
161
162    /// Spam folder.
163    Spam,
164    Inbox,
165    Mvbox,
166    Trash,
167
168    /// Virtual folders.
169    ///
170    /// On Gmail there are virtual folders marked as \\All, \\Important and \\Flagged.
171    /// Delta Chat ignores these folders because the same messages can be fetched
172    /// from the real folder and the result of moving and deleting messages via
173    /// virtual folder is unclear.
174    Virtual,
175}
176
177impl FolderMeaning {
178    pub fn to_config(self) -> Option<Config> {
179        match self {
180            FolderMeaning::Unknown => None,
181            FolderMeaning::Spam => None,
182            FolderMeaning::Inbox => Some(Config::ConfiguredInboxFolder),
183            FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
184            FolderMeaning::Trash => None,
185            FolderMeaning::Virtual => None,
186        }
187    }
188}
189
190struct UidGrouper<T: Iterator<Item = (i64, u32, String)>> {
191    inner: Peekable<T>,
192}
193
194impl<T, I> From<I> for UidGrouper<T>
195where
196    T: Iterator<Item = (i64, u32, String)>,
197    I: IntoIterator<IntoIter = T>,
198{
199    fn from(inner: I) -> Self {
200        Self {
201            inner: inner.into_iter().peekable(),
202        }
203    }
204}
205
206impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
207    // Tuple of folder, row IDs, and UID range as a string.
208    type Item = (String, Vec<i64>, String);
209
210    #[expect(clippy::arithmetic_side_effects)]
211    fn next(&mut self) -> Option<Self::Item> {
212        let (_, _, folder) = self.inner.peek().cloned()?;
213
214        let mut uid_set = String::new();
215        let mut rowid_set = Vec::new();
216
217        while uid_set.len() < 1000 {
218            // Construct a new range.
219            if let Some((start_rowid, start_uid, _)) = self
220                .inner
221                .next_if(|(_, _, start_folder)| start_folder == &folder)
222            {
223                rowid_set.push(start_rowid);
224                let mut end_uid = start_uid;
225
226                while let Some((next_rowid, next_uid, _)) =
227                    self.inner.next_if(|(_, next_uid, next_folder)| {
228                        next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
229                    })
230                {
231                    end_uid = next_uid;
232                    rowid_set.push(next_rowid);
233                }
234
235                let uid_range = UidRange {
236                    start: start_uid,
237                    end: end_uid,
238                };
239                if !uid_set.is_empty() {
240                    uid_set.push(',');
241                }
242                uid_set.push_str(&uid_range.to_string());
243            } else {
244                break;
245            }
246        }
247
248        Some((folder, rowid_set, uid_set))
249    }
250}
251
252impl Imap {
253    /// Creates new disconnected IMAP client using the specific login parameters.
254    pub async fn new(
255        context: &Context,
256        transport_id: u32,
257        param: ConfiguredLoginParam,
258        idle_interrupt_receiver: Receiver<()>,
259    ) -> Result<Self> {
260        let lp = param.imap.clone();
261        let password = param.imap_password.clone();
262        let proxy_config = ProxyConfig::load(context).await?;
263        let addr = &param.addr;
264        let strict_tls = param.strict_tls(proxy_config.is_some());
265        let oauth2 = param.oauth2;
266        let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
267        Ok(Imap {
268            transport_id,
269            idle_interrupt_receiver,
270            addr: addr.to_string(),
271            lp,
272            password,
273            proxy_config,
274            strict_tls,
275            oauth2,
276            authentication_failed_once: false,
277            connectivity: Default::default(),
278            conn_last_try: UNIX_EPOCH,
279            conn_backoff_ms: 0,
280            // 1 connection per minute + a burst of 2.
281            ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
282            resync_request_sender,
283            resync_request_receiver,
284        })
285    }
286
287    /// Creates new disconnected IMAP client using configured parameters.
288    pub async fn new_configured(
289        context: &Context,
290        idle_interrupt_receiver: Receiver<()>,
291    ) -> Result<Self> {
292        let (transport_id, param) = ConfiguredLoginParam::load(context)
293            .await?
294            .context("Not configured")?;
295        let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
296        Ok(imap)
297    }
298
299    /// Connects to IMAP server and returns a new IMAP session.
300    ///
301    /// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
302    /// instead if you are going to actually use connection rather than trying connection
303    /// parameters.
304    pub(crate) async fn connect(
305        &mut self,
306        context: &Context,
307        configuring: bool,
308    ) -> Result<Session> {
309        let now = tools::Time::now();
310        let until_can_send = max(
311            min(self.conn_last_try, now)
312                .checked_add(Duration::from_millis(self.conn_backoff_ms))
313                .unwrap_or(now),
314            now,
315        )
316        .duration_since(now)?;
317        let ratelimit_duration = max(until_can_send, self.ratelimit.until_can_send());
318        if !ratelimit_duration.is_zero() {
319            warn!(
320                context,
321                "IMAP got rate limited, waiting for {} until can connect.",
322                duration_to_str(ratelimit_duration),
323            );
324            let interrupted = async {
325                tokio::time::sleep(ratelimit_duration).await;
326                false
327            }
328            .race(self.idle_interrupt_receiver.recv().map(|_| true))
329            .await;
330            if interrupted {
331                info!(
332                    context,
333                    "Connecting to IMAP without waiting for ratelimit due to interrupt."
334                );
335            }
336        }
337
338        info!(context, "Connecting to IMAP server.");
339        self.connectivity.set_connecting(context);
340
341        self.conn_last_try = tools::Time::now();
342        const BACKOFF_MIN_MS: u64 = 2000;
343        const BACKOFF_MAX_MS: u64 = 80_000;
344        self.conn_backoff_ms = min(self.conn_backoff_ms, BACKOFF_MAX_MS / 2);
345        self.conn_backoff_ms = self.conn_backoff_ms.saturating_add(rand::random_range(
346            (self.conn_backoff_ms / 2)..=self.conn_backoff_ms,
347        ));
348        self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
349
350        let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
351        let mut first_error = None;
352        for lp in login_params {
353            info!(context, "IMAP trying to connect to {}.", &lp.connection);
354            let connection_candidate = lp.connection.clone();
355            let client = match Client::connect(
356                context,
357                self.proxy_config.clone(),
358                self.strict_tls,
359                &connection_candidate,
360            )
361            .await
362            .with_context(|| format!("IMAP failed to connect to {connection_candidate}"))
363            {
364                Ok(client) => client,
365                Err(err) => {
366                    warn!(context, "{err:#}.");
367                    first_error.get_or_insert(err);
368                    continue;
369                }
370            };
371
372            self.conn_backoff_ms = BACKOFF_MIN_MS;
373            self.ratelimit.send();
374
375            let imap_user: &str = lp.user.as_ref();
376            let imap_pw: &str = &self.password;
377
378            let login_res = if self.oauth2 {
379                info!(context, "Logging into IMAP server with OAuth 2.");
380                let addr: &str = self.addr.as_ref();
381
382                let token = get_oauth2_access_token(context, addr, imap_pw, true)
383                    .await?
384                    .context("IMAP could not get OAUTH token")?;
385                let auth = OAuth2 {
386                    user: imap_user.into(),
387                    access_token: token,
388                };
389                client.authenticate("XOAUTH2", auth).await
390            } else {
391                info!(context, "Logging into IMAP server with LOGIN.");
392                client.login(imap_user, imap_pw).await
393            };
394
395            match login_res {
396                Ok(mut session) => {
397                    let capabilities = determine_capabilities(&mut session).await?;
398                    let resync_request_sender = self.resync_request_sender.clone();
399
400                    let session = if capabilities.can_compress {
401                        info!(context, "Enabling IMAP compression.");
402                        let compressed_session = session
403                            .compress(|s| {
404                                let session_stream: Box<dyn SessionStream> = Box::new(s);
405                                session_stream
406                            })
407                            .await
408                            .context("Failed to enable IMAP compression")?;
409                        Session::new(
410                            compressed_session,
411                            capabilities,
412                            resync_request_sender,
413                            self.transport_id,
414                        )
415                    } else {
416                        Session::new(
417                            session,
418                            capabilities,
419                            resync_request_sender,
420                            self.transport_id,
421                        )
422                    };
423
424                    // Store server ID in the context to display in account info.
425                    let mut lock = context.server_id.write().await;
426                    lock.clone_from(&session.capabilities.server_id);
427
428                    self.authentication_failed_once = false;
429                    context.emit_event(EventType::ImapConnected(format!(
430                        "IMAP-LOGIN as {}",
431                        lp.user
432                    )));
433                    self.connectivity.set_preparing(context);
434                    info!(context, "Successfully logged into IMAP server.");
435                    return Ok(session);
436                }
437
438                Err(err) => {
439                    let imap_user = lp.user.to_owned();
440                    let message = stock_str::cannot_login(context, &imap_user).await;
441
442                    warn!(context, "IMAP failed to login: {err:#}.");
443                    first_error.get_or_insert(format_err!("{message} ({err:#})"));
444
445                    // If it looks like the password is wrong, send a notification:
446                    let _lock = context.wrong_pw_warning_mutex.lock().await;
447                    if err.to_string().to_lowercase().contains("authentication") {
448                        if self.authentication_failed_once
449                            && !configuring
450                            && context.get_config_bool(Config::NotifyAboutWrongPw).await?
451                        {
452                            let mut msg = Message::new_text(message);
453                            if let Err(e) = chat::add_device_msg_with_importance(
454                                context,
455                                None,
456                                Some(&mut msg),
457                                true,
458                            )
459                            .await
460                            {
461                                warn!(context, "Failed to add device message: {e:#}.");
462                            } else {
463                                context
464                                    .set_config_internal(Config::NotifyAboutWrongPw, None)
465                                    .await
466                                    .log_err(context)
467                                    .ok();
468                            }
469                        } else {
470                            self.authentication_failed_once = true;
471                        }
472                    } else {
473                        self.authentication_failed_once = false;
474                    }
475                }
476            }
477        }
478
479        Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
480    }
481
482    /// Prepare a new IMAP session.
483    ///
484    /// This creates a new IMAP connection and ensures
485    /// that folders are created and IMAP capabilities are determined.
486    pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
487        let configuring = false;
488        let mut session = match self.connect(context, configuring).await {
489            Ok(session) => session,
490            Err(err) => {
491                self.connectivity.set_err(context, &err);
492                return Err(err);
493            }
494        };
495
496        let folders_configured = context
497            .sql
498            .get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
499            .await?;
500        if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
501            self.configure_folders(context, &mut session).await?;
502        }
503
504        Ok(session)
505    }
506
507    /// FETCH-MOVE-DELETE iteration.
508    ///
509    /// Prefetches headers and downloads new message from the folder, moves messages away from the
510    /// folder and deletes messages in the folder.
511    pub async fn fetch_move_delete(
512        &mut self,
513        context: &Context,
514        session: &mut Session,
515        watch_folder: &str,
516        folder_meaning: FolderMeaning,
517    ) -> Result<()> {
518        if !context.sql.is_open().await {
519            // probably shutdown
520            bail!("IMAP operation attempted while it is torn down");
521        }
522
523        let msgs_fetched = self
524            .fetch_new_messages(context, session, watch_folder, folder_meaning)
525            .await
526            .context("fetch_new_messages")?;
527        if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
528            // New messages were fetched and shall be deleted later, restart ephemeral loop.
529            // Note that the `Config::DeleteDeviceAfter` timer starts as soon as the messages are
530            // fetched while the per-chat ephemeral timers start as soon as the messages are marked
531            // as noticed.
532            context.scheduler.interrupt_ephemeral_task().await;
533        }
534
535        session
536            .move_delete_messages(context, watch_folder)
537            .await
538            .context("move_delete_messages")?;
539
540        Ok(())
541    }
542
543    /// Fetches new messages.
544    ///
545    /// Returns true if at least one message was fetched.
546    #[expect(clippy::arithmetic_side_effects)]
547    pub(crate) async fn fetch_new_messages(
548        &mut self,
549        context: &Context,
550        session: &mut Session,
551        folder: &str,
552        folder_meaning: FolderMeaning,
553    ) -> Result<bool> {
554        if should_ignore_folder(context, folder, folder_meaning).await? {
555            info!(context, "Not fetching from {folder:?}.");
556            session.new_mail = false;
557            return Ok(false);
558        }
559
560        let folder_exists = session
561            .select_with_uidvalidity(context, folder)
562            .await
563            .with_context(|| format!("Failed to select folder {folder:?}"))?;
564        if !folder_exists {
565            return Ok(false);
566        }
567
568        if !session.new_mail {
569            info!(context, "No new emails in folder {folder:?}.");
570            return Ok(false);
571        }
572        session.new_mail = false;
573
574        let mut read_cnt = 0;
575        loop {
576            let (n, fetch_more) = self
577                .fetch_new_msg_batch(context, session, folder, folder_meaning)
578                .await?;
579            read_cnt += n;
580            if !fetch_more {
581                return Ok(read_cnt > 0);
582            }
583        }
584    }
585
586    /// Returns number of messages processed and whether the function should be called again.
587    #[expect(clippy::arithmetic_side_effects)]
588    async fn fetch_new_msg_batch(
589        &mut self,
590        context: &Context,
591        session: &mut Session,
592        folder: &str,
593        folder_meaning: FolderMeaning,
594    ) -> Result<(usize, bool)> {
595        let transport_id = self.transport_id;
596        let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
597        let old_uid_next = get_uid_next(context, transport_id, folder).await?;
598        info!(
599            context,
600            "fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
601        );
602
603        let uids_to_prefetch = 500;
604        let msgs = session
605            .prefetch(old_uid_next, uids_to_prefetch)
606            .await
607            .context("prefetch")?;
608        let read_cnt = msgs.len();
609        let _fetch_msgs_lock_guard = context.fetch_msgs_mutex.lock().await;
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 has_chat_version {
1978        Ok(true)
1979    } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
1980        match parent.is_dc_message {
1981            MessengerMessage::No => Ok(false),
1982            MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
1983        }
1984    } else {
1985        Ok(false)
1986    }
1987}
1988
1989/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
1990// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
1991// however, if we fail to find out the sent-folder,
1992// only watching this folder is not working. at least, this is no show stopper.
1993// CAVE: if possible, take care not to add a name here that is "sent" in one language
1994// but sth. different in others - a hard job.
1995fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
1996    // source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
1997    const SPAM_NAMES: &[&str] = &[
1998        "spam",
1999        "junk",
2000        "Correio electrónico não solicitado",
2001        "Correo basura",
2002        "Lixo",
2003        "Nettsøppel",
2004        "Nevyžádaná pošta",
2005        "No solicitado",
2006        "Ongewenst",
2007        "Posta indesiderata",
2008        "Skräp",
2009        "Wiadomości-śmieci",
2010        "Önemsiz",
2011        "Ανεπιθύμητα",
2012        "Спам",
2013        "垃圾邮件",
2014        "垃圾郵件",
2015        "迷惑メール",
2016        "스팸",
2017    ];
2018    const TRASH_NAMES: &[&str] = &[
2019        "Trash",
2020        "Bin",
2021        "Caixote do lixo",
2022        "Cestino",
2023        "Corbeille",
2024        "Papelera",
2025        "Papierkorb",
2026        "Papirkurv",
2027        "Papperskorgen",
2028        "Prullenbak",
2029        "Rubujo",
2030        "Κάδος απορριμμάτων",
2031        "Корзина",
2032        "Кошик",
2033        "ゴミ箱",
2034        "垃圾桶",
2035        "已删除邮件",
2036        "휴지통",
2037    ];
2038    let lower = folder_name.to_lowercase();
2039
2040    if lower == "inbox" {
2041        FolderMeaning::Inbox
2042    } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2043        FolderMeaning::Spam
2044    } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2045        FolderMeaning::Trash
2046    } else {
2047        FolderMeaning::Unknown
2048    }
2049}
2050
2051fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2052    for attr in folder_attrs {
2053        match attr {
2054            NameAttribute::Trash => return FolderMeaning::Trash,
2055            NameAttribute::Junk => return FolderMeaning::Spam,
2056            NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2057            NameAttribute::Extension(label) => {
2058                match label.as_ref() {
2059                    "\\Spam" => return FolderMeaning::Spam,
2060                    "\\Important" => return FolderMeaning::Virtual,
2061                    _ => {}
2062                };
2063            }
2064            _ => {}
2065        }
2066    }
2067    FolderMeaning::Unknown
2068}
2069
2070pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2071    match get_folder_meaning_by_attrs(folder.attributes()) {
2072        FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2073        meaning => meaning,
2074    }
2075}
2076
2077/// Parses the headers from the FETCH result.
2078fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2079    match prefetch_msg.header() {
2080        Some(header_bytes) => {
2081            let (headers, _) = mailparse::parse_headers(header_bytes)?;
2082            Ok(headers)
2083        }
2084        None => Ok(Vec::new()),
2085    }
2086}
2087
2088pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2089    headers
2090        .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2091        .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2092        .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2093}
2094
2095pub(crate) fn create_message_id() -> String {
2096    format!("{}{}", GENERATED_PREFIX, create_id())
2097}
2098
2099/// Determines whether the message should be downloaded based on prefetched headers.
2100pub(crate) async fn prefetch_should_download(
2101    context: &Context,
2102    headers: &[mailparse::MailHeader<'_>],
2103    message_id: &str,
2104    mut flags: impl Iterator<Item = Flag<'_>>,
2105) -> Result<bool> {
2106    if message::rfc724_mid_download_tried(context, message_id).await? {
2107        if let Some(from) = mimeparser::get_from(headers)
2108            && context.is_self_addr(&from.addr).await?
2109        {
2110            markseen_on_imap_table(context, message_id).await?;
2111        }
2112        return Ok(false);
2113    }
2114
2115    // We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
2116    // the further process).
2117
2118    let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2119        let from = from.to_ascii_lowercase();
2120        from.contains("mailer-daemon") || from.contains("mail-daemon")
2121    } else {
2122        false
2123    };
2124
2125    let from = match mimeparser::get_from(headers) {
2126        Some(f) => f,
2127        None => return Ok(false),
2128    };
2129    let (_from_id, blocked_contact, _origin) =
2130        match from_field_to_contact_id(context, &from, None, true, true).await? {
2131            Some(res) => res,
2132            None => return Ok(false),
2133        };
2134    // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
2135    // (prevent_rename is the last argument of from_field_to_contact_id())
2136
2137    if flags.any(|f| f == Flag::Draft) {
2138        info!(context, "Ignoring draft message");
2139        return Ok(false);
2140    }
2141
2142    let should_download = (!blocked_contact) || maybe_ndn;
2143    Ok(should_download)
2144}
2145
2146/// Marks messages in `msgs` table as seen, searching for them by UID.
2147///
2148/// Returns updated chat ID if any message was marked as seen.
2149async fn mark_seen_by_uid(
2150    context: &Context,
2151    transport_id: u32,
2152    folder: &str,
2153    uid_validity: u32,
2154    uid: u32,
2155) -> Result<Option<ChatId>> {
2156    if let Some((msg_id, chat_id)) = context
2157        .sql
2158        .query_row_optional(
2159            "SELECT id, chat_id FROM msgs
2160                 WHERE id > 9 AND rfc724_mid IN (
2161                   SELECT rfc724_mid FROM imap
2162                   WHERE transport_id=?
2163                   AND folder=?
2164                   AND uidvalidity=?
2165                   AND uid=?
2166                   LIMIT 1
2167                 )",
2168            (transport_id, &folder, uid_validity, uid),
2169            |row| {
2170                let msg_id: MsgId = row.get(0)?;
2171                let chat_id: ChatId = row.get(1)?;
2172                Ok((msg_id, chat_id))
2173            },
2174        )
2175        .await
2176        .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2177    {
2178        let updated = context
2179            .sql
2180            .execute(
2181                "UPDATE msgs SET state=?1
2182                     WHERE (state=?2 OR state=?3)
2183                     AND id=?4",
2184                (
2185                    MessageState::InSeen,
2186                    MessageState::InFresh,
2187                    MessageState::InNoticed,
2188                    msg_id,
2189                ),
2190            )
2191            .await
2192            .with_context(|| format!("failed to update msg {msg_id} state"))?
2193            > 0;
2194
2195        if updated {
2196            msg_id
2197                .start_ephemeral_timer(context)
2198                .await
2199                .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2200            Ok(Some(chat_id))
2201        } else {
2202            // Message state has not changed.
2203            Ok(None)
2204        }
2205    } else {
2206        // There is no message is `msgs` table matching the given UID.
2207        Ok(None)
2208    }
2209}
2210
2211/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
2212/// the given Message-ID to `imap_markseen` table.
2213pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2214    context
2215        .sql
2216        .execute(
2217            "INSERT OR IGNORE INTO imap_markseen (id)
2218             SELECT id FROM imap WHERE rfc724_mid=?",
2219            (message_id,),
2220        )
2221        .await?;
2222    context.scheduler.interrupt_inbox().await;
2223
2224    Ok(())
2225}
2226
2227/// uid_next is the next unique identifier value from the last time we fetched a folder
2228/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2229/// This function is used to update our uid_next after fetching messages.
2230pub(crate) async fn set_uid_next(
2231    context: &Context,
2232    transport_id: u32,
2233    folder: &str,
2234    uid_next: u32,
2235) -> Result<()> {
2236    context
2237        .sql
2238        .execute(
2239            "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2240                ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2241            (transport_id, folder, uid_next),
2242        )
2243        .await?;
2244    Ok(())
2245}
2246
2247/// uid_next is the next unique identifier value from the last time we fetched a folder
2248/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2249/// This method returns the uid_next from the last time we fetched messages.
2250/// We can compare this to the current uid_next to find out whether there are new messages
2251/// and fetch from this value on to get all new messages.
2252async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2253    Ok(context
2254        .sql
2255        .query_get_value(
2256            "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2257            (transport_id, folder),
2258        )
2259        .await?
2260        .unwrap_or(0))
2261}
2262
2263pub(crate) async fn set_uidvalidity(
2264    context: &Context,
2265    transport_id: u32,
2266    folder: &str,
2267    uidvalidity: u32,
2268) -> Result<()> {
2269    context
2270        .sql
2271        .execute(
2272            "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2273                ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2274            (transport_id, folder, uidvalidity),
2275        )
2276        .await?;
2277    Ok(())
2278}
2279
2280async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2281    Ok(context
2282        .sql
2283        .query_get_value(
2284            "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2285            (transport_id, folder),
2286        )
2287        .await?
2288        .unwrap_or(0))
2289}
2290
2291pub(crate) async fn set_modseq(
2292    context: &Context,
2293    transport_id: u32,
2294    folder: &str,
2295    modseq: u64,
2296) -> Result<()> {
2297    context
2298        .sql
2299        .execute(
2300            "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2301                ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2302            (transport_id, folder, modseq),
2303        )
2304        .await?;
2305    Ok(())
2306}
2307
2308async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2309    Ok(context
2310        .sql
2311        .query_get_value(
2312            "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2313            (transport_id, folder),
2314        )
2315        .await?
2316        .unwrap_or(0))
2317}
2318
2319/// Whether to ignore fetching messages from a folder.
2320///
2321/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
2322/// not explicitly watched should not be fetched.
2323async fn should_ignore_folder(
2324    context: &Context,
2325    folder: &str,
2326    folder_meaning: FolderMeaning,
2327) -> Result<bool> {
2328    if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2329        return Ok(false);
2330    }
2331    Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2332}
2333
2334/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
2335/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
2336/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
2337#[expect(clippy::arithmetic_side_effects)]
2338fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2339    // first, try to find consecutive ranges:
2340    let mut ranges: Vec<UidRange> = vec![];
2341
2342    for &current in uids {
2343        if let Some(last) = ranges.last_mut()
2344            && last.end + 1 == current
2345        {
2346            last.end = current;
2347            continue;
2348        }
2349
2350        ranges.push(UidRange {
2351            start: current,
2352            end: current,
2353        });
2354    }
2355
2356    // Second, sort the uids into uid sets that are each below ~1000 characters
2357    let mut result = vec![];
2358    let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2359    for range in ranges {
2360        last_uids.reserve((range.end - range.start + 1).try_into()?);
2361        (range.start..=range.end).for_each(|u| last_uids.push(u));
2362        if !last_str.is_empty() {
2363            last_str.push(',');
2364        }
2365        last_str.push_str(&range.to_string());
2366
2367        if last_str.len() > 990 {
2368            result.push((take(&mut last_uids), take(&mut last_str)));
2369        }
2370    }
2371    result.push((last_uids, last_str));
2372
2373    result.retain(|(_, s)| !s.is_empty());
2374    Ok(result)
2375}
2376
2377struct UidRange {
2378    start: u32,
2379    end: u32,
2380    // If start == end, then this range represents a single number
2381}
2382
2383impl std::fmt::Display for UidRange {
2384    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2385        if self.start == self.end {
2386            write!(f, "{}", self.start)
2387        } else {
2388            write!(f, "{}:{}", self.start, self.end)
2389        }
2390    }
2391}
2392
2393pub(crate) async fn get_watched_folder_configs(context: &Context) -> Result<Vec<Config>> {
2394    let mut res = vec![Config::ConfiguredInboxFolder];
2395    if context.should_watch_mvbox().await? {
2396        res.push(Config::ConfiguredMvboxFolder);
2397    }
2398    Ok(res)
2399}
2400
2401pub(crate) async fn get_watched_folders(context: &Context) -> Result<Vec<String>> {
2402    let mut res = Vec::new();
2403    for folder_config in get_watched_folder_configs(context).await? {
2404        if let Some(folder) = context.get_config(folder_config).await? {
2405            res.push(folder);
2406        }
2407    }
2408    Ok(res)
2409}
2410
2411#[cfg(test)]
2412mod imap_tests;