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