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