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