//! # Support for IMAP QUOTA extension. use std::collections::BTreeMap; use anyhow::{anyhow, Context as _, Result}; use async_imap::types::{Quota, QuotaResource}; use crate::chat::add_device_msg_with_importance; use crate::config::Config; use crate::context::Context; use crate::imap::scan_folders::get_watched_folders; use crate::imap::session::Session as ImapSession; use crate::message::{Message, Viewtype}; use crate::tools; use crate::{stock_str, EventType}; /// warn about a nearly full mailbox after this usage percentage is reached. /// quota icon is "yellow". pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80; /// warning again after this usage percentage is reached, /// quota icon is "red". pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95; /// if quota is below this value (again), /// QuotaExceeding is cleared. /// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to /// avoid jittering and lots of warnings when quota is exactly at the warning threshold. /// /// We do not repeat warnings on a daily base or so as some provider /// providers report bad values and we would then spam the user. pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75; /// Server quota information with an update timestamp. #[derive(Debug)] pub struct QuotaInfo { /// Recently loaded quota information. /// set to `Err()` if the provider does not support quota or on other errors, /// set to `Ok()` for valid quota information. pub(crate) recent: Result>>, /// When the structure was modified. pub(crate) modified: tools::Time, } async fn get_unique_quota_roots_and_usage( session: &mut ImapSession, folders: Vec, ) -> Result>> { let mut unique_quota_roots: BTreeMap> = BTreeMap::new(); for folder in folders { let (quota_roots, quotas) = &session.get_quota_root(&folder).await?; // if there are new quota roots found in this imap folder, add them to the list for qr_entries in quota_roots { for quota_root_name in &qr_entries.quota_root_names { // the quota for that quota root let quota: Quota = quotas .iter() .find(|q| &q.root_name == quota_root_name) .cloned() .context("quota_root should have a quota")?; // replace old quotas, because between fetching quotaroots for folders, // messages could be received and so the usage could have been changed *unique_quota_roots .entry(quota_root_name.clone()) .or_default() = quota.resources; } } } Ok(unique_quota_roots) } fn get_highest_usage<'t>( unique_quota_roots: &'t BTreeMap>, ) -> Result<(u64, &'t String, &QuotaResource)> { let mut highest: Option<(u64, &'t String, &QuotaResource)> = None; for (name, resources) in unique_quota_roots { for r in resources { let usage_percent = r.get_usage_percentage(); match highest { None => { highest = Some((usage_percent, name, r)); } Some((up, ..)) => { if up <= usage_percent { highest = Some((usage_percent, name, r)); } } }; } } highest.context("no quota_resource found, this is unexpected") } /// Checks if a quota warning is needed. pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> bool { (curr_percentage >= QUOTA_WARN_THRESHOLD_PERCENTAGE && warned_at_percentage < QUOTA_WARN_THRESHOLD_PERCENTAGE) || (curr_percentage >= QUOTA_ERROR_THRESHOLD_PERCENTAGE && warned_at_percentage < QUOTA_ERROR_THRESHOLD_PERCENTAGE) } impl Context { /// Updates `quota.recent`, sets `quota.modified` to the current time /// and emits an event to let the UIs update connectivity view. /// /// Moreover, once each time quota gets larger than `QUOTA_WARN_THRESHOLD_PERCENTAGE`, /// a device message is added. /// As the message is added only once, the user is not spammed /// in case for some providers the quota is always at ~100% /// and new space is allocated as needed. pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> { let quota = if session.can_check_quota() { let folders = get_watched_folders(self).await?; get_unique_quota_roots_and_usage(session, folders).await } else { Err(anyhow!(stock_str::not_supported_by_provider(self).await)) }; if let Ok(quota) = "a { match get_highest_usage(quota) { Ok((highest, _, _)) => { if needs_quota_warning( highest, self.get_config_int(Config::QuotaExceeding).await? as u64, ) { self.set_config_internal( Config::QuotaExceeding, Some(&highest.to_string()), ) .await?; let mut msg = Message::new(Viewtype::Text); msg.text = stock_str::quota_exceeding(self, highest).await; add_device_msg_with_importance(self, None, Some(&mut msg), true).await?; } else if highest <= QUOTA_ALLCLEAR_PERCENTAGE { self.set_config_internal(Config::QuotaExceeding, None) .await?; } } Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err), } } *self.quota.write().await = Some(QuotaInfo { recent: quota, modified: tools::Time::now(), }); self.emit_event(EventType::ConnectivityChanged); Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_needs_quota_warning() -> Result<()> { assert!(!needs_quota_warning(0, 0)); assert!(!needs_quota_warning(10, 0)); assert!(!needs_quota_warning(70, 0)); assert!(!needs_quota_warning(75, 0)); assert!(!needs_quota_warning(79, 0)); assert!(needs_quota_warning(80, 0)); assert!(needs_quota_warning(81, 0)); assert!(!needs_quota_warning(85, 80)); assert!(!needs_quota_warning(85, 81)); assert!(needs_quota_warning(95, 82)); assert!(!needs_quota_warning(97, 95)); assert!(!needs_quota_warning(97, 96)); assert!(!needs_quota_warning(1000, 96)); Ok(()) } #[allow(clippy::assertions_on_constants)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_quota_thresholds() -> anyhow::Result<()> { assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50); assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE); assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE); assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100); Ok(()) } }