deltachat/
imap.rs

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