mirror of
https://github.com/chatmail/core.git
synced 2026-04-18 22:16:30 +03:00
The problem was: When opening the connectivity view while there is no network, get_connectivity_html() calls schedule_quota_update(), which schedules a UpdateRecentQuota job. But in update_recent_quota(), connecting fails and a ConnectivityChanged event is emitted (connectivity changes from Error to Connecting and back). Therefore the UI calls get_connectivity_html() again, and the loop is complete. This made the UI completely unresponsible. To reproduce, just turn wi-fi off and open the connectivity view. The fix is: schedule_quota_update() now only schedules a new job if there is no old job. To prevent the possible (though probably unlikely) problem that an old quota update job has a backoff of, like, a day and therefore quota is not updated, I reduced the backoff time for quota jobs to 10s. Fixes possibly https://github.com/deltachat/deltachat-android/issues/2043, but we should "re-try" this
212 lines
7.9 KiB
Rust
212 lines
7.9 KiB
Rust
//! # Support for IMAP QUOTA extension.
|
|
|
|
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, 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;
|
|
|
|
// if recent quota is older,
|
|
// it is re-fetched on dc_get_connectivity_html()
|
|
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
|
|
|
|
#[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.
|
|
/// Updated by `Action::UpdateRecentQuota`
|
|
pub(crate) recent: Result<IndexMap<String, Vec<QuotaResource>>>,
|
|
|
|
/// Timestamp when structure was modified.
|
|
pub(crate) modified: i64,
|
|
}
|
|
|
|
async fn get_unique_quota_roots_and_usage(
|
|
folders: Vec<String>,
|
|
imap: &mut Imap,
|
|
) -> Result<IndexMap<String, Vec<QuotaResource>>> {
|
|
let mut unique_quota_roots: IndexMap<String, Vec<QuotaResource>> = IndexMap::new();
|
|
for folder in folders {
|
|
let (quota_roots, quotas) = &imap.get_quota_roots(&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()
|
|
.ok_or_else(|| anyhow!("quota_root should have a quota"))?;
|
|
// replace old quotas, because between fetching quotaroots for folders,
|
|
// messages could be recieved and so the usage could have been changed
|
|
*unique_quota_roots
|
|
.entry(quota_root_name.clone())
|
|
.or_insert(vec![]) = quota.resources;
|
|
}
|
|
}
|
|
}
|
|
Ok(unique_quota_roots)
|
|
}
|
|
|
|
fn get_highest_usage<'t>(
|
|
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
|
|
) -> 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"))
|
|
}
|
|
|
|
/// 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 {
|
|
// Adds a job to update `quota.recent`
|
|
pub(crate) async fn schedule_quota_update(&self) -> Result<()> {
|
|
if !job::action_exists(self, Action::UpdateRecentQuota).await? {
|
|
job::add(
|
|
self,
|
|
job::Job::new(Action::UpdateRecentQuota, 0, Params::new(), 0),
|
|
)
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// 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) -> Result<Status> {
|
|
if let Err(err) = imap.prepare(self).await {
|
|
warn!(self, "could not connect: {:?}", err);
|
|
return Ok(Status::RetryNow);
|
|
}
|
|
|
|
let quota = if imap.can_check_quota() {
|
|
let folders = get_watched_folders(self).await;
|
|
get_unique_quota_roots_and_usage(folders, imap).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(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);
|
|
Ok(Status::Finished(Ok(())))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::quota::{
|
|
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
|
|
QUOTA_WARN_THRESHOLD_PERCENTAGE,
|
|
};
|
|
|
|
#[async_std::test]
|
|
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)]
|
|
#[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(())
|
|
}
|
|
}
|