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