deltachat/imap/
select_folder.rs

1//! # IMAP folder selection module.
2
3use anyhow::Context as _;
4
5use super::session::Session as ImapSession;
6use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
7use crate::context::Context;
8use crate::ensure_and_debug_assert;
9use crate::log::warn;
10
11type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    #[error(
16        "Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}"
17    )]
18    NoFolder(String, String),
19
20    #[error("IMAP other error: {0}")]
21    Other(String),
22}
23
24impl From<anyhow::Error> for Error {
25    fn from(err: anyhow::Error) -> Error {
26        Error::Other(format!("{err:#}"))
27    }
28}
29
30impl ImapSession {
31    /// Issues a CLOSE command if selected folder needs expunge,
32    /// i.e. if Delta Chat marked a message there as deleted previously.
33    ///
34    /// CLOSE is considerably faster than an EXPUNGE
35    /// because no EXPUNGE responses are sent, see
36    /// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
37    pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
38        if let Some(folder) = &self.selected_folder
39            && self.selected_folder_needs_expunge
40        {
41            info!(context, "Expunge messages in {folder:?}.");
42
43            self.close().await.context("IMAP close/expunge failed")?;
44            info!(context, "Close/expunge succeeded.");
45            self.selected_folder = None;
46            self.selected_folder_needs_expunge = false;
47            self.new_mail = false;
48        }
49        Ok(())
50    }
51
52    /// Selects a folder, possibly updating uid_validity and, if needed,
53    /// expunging the folder to remove delete-marked messages.
54    /// Returns whether a new folder was selected.
55    async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
56        // if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
57        // if there is _no_ new folder, we continue as we might want to expunge below.
58        if let Some(selected_folder) = &self.selected_folder
59            && folder == selected_folder
60        {
61            return Ok(NewlySelected::No);
62        }
63
64        // deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
65        self.maybe_close_folder(context).await?;
66
67        // select new folder
68        let res = if self.can_condstore() {
69            self.select_condstore(folder).await
70        } else {
71            self.select(folder).await
72        };
73
74        // <https://tools.ietf.org/html/rfc3501#section-6.3.1>
75        // says that if the server reports select failure we are in
76        // authenticated (not-select) state.
77
78        match res {
79            Ok(mailbox) => {
80                info!(context, "Selected folder {folder:?}.");
81                self.selected_folder = Some(folder.to_string());
82                self.selected_mailbox = Some(mailbox);
83                Ok(NewlySelected::Yes)
84            }
85            Err(async_imap::error::Error::No(response)) => {
86                Err(Error::NoFolder(folder.to_string(), response))
87            }
88            Err(err) => Err(Error::Other(err.to_string())),
89        }
90    }
91
92    /// Selects a folder. Tries to create it once and select again if the folder does not exist.
93    pub(super) async fn select_or_create_folder(
94        &mut self,
95        context: &Context,
96        folder: &str,
97    ) -> anyhow::Result<NewlySelected> {
98        match self.select_folder(context, folder).await {
99            Ok(newly_selected) => Ok(newly_selected),
100            Err(err) => match err {
101                Error::NoFolder(..) => {
102                    info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it.");
103                    let create_res = self.create(folder).await;
104                    if let Err(ref err) = create_res {
105                        info!(context, "Couldn't select folder, then create() failed: {err:#}.");
106                        // Need to recheck, could have been created in parallel.
107                    }
108                    let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
109                    if select_res.is_err() {
110                        create_res?;
111                    }
112                    select_res
113                }
114                _ => Err(err).with_context(|| format!("failed to select folder {folder} with error other than NO, not trying to create it")),
115            },
116        }
117    }
118
119    /// Selects a folder optionally creating it and takes care of UIDVALIDITY changes. Returns false
120    /// iff `folder` doesn't exist.
121    ///
122    /// When selecting a folder for the first time, sets the uid_next to the current
123    /// mailbox.uid_next so that no old emails are fetched.
124    ///
125    /// Updates `self.new_mail` if folder was previously unselected
126    /// and new mails are detected after selecting,
127    /// i.e. UIDNEXT advanced while the folder was closed.
128    pub(crate) async fn select_with_uidvalidity(
129        &mut self,
130        context: &Context,
131        folder: &str,
132        create: bool,
133    ) -> anyhow::Result<bool> {
134        let newly_selected = if create {
135            self.select_or_create_folder(context, folder)
136                .await
137                .with_context(|| format!("Failed to select or create folder {folder:?}"))?
138        } else {
139            match self.select_folder(context, folder).await {
140                Ok(newly_selected) => newly_selected,
141                Err(err) => match err {
142                    Error::NoFolder(..) => return Ok(false),
143                    _ => {
144                        return Err(err)
145                            .with_context(|| format!("Failed to select folder {folder:?}"))?;
146                    }
147                },
148            }
149        };
150        let transport_id = self.transport_id();
151
152        // Folders should not be selected when transport_id is not assigned yet
153        // because we cannot save UID validity then.
154        ensure_and_debug_assert!(
155            transport_id > 0,
156            "Cannot select folder when transport ID is unknown"
157        );
158
159        let mailbox = self
160            .selected_mailbox
161            .as_mut()
162            .with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
163
164        let old_uid_validity = get_uidvalidity(context, transport_id, folder)
165            .await
166            .with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
167        let old_uid_next = get_uid_next(context, transport_id, folder)
168            .await
169            .with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
170
171        let new_uid_validity = mailbox
172            .uid_validity
173            .with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
174        let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
175            Some(uid_next)
176        } else {
177            warn!(
178                context,
179                "SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
180            );
181
182            // RFC 3501 says STATUS command SHOULD NOT be used
183            // on the currently selected mailbox because the same
184            // information can be obtained by other means,
185            // such as reading SELECT response.
186            //
187            // However, it also says that UIDNEXT is REQUIRED
188            // in the SELECT response and if we are here,
189            // it is actually not returned.
190            //
191            // In particular, Winmail Pro Mail Server 5.1.0616
192            // never returns UIDNEXT in SELECT response,
193            // but responds to "STATUS INBOX (UIDNEXT)" command.
194            let status = self
195                .inner
196                .status(folder, "(UIDNEXT)")
197                .await
198                .with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
199
200            if status.uid_next.is_none() {
201                // This happens with mail.163.com as of 2023-11-26.
202                // It does not return UIDNEXT on SELECT and returns invalid
203                // `* STATUS "INBOX" ()` response on explicit request for UIDNEXT.
204                warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT.");
205            }
206            status.uid_next
207        };
208        mailbox.uid_next = new_uid_next;
209
210        if new_uid_validity == old_uid_validity {
211            if newly_selected == NewlySelected::Yes {
212                if let Some(new_uid_next) = new_uid_next {
213                    if new_uid_next < old_uid_next {
214                        warn!(
215                            context,
216                            "The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
217                        );
218                        set_uid_next(context, transport_id, folder, new_uid_next).await?;
219                        self.resync_request_sender.try_send(()).ok();
220                    }
221
222                    // If UIDNEXT changed, there are new emails.
223                    self.new_mail |= new_uid_next != old_uid_next;
224                } else {
225                    warn!(
226                        context,
227                        "Folder {folder} was just selected but we failed to determine UIDNEXT, assume that it has new mail."
228                    );
229                    self.new_mail = true;
230                }
231            }
232
233            return Ok(true);
234        }
235
236        // UIDVALIDITY is modified, reset highest seen MODSEQ.
237        set_modseq(context, transport_id, folder, 0).await?;
238
239        // ==============  uid_validity has changed or is being set the first time.  ==============
240
241        let new_uid_next = new_uid_next.unwrap_or_default();
242        set_uid_next(context, transport_id, folder, new_uid_next).await?;
243        set_uidvalidity(context, transport_id, folder, new_uid_validity).await?;
244        self.new_mail = true;
245
246        // Collect garbage entries in `imap` table.
247        context
248            .sql
249            .execute(
250                "DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?",
251                (transport_id, &folder, new_uid_validity),
252            )
253            .await?;
254
255        if old_uid_validity != 0 || old_uid_next != 0 {
256            self.resync_request_sender.try_send(()).ok();
257        }
258        info!(
259            context,
260            "transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.",
261        );
262        Ok(true)
263    }
264}
265
266#[derive(PartialEq, Debug, Copy, Clone, Eq)]
267pub(crate) enum NewlySelected {
268    /// The folder was newly selected during this call to select_folder().
269    Yes,
270    /// No SELECT command was run because the folder already was selected
271    /// and self.config.selected_mailbox was not updated (so, e.g. it may contain an outdated uid_next)
272    No,
273}