1use std::collections::BTreeMap;
4use std::time::Duration;
5
6use anyhow::{Context as _, Result, anyhow};
7use async_imap::types::{Quota, QuotaResource};
8
9use crate::context::Context;
10use crate::imap::session::Session as ImapSession;
11use crate::tools::{self, time_elapsed};
12use crate::{EventType, stock_str};
13
14pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
16
17pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 95;
19
20#[derive(Debug)]
22pub struct QuotaInfo {
23 pub(crate) recent: Result<BTreeMap<String, Vec<QuotaResource>>>,
27
28 pub(crate) modified: tools::Time,
30}
31
32async fn get_unique_quota_roots_and_usage(
33 session: &mut ImapSession,
34 folder: &str,
35) -> Result<BTreeMap<String, Vec<QuotaResource>>> {
36 let mut unique_quota_roots: BTreeMap<String, Vec<QuotaResource>> = BTreeMap::new();
37 let (quota_roots, quotas) = &session.get_quota_root(folder).await?;
38 for qr_entries in quota_roots {
40 for quota_root_name in &qr_entries.quota_root_names {
41 let quota: Quota = quotas
43 .iter()
44 .find(|q| &q.root_name == quota_root_name)
45 .cloned()
46 .context("quota_root should have a quota")?;
47 *unique_quota_roots
50 .entry(quota_root_name.clone())
51 .or_default() = quota.resources;
52 }
53 }
54 Ok(unique_quota_roots)
55}
56
57impl Context {
58 pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool {
61 let quota = self.quota.read().await;
62 quota.get(&transport_id).is_none_or(|quota| {
63 time_elapsed("a.modified) >= Duration::from_secs(ratelimit_secs)
64 })
65 }
66
67 pub(crate) async fn update_recent_quota(
76 &self,
77 session: &mut ImapSession,
78 folder: &str,
79 ) -> Result<()> {
80 let transport_id = session.transport_id();
81
82 info!(self, "Transport {transport_id}: Updating quota.");
83
84 let quota = if session.can_check_quota() {
85 get_unique_quota_roots_and_usage(session, folder).await
86 } else {
87 Err(anyhow!(stock_str::not_supported_by_provider(self)))
88 };
89
90 self.quota.write().await.insert(
91 transport_id,
92 QuotaInfo {
93 recent: quota,
94 modified: tools::Time::now(),
95 },
96 );
97
98 info!(self, "Transport {transport_id}: Updated quota.");
99 self.emit_event(EventType::ConnectivityChanged);
100 Ok(())
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::test_utils::TestContextManager;
108
109 #[expect(clippy::assertions_on_constants)]
110 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
111 async fn test_quota_thresholds() -> anyhow::Result<()> {
112 assert!(0 < QUOTA_WARN_THRESHOLD_PERCENTAGE);
113 assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
114 assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
115 Ok(())
116 }
117
118 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
119 async fn test_quota_needs_update() -> Result<()> {
120 let mut tcm = TestContextManager::new();
121 let t = &tcm.unconfigured().await;
122 const TIMEOUT: u64 = 60;
123 assert!(t.quota_needs_update(0, TIMEOUT).await);
124
125 *t.quota.write().await = {
126 let mut map = BTreeMap::new();
127 map.insert(
128 0,
129 QuotaInfo {
130 recent: Ok(Default::default()),
131 modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
132 },
133 );
134 map
135 };
136 assert!(t.quota_needs_update(0, TIMEOUT).await);
137
138 *t.quota.write().await = {
139 let mut map = BTreeMap::new();
140 map.insert(
141 0,
142 QuotaInfo {
143 recent: Ok(Default::default()),
144 modified: tools::Time::now(),
145 },
146 );
147 map
148 };
149 assert!(!t.quota_needs_update(0, TIMEOUT).await);
150
151 t.evtracker.clear_events();
152 t.set_primary_self_addr("new@addr").await?;
153 assert!(t.quota.read().await.is_empty());
154 t.evtracker
155 .get_matching(|evt| matches!(evt, EventType::ConnectivityChanged))
156 .await;
157 assert!(t.quota_needs_update(0, TIMEOUT).await);
158
159 Ok(())
160 }
161}