deltachat/
imap.rs

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