1use std::collections::BTreeMap;
4use std::time::Duration;
5
6use anyhow::{anyhow, Context as _, Result};
7use async_imap::types::{Quota, QuotaResource};
8
9use crate::chat::add_device_msg_with_importance;
10use crate::config::Config;
11use crate::context::Context;
12use crate::imap::scan_folders::get_watched_folders;
13use crate::imap::session::Session as ImapSession;
14use crate::message::Message;
15use crate::tools::{self, time_elapsed};
16use crate::{stock_str, EventType};
17
18pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
21
22pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
25
26pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
35
36#[derive(Debug)]
38pub struct QuotaInfo {
39 pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
43
44 pub(crate) modified: tools::Time,
46}
47
48async fn get_unique_quota_roots_and_usage(
49 session: &mut ImapSession,
50 folders: Vec<String>,
51) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
52 let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
53 for folder in folders {
54 let (quota_roots, quotas) = &session.get_quota_root(&folder).await?;
55 for qr_entries in quota_roots {
57 for quota_root_name in &qr_entries.quota_root_names {
58 let quota: Quota = quotas
60 .iter()
61 .find(|q| &q.root_name == quota_root_name)
62 .cloned()
63 .context("quota_root should have a quota")?;
64 *unique_quota_roots
67 .entry(quota_root_name.clone())
68 .or_default() = quota.resources;
69 }
70 }
71 }
72 Ok(unique_quota_roots)
73}
74
75fn get_highest_usage<'t>(
76 unique_quota_roots: &'t BTreeMap<String, Vec<QuotaResource>>,
77) -> Result<(u64, &'t String, &'t QuotaResource)> {
78 let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
79 for (name, resources) in unique_quota_roots {
80 for r in resources {
81 let usage_percent = r.get_usage_percentage();
82 match highest {
83 None => {
84 highest = Some((usage_percent, name, r));
85 }
86 Some((up, ..)) => {
87 if up <= usage_percent {
88 highest = Some((usage_percent, name, r));
89 }
90 }
91 };
92 }
93 }
94
95 highest.context("no quota_resource found, this is unexpected")
96}
97
98pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> bool {
100 (curr_percentage >= QUOTA_WARN_THRESHOLD_PERCENTAGE
101 && warned_at_percentage < QUOTA_WARN_THRESHOLD_PERCENTAGE)
102 || (curr_percentage >= QUOTA_ERROR_THRESHOLD_PERCENTAGE
103 && warned_at_percentage < QUOTA_ERROR_THRESHOLD_PERCENTAGE)
104}
105
106impl Context {
107 pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
110 let quota = self.quota.read().await;
111 quota
112 .as_ref()
113 .filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
114 .is_none()
115 }
116
117 pub(crate) async fn update_recent_quota(&self, session: &mut ImapSession) -> Result<()> {
126 let quota = if session.can_check_quota() {
127 let folders = get_watched_folders(self).await?;
128 get_unique_quota_roots_and_usage(session, folders).await
129 } else {
130 Err(anyhow!(stock_str::not_supported_by_provider(self).await))
131 };
132
133 if let Ok(quota) = "a {
134 match get_highest_usage(quota) {
135 Ok((highest, _, _)) => {
136 if needs_quota_warning(
137 highest,
138 self.get_config_int(Config::QuotaExceeding).await? as u64,
139 ) {
140 self.set_config_internal(
141 Config::QuotaExceeding,
142 Some(&highest.to_string()),
143 )
144 .await?;
145 let mut msg =
146 Message::new_text(stock_str::quota_exceeding(self, highest).await);
147 add_device_msg_with_importance(self, None, Some(&mut msg), true).await?;
148 } else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
149 self.set_config_internal(Config::QuotaExceeding, None)
150 .await?;
151 }
152 }
153 Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err),
154 }
155 }
156
157 *self.quota.write().await = Some(QuotaInfo {
158 recent: quota,
159 modified: tools::Time::now(),
160 });
161
162 self.emit_event(EventType::ConnectivityChanged);
163 Ok(())
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::test_utils::TestContextManager;
171
172 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
173 async fn test_needs_quota_warning() -> Result<()> {
174 assert!(!needs_quota_warning(0, 0));
175 assert!(!needs_quota_warning(10, 0));
176 assert!(!needs_quota_warning(70, 0));
177 assert!(!needs_quota_warning(75, 0));
178 assert!(!needs_quota_warning(79, 0));
179 assert!(needs_quota_warning(80, 0));
180 assert!(needs_quota_warning(81, 0));
181 assert!(!needs_quota_warning(85, 80));
182 assert!(!needs_quota_warning(85, 81));
183 assert!(needs_quota_warning(95, 82));
184 assert!(!needs_quota_warning(97, 95));
185 assert!(!needs_quota_warning(97, 96));
186 assert!(!needs_quota_warning(1000, 96));
187 Ok(())
188 }
189
190 #[expect(clippy::assertions_on_constants)]
191 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
192 async fn test_quota_thresholds() -> anyhow::Result<()> {
193 assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
194 assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
195 assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
196 assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
197 Ok(())
198 }
199
200 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
201 async fn test_quota_needs_update() -> Result<()> {
202 let mut tcm = TestContextManager::new();
203 let t = &tcm.unconfigured().await;
204 const TIMEOUT: u64 = 60;
205 assert!(t.quota_needs_update(TIMEOUT).await);
206
207 *t.quota.write().await = Some(QuotaInfo {
208 recent: Ok(Default::default()),
209 modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
210 });
211 assert!(t.quota_needs_update(TIMEOUT).await);
212
213 *t.quota.write().await = Some(QuotaInfo {
214 recent: Ok(Default::default()),
215 modified: tools::Time::now(),
216 });
217 assert!(!t.quota_needs_update(TIMEOUT).await);
218
219 t.evtracker.clear_events();
220 t.set_primary_self_addr("new@addr").await?;
221 assert!(t.quota.read().await.is_none());
222 t.evtracker
223 .get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
224 .await;
225 assert!(t.quota_needs_update(TIMEOUT).await);
226 Ok(())
227 }
228}