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