From 104e6309ef8734faacd17214694a15121075f94f Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 19 Dec 2025 21:56:03 +0100 Subject: [PATCH 01/10] show quota of all relays --- src/config.rs | 2 +- src/configure.rs | 6 +- src/context.rs | 15 ++- src/quota.rs | 65 ++++++++----- src/scheduler.rs | 2 +- src/scheduler/connectivity.rs | 169 ++++++++++++++++++---------------- 6 files changed, 150 insertions(+), 109 deletions(-) diff --git a/src/config.rs b/src/config.rs index 62feb6bca1..b4d65d798d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -944,7 +944,7 @@ impl Context { /// This should only be used by test code and during configure. #[cfg(test)] // AEAP is disabled, but there are still tests for it pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> { - self.quota.write().await.take(); + self.quota.write().await.clear(); self.sql .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new)) diff --git a/src/configure.rs b/src/configure.rs index e72315e7e4..11dc696939 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -210,7 +210,8 @@ impl Context { /// (i.e. [EnteredLoginParam::addr]). pub async fn delete_transport(&self, addr: &str) -> Result<()> { let now = time(); - self.sql + let removed_transport_id = self + .sql .transaction(|transaction| { let primary_addr = transaction.query_row( "SELECT value FROM config WHERE keyname='configured_addr'", @@ -251,10 +252,11 @@ impl Context { (addr, remove_timestamp), )?; - Ok(()) + Ok(transport_id) }) .await?; send_sync_transports(self).await?; + self.quota.write().await.remove(&removed_transport_id); Ok(()) } diff --git a/src/context.rs b/src/context.rs index afff22e5ab..4f079f9417 100644 --- a/src/context.rs +++ b/src/context.rs @@ -243,9 +243,9 @@ pub struct InnerContext { pub(crate) scheduler: SchedulerState, pub(crate) ratelimit: RwLock, - /// Recently loaded quota information, if any. - /// Set to `None` if quota was never tried to load. - pub(crate) quota: RwLock>, + /// Recently loaded quota information for each trasnport, if any. + /// If quota was never tried to load, then the transport doesn't have an entry in the BTreeMap. + pub(crate) quota: RwLock>, /// Notify about new messages. /// @@ -479,7 +479,7 @@ impl Context { events, scheduler: SchedulerState::new(), ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3. - quota: RwLock::new(None), + quota: RwLock::new(BTreeMap::new()), new_msgs_notify, server_id: RwLock::new(None), metadata: RwLock::new(None), @@ -614,8 +614,13 @@ impl Context { } // Update quota (to send warning if full) - but only check it once in a while. + // note: For now this only checks quota of primary transport, + // because background check only checks primary transport at the moment if self - .quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT) + .quota_needs_update( + session.transport_id(), + DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, + ) .await && let Err(err) = self.update_recent_quota(&mut session).await { diff --git a/src/quota.rs b/src/quota.rs index d6ba14f79a..f861be4abb 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -107,10 +107,10 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b impl Context { /// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be /// called. - pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool { + pub(crate) async fn quota_needs_update(&self, transport_id: u32, ratelimit_secs: u64) -> bool { let quota = self.quota.read().await; quota - .as_ref() + .get(&transport_id) .filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs)) .is_none() } @@ -155,10 +155,13 @@ impl Context { } } - *self.quota.write().await = Some(QuotaInfo { - recent: quota, - modified: tools::Time::now(), - }); + self.quota.write().await.insert( + session.transport_id(), + QuotaInfo { + recent: quota, + modified: tools::Time::now(), + }, + ); self.emit_event(EventType::ConnectivityChanged); Ok(()) @@ -203,27 +206,47 @@ mod tests { let mut tcm = TestContextManager::new(); let t = &tcm.unconfigured().await; const TIMEOUT: u64 = 60; - assert!(t.quota_needs_update(TIMEOUT).await); - - *t.quota.write().await = Some(QuotaInfo { - recent: Ok(Default::default()), - modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1), - }); - assert!(t.quota_needs_update(TIMEOUT).await); - - *t.quota.write().await = Some(QuotaInfo { - recent: Ok(Default::default()), - modified: tools::Time::now(), - }); - assert!(!t.quota_needs_update(TIMEOUT).await); + assert!(t.quota_needs_update(0, TIMEOUT).await); + + *t.quota.write().await = { + let mut map = BTreeMap::new(); + map.insert( + 0, + QuotaInfo { + recent: Ok(Default::default()), + modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1), + }, + ); + map + }; + assert!(t.quota_needs_update(0, TIMEOUT).await); + + *t.quota.write().await = { + let mut map = BTreeMap::new(); + map.insert( + 0, + QuotaInfo { + recent: Ok(Default::default()), + modified: tools::Time::now(), + }, + ); + map + }; + assert!(!t.quota_needs_update(0, TIMEOUT).await); t.evtracker.clear_events(); t.set_primary_self_addr("new@addr").await?; - assert!(t.quota.read().await.is_none()); + assert!(t.quota.read().await.is_empty()); t.evtracker .get_matching(|evt| matches!(evt, EventType::ConnectivityChanged)) .await; - assert!(t.quota_needs_update(TIMEOUT).await); + assert!(t.quota_needs_update(0, TIMEOUT).await); + + // add entry and quota, check that it exists + // + // then remove the seccond transport + // + // check that only removed transport quota was removed from quota overview Ok(()) } } diff --git a/src/scheduler.rs b/src/scheduler.rs index 7dbd625761..538cc1aecf 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -481,7 +481,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) } // Update quota no more than once a minute. - if ctx.quota_needs_update(60).await + if ctx.quota_needs_update(session.transport_id(), 60).await && let Err(err) = ctx.update_recent_quota(&mut session).await { warn!(ctx, "Failed to update quota: {:#}.", err); diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 36b3641072..805b1f7a81 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -467,95 +467,106 @@ impl Context { .domain; let storage_on_domain = escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await); - ret += &format!("

{storage_on_domain}

    "); + ret += &format!("

    {storage_on_domain}

    "); // TODO: better string + let transports = self + .sql + .query_map_vec("SELECT id, addr FROM transports", (), |row| { + let transport_id: u32 = row.get(0)?; + let addr: String = row.get(1)?; + Ok((transport_id, addr)) + }) + .await?; let quota = self.quota.read().await; - if let Some(quota) = &*quota { - match "a.recent { - Ok(quota) => { - if !quota.is_empty() { - for (root_name, resources) in quota { - use async_imap::types::QuotaResourceName::*; - for resource in resources { - ret += "
  • "; - - // root name is empty eg. for gmail and redundant eg. for riseup. - // therefore, use it only if there are really several roots. - if quota.len() > 1 && !root_name.is_empty() { - ret += &format!( - "{}: ", - &*escaper::encode_minimal(root_name) - ); - } else { - info!( + for (transport_id, transport_addr) in transports { + if let Some(quota) = quota.get(&transport_id) { + ret += &format!("

    {transport_addr}

      "); + match "a.recent { + Ok(quota) => { + if !quota.is_empty() { + for (root_name, resources) in quota { + use async_imap::types::QuotaResourceName::*; + for resource in resources { + ret += "
    • "; + + // root name is empty eg. for gmail and redundant eg. for riseup. + // therefore, use it only if there are really several roots. + if quota.len() > 1 && !root_name.is_empty() { + ret += &format!( + "{}: ", + &*escaper::encode_minimal(root_name) + ); + } else { + info!( + self, + "connectivity: root name hidden: \"{}\"", root_name + ); + } + + let messages = stock_str::messages(self).await; + let part_of_total_used = stock_str::part_of_total_used( self, - "connectivity: root name hidden: \"{}\"", root_name + &resource.usage.to_string(), + &resource.limit.to_string(), + ) + .await; + ret += &match &resource.name { + Atom(resource_name) => { + format!( + "{}: {}", + &*escaper::encode_minimal(resource_name), + part_of_total_used + ) + } + Message => { + format!("{part_of_total_used}: {messages}") + } + Storage => { + // do not use a special title needed for "Storage": + // - it is usually shown directly under the "Storage" headline + // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used" + // - the string is not longer than the other strings that way (minus title, plus units) - + // additional linebreaks on small displays are unlikely therefore + // - most times, this is the only item anyway + let usage = &format_size(resource.usage * 1024, BINARY); + let limit = &format_size(resource.limit * 1024, BINARY); + stock_str::part_of_total_used(self, usage, limit).await + } + }; + + let percent = resource.get_usage_percentage(); + let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE { + "red" + } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE { + "yellow" + } else { + "green" + }; + let div_width_percent = min(100, percent); + ret += &format!( + "
      {percent}%
      " ); - } - let messages = stock_str::messages(self).await; - let part_of_total_used = stock_str::part_of_total_used( - self, - &resource.usage.to_string(), - &resource.limit.to_string(), - ) - .await; - ret += &match &resource.name { - Atom(resource_name) => { - format!( - "{}: {}", - &*escaper::encode_minimal(resource_name), - part_of_total_used - ) - } - Message => { - format!("{part_of_total_used}: {messages}") - } - Storage => { - // do not use a special title needed for "Storage": - // - it is usually shown directly under the "Storage" headline - // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used" - // - the string is not longer than the other strings that way (minus title, plus units) - - // additional linebreaks on small displays are unlikely therefore - // - most times, this is the only item anyway - let usage = &format_size(resource.usage * 1024, BINARY); - let limit = &format_size(resource.limit * 1024, BINARY); - stock_str::part_of_total_used(self, usage, limit).await - } - }; - - let percent = resource.get_usage_percentage(); - let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE { - "red" - } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE { - "yellow" - } else { - "green" - }; - let div_width_percent = min(100, percent); - ret += &format!( - "
      {percent}%
      " - ); - - ret += "
    • "; + ret += ""; + } } + } else { + let domain_escaped = escaper::encode_minimal(domain); + ret += &format!( + "
    • Warning: {domain_escaped} claims to support quota but gives no information
    • " + ); } - } else { - let domain_escaped = escaper::encode_minimal(domain); - ret += &format!( - "
    • Warning: {domain_escaped} claims to support quota but gives no information
    • " - ); + } + Err(e) => { + let error_escaped = escaper::encode_minimal(&e.to_string()); + ret += &format!("
    • {error_escaped}
    • "); } } - Err(e) => { - let error_escaped = escaper::encode_minimal(&e.to_string()); - ret += &format!("
    • {error_escaped}
    • "); - } + } else { + let not_connected = stock_str::not_connected(self).await; + ret += &format!("
    • {not_connected}
    • "); } - } else { - let not_connected = stock_str::not_connected(self).await; - ret += &format!("
    • {not_connected}
    • "); + ret += "
    "; } - ret += "
"; // ============================================================================================= From a3e248e654f800839ac59397804510e155ee827e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 19 Dec 2025 21:58:37 +0100 Subject: [PATCH 02/10] only show host instead of full address --- src/scheduler/connectivity.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 805b1f7a81..708a6bc48f 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -479,7 +479,13 @@ impl Context { let quota = self.quota.read().await; for (transport_id, transport_addr) in transports { if let Some(quota) = quota.get(&transport_id) { - ret += &format!("

{transport_addr}

    "); + ret += &format!( + "

    {}

      ", + transport_addr + .split('@') + .next_back() + .unwrap_or(&transport_addr) + ); match "a.recent { Ok(quota) => { if !quota.is_empty() { From f23b221270d55ac685b383104c04b3c3c3c11db0 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 19 Dec 2025 22:25:43 +0100 Subject: [PATCH 03/10] make indentation consistent for slightly better design --- src/scheduler/connectivity.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 708a6bc48f..568c174f13 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -477,10 +477,11 @@ impl Context { }) .await?; let quota = self.quota.read().await; + ret += "
        "; for (transport_id, transport_addr) in transports { if let Some(quota) = quota.get(&transport_id) { ret += &format!( - "

        {}

          ", + "
        • {}

            ", transport_addr .split('@') .next_back() @@ -571,8 +572,9 @@ impl Context { let not_connected = stock_str::not_connected(self).await; ret += &format!("
          • {not_connected}
          • "); } - ret += "
          "; + ret += "
        "; } + ret += "
      "; // ============================================================================================= From 2cfbb7dc895cd86fa7ee8a5be01d64d8781c62ef Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Fri, 19 Dec 2025 22:27:50 +0100 Subject: [PATCH 04/10] remove `DC_STR_STORAGE_ON_DOMAIN` stock string and name the section Relay Capacity until we come up with a better name. and use email parse method to extract domain from email --- deltachat-ffi/deltachat.h | 6 ------ src/scheduler/connectivity.rs | 18 +++++------------- src/stock_str.rs | 8 -------- 3 files changed, 5 insertions(+), 27 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index b540cc5339..322f4cd98b 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -7304,12 +7304,6 @@ void dc_event_unref(dc_event_t* event); /// Used as a headline in the connectivity view. #define DC_STR_OUTGOING_MESSAGES 104 -/// "Storage on %1$s" -/// -/// Used as a headline in the connectivity view. -/// -/// `%1$s` will be replaced by the domain of the configured e-mail address. -#define DC_STR_STORAGE_ON_DOMAIN 105 /// @deprecated Deprecated 2022-04-16, this string is no longer needed. #define DC_STR_ONE_MOMENT 106 diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 568c174f13..cc4ff765d6 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -462,12 +462,9 @@ impl Context { // [======67%===== ] // ============================================================================================= - let domain = - &deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)? - .domain; - let storage_on_domain = - escaper::encode_minimal(&stock_str::storage_on_domain(self, domain).await); - ret += &format!("

      {storage_on_domain}

      "); // TODO: better string + // TODO: stock string - when we decided on a good term, + // see discussion on https://github.com/chatmail/core/issues/7580#issuecomment-3633803432 + ret += "

      Relay Capacity

      "; let transports = self .sql .query_map_vec("SELECT id, addr FROM transports", (), |row| { @@ -480,13 +477,8 @@ impl Context { ret += "
        "; for (transport_id, transport_addr) in transports { if let Some(quota) = quota.get(&transport_id) { - ret += &format!( - "
      • {}

          ", - transport_addr - .split('@') - .next_back() - .unwrap_or(&transport_addr) - ); + let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)?.domain; + ret += &format!("
        • {}

            ", domain); match "a.recent { Ok(quota) => { if !quota.is_empty() { diff --git a/src/stock_str.rs b/src/stock_str.rs index 291141f0d9..6b0d0ee969 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1144,14 +1144,6 @@ pub(crate) async fn outgoing_messages(context: &Context) -> String { translated(context, StockMessage::OutgoingMessages).await } -/// Stock string: `Storage on %1$s`. -/// `%1$s` will be replaced by the domain of the configured email-address. -pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String { - translated(context, StockMessage::StorageOnDomain) - .await - .replace1(domain) -} - /// Stock string: `Not connected`. pub(crate) async fn not_connected(context: &Context) -> String { translated(context, StockMessage::NotConnected).await From aa26026125d2cd9b50afca3dab455a562c917789 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 15:29:34 +0100 Subject: [PATCH 05/10] remove todo comments for a test --- src/quota.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/quota.rs b/src/quota.rs index f861be4abb..f8c70d71da 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -242,11 +242,6 @@ mod tests { .await; assert!(t.quota_needs_update(0, TIMEOUT).await); - // add entry and quota, check that it exists - // - // then remove the seccond transport - // - // check that only removed transport quota was removed from quota overview Ok(()) } } From f75d587d3582d03dfe1d34943d96f680338ba8e9 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 15:35:12 +0100 Subject: [PATCH 06/10] let else --- src/scheduler/connectivity.rs | 161 +++++++++++++++++----------------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index cc4ff765d6..6706dfd231 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -476,94 +476,95 @@ impl Context { let quota = self.quota.read().await; ret += "
              "; for (transport_id, transport_addr) in transports { - if let Some(quota) = quota.get(&transport_id) { - let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)?.domain; - ret += &format!("
            • {}

                ", domain); - match "a.recent { - Ok(quota) => { - if !quota.is_empty() { - for (root_name, resources) in quota { - use async_imap::types::QuotaResourceName::*; - for resource in resources { - ret += "
              • "; - - // root name is empty eg. for gmail and redundant eg. for riseup. - // therefore, use it only if there are really several roots. - if quota.len() > 1 && !root_name.is_empty() { - ret += &format!( - "{}: ", - &*escaper::encode_minimal(root_name) - ); - } else { - info!( - self, - "connectivity: root name hidden: \"{}\"", root_name - ); - } - - let messages = stock_str::messages(self).await; - let part_of_total_used = stock_str::part_of_total_used( - self, - &resource.usage.to_string(), - &resource.limit.to_string(), - ) - .await; - ret += &match &resource.name { - Atom(resource_name) => { - format!( - "{}: {}", - &*escaper::encode_minimal(resource_name), - part_of_total_used - ) - } - Message => { - format!("{part_of_total_used}: {messages}") - } - Storage => { - // do not use a special title needed for "Storage": - // - it is usually shown directly under the "Storage" headline - // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used" - // - the string is not longer than the other strings that way (minus title, plus units) - - // additional linebreaks on small displays are unlikely therefore - // - most times, this is the only item anyway - let usage = &format_size(resource.usage * 1024, BINARY); - let limit = &format_size(resource.limit * 1024, BINARY); - stock_str::part_of_total_used(self, usage, limit).await - } - }; - - let percent = resource.get_usage_percentage(); - let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE { - "red" - } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE { - "yellow" - } else { - "green" - }; - let div_width_percent = min(100, percent); + let Some(quota) = quota.get(&transport_id) else { + let not_connected = stock_str::not_connected(self).await; + ret += &format!("
              • {not_connected}
            • "); + continue; + }; + let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)?.domain; + ret += &format!("
            • {}

                ", domain); + match "a.recent { + Ok(quota) => { + if !quota.is_empty() { + for (root_name, resources) in quota { + use async_imap::types::QuotaResourceName::*; + for resource in resources { + ret += "
              • "; + + // root name is empty eg. for gmail and redundant eg. for riseup. + // therefore, use it only if there are really several roots. + if quota.len() > 1 && !root_name.is_empty() { ret += &format!( - "
                {percent}%
                " + "{}: ", + &*escaper::encode_minimal(root_name) + ); + } else { + info!( + self, + "connectivity: root name hidden: \"{}\"", root_name ); - - ret += "
              • "; } + + let messages = stock_str::messages(self).await; + let part_of_total_used = stock_str::part_of_total_used( + self, + &resource.usage.to_string(), + &resource.limit.to_string(), + ) + .await; + ret += &match &resource.name { + Atom(resource_name) => { + format!( + "{}: {}", + &*escaper::encode_minimal(resource_name), + part_of_total_used + ) + } + Message => { + format!("{part_of_total_used}: {messages}") + } + Storage => { + // do not use a special title needed for "Storage": + // - it is usually shown directly under the "Storage" headline + // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used" + // - the string is not longer than the other strings that way (minus title, plus units) - + // additional linebreaks on small displays are unlikely therefore + // - most times, this is the only item anyway + let usage = &format_size(resource.usage * 1024, BINARY); + let limit = &format_size(resource.limit * 1024, BINARY); + stock_str::part_of_total_used(self, usage, limit).await + } + }; + + let percent = resource.get_usage_percentage(); + let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE { + "red" + } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE { + "yellow" + } else { + "green" + }; + let div_width_percent = min(100, percent); + ret += &format!( + "
                {percent}%
                " + ); + + ret += ""; } - } else { - let domain_escaped = escaper::encode_minimal(domain); - ret += &format!( - "
              • Warning: {domain_escaped} claims to support quota but gives no information
              • " - ); } - } - Err(e) => { - let error_escaped = escaper::encode_minimal(&e.to_string()); - ret += &format!("
              • {error_escaped}
              • "); + } else { + let domain_escaped = escaper::encode_minimal(domain); + ret += &format!( + "
              • Warning: {domain_escaped} claims to support quota but gives no information
              • " + ); } } - } else { - let not_connected = stock_str::not_connected(self).await; - ret += &format!("
              • {not_connected}
              • "); + Err(e) => { + let error_escaped = escaper::encode_minimal(&e.to_string()); + ret += &format!("
              • {error_escaped}
              • "); + } } + ret += "
            • "; } ret += "
            "; From f23dbd0eb25859c901bb3a6ea4c40575df62cf2d Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 15:49:56 +0100 Subject: [PATCH 07/10] new, more compact design --- src/scheduler/connectivity.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 6706dfd231..970c1fa2ec 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -476,20 +476,29 @@ impl Context { let quota = self.quota.read().await; ret += "
              "; for (transport_id, transport_addr) in transports { + let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr) + .map_or(transport_addr, |email| email.domain); + let domain_escaped = escaper::encode_minimal(domain); let Some(quota) = quota.get(&transport_id) else { let not_connected = stock_str::not_connected(self).await; - ret += &format!("
            • {not_connected}
            "); + ret += &format!("
          • {domain_escaped} • {not_connected}
          • "); continue; }; - let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)?.domain; - ret += &format!("
          • {}

              ", domain); match "a.recent { + Err(e) => { + let error_escaped = escaper::encode_minimal(&e.to_string()); + ret += &format!("
            • {domain_escaped} • {error_escaped}
            • "); + } Ok(quota) => { - if !quota.is_empty() { + if quota.is_empty() { + ret += &format!( + "
            • {domain_escaped} • Warning: {domain_escaped} claims to support quota but gives no information
            • " + ); + } else { for (root_name, resources) in quota { use async_imap::types::QuotaResourceName::*; for resource in resources { - ret += "
            • "; + ret += "
            • {domain_escaped} • "; // root name is empty eg. for gmail and redundant eg. for riseup. // therefore, use it only if there are really several roots. @@ -552,20 +561,9 @@ impl Context { ret += "
            • "; } } - } else { - let domain_escaped = escaper::encode_minimal(domain); - ret += &format!( - "
            • Warning: {domain_escaped} claims to support quota but gives no information
            • " - ); } } - Err(e) => { - let error_escaped = escaper::encode_minimal(&e.to_string()); - ret += &format!("
            • {error_escaped}
            • "); - } } - - ret += "
          • "; } ret += "
          "; From 0624999484fca01221dfed901e9d7c936ffb621e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 22:29:28 +0100 Subject: [PATCH 08/10] fix missing format statement --- src/scheduler/connectivity.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 970c1fa2ec..1c13826890 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -498,7 +498,7 @@ impl Context { for (root_name, resources) in quota { use async_imap::types::QuotaResourceName::*; for resource in resources { - ret += "
        • {domain_escaped} • "; + ret += &format!("
        • {domain_escaped} • "); // root name is empty eg. for gmail and redundant eg. for riseup. // therefore, use it only if there are really several roots. From 2a4fdda2075da1a62a78287283fa404dd058584b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 22:32:09 +0100 Subject: [PATCH 09/10] use "·" instead of "•" --- src/scheduler/connectivity.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 1c13826890..b478fb9a24 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -481,24 +481,24 @@ impl Context { let domain_escaped = escaper::encode_minimal(domain); let Some(quota) = quota.get(&transport_id) else { let not_connected = stock_str::not_connected(self).await; - ret += &format!("
        • {domain_escaped} • {not_connected}
        • "); + ret += &format!("
        • {domain_escaped} · {not_connected}
        • "); continue; }; match "a.recent { Err(e) => { let error_escaped = escaper::encode_minimal(&e.to_string()); - ret += &format!("
        • {domain_escaped} • {error_escaped}
        • "); + ret += &format!("
        • {domain_escaped} · {error_escaped}
        • "); } Ok(quota) => { if quota.is_empty() { ret += &format!( - "
        • {domain_escaped} • Warning: {domain_escaped} claims to support quota but gives no information
        • " + "
        • {domain_escaped} · Warning: {domain_escaped} claims to support quota but gives no information
        • " ); } else { for (root_name, resources) in quota { use async_imap::types::QuotaResourceName::*; for resource in resources { - ret += &format!("
        • {domain_escaped} • "); + ret += &format!("
        • {domain_escaped} · "); // root name is empty eg. for gmail and redundant eg. for riseup. // therefore, use it only if there are really several roots. From 97de469090f371c178e535b6771480934f2b7464 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 21 Dec 2025 22:45:34 +0100 Subject: [PATCH 10/10] change header to "Message Buffers" --- src/scheduler/connectivity.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index b478fb9a24..c131242ff1 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -462,9 +462,7 @@ impl Context { // [======67%===== ] // ============================================================================================= - // TODO: stock string - when we decided on a good term, - // see discussion on https://github.com/chatmail/core/issues/7580#issuecomment-3633803432 - ret += "

          Relay Capacity

          "; + ret += "

          Message Buffers

          "; let transports = self .sql .query_map_vec("SELECT id, addr FROM transports", (), |row| {