deltachat/
imap.rs

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