| | |
| |
|
| | use anyhow::Context as _; |
| |
|
| | use super::session::Session as ImapSession; |
| | use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity}; |
| | use crate::context::Context; |
| | use crate::ensure_and_debug_assert; |
| | use crate::log::warn; |
| |
|
| | type Result<T> = std::result::Result<T, Error>; |
| |
|
| | #[derive(Debug, thiserror::Error)] |
| | pub enum Error { |
| | #[error( |
| | "Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}" |
| | )] |
| | NoFolder(String, String), |
| |
|
| | #[error("IMAP other error: {0}")] |
| | Other(String), |
| | } |
| |
|
| | impl From<anyhow::Error> for Error { |
| | fn from(err: anyhow::Error) -> Error { |
| | Error::Other(format!("{err:#}")) |
| | } |
| | } |
| |
|
| | impl ImapSession { |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> { |
| | if let Some(folder) = &self.selected_folder |
| | && self.selected_folder_needs_expunge |
| | { |
| | info!(context, "Expunge messages in {folder:?}."); |
| |
|
| | self.close().await.context("IMAP close/expunge failed")?; |
| | info!(context, "Close/expunge succeeded."); |
| | self.selected_folder = None; |
| | self.selected_folder_needs_expunge = false; |
| | self.new_mail = false; |
| | } |
| | Ok(()) |
| | } |
| |
|
| | |
| | |
| | |
| | async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> { |
| | |
| | |
| | if let Some(selected_folder) = &self.selected_folder |
| | && folder == selected_folder |
| | { |
| | return Ok(NewlySelected::No); |
| | } |
| |
|
| | |
| | self.maybe_close_folder(context).await?; |
| |
|
| | |
| | let res = if self.can_condstore() { |
| | self.select_condstore(folder).await |
| | } else { |
| | self.select(folder).await |
| | }; |
| |
|
| | |
| | |
| | |
| |
|
| | match res { |
| | Ok(mailbox) => { |
| | info!(context, "Selected folder {folder:?}."); |
| | self.selected_folder = Some(folder.to_string()); |
| | self.selected_mailbox = Some(mailbox); |
| | Ok(NewlySelected::Yes) |
| | } |
| | Err(async_imap::error::Error::No(response)) => { |
| | Err(Error::NoFolder(folder.to_string(), response)) |
| | } |
| | Err(err) => Err(Error::Other(err.to_string())), |
| | } |
| | } |
| |
|
| | |
| | pub(super) async fn select_or_create_folder( |
| | &mut self, |
| | context: &Context, |
| | folder: &str, |
| | ) -> anyhow::Result<NewlySelected> { |
| | match self.select_folder(context, folder).await { |
| | Ok(newly_selected) => Ok(newly_selected), |
| | Err(err) => match err { |
| | Error::NoFolder(..) => { |
| | info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it."); |
| | let create_res = self.create(folder).await; |
| | if let Err(ref err) = create_res { |
| | info!(context, "Couldn't select folder, then create() failed: {err:#}."); |
| | |
| | } |
| | let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}")); |
| | if select_res.is_err() { |
| | create_res?; |
| | } |
| | select_res |
| | } |
| | _ => Err(err).with_context(|| format!("failed to select folder {folder} with error other than NO, not trying to create it")), |
| | }, |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | pub(crate) async fn select_with_uidvalidity( |
| | &mut self, |
| | context: &Context, |
| | folder: &str, |
| | create: bool, |
| | ) -> anyhow::Result<bool> { |
| | let newly_selected = if create { |
| | self.select_or_create_folder(context, folder) |
| | .await |
| | .with_context(|| format!("Failed to select or create folder {folder:?}"))? |
| | } else { |
| | match self.select_folder(context, folder).await { |
| | Ok(newly_selected) => newly_selected, |
| | Err(err) => match err { |
| | Error::NoFolder(..) => return Ok(false), |
| | _ => { |
| | return Err(err) |
| | .with_context(|| format!("Failed to select folder {folder:?}"))?; |
| | } |
| | }, |
| | } |
| | }; |
| | let transport_id = self.transport_id(); |
| |
|
| | |
| | |
| | ensure_and_debug_assert!( |
| | transport_id > 0, |
| | "Cannot select folder when transport ID is unknown" |
| | ); |
| |
|
| | let mailbox = self |
| | .selected_mailbox |
| | .as_mut() |
| | .with_context(|| format!("No mailbox selected, folder: {folder:?}"))?; |
| |
|
| | let old_uid_validity = get_uidvalidity(context, transport_id, folder) |
| | .await |
| | .with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?; |
| | let old_uid_next = get_uid_next(context, transport_id, folder) |
| | .await |
| | .with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?; |
| |
|
| | let new_uid_validity = mailbox |
| | .uid_validity |
| | .with_context(|| format!("No UIDVALIDITY for folder {folder}"))?; |
| | let new_uid_next = if let Some(uid_next) = mailbox.uid_next { |
| | Some(uid_next) |
| | } else { |
| | warn!( |
| | context, |
| | "SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command." |
| | ); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | let status = self |
| | .inner |
| | .status(folder, "(UIDNEXT)") |
| | .await |
| | .with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?; |
| |
|
| | if status.uid_next.is_none() { |
| | |
| | |
| | |
| | warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT."); |
| | } |
| | status.uid_next |
| | }; |
| | mailbox.uid_next = new_uid_next; |
| |
|
| | if new_uid_validity == old_uid_validity { |
| | if newly_selected == NewlySelected::Yes { |
| | if let Some(new_uid_next) = new_uid_next { |
| | if new_uid_next < old_uid_next { |
| | warn!( |
| | context, |
| | "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...", |
| | ); |
| | set_uid_next(context, transport_id, folder, new_uid_next).await?; |
| | self.resync_request_sender.try_send(()).ok(); |
| | } |
| |
|
| | |
| | self.new_mail |= new_uid_next != old_uid_next; |
| | } else { |
| | warn!( |
| | context, |
| | "Folder {folder} was just selected but we failed to determine UIDNEXT, assume that it has new mail." |
| | ); |
| | self.new_mail = true; |
| | } |
| | } |
| |
|
| | return Ok(true); |
| | } |
| |
|
| | |
| | set_modseq(context, transport_id, folder, 0).await?; |
| |
|
| | |
| |
|
| | let new_uid_next = new_uid_next.unwrap_or_default(); |
| | set_uid_next(context, transport_id, folder, new_uid_next).await?; |
| | set_uidvalidity(context, transport_id, folder, new_uid_validity).await?; |
| | self.new_mail = true; |
| |
|
| | |
| | context |
| | .sql |
| | .execute( |
| | "DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?", |
| | (transport_id, &folder, new_uid_validity), |
| | ) |
| | .await?; |
| |
|
| | if old_uid_validity != 0 || old_uid_next != 0 { |
| | self.resync_request_sender.try_send(()).ok(); |
| | } |
| | info!( |
| | context, |
| | "transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.", |
| | ); |
| | Ok(true) |
| | } |
| | } |
| |
|
| | #[derive(PartialEq, Debug, Copy, Clone, Eq)] |
| | pub(crate) enum NewlySelected { |
| | |
| | Yes, |
| | |
| | |
| | No, |
| | } |
| |
|