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