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