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