1use core::fmt;
2use std::cmp::min;
3use std::{iter::once, ops::Deref, sync::Arc};
4
5use anyhow::Result;
6use humansize::{BINARY, format_size};
7
8use crate::context::Context;
9use crate::events::EventType;
10use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
11use crate::stock_str;
12
13use super::InnerSchedulerState;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
17pub enum Connectivity {
18 NotConnected = 1000,
25
26 Connecting = 2000,
28
29 Working = 3000,
31
32 Connected = 4000,
41}
42
43#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
48enum DetailedConnectivity {
49 Error(String),
50 #[default]
51 Uninitialized,
52
53 Connecting,
56
57 Preparing,
60
61 Working,
64
65 InterruptingIdle,
66
67 Idle,
69}
70
71impl DetailedConnectivity {
72 fn to_basic(&self) -> Connectivity {
73 match self {
74 DetailedConnectivity::Error(_) => Connectivity::NotConnected,
75 DetailedConnectivity::Uninitialized => Connectivity::NotConnected,
76 DetailedConnectivity::Connecting => Connectivity::Connecting,
77 DetailedConnectivity::Working => Connectivity::Working,
78 DetailedConnectivity::InterruptingIdle => Connectivity::Working,
79
80 DetailedConnectivity::Preparing => Connectivity::Working,
86
87 DetailedConnectivity::Idle => Connectivity::Connected,
88 }
89 }
90
91 fn to_icon(&self) -> String {
92 match self {
93 DetailedConnectivity::Error(_) | DetailedConnectivity::Uninitialized => {
94 "<span class=\"red dot\"></span>".to_string()
95 }
96 DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
97 DetailedConnectivity::Preparing
98 | DetailedConnectivity::Working
99 | DetailedConnectivity::InterruptingIdle
100 | DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
101 }
102 }
103
104 fn to_string_imap(&self, context: &Context) -> String {
105 match self {
106 DetailedConnectivity::Error(e) => stock_str::error(context, e),
107 DetailedConnectivity::Uninitialized => "Not started".to_string(),
108 DetailedConnectivity::Connecting => stock_str::connecting(context),
109 DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
110 stock_str::updating(context)
111 }
112 DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
113 stock_str::connected(context)
114 }
115 }
116 }
117
118 fn to_string_smtp(&self, context: &Context) -> String {
119 match self {
120 DetailedConnectivity::Error(e) => stock_str::error(context, e),
121 DetailedConnectivity::Uninitialized => {
122 "You did not try to send a message recently.".to_string()
123 }
124 DetailedConnectivity::Connecting => stock_str::connecting(context),
125 DetailedConnectivity::Working => stock_str::sending(context),
126
127 DetailedConnectivity::InterruptingIdle
131 | DetailedConnectivity::Preparing
132 | DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context),
133 }
134 }
135
136 fn all_work_done(&self) -> bool {
137 match self {
138 DetailedConnectivity::Error(_) => true,
139 DetailedConnectivity::Uninitialized => false,
140 DetailedConnectivity::Connecting => false,
141 DetailedConnectivity::Working => false,
142 DetailedConnectivity::InterruptingIdle => false,
143 DetailedConnectivity::Preparing => false, DetailedConnectivity::Idle => true,
145 }
146 }
147}
148
149#[derive(Clone, Default)]
150pub(crate) struct ConnectivityStore(Arc<parking_lot::Mutex<DetailedConnectivity>>);
151
152impl ConnectivityStore {
153 fn set(&self, context: &Context, v: DetailedConnectivity) {
154 {
155 *self.0.lock() = v;
156 }
157 context.emit_event(EventType::ConnectivityChanged);
158 }
159
160 pub(crate) fn set_err(&self, context: &Context, e: String) {
161 self.set(context, DetailedConnectivity::Error(e));
162 }
163 pub(crate) fn set_connecting(&self, context: &Context) {
164 self.set(context, DetailedConnectivity::Connecting);
165 }
166 pub(crate) fn set_working(&self, context: &Context) {
167 self.set(context, DetailedConnectivity::Working);
168 }
169 pub(crate) fn set_preparing(&self, context: &Context) {
170 self.set(context, DetailedConnectivity::Preparing);
171 }
172 pub(crate) fn set_idle(&self, context: &Context) {
173 self.set(context, DetailedConnectivity::Idle);
174 }
175
176 fn get_detailed(&self) -> DetailedConnectivity {
177 self.0.lock().deref().clone()
178 }
179 fn get_basic(&self) -> Connectivity {
180 self.0.lock().to_basic()
181 }
182 fn get_all_work_done(&self) -> bool {
183 self.0.lock().all_work_done()
184 }
185}
186
187pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>) {
191 for inbox in inboxes {
192 let mut connectivity_lock = inbox.0.lock();
193 if *connectivity_lock == DetailedConnectivity::Idle {
194 *connectivity_lock = DetailedConnectivity::InterruptingIdle;
195 }
196 }
197
198 }
201
202pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
206 for store in &stores {
207 let mut connectivity_lock = store.0.lock();
208 if !matches!(
209 *connectivity_lock,
210 DetailedConnectivity::Uninitialized | DetailedConnectivity::Error(_)
211 ) {
212 *connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
213 }
214 }
215 context.emit_event(EventType::ConnectivityChanged);
216}
217
218impl fmt::Debug for ConnectivityStore {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 if let Some(guard) = self.0.try_lock() {
221 write!(f, "ConnectivityStore {:?}", &*guard)
222 } else {
223 write!(f, "ConnectivityStore [LOCKED]")
224 }
225 }
226}
227
228impl Context {
229 pub fn get_connectivity(&self) -> Connectivity {
244 let stores = self.connectivities.lock().clone();
245 let mut connectivities = Vec::new();
246 for s in stores {
247 let connectivity = s.get_basic();
248 connectivities.push(connectivity);
249 }
250 connectivities
251 .into_iter()
252 .min()
253 .unwrap_or(Connectivity::NotConnected)
254 }
255
256 pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
257 let stores: Vec<_> = match sched {
258 InnerSchedulerState::Started(sched) => sched
259 .boxes()
260 .map(|b| b.conn_state.state.connectivity.clone())
261 .collect(),
262 _ => Vec::new(),
263 };
264 *self.connectivities.lock() = stores;
265 }
266
267 #[expect(clippy::arithmetic_side_effects)]
277 pub async fn get_connectivity_html(&self) -> Result<String> {
278 let mut ret = r#"<!DOCTYPE html>
279 <html>
280 <head>
281 <meta charset="UTF-8" />
282 <meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
283 <style>
284 ul {
285 list-style-type: none;
286 padding-left: 1em;
287 }
288 .dot {
289 height: 0.9em; width: 0.9em;
290 border: 1px solid #888;
291 border-radius: 50%;
292 display: inline-block;
293 position: relative; left: -0.1em; top: 0.1em;
294 }
295 .bar {
296 width: 90%;
297 border: 1px solid #888;
298 border-radius: .5em;
299 margin-top: .2em;
300 margin-bottom: 1em;
301 position: relative; left: -0.2em;
302 }
303 .progress {
304 min-width:1.8em;
305 height: 1em;
306 border-radius: .45em;
307 color: white;
308 text-align: center;
309 padding-bottom: 2px;
310 }
311 .red {
312 background-color: #f33b2d;
313 }
314 .green {
315 background-color: #34c759;
316 }
317 .grey {
318 background-color: #808080;
319 }
320 .yellow {
321 background-color: #fdc625;
322 }
323 .transport {
324 margin-bottom: 1em;
325 }
326 .quota-list {
327 padding-left: 0;
328 }
329 </style>
330 </head>
331 <body>"#
332 .to_string();
333
334 if self
339 .get_config_bool(crate::config::Config::ProxyEnabled)
340 .await?
341 {
342 let proxy_enabled = stock_str::proxy_enabled(self);
343 let proxy_description = stock_str::proxy_description(self);
344 ret += &format!("<h3>{proxy_enabled}</h3><ul><li>{proxy_description}</li></ul>");
345 }
346
347 let lock = self.scheduler.inner.read().await;
352 let (folders_states, smtp) = match *lock {
353 InnerSchedulerState::Started(ref sched) => (
354 sched
355 .boxes()
356 .map(|b| {
357 (
358 b.addr.clone(),
359 b.folder.clone(),
360 b.conn_state.state.connectivity.clone(),
361 )
362 })
363 .collect::<Vec<_>>(),
364 sched.smtp.state.connectivity.clone(),
365 ),
366 _ => {
367 ret += &format!(
368 "<h3>{}</h3>\n</body></html>\n",
369 stock_str::not_connected(self)
370 );
371 return Ok(ret);
372 }
373 };
374 drop(lock);
375
376 let incoming_messages = stock_str::incoming_messages(self);
385 ret += &format!("<h3>{incoming_messages}</h3><ul>");
386
387 let transports = self
388 .sql
389 .query_map_vec("SELECT id, addr FROM transports", (), |row| {
390 let transport_id: u32 = row.get(0)?;
391 let addr: String = row.get(1)?;
392 Ok((transport_id, addr))
393 })
394 .await?;
395 let quota = self.quota.read().await;
396 for (transport_id, transport_addr) in transports {
397 let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
398 .map_or(transport_addr.clone(), |email| email.domain);
399 let domain_escaped = escaper::encode_minimal(domain);
400
401 ret += "<li class=\"transport\">";
402 let folders = folders_states
403 .iter()
404 .filter(|(folder_addr, ..)| *folder_addr == transport_addr);
405 for (_addr, _folder, state) in folders {
406 let detailed = &state.get_detailed();
407 ret += &*detailed.to_icon();
408 ret += " <b>";
409 ret += &*domain_escaped;
410 ret += ":</b> ";
411 ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
412 ret += "<br />";
413 }
414
415 let Some(quota) = quota.get(&transport_id) else {
416 ret += "</li>";
417 continue;
418 };
419 match "a.recent {
420 Err(e) => {
421 ret += &escaper::encode_minimal(&e.to_string());
422 }
423 Ok(quota) => {
424 if quota.is_empty() {
425 ret += &format!(
426 "Warning: {domain_escaped} claims to support quota but gives no information"
427 );
428 } else {
429 ret += "<ul class=\"quota-list\">";
430 for (root_name, resources) in quota {
431 use async_imap::types::QuotaResourceName::*;
432 for resource in resources {
433 ret += "<li>";
434
435 if quota.len() > 1 && !root_name.is_empty() {
438 ret += &format!(
439 "<b>{}:</b> ",
440 &*escaper::encode_minimal(root_name)
441 );
442 } else {
443 info!(
444 self,
445 "connectivity: root name hidden: \"{}\"", root_name
446 );
447 }
448
449 let messages = stock_str::messages(self);
450 let part_of_total_used = stock_str::part_of_total_used(
451 self,
452 &resource.usage.to_string(),
453 &resource.limit.to_string(),
454 );
455 ret += &match &resource.name {
456 Atom(resource_name) => {
457 format!(
458 "<b>{}:</b> {}",
459 &*escaper::encode_minimal(resource_name),
460 part_of_total_used
461 )
462 }
463 Message => {
464 format!("<b>{part_of_total_used}:</b> {messages}")
465 }
466 Storage => {
467 let usage = &format_size(resource.usage * 1024, BINARY);
474 let limit = &format_size(resource.limit * 1024, BINARY);
475 stock_str::part_of_total_used(self, usage, limit)
476 }
477 };
478
479 let percent = resource.get_usage_percentage();
480 let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
481 "red"
482 } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
483 "yellow"
484 } else {
485 "grey"
486 };
487 let div_width_percent = min(100, percent);
488 ret += &format!(
489 "<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>"
490 );
491
492 ret += "</li>";
493 }
494 }
495 ret += "</ul>";
496 }
497 }
498 }
499 ret += "</li>";
500 }
501 ret += "</ul>";
502
503 let outgoing_messages = stock_str::outgoing_messages(self);
510 ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
511 let detailed = smtp.get_detailed();
512 ret += &*detailed.to_icon();
513 ret += " ";
514 ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
515 ret += "</li></ul>";
516
517 ret += "</body></html>\n";
520 Ok(ret)
521 }
522
523 async fn all_work_done(&self) -> bool {
525 let lock = self.scheduler.inner.read().await;
526 let stores: Vec<_> = match *lock {
527 InnerSchedulerState::Started(ref sched) => sched
528 .boxes()
529 .map(|b| &b.conn_state.state)
530 .chain(once(&sched.smtp.state))
531 .map(|state| state.connectivity.clone())
532 .collect(),
533 _ => return false,
534 };
535 drop(lock);
536
537 for s in &stores {
538 if !s.get_all_work_done() {
539 return false;
540 }
541 }
542 true
543 }
544
545 pub async fn wait_for_all_work_done(&self) {
547 for _ in 0..10 {
552 if self.all_work_done().await {
553 break;
554 }
555 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
556 }
557
558 while !self.all_work_done().await {
560 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
561 }
562 }
563}