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                if 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
1273            if let Some(modseq) = fetch.modseq {
1274                if modseq > highest_modseq {
1275                    highest_modseq = modseq;
1276                }
1277            } else {
1278                warn!(context, "FETCH result contains no MODSEQ");
1279            }
1280        }
1281        drop(list);
1282
1283        if got_unsolicited_fetch {
1284            // We got unsolicited FETCH, which means some flags
1285            // have been modified while our request was in progress.
1286            // We may or may not have these new flags as a part of the response,
1287            // so better skip next IDLE and do another round of flag synchronization.
1288            self.new_mail = true;
1289        }
1290
1291        set_modseq(context, folder, highest_modseq)
1292            .await
1293            .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1294        if !updated_chat_ids.is_empty() {
1295            context.on_archived_chats_maybe_noticed();
1296        }
1297        for updated_chat_id in updated_chat_ids {
1298            context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1299            chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1300        }
1301
1302        Ok(())
1303    }
1304
1305    /// Gets the from, to and bcc addresses from all existing outgoing emails.
1306    pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1307        let mut uids: Vec<_> = self
1308            .uid_search(get_imap_self_sent_search_command(context).await?)
1309            .await?
1310            .into_iter()
1311            .collect();
1312        uids.sort_unstable();
1313
1314        let mut result = Vec::new();
1315        for (_, uid_set) in build_sequence_sets(&uids)? {
1316            let mut list = self
1317                .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1318                .await
1319                .context("IMAP Could not fetch")?;
1320
1321            while let Some(msg) = list.try_next().await? {
1322                match get_fetch_headers(&msg) {
1323                    Ok(headers) => {
1324                        if let Some(from) = mimeparser::get_from(&headers) {
1325                            if context.is_self_addr(&from.addr).await? {
1326                                result.extend(mimeparser::get_recipients(&headers));
1327                            }
1328                        }
1329                    }
1330                    Err(err) => {
1331                        warn!(context, "{}", err);
1332                        continue;
1333                    }
1334                };
1335            }
1336        }
1337        Ok(result)
1338    }
1339
1340    /// Fetches a list of messages by server UID.
1341    ///
1342    /// Sends pairs of UID and info about each downloaded message to the provided channel.
1343    /// Received message info is optional because UID may be ignored
1344    /// if the message has a `\Deleted` flag.
1345    ///
1346    /// The channel is used to return the results because the function may fail
1347    /// due to network errors before it finishes fetching all the messages.
1348    /// In this case caller still may want to process all the results
1349    /// received over the channel and persist last seen UID in the database
1350    /// before bubbling up the failure.
1351    ///
1352    /// If the message is incorrect or there is a failure to write a message to the database,
1353    /// it is skipped and the error is logged.
1354    pub(crate) async fn fetch_many_msgs(
1355        &mut self,
1356        context: &Context,
1357        folder: &str,
1358        request_uids: Vec<u32>,
1359        uid_message_ids: &BTreeMap<u32, String>,
1360        fetch_partially: bool,
1361        received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1362    ) -> Result<()> {
1363        if request_uids.is_empty() {
1364            return Ok(());
1365        }
1366
1367        for (request_uids, set) in build_sequence_sets(&request_uids)? {
1368            info!(
1369                context,
1370                "Starting a {} FETCH of message set \"{}\".",
1371                if fetch_partially { "partial" } else { "full" },
1372                set
1373            );
1374            let mut fetch_responses = self
1375                .uid_fetch(
1376                    &set,
1377                    if fetch_partially {
1378                        BODY_PARTIAL
1379                    } else {
1380                        BODY_FULL
1381                    },
1382                )
1383                .await
1384                .with_context(|| {
1385                    format!("fetching messages {} from folder \"{}\"", &set, folder)
1386                })?;
1387
1388            // Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
1389            // when we want to process other messages first.
1390            let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1391
1392            let mut count = 0;
1393            for &request_uid in &request_uids {
1394                // Check if FETCH response is already in `uid_msgs`.
1395                let mut fetch_response = uid_msgs.remove(&request_uid);
1396
1397                // Try to find a requested UID in returned FETCH responses.
1398                while fetch_response.is_none() {
1399                    let Some(next_fetch_response) = fetch_responses
1400                        .try_next()
1401                        .await
1402                        .context("Failed to process IMAP FETCH result")?
1403                    else {
1404                        // No more FETCH responses received from the server.
1405                        break;
1406                    };
1407
1408                    if let Some(next_uid) = next_fetch_response.uid {
1409                        if next_uid == request_uid {
1410                            fetch_response = Some(next_fetch_response);
1411                        } else if !request_uids.contains(&next_uid) {
1412                            // (size of `request_uids` is bounded by IMAP command length limit,
1413                            // search in this vector is always fast)
1414
1415                            // Unwanted UIDs are possible because of unsolicited responses, e.g. if
1416                            // another client changes \Seen flag on a message after we do a prefetch but
1417                            // before fetch. It's not an error if we receive such unsolicited response.
1418                            info!(
1419                                context,
1420                                "Skipping not requested FETCH response for UID {}.", next_uid
1421                            );
1422                        } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1423                            warn!(context, "Got duplicated UID {}.", next_uid);
1424                        }
1425                    } else {
1426                        info!(context, "Skipping FETCH response without UID.");
1427                    }
1428                }
1429
1430                let fetch_response = match fetch_response {
1431                    Some(fetch) => fetch,
1432                    None => {
1433                        warn!(
1434                            context,
1435                            "Missed UID {} in the server response.", request_uid
1436                        );
1437                        continue;
1438                    }
1439                };
1440                count += 1;
1441
1442                let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1443                let (body, partial) = if fetch_partially {
1444                    (fetch_response.header(), fetch_response.size) // `BODY.PEEK[HEADER]` goes to header() ...
1445                } else {
1446                    (fetch_response.body(), None) // ... while `BODY.PEEK[]` goes to body() - and includes header()
1447                };
1448
1449                if is_deleted {
1450                    info!(context, "Not processing deleted msg {}.", request_uid);
1451                    received_msgs_channel.send((request_uid, None)).await?;
1452                    continue;
1453                }
1454
1455                let body = if let Some(body) = body {
1456                    body
1457                } else {
1458                    info!(
1459                        context,
1460                        "Not processing message {} without a BODY.", request_uid
1461                    );
1462                    received_msgs_channel.send((request_uid, None)).await?;
1463                    continue;
1464                };
1465
1466                let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1467
1468                let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1469                    error!(
1470                        context,
1471                        "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1472                        request_uid
1473                    );
1474                    continue;
1475                };
1476
1477                info!(
1478                    context,
1479                    "Passing message UID {} to receive_imf().", request_uid
1480                );
1481                let res = receive_imf_inner(context, rfc724_mid, body, is_seen, partial).await;
1482                let received_msg = match res {
1483                    Err(err) => {
1484                        warn!(context, "receive_imf error: {err:#}.");
1485
1486                        let text = format!(
1487                            "❌ Failed to receive a message: {err:#}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
1488                        );
1489                        let mut msg = Message::new_text(text);
1490                        add_device_msg(context, None, Some(&mut msg)).await?;
1491                        None
1492                    }
1493                    Ok(msg) => msg,
1494                };
1495                received_msgs_channel
1496                    .send((request_uid, received_msg))
1497                    .await?;
1498            }
1499
1500            // If we don't process the whole response, IMAP client is left in a broken state where
1501            // it will try to process the rest of response as the next response.
1502            //
1503            // Make sure to not ignore the errors, because
1504            // if connection times out, it will return
1505            // infinite stream of `Some(Err(_))` results.
1506            while fetch_responses
1507                .try_next()
1508                .await
1509                .context("Failed to drain FETCH responses")?
1510                .is_some()
1511            {}
1512
1513            if count != request_uids.len() {
1514                warn!(
1515                    context,
1516                    "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1517                    count,
1518                    request_uids.len(),
1519                    request_uids,
1520                );
1521            } else {
1522                info!(
1523                    context,
1524                    "Successfully received {} UIDs.",
1525                    request_uids.len()
1526                );
1527            }
1528        }
1529
1530        Ok(())
1531    }
1532
1533    /// Retrieves server metadata if it is supported.
1534    ///
1535    /// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
1536    /// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
1537    /// metadata.
1538    pub(crate) async fn fetch_metadata(&mut self, context: &Context) -> Result<()> {
1539        if !self.can_metadata() {
1540            return Ok(());
1541        }
1542
1543        let mut lock = context.metadata.write().await;
1544        if let Some(ref mut old_metadata) = *lock {
1545            let now = time();
1546
1547            // Refresh TURN server credentials if they expire in 12 hours.
1548            if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1549                return Ok(());
1550            }
1551
1552            info!(context, "ICE servers expired, requesting new credentials.");
1553            let mailbox = "";
1554            let options = "";
1555            let metadata = self
1556                .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1557                .await?;
1558            let mut got_turn_server = false;
1559            for m in metadata {
1560                if m.entry == "/shared/vendor/deltachat/turn" {
1561                    if let Some(value) = m.value {
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
1576            if !got_turn_server {
1577                // Set expiration timestamp 7 days in the future so we don't request it again.
1578                old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1579                old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
1580            }
1581            return Ok(());
1582        }
1583
1584        info!(
1585            context,
1586            "Server supports metadata, retrieving server comment and admin contact."
1587        );
1588
1589        let mut comment = None;
1590        let mut admin = None;
1591        let mut iroh_relay = None;
1592        let mut ice_servers = None;
1593        let mut ice_servers_expiration_timestamp = 0;
1594
1595        let mailbox = "";
1596        let options = "";
1597        let metadata = self
1598            .get_metadata(
1599                mailbox,
1600                options,
1601                "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1602            )
1603            .await?;
1604        for m in metadata {
1605            match m.entry.as_ref() {
1606                "/shared/comment" => {
1607                    comment = m.value;
1608                }
1609                "/shared/admin" => {
1610                    admin = m.value;
1611                }
1612                "/shared/vendor/deltachat/irohrelay" => {
1613                    if let Some(value) = m.value {
1614                        if let Ok(url) = Url::parse(&value) {
1615                            iroh_relay = Some(url);
1616                        } else {
1617                            warn!(
1618                                context,
1619                                "Got invalid URL from iroh relay metadata: {:?}.", value
1620                            );
1621                        }
1622                    }
1623                }
1624                "/shared/vendor/deltachat/turn" => {
1625                    if let Some(value) = m.value {
1626                        match create_ice_servers_from_metadata(context, &value).await {
1627                            Ok((parsed_timestamp, parsed_ice_servers)) => {
1628                                ice_servers_expiration_timestamp = parsed_timestamp;
1629                                ice_servers = Some(parsed_ice_servers);
1630                            }
1631                            Err(err) => {
1632                                warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1633                            }
1634                        }
1635                    }
1636                }
1637                _ => {}
1638            }
1639        }
1640        let ice_servers = if let Some(ice_servers) = ice_servers {
1641            ice_servers
1642        } else {
1643            // Set expiration timestamp 7 days in the future so we don't request it again.
1644            ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1645            create_fallback_ice_servers(context).await?
1646        };
1647
1648        *lock = Some(ServerMetadata {
1649            comment,
1650            admin,
1651            iroh_relay,
1652            ice_servers,
1653            ice_servers_expiration_timestamp,
1654        });
1655        Ok(())
1656    }
1657
1658    /// Stores device token into /private/devicetoken IMAP METADATA of the Inbox.
1659    pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1660        if context.push_subscribed.load(Ordering::Relaxed) {
1661            return Ok(());
1662        }
1663
1664        let Some(device_token) = context.push_subscriber.device_token().await else {
1665            return Ok(());
1666        };
1667
1668        if self.can_metadata() && self.can_push() {
1669            let old_encrypted_device_token =
1670                context.get_config(Config::EncryptedDeviceToken).await?;
1671
1672            // Whether we need to update encrypted device token.
1673            let device_token_changed = old_encrypted_device_token.is_none()
1674                || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1675
1676            let new_encrypted_device_token;
1677            if device_token_changed {
1678                let encrypted_device_token = encrypt_device_token(&device_token)
1679                    .context("Failed to encrypt device token")?;
1680
1681                // We expect that the server supporting `XDELTAPUSH` capability
1682                // has non-synchronizing literals support as well:
1683                // <https://www.rfc-editor.org/rfc/rfc7888>.
1684                let encrypted_device_token_len = encrypted_device_token.len();
1685
1686                // Store device token saved on the server
1687                // to prevent storing duplicate tokens.
1688                // The server cannot deduplicate on its own
1689                // because encryption gives a different
1690                // result each time.
1691                context
1692                    .set_config_internal(Config::DeviceToken, Some(&device_token))
1693                    .await?;
1694                context
1695                    .set_config_internal(
1696                        Config::EncryptedDeviceToken,
1697                        Some(&encrypted_device_token),
1698                    )
1699                    .await?;
1700
1701                if encrypted_device_token_len <= 4096 {
1702                    new_encrypted_device_token = Some(encrypted_device_token);
1703                } else {
1704                    // If Apple or Google (FCM) gives us a very large token,
1705                    // do not even try to give it to IMAP servers.
1706                    //
1707                    // Limit of 4096 is arbitrarily selected
1708                    // to be the same as required by LITERAL- IMAP extension.
1709                    //
1710                    // Dovecot supports LITERAL+ and non-synchronizing literals
1711                    // of any length, but there is no reason for tokens
1712                    // to be that large even after OpenPGP encryption.
1713                    warn!(context, "Device token is too long for LITERAL-, ignoring.");
1714                    new_encrypted_device_token = None;
1715                }
1716            } else {
1717                new_encrypted_device_token = old_encrypted_device_token;
1718            }
1719
1720            // Store new encrypted device token on the server
1721            // even if it is the same as the old one.
1722            if let Some(encrypted_device_token) = new_encrypted_device_token {
1723                let folder = context
1724                    .get_config(Config::ConfiguredInboxFolder)
1725                    .await?
1726                    .context("INBOX is not configured")?;
1727
1728                self.run_command_and_check_ok(&format_setmetadata(
1729                    &folder,
1730                    &encrypted_device_token,
1731                ))
1732                .await
1733                .context("SETMETADATA command failed")?;
1734
1735                context.push_subscribed.store(true, Ordering::Relaxed);
1736            }
1737        } else if !context.push_subscriber.heartbeat_subscribed().await {
1738            let context = context.clone();
1739            // Subscribe for heartbeat notifications.
1740            tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1741        }
1742
1743        Ok(())
1744    }
1745}
1746
1747fn format_setmetadata(folder: &str, device_token: &str) -> String {
1748    let device_token_len = device_token.len();
1749    format!(
1750        "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1751    )
1752}
1753
1754impl Session {
1755    /// Returns success if we successfully set the flag or we otherwise
1756    /// think add_flag should not be retried: Disconnection during setting
1757    /// the flag, or other imap-errors, returns Ok as well.
1758    ///
1759    /// Returning error means that the operation can be retried.
1760    async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1761        if flag == "\\Deleted" {
1762            self.selected_folder_needs_expunge = true;
1763        }
1764        let query = format!("+FLAGS ({flag})");
1765        let mut responses = self
1766            .uid_store(uid_set, &query)
1767            .await
1768            .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1769        while let Some(_response) = responses.try_next().await? {
1770            // Read all the responses
1771        }
1772        Ok(())
1773    }
1774
1775    /// Attempts to configure mvbox.
1776    ///
1777    /// Tries to find any folder examining `folders` in the order they go. If none is found, tries
1778    /// to create any folder in the same order. This method does not use LIST command to ensure that
1779    /// configuration works even if mailbox lookup is forbidden via Access Control List (see
1780    /// <https://datatracker.ietf.org/doc/html/rfc4314>).
1781    ///
1782    /// Returns first found or created folder name.
1783    async fn configure_mvbox<'a>(
1784        &mut self,
1785        context: &Context,
1786        folders: &[&'a str],
1787        create_mvbox: bool,
1788    ) -> Result<Option<&'a str>> {
1789        // Close currently selected folder if needed.
1790        // We are going to select folders using low-level EXAMINE operations below.
1791        self.maybe_close_folder(context).await?;
1792
1793        for folder in folders {
1794            info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1795            let res = self.examine(&folder).await;
1796            if res.is_ok() {
1797                info!(
1798                    context,
1799                    "MVBOX-folder {:?} successfully selected, using it.", &folder
1800                );
1801                self.close().await?;
1802                // Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
1803                // emails moved before that wouldn't be fetched but considered "old" instead.
1804                let create = false;
1805                let folder_exists = self
1806                    .select_with_uidvalidity(context, folder, create)
1807                    .await?;
1808                ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1809                return Ok(Some(folder));
1810            }
1811        }
1812
1813        if !create_mvbox {
1814            return Ok(None);
1815        }
1816        // Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
1817        // the variants here.
1818        for folder in folders {
1819            match self
1820                .select_with_uidvalidity(context, folder, create_mvbox)
1821                .await
1822            {
1823                Ok(_) => {
1824                    info!(context, "MVBOX-folder {} created.", folder);
1825                    return Ok(Some(folder));
1826                }
1827                Err(err) => {
1828                    warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1829                }
1830            }
1831        }
1832        Ok(None)
1833    }
1834}
1835
1836impl Imap {
1837    pub(crate) async fn configure_folders(
1838        &mut self,
1839        context: &Context,
1840        session: &mut Session,
1841        create_mvbox: bool,
1842    ) -> Result<()> {
1843        let mut folders = session
1844            .list(Some(""), Some("*"))
1845            .await
1846            .context("list_folders failed")?;
1847        let mut delimiter = ".".to_string();
1848        let mut delimiter_is_default = true;
1849        let mut folder_configs = BTreeMap::new();
1850
1851        while let Some(folder) = folders.try_next().await? {
1852            info!(context, "Scanning folder: {:?}", folder);
1853
1854            // Update the delimiter iff there is a different one, but only once.
1855            if let Some(d) = folder.delimiter() {
1856                if delimiter_is_default && !d.is_empty() && delimiter != d {
1857                    delimiter = d.to_string();
1858                    delimiter_is_default = false;
1859                }
1860            }
1861
1862            let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1863            let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1864            if let Some(config) = folder_meaning.to_config() {
1865                // Always takes precedence
1866                folder_configs.insert(config, folder.name().to_string());
1867            } else if let Some(config) = folder_name_meaning.to_config() {
1868                // only set if none has been already set
1869                folder_configs
1870                    .entry(config)
1871                    .or_insert_with(|| folder.name().to_string());
1872            }
1873        }
1874        drop(folders);
1875
1876        info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1877
1878        let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1879        let mvbox_folder = session
1880            .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1881            .await
1882            .context("failed to configure mvbox")?;
1883
1884        context
1885            .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1886            .await?;
1887        if let Some(mvbox_folder) = mvbox_folder {
1888            info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1889            context
1890                .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1891                .await?;
1892        }
1893        for (config, name) in folder_configs {
1894            context.set_config_internal(config, Some(&name)).await?;
1895        }
1896        context
1897            .sql
1898            .set_raw_config_int(
1899                constants::DC_FOLDERS_CONFIGURED_KEY,
1900                constants::DC_FOLDERS_CONFIGURED_VERSION,
1901            )
1902            .await?;
1903
1904        info!(context, "FINISHED configuring IMAP-folders.");
1905        Ok(())
1906    }
1907}
1908
1909impl Session {
1910    /// Return whether the server sent an unsolicited EXISTS or FETCH response.
1911    ///
1912    /// Drains all responses from `session.unsolicited_responses` in the process.
1913    ///
1914    /// If this returns `true`, this means that new emails arrived
1915    /// or flags have been changed.
1916    /// In this case we may want to skip next IDLE and do a round
1917    /// of fetching new messages and synchronizing seen flags.
1918    fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1919        use UnsolicitedResponse::*;
1920        use async_imap::imap_proto::Response;
1921        use async_imap::imap_proto::ResponseCode;
1922
1923        let folder = self.selected_folder.as_deref().unwrap_or_default();
1924        let mut should_refetch = false;
1925        while let Ok(response) = self.unsolicited_responses.try_recv() {
1926            match response {
1927                Exists(_) => {
1928                    info!(
1929                        context,
1930                        "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1931                    );
1932                    should_refetch = true;
1933                }
1934
1935                Expunge(_) | Recent(_) => {}
1936                Other(ref response_data) => {
1937                    match response_data.parsed() {
1938                        Response::Fetch { .. } => {
1939                            info!(
1940                                context,
1941                                "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1942                            );
1943                            should_refetch = true;
1944                        }
1945
1946                        // We are not interested in the following responses and they are are
1947                        // sent quite frequently, so, we ignore them without logging them.
1948                        Response::Done {
1949                            code: Some(ResponseCode::CopyUid(_, _, _)),
1950                            ..
1951                        } => {}
1952
1953                        _ => {
1954                            info!(context, "{folder:?}: got unsolicited response {response:?}")
1955                        }
1956                    }
1957                }
1958                _ => {
1959                    info!(context, "{folder:?}: got unsolicited response {response:?}")
1960                }
1961            }
1962        }
1963        Ok(should_refetch)
1964    }
1965}
1966
1967async fn should_move_out_of_spam(
1968    context: &Context,
1969    headers: &[mailparse::MailHeader<'_>],
1970) -> Result<bool> {
1971    if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1972        // If this is a chat message (i.e. has a ChatVersion header), then this might be
1973        // a securejoin message. We can't find out at this point as we didn't prefetch
1974        // the SecureJoin header. So, we always move chat messages out of Spam.
1975        // Two possibilities to change this would be:
1976        // 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
1977        // `fetch_new_messages()`, and then let `receive_imf()` check
1978        // if it's a spam message and should be hidden.
1979        // 2. Or add a flag to the ChatVersion header that this is a securejoin
1980        // request, and return `true` here only if the message has this flag.
1981        // `receive_imf()` can then check if the securejoin request is valid.
1982        return Ok(true);
1983    }
1984
1985    if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1986        if msg.chat_blocked != Blocked::Not {
1987            // Blocked or contact request message in the spam folder, leave it there.
1988            return Ok(false);
1989        }
1990    } else {
1991        let from = match mimeparser::get_from(headers) {
1992            Some(f) => f,
1993            None => return Ok(false),
1994        };
1995        // No chat found.
1996        let (from_id, blocked_contact, _origin) =
1997            match from_field_to_contact_id(context, &from, None, true, true)
1998                .await
1999                .context("from_field_to_contact_id")?
2000            {
2001                Some(res) => res,
2002                None => {
2003                    warn!(
2004                        context,
2005                        "Contact with From address {:?} cannot exist, not moving out of spam", from
2006                    );
2007                    return Ok(false);
2008                }
2009            };
2010        if blocked_contact {
2011            // Contact is blocked, leave the message in spam.
2012            return Ok(false);
2013        }
2014
2015        if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2016            if chat_id_blocked.blocked != Blocked::Not {
2017                return Ok(false);
2018            }
2019        } else if from_id != ContactId::SELF {
2020            // No chat with this contact found.
2021            return Ok(false);
2022        }
2023    }
2024
2025    Ok(true)
2026}
2027
2028/// Returns target folder for a message found in the Spam folder.
2029/// If this returns None, the message will not be moved out of the
2030/// Spam folder, and as `fetch_new_messages()` doesn't download
2031/// messages from the Spam folder, the message will be ignored.
2032async fn spam_target_folder_cfg(
2033    context: &Context,
2034    headers: &[mailparse::MailHeader<'_>],
2035) -> Result<Option<Config>> {
2036    if !should_move_out_of_spam(context, headers).await? {
2037        return Ok(None);
2038    }
2039
2040    if needs_move_to_mvbox(context, headers).await?
2041        // If OnlyFetchMvbox is set, we don't want to move the message to
2042        // the inbox where we wouldn't fetch it again:
2043        || context.get_config_bool(Config::OnlyFetchMvbox).await?
2044    {
2045        Ok(Some(Config::ConfiguredMvboxFolder))
2046    } else {
2047        Ok(Some(Config::ConfiguredInboxFolder))
2048    }
2049}
2050
2051/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
2052/// the message needs to be moved from `folder`. Otherwise returns `None`.
2053pub async fn target_folder_cfg(
2054    context: &Context,
2055    folder: &str,
2056    folder_meaning: FolderMeaning,
2057    headers: &[mailparse::MailHeader<'_>],
2058) -> Result<Option<Config>> {
2059    if context.is_mvbox(folder).await? {
2060        return Ok(None);
2061    }
2062
2063    if folder_meaning == FolderMeaning::Spam {
2064        spam_target_folder_cfg(context, headers).await
2065    } else if folder_meaning == FolderMeaning::Inbox
2066        && needs_move_to_mvbox(context, headers).await?
2067    {
2068        Ok(Some(Config::ConfiguredMvboxFolder))
2069    } else {
2070        Ok(None)
2071    }
2072}
2073
2074pub async fn target_folder(
2075    context: &Context,
2076    folder: &str,
2077    folder_meaning: FolderMeaning,
2078    headers: &[mailparse::MailHeader<'_>],
2079) -> Result<String> {
2080    match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2081        Some(config) => match context.get_config(config).await? {
2082            Some(target) => Ok(target),
2083            None => Ok(folder.to_string()),
2084        },
2085        None => Ok(folder.to_string()),
2086    }
2087}
2088
2089async fn needs_move_to_mvbox(
2090    context: &Context,
2091    headers: &[mailparse::MailHeader<'_>],
2092) -> Result<bool> {
2093    let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2094    if !context.get_config_bool(Config::IsChatmail).await?
2095        && has_chat_version
2096        && headers
2097            .get_header_value(HeaderDef::AutoSubmitted)
2098            .filter(|val| val.eq_ignore_ascii_case("auto-generated"))
2099            .is_some()
2100    {
2101        if let Some(from) = mimeparser::get_from(headers) {
2102            if context.is_self_addr(&from.addr).await? {
2103                return Ok(true);
2104            }
2105        }
2106    }
2107    if !context.get_config_bool(Config::MvboxMove).await? {
2108        return Ok(false);
2109    }
2110
2111    if headers
2112        .get_header_value(HeaderDef::AutocryptSetupMessage)
2113        .is_some()
2114    {
2115        // do not move setup messages;
2116        // there may be a non-delta device that wants to handle it
2117        return Ok(false);
2118    }
2119
2120    if has_chat_version {
2121        Ok(true)
2122    } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2123        match parent.is_dc_message {
2124            MessengerMessage::No => Ok(false),
2125            MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2126        }
2127    } else {
2128        Ok(false)
2129    }
2130}
2131
2132/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
2133// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
2134// however, if we fail to find out the sent-folder,
2135// only watching this folder is not working. at least, this is no show stopper.
2136// CAVE: if possible, take care not to add a name here that is "sent" in one language
2137// but sth. different in others - a hard job.
2138fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2139    // source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
2140    const SPAM_NAMES: &[&str] = &[
2141        "spam",
2142        "junk",
2143        "Correio electrónico não solicitado",
2144        "Correo basura",
2145        "Lixo",
2146        "Nettsøppel",
2147        "Nevyžádaná pošta",
2148        "No solicitado",
2149        "Ongewenst",
2150        "Posta indesiderata",
2151        "Skräp",
2152        "Wiadomości-śmieci",
2153        "Önemsiz",
2154        "Ανεπιθύμητα",
2155        "Спам",
2156        "垃圾邮件",
2157        "垃圾郵件",
2158        "迷惑メール",
2159        "스팸",
2160    ];
2161    const TRASH_NAMES: &[&str] = &[
2162        "Trash",
2163        "Bin",
2164        "Caixote do lixo",
2165        "Cestino",
2166        "Corbeille",
2167        "Papelera",
2168        "Papierkorb",
2169        "Papirkurv",
2170        "Papperskorgen",
2171        "Prullenbak",
2172        "Rubujo",
2173        "Κάδος απορριμμάτων",
2174        "Корзина",
2175        "Кошик",
2176        "ゴミ箱",
2177        "垃圾桶",
2178        "已删除邮件",
2179        "휴지통",
2180    ];
2181    let lower = folder_name.to_lowercase();
2182
2183    if lower == "inbox" {
2184        FolderMeaning::Inbox
2185    } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2186        FolderMeaning::Spam
2187    } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2188        FolderMeaning::Trash
2189    } else {
2190        FolderMeaning::Unknown
2191    }
2192}
2193
2194fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2195    for attr in folder_attrs {
2196        match attr {
2197            NameAttribute::Trash => return FolderMeaning::Trash,
2198            NameAttribute::Junk => return FolderMeaning::Spam,
2199            NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2200            NameAttribute::Extension(label) => {
2201                match label.as_ref() {
2202                    "\\Spam" => return FolderMeaning::Spam,
2203                    "\\Important" => return FolderMeaning::Virtual,
2204                    _ => {}
2205                };
2206            }
2207            _ => {}
2208        }
2209    }
2210    FolderMeaning::Unknown
2211}
2212
2213pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2214    match get_folder_meaning_by_attrs(folder.attributes()) {
2215        FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2216        meaning => meaning,
2217    }
2218}
2219
2220/// Parses the headers from the FETCH result.
2221fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2222    match prefetch_msg.header() {
2223        Some(header_bytes) => {
2224            let (headers, _) = mailparse::parse_headers(header_bytes)?;
2225            Ok(headers)
2226        }
2227        None => Ok(Vec::new()),
2228    }
2229}
2230
2231pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2232    headers
2233        .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2234        .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2235        .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2236}
2237
2238pub(crate) fn create_message_id() -> String {
2239    format!("{}{}", GENERATED_PREFIX, create_id())
2240}
2241
2242/// Returns chat by prefetched headers.
2243async fn prefetch_get_chat(
2244    context: &Context,
2245    headers: &[mailparse::MailHeader<'_>],
2246) -> Result<Option<chat::Chat>> {
2247    let parent = get_prefetch_parent_message(context, headers).await?;
2248    if let Some(parent) = &parent {
2249        return Ok(Some(
2250            chat::Chat::load_from_db(context, parent.get_chat_id()).await?,
2251        ));
2252    }
2253
2254    Ok(None)
2255}
2256
2257/// Determines whether the message should be downloaded based on prefetched headers.
2258pub(crate) async fn prefetch_should_download(
2259    context: &Context,
2260    headers: &[mailparse::MailHeader<'_>],
2261    message_id: &str,
2262    mut flags: impl Iterator<Item = Flag<'_>>,
2263) -> Result<bool> {
2264    if message::rfc724_mid_exists(context, message_id)
2265        .await?
2266        .is_some()
2267    {
2268        markseen_on_imap_table(context, message_id).await?;
2269        return Ok(false);
2270    }
2271
2272    // We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
2273    // the further process).
2274
2275    if let Some(chat) = prefetch_get_chat(context, headers).await? {
2276        if chat.typ == Chattype::Group && !chat.id.is_special() {
2277            // This might be a group command, like removing a group member.
2278            // We really need to fetch this to avoid inconsistent group state.
2279            return Ok(true);
2280        }
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            if last.end + 1 == current {
2529                last.end = current;
2530                continue;
2531            }
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;