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 = self.select(folder).await;
69
70 let transport_id = self.transport_id();
71
72 match res {
77 Ok(mailbox) => {
78 info!(
79 context,
80 "Transport {transport_id}: Selected folder {folder:?}."
81 );
82 self.selected_folder = Some(folder.to_string());
83 self.selected_mailbox = Some(mailbox);
84 Ok(NewlySelected::Yes)
85 }
86 Err(async_imap::error::Error::No(response)) => {
87 Err(Error::NoFolder(folder.to_string(), response))
88 }
89 Err(err) => Err(Error::Other(err.to_string())),
90 }
91 }
92
93 pub(crate) async fn select_with_uidvalidity(
103 &mut self,
104 context: &Context,
105 folder: &str,
106 ) -> anyhow::Result<bool> {
107 let newly_selected = match self.select_folder(context, folder).await {
108 Ok(newly_selected) => newly_selected,
109 Err(err) => match err {
110 Error::NoFolder(..) => return Ok(false),
111 _ => {
112 return Err(err)
113 .with_context(|| format!("Failed to select folder {folder:?}"))?;
114 }
115 },
116 };
117 let transport_id = self.transport_id();
118
119 ensure_and_debug_assert!(
122 transport_id > 0,
123 "Cannot select folder when transport ID is unknown"
124 );
125
126 let mailbox = self
127 .selected_mailbox
128 .as_mut()
129 .with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
130
131 let old_uid_validity = get_uidvalidity(context, transport_id, folder)
132 .await
133 .with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
134 let old_uid_next = get_uid_next(context, transport_id, folder)
135 .await
136 .with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
137
138 let new_uid_validity = mailbox
139 .uid_validity
140 .with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
141 let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
142 Some(uid_next)
143 } else {
144 warn!(
145 context,
146 "SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
147 );
148
149 let status = self
162 .inner
163 .status(folder, "(UIDNEXT)")
164 .await
165 .with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
166
167 if status.uid_next.is_none() {
168 warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT.");
172 }
173 status.uid_next
174 };
175 mailbox.uid_next = new_uid_next;
176
177 if new_uid_validity == old_uid_validity {
178 if newly_selected == NewlySelected::Yes {
179 if let Some(new_uid_next) = new_uid_next {
180 if new_uid_next < old_uid_next {
181 warn!(
182 context,
183 "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...",
184 );
185 set_uid_next(context, transport_id, folder, new_uid_next).await?;
186 self.resync_request_sender.try_send(()).ok();
187 }
188
189 self.new_mail |= new_uid_next != old_uid_next;
191 } else {
192 warn!(
193 context,
194 "Folder {folder} was just selected but we failed to determine UIDNEXT, assume that it has new mail."
195 );
196 self.new_mail = true;
197 }
198 }
199
200 return Ok(true);
201 }
202
203 set_modseq(context, transport_id, folder, 0).await?;
205
206 let new_uid_next = new_uid_next.unwrap_or_default();
209 set_uid_next(context, transport_id, folder, new_uid_next).await?;
210 set_uidvalidity(context, transport_id, folder, new_uid_validity).await?;
211 self.new_mail = true;
212
213 context
215 .sql
216 .execute(
217 "DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?",
218 (transport_id, &folder, new_uid_validity),
219 )
220 .await?;
221
222 if old_uid_validity != 0 || old_uid_next != 0 {
223 self.resync_request_sender.try_send(()).ok();
224 }
225 info!(
226 context,
227 "transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.",
228 );
229 Ok(true)
230 }
231}
232
233#[derive(PartialEq, Debug, Copy, Clone, Eq)]
234pub(crate) enum NewlySelected {
235 Yes,
237 No,
240}