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::events::EventType;
9use crate::imap::{FolderMeaning, get_watched_folder_configs};
10use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
11use crate::stock_str;
12use crate::{context::Context, log::LogExt};
13
14use super::InnerSchedulerState;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
18pub enum Connectivity {
19 NotConnected = 1000,
26
27 Connecting = 2000,
29
30 Working = 3000,
32
33 Connected = 4000,
42}
43
44#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
49enum DetailedConnectivity {
50 Error(String),
51 #[default]
52 Uninitialized,
53
54 Connecting,
57
58 Preparing,
61
62 Working,
65
66 InterruptingIdle,
67
68 Idle,
70
71 NotConfigured,
73}
74
75impl DetailedConnectivity {
76 fn to_basic(&self) -> Option<Connectivity> {
77 match self {
78 DetailedConnectivity::Error(_) => Some(Connectivity::NotConnected),
79 DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected),
80 DetailedConnectivity::Connecting => Some(Connectivity::Connecting),
81 DetailedConnectivity::Working => Some(Connectivity::Working),
82 DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working),
83
84 DetailedConnectivity::Preparing => Some(Connectivity::Working),
90
91 DetailedConnectivity::NotConfigured => None,
94
95 DetailedConnectivity::Idle => Some(Connectivity::Connected),
96 }
97 }
98
99 fn to_icon(&self) -> String {
100 match self {
101 DetailedConnectivity::Error(_)
102 | DetailedConnectivity::Uninitialized
103 | DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(),
104 DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
105 DetailedConnectivity::Preparing
106 | DetailedConnectivity::Working
107 | DetailedConnectivity::InterruptingIdle
108 | DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
109 }
110 }
111
112 async fn to_string_imap(&self, context: &Context) -> String {
113 match self {
114 DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
115 DetailedConnectivity::Uninitialized => "Not started".to_string(),
116 DetailedConnectivity::Connecting => stock_str::connecting(context).await,
117 DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
118 stock_str::updating(context).await
119 }
120 DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
121 stock_str::connected(context).await
122 }
123 DetailedConnectivity::NotConfigured => "Not configured".to_string(),
124 }
125 }
126
127 async fn to_string_smtp(&self, context: &Context) -> String {
128 match self {
129 DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
130 DetailedConnectivity::Uninitialized => {
131 "You did not try to send a message recently.".to_string()
132 }
133 DetailedConnectivity::Connecting => stock_str::connecting(context).await,
134 DetailedConnectivity::Working => stock_str::sending(context).await,
135
136 DetailedConnectivity::InterruptingIdle
140 | DetailedConnectivity::Preparing
141 | DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await,
142 DetailedConnectivity::NotConfigured => "Not configured".to_string(),
143 }
144 }
145
146 fn all_work_done(&self) -> bool {
147 match self {
148 DetailedConnectivity::Error(_) => true,
149 DetailedConnectivity::Uninitialized => false,
150 DetailedConnectivity::Connecting => false,
151 DetailedConnectivity::Working => false,
152 DetailedConnectivity::InterruptingIdle => false,
153 DetailedConnectivity::Preparing => false, DetailedConnectivity::NotConfigured => true,
155 DetailedConnectivity::Idle => true,
156 }
157 }
158}
159
160#[derive(Clone, Default)]
161pub(crate) struct ConnectivityStore(Arc<parking_lot::Mutex<DetailedConnectivity>>);
162
163impl ConnectivityStore {
164 fn set(&self, context: &Context, v: DetailedConnectivity) {
165 {
166 *self.0.lock() = v;
167 }
168 context.emit_event(EventType::ConnectivityChanged);
169 }
170
171 pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
172 self.set(context, DetailedConnectivity::Error(e.to_string()));
173 }
174 pub(crate) fn set_connecting(&self, context: &Context) {
175 self.set(context, DetailedConnectivity::Connecting);
176 }
177 pub(crate) fn set_working(&self, context: &Context) {
178 self.set(context, DetailedConnectivity::Working);
179 }
180 pub(crate) fn set_preparing(&self, context: &Context) {
181 self.set(context, DetailedConnectivity::Preparing);
182 }
183 pub(crate) fn set_not_configured(&self, context: &Context) {
184 self.set(context, DetailedConnectivity::NotConfigured);
185 }
186 pub(crate) fn set_idle(&self, context: &Context) {
187 self.set(context, DetailedConnectivity::Idle);
188 }
189
190 fn get_detailed(&self) -> DetailedConnectivity {
191 self.0.lock().deref().clone()
192 }
193 fn get_basic(&self) -> Option<Connectivity> {
194 self.0.lock().to_basic()
195 }
196 fn get_all_work_done(&self) -> bool {
197 self.0.lock().all_work_done()
198 }
199}
200
201pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) {
205 for inbox in inboxes {
206 let mut connectivity_lock = inbox.0.lock();
207 if *connectivity_lock == DetailedConnectivity::Idle
213 || *connectivity_lock == DetailedConnectivity::NotConfigured
214 {
215 *connectivity_lock = DetailedConnectivity::InterruptingIdle;
216 }
217 }
218
219 for state in oboxes {
220 let mut connectivity_lock = state.0.lock();
221 if *connectivity_lock == DetailedConnectivity::Idle {
222 *connectivity_lock = DetailedConnectivity::InterruptingIdle;
223 }
224 }
225 }
228
229pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
233 for store in &stores {
234 let mut connectivity_lock = store.0.lock();
235 if !matches!(
236 *connectivity_lock,
237 DetailedConnectivity::Uninitialized
238 | DetailedConnectivity::Error(_)
239 | DetailedConnectivity::NotConfigured,
240 ) {
241 *connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
242 }
243 }
244 context.emit_event(EventType::ConnectivityChanged);
245}
246
247impl fmt::Debug for ConnectivityStore {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 if let Some(guard) = self.0.try_lock() {
250 write!(f, "ConnectivityStore {:?}", &*guard)
251 } else {
252 write!(f, "ConnectivityStore [LOCKED]")
253 }
254 }
255}
256
257impl Context {
258 pub fn get_connectivity(&self) -> Connectivity {
273 let stores = self.connectivities.lock().clone();
274 let mut connectivities = Vec::new();
275 for s in stores {
276 if let Some(connectivity) = s.get_basic() {
277 connectivities.push(connectivity);
278 }
279 }
280 connectivities
281 .into_iter()
282 .min()
283 .unwrap_or(Connectivity::NotConnected)
284 }
285
286 pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
287 let stores: Vec<_> = match sched {
288 InnerSchedulerState::Started(sched) => sched
289 .boxes()
290 .map(|b| b.conn_state.state.connectivity.clone())
291 .collect(),
292 _ => Vec::new(),
293 };
294 *self.connectivities.lock() = stores;
295 }
296
297 #[expect(clippy::arithmetic_side_effects)]
307 pub async fn get_connectivity_html(&self) -> Result<String> {
308 let mut ret = r#"<!DOCTYPE html>
309 <html>
310 <head>
311 <meta charset="UTF-8" />
312 <meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
313 <style>
314 ul {
315 list-style-type: none;
316 padding-left: 1em;
317 }
318 .dot {
319 height: 0.9em; width: 0.9em;
320 border: 1px solid #888;
321 border-radius: 50%;
322 display: inline-block;
323 position: relative; left: -0.1em; top: 0.1em;
324 }
325 .bar {
326 width: 90%;
327 border: 1px solid #888;
328 border-radius: .5em;
329 margin-top: .2em;
330 margin-bottom: 1em;
331 position: relative; left: -0.2em;
332 }
333 .progress {
334 min-width:1.8em;
335 height: 1em;
336 border-radius: .45em;
337 color: white;
338 text-align: center;
339 padding-bottom: 2px;
340 }
341 .red {
342 background-color: #f33b2d;
343 }
344 .green {
345 background-color: #34c759;
346 }
347 .grey {
348 background-color: #808080;
349 }
350 .yellow {
351 background-color: #fdc625;
352 }
353 .transport {
354 margin-bottom: 1em;
355 }
356 .quota-list {
357 padding-left: 0;
358 }
359 </style>
360 </head>
361 <body>"#
362 .to_string();
363
364 if self
369 .get_config_bool(crate::config::Config::ProxyEnabled)
370 .await?
371 {
372 let proxy_enabled = stock_str::proxy_enabled(self).await;
373 let proxy_description = stock_str::proxy_description(self).await;
374 ret += &format!("<h3>{proxy_enabled}</h3><ul><li>{proxy_description}</li></ul>");
375 }
376
377 let lock = self.scheduler.inner.read().await;
382 let (folders_states, smtp) = match *lock {
383 InnerSchedulerState::Started(ref sched) => (
384 sched
385 .boxes()
386 .map(|b| {
387 (
388 b.addr.clone(),
389 b.meaning,
390 b.conn_state.state.connectivity.clone(),
391 )
392 })
393 .collect::<Vec<_>>(),
394 sched.smtp.state.connectivity.clone(),
395 ),
396 _ => {
397 ret += &format!(
398 "<h3>{}</h3>\n</body></html>\n",
399 stock_str::not_connected(self).await
400 );
401 return Ok(ret);
402 }
403 };
404 drop(lock);
405
406 let watched_folders = get_watched_folder_configs(self).await?;
415 let incoming_messages = stock_str::incoming_messages(self).await;
416 ret += &format!("<h3>{incoming_messages}</h3><ul>");
417
418 let transports = self
419 .sql
420 .query_map_vec("SELECT id, addr FROM transports", (), |row| {
421 let transport_id: u32 = row.get(0)?;
422 let addr: String = row.get(1)?;
423 Ok((transport_id, addr))
424 })
425 .await?;
426 let quota = self.quota.read().await;
427 for (transport_id, transport_addr) in transports {
428 let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
429 .map_or(transport_addr.clone(), |email| email.domain);
430 let domain_escaped = escaper::encode_minimal(domain);
431
432 ret += "<li class=\"transport\">";
433 let folders = folders_states
434 .iter()
435 .filter(|(folder_addr, ..)| *folder_addr == transport_addr);
436 for (_addr, folder, state) in folders {
437 let mut folder_added = false;
438
439 if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
440 let f = self.get_config(config).await.log_err(self).ok().flatten();
441
442 if let Some(foldername) = f {
443 let detailed = &state.get_detailed();
444 ret += &*detailed.to_icon();
445 ret += " <b>";
446 if folder == &FolderMeaning::Inbox {
447 ret += &*domain_escaped;
448 } else {
449 ret += &*escaper::encode_minimal(&foldername);
450 }
451 ret += ":</b> ";
452 ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
453 ret += "<br />";
454
455 folder_added = true;
456 }
457 }
458
459 if !folder_added && folder == &FolderMeaning::Inbox {
460 let detailed = &state.get_detailed();
461 if let DetailedConnectivity::Error(_) = detailed {
462 ret += &*detailed.to_icon();
466 ret += " ";
467 ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
468 ret += "<br />";
469 }
470 }
471 }
472
473 let Some(quota) = quota.get(&transport_id) else {
474 ret += "</li>";
475 continue;
476 };
477 match "a.recent {
478 Err(e) => {
479 ret += &escaper::encode_minimal(&e.to_string());
480 }
481 Ok(quota) => {
482 if quota.is_empty() {
483 ret += &format!(
484 "Warning: {domain_escaped} claims to support quota but gives no information"
485 );
486 } else {
487 ret += "<ul class=\"quota-list\">";
488 for (root_name, resources) in quota {
489 use async_imap::types::QuotaResourceName::*;
490 for resource in resources {
491 ret += "<li>";
492
493 if quota.len() > 1 && !root_name.is_empty() {
496 ret += &format!(
497 "<b>{}:</b> ",
498 &*escaper::encode_minimal(root_name)
499 );
500 } else {
501 info!(
502 self,
503 "connectivity: root name hidden: \"{}\"", root_name
504 );
505 }
506
507 let messages = stock_str::messages(self).await;
508 let part_of_total_used = stock_str::part_of_total_used(
509 self,
510 &resource.usage.to_string(),
511 &resource.limit.to_string(),
512 )
513 .await;
514 ret += &match &resource.name {
515 Atom(resource_name) => {
516 format!(
517 "<b>{}:</b> {}",
518 &*escaper::encode_minimal(resource_name),
519 part_of_total_used
520 )
521 }
522 Message => {
523 format!("<b>{part_of_total_used}:</b> {messages}")
524 }
525 Storage => {
526 let usage = &format_size(resource.usage * 1024, BINARY);
533 let limit = &format_size(resource.limit * 1024, BINARY);
534 stock_str::part_of_total_used(self, usage, limit).await
535 }
536 };
537
538 let percent = resource.get_usage_percentage();
539 let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
540 "red"
541 } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
542 "yellow"
543 } else {
544 "grey"
545 };
546 let div_width_percent = min(100, percent);
547 ret += &format!(
548 "<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>"
549 );
550
551 ret += "</li>";
552 }
553 }
554 ret += "</ul>";
555 }
556 }
557 }
558 ret += "</li>";
559 }
560 ret += "</ul>";
561
562 let outgoing_messages = stock_str::outgoing_messages(self).await;
569 ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
570 let detailed = smtp.get_detailed();
571 ret += &*detailed.to_icon();
572 ret += " ";
573 ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
574 ret += "</li></ul>";
575
576 ret += "</body></html>\n";
579 Ok(ret)
580 }
581
582 async fn all_work_done(&self) -> bool {
584 let lock = self.scheduler.inner.read().await;
585 let stores: Vec<_> = match *lock {
586 InnerSchedulerState::Started(ref sched) => sched
587 .boxes()
588 .map(|b| &b.conn_state.state)
589 .chain(once(&sched.smtp.state))
590 .map(|state| state.connectivity.clone())
591 .collect(),
592 _ => return false,
593 };
594 drop(lock);
595
596 for s in &stores {
597 if !s.get_all_work_done() {
598 return false;
599 }
600 }
601 true
602 }
603
604 pub async fn wait_for_all_work_done(&self) {
606 for _ in 0..10 {
611 if self.all_work_done().await {
612 break;
613 }
614 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
615 }
616
617 while !self.all_work_done().await {
619 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
620 }
621 }
622}