deltachat/
imap.rs

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