diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 669ee3e45..c540ee47f 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -377,6 +377,9 @@ int dc_set_config (dc_context_t* context, const char* * an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway. * - `sys.config_keys` = get a space-separated list of all config-keys available. * The config-keys are the keys that can be passed to the parameter `key` of this function. + * - `quota_exceeding` = 0: quota is unknown or in normal range; + * >=80: quota is about to exceed, the value is the concrete percentage, + * a device message is added when that happens, however, that value may still be interesting for bots. * * @memberof dc_context_t * @param context The context object. For querying system values, this can be NULL. @@ -5675,6 +5678,13 @@ void dc_event_unref(dc_event_t* event); /// Used in message summary text for notifications and chatlist. #define DC_STR_FORWARDED 97 +/// "Quota exceeding, already %1$s%% used." +/// +/// Used as device message text. +/// +/// `%1$s` will be replaced by the percentage used +#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98 + /** * @} */ diff --git a/src/config.rs b/src/config.rs index 197ac049c..f5654e678 100644 --- a/src/config.rs +++ b/src/config.rs @@ -156,6 +156,11 @@ pub enum Config { #[strum(props(default = "0"))] NotifyAboutWrongPw, + /// If a warning about exceeding quota was shown recently, + /// this is the percentage of quota at the time the warning was given. + /// Unset, when quota falls below minimal warning threshold again. + QuotaExceeding, + /// address to webrtc instance to use for videochats WebrtcInstance, diff --git a/src/context.rs b/src/context.rs index 705658cc1..f233e2eab 100644 --- a/src/context.rs +++ b/src/context.rs @@ -423,6 +423,12 @@ impl Context { .await? .to_string(), ); + res.insert( + "quota_exceeding", + self.get_config_int(Config::QuotaExceeding) + .await? + .to_string(), + ); let elapsed = self.creation_time.elapsed(); res.insert("uptime", duration_to_str(elapsed.unwrap_or_default())); diff --git a/src/job.rs b/src/job.rs index 719ae725d..2805b089d 100644 --- a/src/job.rs +++ b/src/job.rs @@ -1152,7 +1152,10 @@ async fn perform_job_action( sql::housekeeping(context).await.ok_or_log(context); Status::Finished(Ok(())) } - Action::UpdateRecentQuota => context.update_recent_quota(connection.inbox()).await, + Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await { + Ok(status) => status, + Err(err) => Status::Finished(Err(err)), + }, }; info!(context, "Finished immediate try {} of job {}", tries, job); diff --git a/src/quota.rs b/src/quota.rs index ca35ca30a..2303f9ad5 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -4,13 +4,17 @@ use anyhow::{anyhow, Result}; use async_imap::types::{Quota, QuotaResource}; use indexmap::IndexMap; +use crate::chat::add_device_msg_with_importance; +use crate::config::Config; +use crate::constants::Viewtype; use crate::context::Context; use crate::dc_tools::time; use crate::imap::scan_folders::get_watched_folders; use crate::imap::Imap; use crate::job::{Action, Status}; +use crate::message::Message; use crate::param::Params; -use crate::{job, EventType}; +use crate::{job, stock_str, EventType}; /// warn about a nearly full mailbox after this usage percentage is reached. /// quota icon is "yellow". @@ -20,6 +24,12 @@ pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80; // this threshold only makes the quota icon "red". pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99; +/// 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. +pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75; + // if recent quota is older, // it is re-fetched on dc_get_connectivity_html() pub const QUOTA_MAX_AGE_SECONDS: i64 = 60; @@ -63,6 +73,29 @@ async fn get_unique_quota_roots_and_usage( Ok(unique_quota_roots) } +fn get_highest_usage<'t>( + unique_quota_roots: &'t IndexMap>, +) -> 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.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected")) +} + impl Context { // Adds a job to update `quota.recent` pub(crate) async fn schedule_quota_update(&self) { @@ -77,11 +110,17 @@ 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. + /// /// Called in response to `Action::UpdateRecentQuota`. - pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Status { + pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result { if let Err(err) = imap.prepare(self).await { warn!(self, "could not connect: {:?}", err); - return Status::RetryNow; + return Ok(Status::RetryNow); } let quota = if imap.can_check_quota() { @@ -91,12 +130,51 @@ impl Context { Err(anyhow!("Quota not supported by your provider.")) }; + if let Ok(quota) = "a { + match get_highest_usage(quota) { + Ok((highest, _, _)) => { + if highest >= QUOTA_WARN_THRESHOLD_PERCENTAGE { + if self.get_config_int(Config::QuotaExceeding).await? == 0 { + self.set_config(Config::QuotaExceeding, Some(&highest.to_string())) + .await?; + + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(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(Config::QuotaExceeding, None).await?; + } + } + Err(err) => warn!(self, "cannot get highest quota usage: {:?}", err), + } + } + *self.quota.write().await = Some(QuotaInfo { recent: quota, modified: time(), }); self.emit_event(EventType::ConnectivityChanged); - Status::Finished(Ok(())) + Ok(Status::Finished(Ok(()))) + } +} + +#[cfg(test)] +mod tests { + use crate::quota::{ + QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE, + QUOTA_WARN_THRESHOLD_PERCENTAGE, + }; + + #[allow(clippy::assertions_on_constants)] + #[async_std::test] + 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(()) } } diff --git a/src/sql.rs b/src/sql.rs index 2bdd09440..fa376252d 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -598,6 +598,8 @@ pub async fn housekeeping(context: &Context) -> Result<()> { ); } + context.schedule_quota_update().await; + if let Err(e) = context .set_config(Config::LastHousekeeping, Some(&time().to_string())) .await diff --git a/src/stock_str.rs b/src/stock_str.rs index f0489c8e5..c1d5113cf 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -258,6 +258,15 @@ pub enum StockMessage { #[strum(props(fallback = "Forwarded"))] Forwarded = 97, + + #[strum(props( + fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\ + You may not be able to receive message when the storage is 100%% used.\n\n\ + 👉 Please check if you can delete old data in the provider's webinterface \ + and consider to enable \"Settings / Delete Old Messages\". \ + You can check your current storage usage anytime at \"Settings / Connectivity\"." + ))] + QuotaExceedingMsgBody = 98, } impl StockMessage { @@ -840,6 +849,14 @@ pub(crate) async fn forwarded(context: &Context) -> String { translated(context, StockMessage::Forwarded).await } +/// Stock string: `⚠️ Your provider's storage is about to exceed...`. +pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String { + translated(context, StockMessage::QuotaExceedingMsgBody) + .await + .replace1(format!("{}", highest_usage)) + .replace("%%", "%") +} + impl Context { /// Set the stock string for the [StockMessage]. /// @@ -1023,6 +1040,16 @@ mod tests { ); } + #[async_std::test] + async fn test_quota_exceeding_stock_str() -> anyhow::Result<()> { + let t = TestContext::new().await; + let str = quota_exceeding(&t, 81).await; + assert!(str.contains("81% ")); + assert!(str.contains("100% ")); + assert!(!str.contains("%%")); + Ok(()) + } + #[async_std::test] async fn test_update_device_chats() { let t = TestContext::new().await;