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