deltachat/
imap.rs

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