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