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            info!(context, "Team profile, skipping seen flag synchronization.");
1144            return Ok(());
1145        }
1146
1147        let transport_id = self.transport_id();
1148        let rows = context
1149            .sql
1150            .query_map_vec(
1151                "SELECT imap.id, uid, folder FROM imap, imap_markseen
1152                 WHERE imap.id = imap_markseen.id
1153                 AND imap.transport_id=?
1154                 AND target = folder
1155                 ORDER BY folder, uid",
1156                (transport_id,),
1157                |row| {
1158                    let rowid: i64 = row.get(0)?;
1159                    let uid: u32 = row.get(1)?;
1160                    let folder: String = row.get(2)?;
1161                    Ok((rowid, uid, folder))
1162                },
1163            )
1164            .await?;
1165
1166        for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
1167            let create = false;
1168            let folder_exists = match self.select_with_uidvalidity(context, &folder, create).await {
1169                Err(err) => {
1170                    warn!(
1171                        context,
1172                        "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
1173                    );
1174                    continue;
1175                }
1176                Ok(folder_exists) => folder_exists,
1177            };
1178            if !folder_exists {
1179                warn!(context, "store_seen_flags_on_imap: No folder {folder}.");
1180            } else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
1181                warn!(
1182                    context,
1183                    "Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
1184                );
1185                continue;
1186            } else {
1187                info!(
1188                    context,
1189                    "Marked messages {} in folder {} as seen.", uid_set, folder
1190                );
1191            }
1192            context
1193                .sql
1194                .transaction(|transaction| {
1195                    let mut stmt = transaction.prepare("DELETE FROM imap_markseen WHERE id = ?")?;
1196                    for rowid in rowid_set {
1197                        stmt.execute((rowid,))?;
1198                    }
1199                    Ok(())
1200                })
1201                .await
1202                .context("Cannot remove messages marked as seen from imap_markseen table")?;
1203        }
1204
1205        Ok(())
1206    }
1207
1208    /// Synchronizes `\Seen` flags using `CONDSTORE` extension.
1209    pub(crate) async fn sync_seen_flags(&mut self, context: &Context, folder: &str) -> Result<()> {
1210        if !self.can_condstore() {
1211            info!(
1212                context,
1213                "Server does not support CONDSTORE, skipping flag synchronization."
1214            );
1215            return Ok(());
1216        }
1217
1218        if context.get_config_bool(Config::TeamProfile).await? {
1219            info!(context, "Team profile, skipping seen flag synchronization.");
1220            return Ok(());
1221        }
1222
1223        let create = false;
1224        let folder_exists = self
1225            .select_with_uidvalidity(context, folder, create)
1226            .await
1227            .context("Failed to select folder")?;
1228        if !folder_exists {
1229            return Ok(());
1230        }
1231
1232        let mailbox = self
1233            .selected_mailbox
1234            .as_ref()
1235            .with_context(|| format!("No mailbox selected, folder: {folder}"))?;
1236
1237        // Check if the mailbox supports MODSEQ.
1238        // We are not interested in actual value of HIGHESTMODSEQ.
1239        if mailbox.highest_modseq.is_none() {
1240            info!(
1241                context,
1242                "Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
1243            );
1244            return Ok(());
1245        }
1246
1247        let transport_id = self.transport_id();
1248        let mut updated_chat_ids = BTreeSet::new();
1249        let uid_validity = get_uidvalidity(context, transport_id, folder)
1250            .await
1251            .with_context(|| format!("failed to get UID validity for folder {folder}"))?;
1252        let mut highest_modseq = get_modseq(context, transport_id, folder)
1253            .await
1254            .with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
1255        let mut list = self
1256            .uid_fetch("1:*", format!("(FLAGS) (CHANGEDSINCE {highest_modseq})"))
1257            .await
1258            .context("failed to fetch flags")?;
1259
1260        let mut got_unsolicited_fetch = false;
1261
1262        while let Some(fetch) = list
1263            .try_next()
1264            .await
1265            .context("failed to get FETCH result")?
1266        {
1267            let uid = if let Some(uid) = fetch.uid {
1268                uid
1269            } else {
1270                info!(context, "FETCH result contains no UID, skipping");
1271                got_unsolicited_fetch = true;
1272                continue;
1273            };
1274            let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
1275            if is_seen
1276                && let Some(chat_id) = mark_seen_by_uid(context, transport_id, folder, uid_validity, uid)
1277                    .await
1278                    .with_context(|| {
1279                        format!("Transport {transport_id}: Failed to update seen status for msg {folder}/{uid}")
1280                    })?
1281            {
1282                updated_chat_ids.insert(chat_id);
1283            }
1284
1285            if let Some(modseq) = fetch.modseq {
1286                if modseq > highest_modseq {
1287                    highest_modseq = modseq;
1288                }
1289            } else {
1290                warn!(context, "FETCH result contains no MODSEQ");
1291            }
1292        }
1293        drop(list);
1294
1295        if got_unsolicited_fetch {
1296            // We got unsolicited FETCH, which means some flags
1297            // have been modified while our request was in progress.
1298            // We may or may not have these new flags as a part of the response,
1299            // so better skip next IDLE and do another round of flag synchronization.
1300            self.new_mail = true;
1301        }
1302
1303        set_modseq(context, transport_id, folder, highest_modseq)
1304            .await
1305            .with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
1306        if !updated_chat_ids.is_empty() {
1307            context.on_archived_chats_maybe_noticed();
1308        }
1309        for updated_chat_id in updated_chat_ids {
1310            context.emit_event(EventType::MsgsNoticed(updated_chat_id));
1311            chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
1312        }
1313
1314        Ok(())
1315    }
1316
1317    /// Gets the from, to and bcc addresses from all existing outgoing emails.
1318    pub async fn get_all_recipients(&mut self, context: &Context) -> Result<Vec<SingleInfo>> {
1319        let mut uids: Vec<_> = self
1320            .uid_search(get_imap_self_sent_search_command(context).await?)
1321            .await?
1322            .into_iter()
1323            .collect();
1324        uids.sort_unstable();
1325
1326        let mut result = Vec::new();
1327        for (_, uid_set) in build_sequence_sets(&uids)? {
1328            let mut list = self
1329                .uid_fetch(uid_set, "(UID BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC)])")
1330                .await
1331                .context("IMAP Could not fetch")?;
1332
1333            while let Some(msg) = list.try_next().await? {
1334                match get_fetch_headers(&msg) {
1335                    Ok(headers) => {
1336                        if let Some(from) = mimeparser::get_from(&headers)
1337                            && context.is_self_addr(&from.addr).await?
1338                        {
1339                            result.extend(mimeparser::get_recipients(&headers));
1340                        }
1341                    }
1342                    Err(err) => {
1343                        warn!(context, "{}", err);
1344                        continue;
1345                    }
1346                };
1347            }
1348        }
1349        Ok(result)
1350    }
1351
1352    /// Fetches a list of messages by server UID.
1353    ///
1354    /// Sends pairs of UID and info about each downloaded message to the provided channel.
1355    /// Received message info is optional because UID may be ignored
1356    /// if the message has a `\Deleted` flag.
1357    ///
1358    /// The channel is used to return the results because the function may fail
1359    /// due to network errors before it finishes fetching all the messages.
1360    /// In this case caller still may want to process all the results
1361    /// received over the channel and persist last seen UID in the database
1362    /// before bubbling up the failure.
1363    ///
1364    /// If the message is incorrect or there is a failure to write a message to the database,
1365    /// it is skipped and the error is logged.
1366    pub(crate) async fn fetch_many_msgs(
1367        &mut self,
1368        context: &Context,
1369        folder: &str,
1370        request_uids: Vec<u32>,
1371        uid_message_ids: &BTreeMap<u32, String>,
1372        received_msgs_channel: Sender<(u32, Option<ReceivedMsg>)>,
1373    ) -> Result<()> {
1374        if request_uids.is_empty() {
1375            return Ok(());
1376        }
1377
1378        for (request_uids, set) in build_sequence_sets(&request_uids)? {
1379            info!(context, "Starting UID FETCH of message set \"{}\".", set);
1380            let mut fetch_responses = self.uid_fetch(&set, BODY_FULL).await.with_context(|| {
1381                format!("fetching messages {} from folder \"{}\"", &set, folder)
1382            })?;
1383
1384            // Map from UIDs to unprocessed FETCH results. We put unprocessed FETCH results here
1385            // when we want to process other messages first.
1386            let mut uid_msgs = HashMap::with_capacity(request_uids.len());
1387
1388            let mut count = 0;
1389            for &request_uid in &request_uids {
1390                // Check if FETCH response is already in `uid_msgs`.
1391                let mut fetch_response = uid_msgs.remove(&request_uid);
1392
1393                // Try to find a requested UID in returned FETCH responses.
1394                while fetch_response.is_none() {
1395                    let Some(next_fetch_response) = fetch_responses
1396                        .try_next()
1397                        .await
1398                        .context("Failed to process IMAP FETCH result")?
1399                    else {
1400                        // No more FETCH responses received from the server.
1401                        break;
1402                    };
1403
1404                    if let Some(next_uid) = next_fetch_response.uid {
1405                        if next_uid == request_uid {
1406                            fetch_response = Some(next_fetch_response);
1407                        } else if !request_uids.contains(&next_uid) {
1408                            // (size of `request_uids` is bounded by IMAP command length limit,
1409                            // search in this vector is always fast)
1410
1411                            // Unwanted UIDs are possible because of unsolicited responses, e.g. if
1412                            // another client changes \Seen flag on a message after we do a prefetch but
1413                            // before fetch. It's not an error if we receive such unsolicited response.
1414                            info!(
1415                                context,
1416                                "Skipping not requested FETCH response for UID {}.", next_uid
1417                            );
1418                        } else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
1419                            warn!(context, "Got duplicated UID {}.", next_uid);
1420                        }
1421                    } else {
1422                        info!(context, "Skipping FETCH response without UID.");
1423                    }
1424                }
1425
1426                let fetch_response = match fetch_response {
1427                    Some(fetch) => fetch,
1428                    None => {
1429                        warn!(
1430                            context,
1431                            "Missed UID {} in the server response.", request_uid
1432                        );
1433                        continue;
1434                    }
1435                };
1436                count += 1;
1437
1438                let is_deleted = fetch_response.flags().any(|flag| flag == Flag::Deleted);
1439                let body = fetch_response.body();
1440
1441                if is_deleted {
1442                    info!(context, "Not processing deleted msg {}.", request_uid);
1443                    received_msgs_channel.send((request_uid, None)).await?;
1444                    continue;
1445                }
1446
1447                let body = if let Some(body) = body {
1448                    body
1449                } else {
1450                    info!(
1451                        context,
1452                        "Not processing message {} without a BODY.", request_uid
1453                    );
1454                    received_msgs_channel.send((request_uid, None)).await?;
1455                    continue;
1456                };
1457
1458                let is_seen = fetch_response.flags().any(|flag| flag == Flag::Seen);
1459
1460                let Some(rfc724_mid) = uid_message_ids.get(&request_uid) else {
1461                    error!(
1462                        context,
1463                        "No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
1464                        request_uid
1465                    );
1466                    continue;
1467                };
1468
1469                info!(
1470                    context,
1471                    "Passing message UID {} to receive_imf().", request_uid
1472                );
1473                let res = receive_imf_inner(context, rfc724_mid, body, is_seen).await;
1474                let received_msg = match res {
1475                    Err(err) => {
1476                        warn!(context, "receive_imf error: {err:#}.");
1477
1478                        let text = format!(
1479                            "❌ 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/.",
1480                        );
1481                        let mut msg = Message::new_text(text);
1482                        add_device_msg(context, None, Some(&mut msg)).await?;
1483                        None
1484                    }
1485                    Ok(msg) => msg,
1486                };
1487                received_msgs_channel
1488                    .send((request_uid, received_msg))
1489                    .await?;
1490            }
1491
1492            // If we don't process the whole response, IMAP client is left in a broken state where
1493            // it will try to process the rest of response as the next response.
1494            //
1495            // Make sure to not ignore the errors, because
1496            // if connection times out, it will return
1497            // infinite stream of `Some(Err(_))` results.
1498            while fetch_responses
1499                .try_next()
1500                .await
1501                .context("Failed to drain FETCH responses")?
1502                .is_some()
1503            {}
1504
1505            if count != request_uids.len() {
1506                warn!(
1507                    context,
1508                    "Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
1509                    count,
1510                    request_uids.len(),
1511                    request_uids,
1512                );
1513            } else {
1514                info!(
1515                    context,
1516                    "Successfully received {} UIDs.",
1517                    request_uids.len()
1518                );
1519            }
1520        }
1521
1522        Ok(())
1523    }
1524
1525    /// Retrieves server metadata if it is supported, otherwise uses fallback one.
1526    ///
1527    /// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
1528    /// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
1529    /// metadata.
1530    pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
1531        let mut lock = context.metadata.write().await;
1532
1533        if !self.can_metadata() {
1534            *lock = Some(Default::default());
1535        }
1536        if let Some(ref mut old_metadata) = *lock {
1537            let now = time();
1538
1539            // Refresh TURN server credentials if they expire in 12 hours.
1540            if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
1541                return Ok(());
1542            }
1543
1544            let mut got_turn_server = false;
1545            if self.can_metadata() {
1546                info!(context, "ICE servers expired, requesting new credentials.");
1547                let mailbox = "";
1548                let options = "";
1549                let metadata = self
1550                    .get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
1551                    .await?;
1552                for m in metadata {
1553                    if m.entry == "/shared/vendor/deltachat/turn"
1554                        && let Some(value) = m.value
1555                    {
1556                        match create_ice_servers_from_metadata(&value).await {
1557                            Ok((parsed_timestamp, parsed_ice_servers)) => {
1558                                old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
1559                                old_metadata.ice_servers = parsed_ice_servers;
1560                                got_turn_server = true;
1561                            }
1562                            Err(err) => {
1563                                warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1564                            }
1565                        }
1566                    }
1567                }
1568            }
1569            if !got_turn_server {
1570                info!(context, "Will use fallback ICE servers.");
1571                // Set expiration timestamp 7 days in the future so we don't request it again.
1572                old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1573                old_metadata.ice_servers = create_fallback_ice_servers();
1574            }
1575            return Ok(());
1576        }
1577
1578        info!(
1579            context,
1580            "Server supports metadata, retrieving server comment and admin contact."
1581        );
1582
1583        let mut comment = None;
1584        let mut admin = None;
1585        let mut iroh_relay = None;
1586        let mut ice_servers = None;
1587        let mut ice_servers_expiration_timestamp = 0;
1588
1589        let mailbox = "";
1590        let options = "";
1591        let metadata = self
1592            .get_metadata(
1593                mailbox,
1594                options,
1595                "(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
1596            )
1597            .await?;
1598        for m in metadata {
1599            match m.entry.as_ref() {
1600                "/shared/comment" => {
1601                    comment = m.value;
1602                }
1603                "/shared/admin" => {
1604                    admin = m.value;
1605                }
1606                "/shared/vendor/deltachat/irohrelay" => {
1607                    if let Some(value) = m.value {
1608                        if let Ok(url) = Url::parse(&value) {
1609                            iroh_relay = Some(url);
1610                        } else {
1611                            warn!(
1612                                context,
1613                                "Got invalid URL from iroh relay metadata: {:?}.", value
1614                            );
1615                        }
1616                    }
1617                }
1618                "/shared/vendor/deltachat/turn" => {
1619                    if let Some(value) = m.value {
1620                        match create_ice_servers_from_metadata(&value).await {
1621                            Ok((parsed_timestamp, parsed_ice_servers)) => {
1622                                ice_servers_expiration_timestamp = parsed_timestamp;
1623                                ice_servers = Some(parsed_ice_servers);
1624                            }
1625                            Err(err) => {
1626                                warn!(context, "Failed to parse TURN server metadata: {err:#}.");
1627                            }
1628                        }
1629                    }
1630                }
1631                _ => {}
1632            }
1633        }
1634        let ice_servers = if let Some(ice_servers) = ice_servers {
1635            ice_servers
1636        } else {
1637            // Set expiration timestamp 7 days in the future so we don't request it again.
1638            ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
1639            create_fallback_ice_servers()
1640        };
1641
1642        *lock = Some(ServerMetadata {
1643            comment,
1644            admin,
1645            iroh_relay,
1646            ice_servers,
1647            ice_servers_expiration_timestamp,
1648        });
1649        Ok(())
1650    }
1651
1652    /// Stores device token into /private/devicetoken IMAP METADATA of the Inbox.
1653    pub(crate) async fn register_token(&mut self, context: &Context) -> Result<()> {
1654        if context.push_subscribed.load(Ordering::Relaxed) {
1655            return Ok(());
1656        }
1657
1658        let Some(device_token) = context.push_subscriber.device_token().await else {
1659            return Ok(());
1660        };
1661
1662        if self.can_metadata() && self.can_push() {
1663            let old_encrypted_device_token =
1664                context.get_config(Config::EncryptedDeviceToken).await?;
1665
1666            // Whether we need to update encrypted device token.
1667            let device_token_changed = old_encrypted_device_token.is_none()
1668                || context.get_config(Config::DeviceToken).await?.as_ref() != Some(&device_token);
1669
1670            let new_encrypted_device_token;
1671            if device_token_changed {
1672                let encrypted_device_token = encrypt_device_token(&device_token)
1673                    .context("Failed to encrypt device token")?;
1674
1675                // We expect that the server supporting `XDELTAPUSH` capability
1676                // has non-synchronizing literals support as well:
1677                // <https://www.rfc-editor.org/rfc/rfc7888>.
1678                let encrypted_device_token_len = encrypted_device_token.len();
1679
1680                // Store device token saved on the server
1681                // to prevent storing duplicate tokens.
1682                // The server cannot deduplicate on its own
1683                // because encryption gives a different
1684                // result each time.
1685                context
1686                    .set_config_internal(Config::DeviceToken, Some(&device_token))
1687                    .await?;
1688                context
1689                    .set_config_internal(
1690                        Config::EncryptedDeviceToken,
1691                        Some(&encrypted_device_token),
1692                    )
1693                    .await?;
1694
1695                if encrypted_device_token_len <= 4096 {
1696                    new_encrypted_device_token = Some(encrypted_device_token);
1697                } else {
1698                    // If Apple or Google (FCM) gives us a very large token,
1699                    // do not even try to give it to IMAP servers.
1700                    //
1701                    // Limit of 4096 is arbitrarily selected
1702                    // to be the same as required by LITERAL- IMAP extension.
1703                    //
1704                    // Dovecot supports LITERAL+ and non-synchronizing literals
1705                    // of any length, but there is no reason for tokens
1706                    // to be that large even after OpenPGP encryption.
1707                    warn!(context, "Device token is too long for LITERAL-, ignoring.");
1708                    new_encrypted_device_token = None;
1709                }
1710            } else {
1711                new_encrypted_device_token = old_encrypted_device_token;
1712            }
1713
1714            // Store new encrypted device token on the server
1715            // even if it is the same as the old one.
1716            if let Some(encrypted_device_token) = new_encrypted_device_token {
1717                let folder = context
1718                    .get_config(Config::ConfiguredInboxFolder)
1719                    .await?
1720                    .context("INBOX is not configured")?;
1721
1722                self.run_command_and_check_ok(&format_setmetadata(
1723                    &folder,
1724                    &encrypted_device_token,
1725                ))
1726                .await
1727                .context("SETMETADATA command failed")?;
1728
1729                context.push_subscribed.store(true, Ordering::Relaxed);
1730            }
1731        } else if !context.push_subscriber.heartbeat_subscribed().await {
1732            let context = context.clone();
1733            // Subscribe for heartbeat notifications.
1734            tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
1735        }
1736
1737        Ok(())
1738    }
1739}
1740
1741fn format_setmetadata(folder: &str, device_token: &str) -> String {
1742    let device_token_len = device_token.len();
1743    format!(
1744        "SETMETADATA \"{folder}\" (/private/devicetoken {{{device_token_len}+}}\r\n{device_token})"
1745    )
1746}
1747
1748impl Session {
1749    /// Returns success if we successfully set the flag or we otherwise
1750    /// think add_flag should not be retried: Disconnection during setting
1751    /// the flag, or other imap-errors, returns Ok as well.
1752    ///
1753    /// Returning error means that the operation can be retried.
1754    async fn add_flag_finalized_with_set(&mut self, uid_set: &str, flag: &str) -> Result<()> {
1755        if flag == "\\Deleted" {
1756            self.selected_folder_needs_expunge = true;
1757        }
1758        let query = format!("+FLAGS ({flag})");
1759        let mut responses = self
1760            .uid_store(uid_set, &query)
1761            .await
1762            .with_context(|| format!("IMAP failed to store: ({uid_set}, {query})"))?;
1763        while let Some(_response) = responses.try_next().await? {
1764            // Read all the responses
1765        }
1766        Ok(())
1767    }
1768
1769    /// Attempts to configure mvbox.
1770    ///
1771    /// Tries to find any folder examining `folders` in the order they go. If none is found, tries
1772    /// to create any folder in the same order. This method does not use LIST command to ensure that
1773    /// configuration works even if mailbox lookup is forbidden via Access Control List (see
1774    /// <https://datatracker.ietf.org/doc/html/rfc4314>).
1775    ///
1776    /// Returns first found or created folder name.
1777    async fn configure_mvbox<'a>(
1778        &mut self,
1779        context: &Context,
1780        folders: &[&'a str],
1781        create_mvbox: bool,
1782    ) -> Result<Option<&'a str>> {
1783        // Close currently selected folder if needed.
1784        // We are going to select folders using low-level EXAMINE operations below.
1785        self.maybe_close_folder(context).await?;
1786
1787        for folder in folders {
1788            info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
1789            let res = self.examine(&folder).await;
1790            if res.is_ok() {
1791                info!(
1792                    context,
1793                    "MVBOX-folder {:?} successfully selected, using it.", &folder
1794                );
1795                self.close().await?;
1796                // Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise
1797                // emails moved before that wouldn't be fetched but considered "old" instead.
1798                let create = false;
1799                let folder_exists = self
1800                    .select_with_uidvalidity(context, folder, create)
1801                    .await?;
1802                ensure!(folder_exists, "No MVBOX folder {:?}??", &folder);
1803                return Ok(Some(folder));
1804            }
1805        }
1806
1807        if !create_mvbox {
1808            return Ok(None);
1809        }
1810        // Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
1811        // the variants here.
1812        for folder in folders {
1813            match self
1814                .select_with_uidvalidity(context, folder, create_mvbox)
1815                .await
1816            {
1817                Ok(_) => {
1818                    info!(context, "MVBOX-folder {} created.", folder);
1819                    return Ok(Some(folder));
1820                }
1821                Err(err) => {
1822                    warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
1823                }
1824            }
1825        }
1826        Ok(None)
1827    }
1828}
1829
1830impl Imap {
1831    pub(crate) async fn configure_folders(
1832        &mut self,
1833        context: &Context,
1834        session: &mut Session,
1835        create_mvbox: bool,
1836    ) -> Result<()> {
1837        let mut folders = session
1838            .list(Some(""), Some("*"))
1839            .await
1840            .context("list_folders failed")?;
1841        let mut delimiter = ".".to_string();
1842        let mut delimiter_is_default = true;
1843        let mut folder_configs = BTreeMap::new();
1844
1845        while let Some(folder) = folders.try_next().await? {
1846            info!(context, "Scanning folder: {:?}", folder);
1847
1848            // Update the delimiter iff there is a different one, but only once.
1849            if let Some(d) = folder.delimiter()
1850                && delimiter_is_default
1851                && !d.is_empty()
1852                && delimiter != d
1853            {
1854                delimiter = d.to_string();
1855                delimiter_is_default = false;
1856            }
1857
1858            let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
1859            let folder_name_meaning = get_folder_meaning_by_name(folder.name());
1860            if let Some(config) = folder_meaning.to_config() {
1861                // Always takes precedence
1862                folder_configs.insert(config, folder.name().to_string());
1863            } else if let Some(config) = folder_name_meaning.to_config() {
1864                // only set if none has been already set
1865                folder_configs
1866                    .entry(config)
1867                    .or_insert_with(|| folder.name().to_string());
1868            }
1869        }
1870        drop(folders);
1871
1872        info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
1873
1874        let fallback_folder = format!("INBOX{delimiter}DeltaChat");
1875        let mvbox_folder = session
1876            .configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
1877            .await
1878            .context("failed to configure mvbox")?;
1879
1880        context
1881            .set_config_internal(Config::ConfiguredInboxFolder, Some("INBOX"))
1882            .await?;
1883        if let Some(mvbox_folder) = mvbox_folder {
1884            info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
1885            context
1886                .set_config_internal(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
1887                .await?;
1888        }
1889        for (config, name) in folder_configs {
1890            context.set_config_internal(config, Some(&name)).await?;
1891        }
1892        context
1893            .sql
1894            .set_raw_config_int(
1895                constants::DC_FOLDERS_CONFIGURED_KEY,
1896                constants::DC_FOLDERS_CONFIGURED_VERSION,
1897            )
1898            .await?;
1899
1900        info!(context, "FINISHED configuring IMAP-folders.");
1901        Ok(())
1902    }
1903}
1904
1905impl Session {
1906    /// Return whether the server sent an unsolicited EXISTS or FETCH response.
1907    ///
1908    /// Drains all responses from `session.unsolicited_responses` in the process.
1909    ///
1910    /// If this returns `true`, this means that new emails arrived
1911    /// or flags have been changed.
1912    /// In this case we may want to skip next IDLE and do a round
1913    /// of fetching new messages and synchronizing seen flags.
1914    fn drain_unsolicited_responses(&self, context: &Context) -> Result<bool> {
1915        use UnsolicitedResponse::*;
1916        use async_imap::imap_proto::Response;
1917        use async_imap::imap_proto::ResponseCode;
1918
1919        let folder = self.selected_folder.as_deref().unwrap_or_default();
1920        let mut should_refetch = false;
1921        while let Ok(response) = self.unsolicited_responses.try_recv() {
1922            match response {
1923                Exists(_) => {
1924                    info!(
1925                        context,
1926                        "Need to refetch {folder:?}, got unsolicited EXISTS {response:?}"
1927                    );
1928                    should_refetch = true;
1929                }
1930
1931                Expunge(_) | Recent(_) => {}
1932                Other(ref response_data) => {
1933                    match response_data.parsed() {
1934                        Response::Fetch { .. } => {
1935                            info!(
1936                                context,
1937                                "Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
1938                            );
1939                            should_refetch = true;
1940                        }
1941
1942                        // We are not interested in the following responses and they are are
1943                        // sent quite frequently, so, we ignore them without logging them.
1944                        Response::Done {
1945                            code: Some(ResponseCode::CopyUid(_, _, _)),
1946                            ..
1947                        } => {}
1948
1949                        _ => {
1950                            info!(context, "{folder:?}: got unsolicited response {response:?}")
1951                        }
1952                    }
1953                }
1954                _ => {
1955                    info!(context, "{folder:?}: got unsolicited response {response:?}")
1956                }
1957            }
1958        }
1959        Ok(should_refetch)
1960    }
1961}
1962
1963async fn should_move_out_of_spam(
1964    context: &Context,
1965    headers: &[mailparse::MailHeader<'_>],
1966) -> Result<bool> {
1967    if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
1968        // If this is a chat message (i.e. has a ChatVersion header), then this might be
1969        // a securejoin message. We can't find out at this point as we didn't prefetch
1970        // the SecureJoin header. So, we always move chat messages out of Spam.
1971        // Two possibilities to change this would be:
1972        // 1. Remove the `&& !context.is_spam_folder(folder).await?` check from
1973        // `fetch_new_messages()`, and then let `receive_imf()` check
1974        // if it's a spam message and should be hidden.
1975        // 2. Or add a flag to the ChatVersion header that this is a securejoin
1976        // request, and return `true` here only if the message has this flag.
1977        // `receive_imf()` can then check if the securejoin request is valid.
1978        return Ok(true);
1979    }
1980
1981    if let Some(msg) = get_prefetch_parent_message(context, headers).await? {
1982        if msg.chat_blocked != Blocked::Not {
1983            // Blocked or contact request message in the spam folder, leave it there.
1984            return Ok(false);
1985        }
1986    } else {
1987        let from = match mimeparser::get_from(headers) {
1988            Some(f) => f,
1989            None => return Ok(false),
1990        };
1991        // No chat found.
1992        let (from_id, blocked_contact, _origin) =
1993            match from_field_to_contact_id(context, &from, None, true, true)
1994                .await
1995                .context("from_field_to_contact_id")?
1996            {
1997                Some(res) => res,
1998                None => {
1999                    warn!(
2000                        context,
2001                        "Contact with From address {:?} cannot exist, not moving out of spam", from
2002                    );
2003                    return Ok(false);
2004                }
2005            };
2006        if blocked_contact {
2007            // Contact is blocked, leave the message in spam.
2008            return Ok(false);
2009        }
2010
2011        if let Some(chat_id_blocked) = ChatIdBlocked::lookup_by_contact(context, from_id).await? {
2012            if chat_id_blocked.blocked != Blocked::Not {
2013                return Ok(false);
2014            }
2015        } else if from_id != ContactId::SELF {
2016            // No chat with this contact found.
2017            return Ok(false);
2018        }
2019    }
2020
2021    Ok(true)
2022}
2023
2024/// Returns target folder for a message found in the Spam folder.
2025/// If this returns None, the message will not be moved out of the
2026/// Spam folder, and as `fetch_new_messages()` doesn't download
2027/// messages from the Spam folder, the message will be ignored.
2028async fn spam_target_folder_cfg(
2029    context: &Context,
2030    headers: &[mailparse::MailHeader<'_>],
2031) -> Result<Option<Config>> {
2032    if !should_move_out_of_spam(context, headers).await? {
2033        return Ok(None);
2034    }
2035
2036    if needs_move_to_mvbox(context, headers).await?
2037        // If OnlyFetchMvbox is set, we don't want to move the message to
2038        // the inbox where we wouldn't fetch it again:
2039        || context.get_config_bool(Config::OnlyFetchMvbox).await?
2040    {
2041        Ok(Some(Config::ConfiguredMvboxFolder))
2042    } else {
2043        Ok(Some(Config::ConfiguredInboxFolder))
2044    }
2045}
2046
2047/// Returns `ConfiguredInboxFolder` or `ConfiguredMvboxFolder` if
2048/// the message needs to be moved from `folder`. Otherwise returns `None`.
2049pub async fn target_folder_cfg(
2050    context: &Context,
2051    folder: &str,
2052    folder_meaning: FolderMeaning,
2053    headers: &[mailparse::MailHeader<'_>],
2054) -> Result<Option<Config>> {
2055    if context.is_mvbox(folder).await? {
2056        return Ok(None);
2057    }
2058
2059    if folder_meaning == FolderMeaning::Spam {
2060        spam_target_folder_cfg(context, headers).await
2061    } else if folder_meaning == FolderMeaning::Inbox
2062        && needs_move_to_mvbox(context, headers).await?
2063    {
2064        Ok(Some(Config::ConfiguredMvboxFolder))
2065    } else {
2066        Ok(None)
2067    }
2068}
2069
2070pub async fn target_folder(
2071    context: &Context,
2072    folder: &str,
2073    folder_meaning: FolderMeaning,
2074    headers: &[mailparse::MailHeader<'_>],
2075) -> Result<String> {
2076    match target_folder_cfg(context, folder, folder_meaning, headers).await? {
2077        Some(config) => match context.get_config(config).await? {
2078            Some(target) => Ok(target),
2079            None => Ok(folder.to_string()),
2080        },
2081        None => Ok(folder.to_string()),
2082    }
2083}
2084
2085async fn needs_move_to_mvbox(
2086    context: &Context,
2087    headers: &[mailparse::MailHeader<'_>],
2088) -> Result<bool> {
2089    let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2090    if !context.get_config_bool(Config::MvboxMove).await? {
2091        return Ok(false);
2092    }
2093
2094    if headers
2095        .get_header_value(HeaderDef::AutocryptSetupMessage)
2096        .is_some()
2097    {
2098        // do not move setup messages;
2099        // there may be a non-delta device that wants to handle it
2100        return Ok(false);
2101    }
2102
2103    if has_chat_version {
2104        Ok(true)
2105    } else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
2106        match parent.is_dc_message {
2107            MessengerMessage::No => Ok(false),
2108            MessengerMessage::Yes | MessengerMessage::Reply => Ok(true),
2109        }
2110    } else {
2111        Ok(false)
2112    }
2113}
2114
2115/// Try to get the folder meaning by the name of the folder only used if the server does not support XLIST.
2116// TODO: lots languages missing - maybe there is a list somewhere on other MUAs?
2117// however, if we fail to find out the sent-folder,
2118// only watching this folder is not working. at least, this is no show stopper.
2119// CAVE: if possible, take care not to add a name here that is "sent" in one language
2120// but sth. different in others - a hard job.
2121fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
2122    // source: <https://stackoverflow.com/questions/2185391/localized-gmail-imap-folders>
2123    const SPAM_NAMES: &[&str] = &[
2124        "spam",
2125        "junk",
2126        "Correio electrónico não solicitado",
2127        "Correo basura",
2128        "Lixo",
2129        "Nettsøppel",
2130        "Nevyžádaná pošta",
2131        "No solicitado",
2132        "Ongewenst",
2133        "Posta indesiderata",
2134        "Skräp",
2135        "Wiadomości-śmieci",
2136        "Önemsiz",
2137        "Ανεπιθύμητα",
2138        "Спам",
2139        "垃圾邮件",
2140        "垃圾郵件",
2141        "迷惑メール",
2142        "스팸",
2143    ];
2144    const TRASH_NAMES: &[&str] = &[
2145        "Trash",
2146        "Bin",
2147        "Caixote do lixo",
2148        "Cestino",
2149        "Corbeille",
2150        "Papelera",
2151        "Papierkorb",
2152        "Papirkurv",
2153        "Papperskorgen",
2154        "Prullenbak",
2155        "Rubujo",
2156        "Κάδος απορριμμάτων",
2157        "Корзина",
2158        "Кошик",
2159        "ゴミ箱",
2160        "垃圾桶",
2161        "已删除邮件",
2162        "휴지통",
2163    ];
2164    let lower = folder_name.to_lowercase();
2165
2166    if lower == "inbox" {
2167        FolderMeaning::Inbox
2168    } else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2169        FolderMeaning::Spam
2170    } else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
2171        FolderMeaning::Trash
2172    } else {
2173        FolderMeaning::Unknown
2174    }
2175}
2176
2177fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning {
2178    for attr in folder_attrs {
2179        match attr {
2180            NameAttribute::Trash => return FolderMeaning::Trash,
2181            NameAttribute::Junk => return FolderMeaning::Spam,
2182            NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
2183            NameAttribute::Extension(label) => {
2184                match label.as_ref() {
2185                    "\\Spam" => return FolderMeaning::Spam,
2186                    "\\Important" => return FolderMeaning::Virtual,
2187                    _ => {}
2188                };
2189            }
2190            _ => {}
2191        }
2192    }
2193    FolderMeaning::Unknown
2194}
2195
2196pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
2197    match get_folder_meaning_by_attrs(folder.attributes()) {
2198        FolderMeaning::Unknown => get_folder_meaning_by_name(folder.name()),
2199        meaning => meaning,
2200    }
2201}
2202
2203/// Parses the headers from the FETCH result.
2204fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
2205    match prefetch_msg.header() {
2206        Some(header_bytes) => {
2207            let (headers, _) = mailparse::parse_headers(header_bytes)?;
2208            Ok(headers)
2209        }
2210        None => Ok(Vec::new()),
2211    }
2212}
2213
2214pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
2215    headers
2216        .get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
2217        .or_else(|| headers.get_header_value(HeaderDef::MessageId))
2218        .and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
2219}
2220
2221pub(crate) fn create_message_id() -> String {
2222    format!("{}{}", GENERATED_PREFIX, create_id())
2223}
2224
2225/// Determines whether the message should be downloaded based on prefetched headers.
2226pub(crate) async fn prefetch_should_download(
2227    context: &Context,
2228    headers: &[mailparse::MailHeader<'_>],
2229    message_id: &str,
2230    mut flags: impl Iterator<Item = Flag<'_>>,
2231) -> Result<bool> {
2232    if message::rfc724_mid_download_tried(context, message_id).await? {
2233        if let Some(from) = mimeparser::get_from(headers)
2234            && context.is_self_addr(&from.addr).await?
2235        {
2236            markseen_on_imap_table(context, message_id).await?;
2237        }
2238        return Ok(false);
2239    }
2240
2241    // We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
2242    // the further process).
2243
2244    let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
2245        let from = from.to_ascii_lowercase();
2246        from.contains("mailer-daemon") || from.contains("mail-daemon")
2247    } else {
2248        false
2249    };
2250
2251    // Autocrypt Setup Message should be shown even if it is from non-chat client.
2252    let is_autocrypt_setup_message = headers
2253        .get_header_value(HeaderDef::AutocryptSetupMessage)
2254        .is_some();
2255
2256    let from = match mimeparser::get_from(headers) {
2257        Some(f) => f,
2258        None => return Ok(false),
2259    };
2260    let (_from_id, blocked_contact, origin) =
2261        match from_field_to_contact_id(context, &from, None, true, true).await? {
2262            Some(res) => res,
2263            None => return Ok(false),
2264        };
2265    // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact.
2266    // (prevent_rename is the last argument of from_field_to_contact_id())
2267
2268    if flags.any(|f| f == Flag::Draft) {
2269        info!(context, "Ignoring draft message");
2270        return Ok(false);
2271    }
2272
2273    let is_chat_message = headers.get_header_value(HeaderDef::ChatVersion).is_some();
2274    let accepted_contact = origin.is_known();
2275    let is_reply_to_chat_message = get_prefetch_parent_message(context, headers)
2276        .await?
2277        .map(|parent| match parent.is_dc_message {
2278            MessengerMessage::No => false,
2279            MessengerMessage::Yes | MessengerMessage::Reply => true,
2280        })
2281        .unwrap_or_default();
2282
2283    let show_emails =
2284        ShowEmails::from_i32(context.get_config_int(Config::ShowEmails).await?).unwrap_or_default();
2285
2286    let show = is_autocrypt_setup_message
2287        || match show_emails {
2288            ShowEmails::Off => is_chat_message || is_reply_to_chat_message,
2289            ShowEmails::AcceptedContacts => {
2290                is_chat_message || is_reply_to_chat_message || accepted_contact
2291            }
2292            ShowEmails::All => true,
2293        };
2294
2295    let should_download = (show && !blocked_contact) || maybe_ndn;
2296    Ok(should_download)
2297}
2298
2299/// Marks messages in `msgs` table as seen, searching for them by UID.
2300///
2301/// Returns updated chat ID if any message was marked as seen.
2302async fn mark_seen_by_uid(
2303    context: &Context,
2304    transport_id: u32,
2305    folder: &str,
2306    uid_validity: u32,
2307    uid: u32,
2308) -> Result<Option<ChatId>> {
2309    if let Some((msg_id, chat_id)) = context
2310        .sql
2311        .query_row_optional(
2312            "SELECT id, chat_id FROM msgs
2313                 WHERE id > 9 AND rfc724_mid IN (
2314                   SELECT rfc724_mid FROM imap
2315                   WHERE transport_id=?
2316                   AND folder=?
2317                   AND uidvalidity=?
2318                   AND uid=?
2319                   LIMIT 1
2320                 )",
2321            (transport_id, &folder, uid_validity, uid),
2322            |row| {
2323                let msg_id: MsgId = row.get(0)?;
2324                let chat_id: ChatId = row.get(1)?;
2325                Ok((msg_id, chat_id))
2326            },
2327        )
2328        .await
2329        .with_context(|| format!("failed to get msg and chat ID for IMAP message {folder}/{uid}"))?
2330    {
2331        let updated = context
2332            .sql
2333            .execute(
2334                "UPDATE msgs SET state=?1
2335                     WHERE (state=?2 OR state=?3)
2336                     AND id=?4",
2337                (
2338                    MessageState::InSeen,
2339                    MessageState::InFresh,
2340                    MessageState::InNoticed,
2341                    msg_id,
2342                ),
2343            )
2344            .await
2345            .with_context(|| format!("failed to update msg {msg_id} state"))?
2346            > 0;
2347
2348        if updated {
2349            msg_id
2350                .start_ephemeral_timer(context)
2351                .await
2352                .with_context(|| format!("failed to start ephemeral timer for message {msg_id}"))?;
2353            Ok(Some(chat_id))
2354        } else {
2355            // Message state has not changed.
2356            Ok(None)
2357        }
2358    } else {
2359        // There is no message is `msgs` table matching the given UID.
2360        Ok(None)
2361    }
2362}
2363
2364/// Schedule marking the message as Seen on IMAP by adding all known IMAP messages corresponding to
2365/// the given Message-ID to `imap_markseen` table.
2366pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str) -> Result<()> {
2367    context
2368        .sql
2369        .execute(
2370            "INSERT OR IGNORE INTO imap_markseen (id)
2371             SELECT id FROM imap WHERE rfc724_mid=?",
2372            (message_id,),
2373        )
2374        .await?;
2375    context.scheduler.interrupt_inbox().await;
2376
2377    Ok(())
2378}
2379
2380/// uid_next is the next unique identifier value from the last time we fetched a folder
2381/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2382/// This function is used to update our uid_next after fetching messages.
2383pub(crate) async fn set_uid_next(
2384    context: &Context,
2385    transport_id: u32,
2386    folder: &str,
2387    uid_next: u32,
2388) -> Result<()> {
2389    context
2390        .sql
2391        .execute(
2392            "INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
2393                ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
2394            (transport_id, folder, uid_next),
2395        )
2396        .await?;
2397    Ok(())
2398}
2399
2400/// uid_next is the next unique identifier value from the last time we fetched a folder
2401/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
2402/// This method returns the uid_next from the last time we fetched messages.
2403/// We can compare this to the current uid_next to find out whether there are new messages
2404/// and fetch from this value on to get all new messages.
2405async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2406    Ok(context
2407        .sql
2408        .query_get_value(
2409            "SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
2410            (transport_id, folder),
2411        )
2412        .await?
2413        .unwrap_or(0))
2414}
2415
2416pub(crate) async fn set_uidvalidity(
2417    context: &Context,
2418    transport_id: u32,
2419    folder: &str,
2420    uidvalidity: u32,
2421) -> Result<()> {
2422    context
2423        .sql
2424        .execute(
2425            "INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
2426                ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
2427            (transport_id, folder, uidvalidity),
2428        )
2429        .await?;
2430    Ok(())
2431}
2432
2433async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
2434    Ok(context
2435        .sql
2436        .query_get_value(
2437            "SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
2438            (transport_id, folder),
2439        )
2440        .await?
2441        .unwrap_or(0))
2442}
2443
2444pub(crate) async fn set_modseq(
2445    context: &Context,
2446    transport_id: u32,
2447    folder: &str,
2448    modseq: u64,
2449) -> Result<()> {
2450    context
2451        .sql
2452        .execute(
2453            "INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
2454                ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
2455            (transport_id, folder, modseq),
2456        )
2457        .await?;
2458    Ok(())
2459}
2460
2461async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
2462    Ok(context
2463        .sql
2464        .query_get_value(
2465            "SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
2466            (transport_id, folder),
2467        )
2468        .await?
2469        .unwrap_or(0))
2470}
2471
2472/// Compute the imap search expression for all self-sent mails (for all self addresses)
2473pub(crate) async fn get_imap_self_sent_search_command(context: &Context) -> Result<String> {
2474    // See https://www.rfc-editor.org/rfc/rfc3501#section-6.4.4 for syntax of SEARCH and OR
2475    let mut search_command = format!("FROM \"{}\"", context.get_primary_self_addr().await?);
2476
2477    for item in context.get_secondary_self_addrs().await? {
2478        search_command = format!("OR ({search_command}) (FROM \"{item}\")");
2479    }
2480
2481    Ok(search_command)
2482}
2483
2484/// Whether to ignore fetching messages from a folder.
2485///
2486/// This caters for the [`Config::OnlyFetchMvbox`] setting which means mails from folders
2487/// not explicitly watched should not be fetched.
2488async fn should_ignore_folder(
2489    context: &Context,
2490    folder: &str,
2491    folder_meaning: FolderMeaning,
2492) -> Result<bool> {
2493    if !context.get_config_bool(Config::OnlyFetchMvbox).await? {
2494        return Ok(false);
2495    }
2496    Ok(!(context.is_mvbox(folder).await? || folder_meaning == FolderMeaning::Spam))
2497}
2498
2499/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
2500/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
2501/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
2502fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
2503    // first, try to find consecutive ranges:
2504    let mut ranges: Vec<UidRange> = vec![];
2505
2506    for &current in uids {
2507        if let Some(last) = ranges.last_mut()
2508            && last.end + 1 == current
2509        {
2510            last.end = current;
2511            continue;
2512        }
2513
2514        ranges.push(UidRange {
2515            start: current,
2516            end: current,
2517        });
2518    }
2519
2520    // Second, sort the uids into uid sets that are each below ~1000 characters
2521    let mut result = vec![];
2522    let (mut last_uids, mut last_str) = (Vec::new(), String::new());
2523    for range in ranges {
2524        last_uids.reserve((range.end - range.start + 1).try_into()?);
2525        (range.start..=range.end).for_each(|u| last_uids.push(u));
2526        if !last_str.is_empty() {
2527            last_str.push(',');
2528        }
2529        last_str.push_str(&range.to_string());
2530
2531        if last_str.len() > 990 {
2532            result.push((take(&mut last_uids), take(&mut last_str)));
2533        }
2534    }
2535    result.push((last_uids, last_str));
2536
2537    result.retain(|(_, s)| !s.is_empty());
2538    Ok(result)
2539}
2540
2541struct UidRange {
2542    start: u32,
2543    end: u32,
2544    // If start == end, then this range represents a single number
2545}
2546
2547impl std::fmt::Display for UidRange {
2548    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2549        if self.start == self.end {
2550            write!(f, "{}", self.start)
2551        } else {
2552            write!(f, "{}:{}", self.start, self.end)
2553        }
2554    }
2555}
2556async fn add_all_recipients_as_contacts(
2557    context: &Context,
2558    session: &mut Session,
2559    folder: Config,
2560) -> Result<()> {
2561    let mailbox = if let Some(m) = context.get_config(folder).await? {
2562        m
2563    } else {
2564        info!(
2565            context,
2566            "Folder {} is not configured, skipping fetching contacts from it.", folder
2567        );
2568        return Ok(());
2569    };
2570    let create = false;
2571    let folder_exists = session
2572        .select_with_uidvalidity(context, &mailbox, create)
2573        .await
2574        .with_context(|| format!("could not select {mailbox}"))?;
2575    if !folder_exists {
2576        return Ok(());
2577    }
2578
2579    let recipients = session
2580        .get_all_recipients(context)
2581        .await
2582        .context("could not get recipients")?;
2583
2584    let mut any_modified = false;
2585    for recipient in recipients {
2586        let recipient_addr = match ContactAddress::new(&recipient.addr) {
2587            Err(err) => {
2588                warn!(
2589                    context,
2590                    "Could not add contact for recipient with address {:?}: {:#}",
2591                    recipient.addr,
2592                    err
2593                );
2594                continue;
2595            }
2596            Ok(recipient_addr) => recipient_addr,
2597        };
2598
2599        let (_, modified) = Contact::add_or_lookup(
2600            context,
2601            &recipient.display_name.unwrap_or_default(),
2602            &recipient_addr,
2603            Origin::OutgoingTo,
2604        )
2605        .await?;
2606        if modified != Modifier::None {
2607            any_modified = true;
2608        }
2609    }
2610    if any_modified {
2611        context.emit_event(EventType::ContactsChanged(None));
2612    }
2613
2614    Ok(())
2615}
2616
2617#[cfg(test)]
2618mod imap_tests;