feat: connectivity view: move quota up and combine with IMAP state. (#7653)

like described in
https://github.com/chatmail/core/pull/7630#discussion_r2641514867


## classical account (with legacy option mvbox)

|||
|---|---|
| <img width="891" height="635" alt="image"
src="https://github.com/user-attachments/assets/723f3dba-79dc-4b57-a14f-c5879c1a1d1d"
/>| <img width="890" height="578" alt="image"
src="https://github.com/user-attachments/assets/d45eaf35-d7b2-40d4-8c37-bbc77947c27d"
/>|

## multi transport

|||
|---|---|
|<img width="891" height="1236" alt="image"
src="https://github.com/user-attachments/assets/053cb088-7d9d-4591-b2bc-6b49399d33a0"
/> |<img width="885" height="1230" alt="image"
src="https://github.com/user-attachments/assets/c455b4f1-f521-4ae8-8884-9042af62ca46"
/>|
This commit is contained in:
Simon Laux
2026-01-09 17:20:59 +00:00
committed by GitHub
parent b536902827
commit 1383e790c3
2 changed files with 83 additions and 81 deletions

View File

@@ -324,8 +324,8 @@ impl Drop for IoPausedGuard {
#[derive(Debug)]
struct SchedBox {
/// Hostname of used chatmail/email relay
host: String,
/// Address at the used chatmail/email relay
addr: String,
meaning: FolderMeaning,
conn_state: ImapConnectionState,
@@ -831,14 +831,9 @@ impl Scheduler {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let host = configured_login_param
.addr
.split("@")
.last()
.context("address has no host")?
.to_owned();
let addr = configured_login_param.addr.clone();
let inbox = SchedBox {
host: host.clone(),
addr: addr.clone(),
meaning: FolderMeaning::Inbox,
conn_state,
handle,
@@ -854,7 +849,7 @@ impl Scheduler {
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
host,
addr,
meaning,
conn_state,
handle,

View File

@@ -343,9 +343,18 @@ impl Context {
.green {
background-color: #34c759;
}
.grey {
background-color: #808080;
}
.yellow {
background-color: #fdc625;
}
.transport {
margin-bottom: 1em;
}
.quota-list {
padding-left: 0;
}
</style>
</head>
<body>"#
@@ -375,7 +384,7 @@ impl Context {
.boxes()
.map(|b| {
(
b.host.clone(),
b.addr.clone(),
b.meaning,
b.conn_state.state.connectivity.clone(),
)
@@ -396,73 +405,15 @@ impl Context {
// =============================================================================================
// Add e.g.
// Incoming messages
// - "Inbox": Connected
// - [X] nine.testrun.org: Connected
// 1.34 GiB of 2 GiB used
// [======67%===== ]
// =============================================================================================
let watched_folders = get_watched_folder_configs(self).await?;
let incoming_messages = stock_str::incoming_messages(self).await;
ret += &format!("<h3>{incoming_messages}</h3><ul>");
for (host, folder, state) in &folders_states {
let mut folder_added = false;
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
let f = self.get_config(config).await.log_err(self).ok().flatten();
if let Some(foldername) = f {
let detailed = &state.get_detailed();
ret += "<li>";
ret += &*detailed.to_icon();
ret += " <b>";
if folder == &FolderMeaning::Inbox {
ret += &*escaper::encode_minimal(host);
} else {
ret += &*escaper::encode_minimal(&foldername);
}
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
folder_added = true;
}
}
if !folder_added && folder == &FolderMeaning::Inbox {
let detailed = &state.get_detailed();
if let DetailedConnectivity::Error(_) = detailed {
// On the inbox thread, we also do some other things like scan_folders and run jobs
// so, maybe, the inbox is not watched, but something else went wrong
ret += "<li>";
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
}
}
}
ret += "</ul>";
// =============================================================================================
// Add e.g.
// Outgoing messages
// Your last message was sent successfully
// =============================================================================================
let outgoing_messages = stock_str::outgoing_messages(self).await;
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
let detailed = smtp.get_detailed();
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
ret += "</li></ul>";
// =============================================================================================
// Add e.g.
// Storage on testrun.org
// 1.34 GiB of 2 GiB used
// [======67%===== ]
// =============================================================================================
ret += "<h3>Message Buffers</h3>";
let transports = self
.sql
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
@@ -472,31 +423,71 @@ impl Context {
})
.await?;
let quota = self.quota.read().await;
ret += "<ul>";
for (transport_id, transport_addr) in transports {
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
.map_or(transport_addr, |email| email.domain);
.map_or(transport_addr.clone(), |email| email.domain);
let domain_escaped = escaper::encode_minimal(domain);
ret += "<li class=\"transport\">";
let folders = folders_states
.iter()
.filter(|(folder_addr, ..)| *folder_addr == transport_addr);
for (_addr, folder, state) in folders {
let mut folder_added = false;
if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
let f = self.get_config(config).await.log_err(self).ok().flatten();
if let Some(foldername) = f {
let detailed = &state.get_detailed();
ret += &*detailed.to_icon();
ret += " <b>";
if folder == &FolderMeaning::Inbox {
ret += &*domain_escaped;
} else {
ret += &*escaper::encode_minimal(&foldername);
}
ret += ":</b> ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "<br />";
folder_added = true;
}
}
if !folder_added && folder == &FolderMeaning::Inbox {
let detailed = &state.get_detailed();
if let DetailedConnectivity::Error(_) = detailed {
// On the inbox thread, we also do some other things like scan_folders and run jobs
// so, maybe, the inbox is not watched, but something else went wrong
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "<br />";
}
}
}
let Some(quota) = quota.get(&transport_id) else {
let not_connected = stock_str::not_connected(self).await;
ret += &format!("<li>{domain_escaped} &middot; {not_connected}</li>");
ret += "</li>";
continue;
};
match &quota.recent {
Err(e) => {
let error_escaped = escaper::encode_minimal(&e.to_string());
ret += &format!("<li>{domain_escaped} &middot; {error_escaped}</li>");
ret += &escaper::encode_minimal(&e.to_string());
}
Ok(quota) => {
if quota.is_empty() {
ret += &format!(
"<li>{domain_escaped} &middot; Warning: {domain_escaped} claims to support quota but gives no information</li>"
"Warning: {domain_escaped} claims to support quota but gives no information"
);
} else {
ret += "<ul class=\"quota-list\">";
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += &format!("<li>{domain_escaped} &middot; ");
ret += "<li>";
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
@@ -549,7 +540,7 @@ impl Context {
} else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
"yellow"
} else {
"green"
"grey"
};
let div_width_percent = min(100, percent);
ret += &format!(
@@ -559,12 +550,28 @@ impl Context {
ret += "</li>";
}
}
ret += "</ul>";
}
}
}
ret += "</li>";
}
ret += "</ul>";
// =============================================================================================
// Add e.g.
// Outgoing messages
// Your last message was sent successfully
// =============================================================================================
let outgoing_messages = stock_str::outgoing_messages(self).await;
ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
let detailed = smtp.get_detailed();
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
ret += "</li></ul>";
// =============================================================================================
ret += "</body></html>\n";