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