deltachat/imap/
select_folder.rs1use 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 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 async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
56 if let Some(selected_folder) = &self.selected_folder
59 && folder == selected_folder
60 {
61 return Ok(NewlySelected::No);
62 }
63
64 self.maybe_close_folder(context).await?;
66
67 let res = if self.can_condstore() {
69 self.select_condstore(folder).await
70 } else {
71 self.select(folder).await
72 };
73
74 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 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 }
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 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 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 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 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 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 set_modseq(context, transport_id, folder, 0).await?;
238
239 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 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 Yes,
270 No,
273}