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