1use core::fmt;
2use std::cmp::min;
3use std::{iter::once, ops::Deref, sync::Arc};
4
5use anyhow::Result;
6use humansize::{BINARY, format_size};
7use tokio::sync::Mutex;
8
9use crate::events::EventType;
10use crate::imap::{FolderMeaning, scan_folders::get_watched_folder_configs};
11use crate::log::info;
12use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
13use crate::stock_str;
14use crate::{context::Context, log::LogExt};
15
16use super::InnerSchedulerState;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
20pub enum Connectivity {
21 NotConnected = 1000,
28
29 Connecting = 2000,
31
32 Working = 3000,
34
35 Connected = 4000,
44}
45
46#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
51enum DetailedConnectivity {
52 Error(String),
53 #[default]
54 Uninitialized,
55
56 Connecting,
59
60 Preparing,
63
64 Working,
67
68 InterruptingIdle,
69
70 Idle,
72
73 NotConfigured,
75}
76
77impl DetailedConnectivity {
78 fn to_basic(&self) -> Option<Connectivity> {
79 match self {
80 DetailedConnectivity::Error(_) => Some(Connectivity::NotConnected),
81 DetailedConnectivity::Uninitialized => Some(Connectivity::NotConnected),
82 DetailedConnectivity::Connecting => Some(Connectivity::Connecting),
83 DetailedConnectivity::Working => Some(Connectivity::Working),
84 DetailedConnectivity::InterruptingIdle => Some(Connectivity::Working),
85
86 DetailedConnectivity::Preparing => Some(Connectivity::Working),
92
93 DetailedConnectivity::NotConfigured => None,
96
97 DetailedConnectivity::Idle => Some(Connectivity::Connected),
98 }
99 }
100
101 fn to_icon(&self) -> String {
102 match self {
103 DetailedConnectivity::Error(_)
104 | DetailedConnectivity::Uninitialized
105 | DetailedConnectivity::NotConfigured => "<span class=\"red dot\"></span>".to_string(),
106 DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
107 DetailedConnectivity::Preparing
108 | DetailedConnectivity::Working
109 | DetailedConnectivity::InterruptingIdle
110 | DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
111 }
112 }
113
114 async fn to_string_imap(&self, context: &Context) -> String {
115 match self {
116 DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
117 DetailedConnectivity::Uninitialized => "Not started".to_string(),
118 DetailedConnectivity::Connecting => stock_str::connecting(context).await,
119 DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
120 stock_str::updating(context).await
121 }
122 DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
123 stock_str::connected(context).await
124 }
125 DetailedConnectivity::NotConfigured => "Not configured".to_string(),
126 }
127 }
128
129 async fn to_string_smtp(&self, context: &Context) -> String {
130 match self {
131 DetailedConnectivity::Error(e) => stock_str::error(context, e).await,
132 DetailedConnectivity::Uninitialized => {
133 "You did not try to send a message recently.".to_string()
134 }
135 DetailedConnectivity::Connecting => stock_str::connecting(context).await,
136 DetailedConnectivity::Working => stock_str::sending(context).await,
137
138 DetailedConnectivity::InterruptingIdle
142 | DetailedConnectivity::Preparing
143 | DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context).await,
144 DetailedConnectivity::NotConfigured => "Not configured".to_string(),
145 }
146 }
147
148 fn all_work_done(&self) -> bool {
149 match self {
150 DetailedConnectivity::Error(_) => true,
151 DetailedConnectivity::Uninitialized => false,
152 DetailedConnectivity::Connecting => false,
153 DetailedConnectivity::Working => false,
154 DetailedConnectivity::InterruptingIdle => false,
155 DetailedConnectivity::Preparing => false, DetailedConnectivity::NotConfigured => true,
157 DetailedConnectivity::Idle => true,
158 }
159 }
160}
161
162#[derive(Clone, Default)]
163pub(crate) struct ConnectivityStore(Arc<Mutex<DetailedConnectivity>>);
164
165impl ConnectivityStore {
166 async fn set(&self, context: &Context, v: DetailedConnectivity) {
167 {
168 *self.0.lock().await = v;
169 }
170 context.emit_event(EventType::ConnectivityChanged);
171 }
172
173 pub(crate) async fn set_err(&self, context: &Context, e: impl ToString) {
174 self.set(context, DetailedConnectivity::Error(e.to_string()))
175 .await;
176 }
177 pub(crate) async fn set_connecting(&self, context: &Context) {
178 self.set(context, DetailedConnectivity::Connecting).await;
179 }
180 pub(crate) async fn set_working(&self, context: &Context) {
181 self.set(context, DetailedConnectivity::Working).await;
182 }
183 pub(crate) async fn set_preparing(&self, context: &Context) {
184 self.set(context, DetailedConnectivity::Preparing).await;
185 }
186 pub(crate) async fn set_not_configured(&self, context: &Context) {
187 self.set(context, DetailedConnectivity::NotConfigured).await;
188 }
189 pub(crate) async fn set_idle(&self, context: &Context) {
190 self.set(context, DetailedConnectivity::Idle).await;
191 }
192
193 async fn get_detailed(&self) -> DetailedConnectivity {
194 self.0.lock().await.deref().clone()
195 }
196 async fn get_basic(&self) -> Option<Connectivity> {
197 self.0.lock().await.to_basic()
198 }
199 async fn get_all_work_done(&self) -> bool {
200 self.0.lock().await.all_work_done()
201 }
202}
203
204pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
208 let mut connectivity_lock = inbox.0.lock().await;
209 if *connectivity_lock == DetailedConnectivity::Idle
215 || *connectivity_lock == DetailedConnectivity::NotConfigured
216 {
217 *connectivity_lock = DetailedConnectivity::InterruptingIdle;
218 }
219 drop(connectivity_lock);
220
221 for state in oboxes {
222 let mut connectivity_lock = state.0.lock().await;
223 if *connectivity_lock == DetailedConnectivity::Idle {
224 *connectivity_lock = DetailedConnectivity::InterruptingIdle;
225 }
226 }
227 }
230
231pub(crate) async fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
235 for store in &stores {
236 let mut connectivity_lock = store.0.lock().await;
237 if !matches!(
238 *connectivity_lock,
239 DetailedConnectivity::Uninitialized
240 | DetailedConnectivity::Error(_)
241 | DetailedConnectivity::NotConfigured,
242 ) {
243 *connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
244 }
245 }
246 context.emit_event(EventType::ConnectivityChanged);
247}
248
249impl fmt::Debug for ConnectivityStore {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 if let Ok(guard) = self.0.try_lock() {
252 write!(f, "ConnectivityStore {:?}", &*guard)
253 } else {
254 write!(f, "ConnectivityStore [LOCKED]")
255 }
256 }
257}
258
259impl Context {
260 pub async fn get_connectivity(&self) -> Connectivity {
275 let stores = self.connectivities.lock().clone();
276 let mut connectivities = Vec::new();
277 for s in stores {
278 if let Some(connectivity) = s.get_basic().await {
279 connectivities.push(connectivity);
280 }
281 }
282 connectivities
283 .into_iter()
284 .min()
285 .unwrap_or(Connectivity::NotConnected)
286 }
287
288 pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
289 let stores: Vec<_> = match sched {
290 InnerSchedulerState::Started(sched) => sched
291 .boxes()
292 .map(|b| b.conn_state.state.connectivity.clone())
293 .collect(),
294 _ => Vec::new(),
295 };
296 *self.connectivities.lock() = stores;
297 }
298
299 pub async fn get_connectivity_html(&self) -> Result<String> {
309 let mut ret = r#"<!DOCTYPE html>
310 <html>
311 <head>
312 <meta charset="UTF-8" />
313 <meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
314 <style>
315 ul {
316 list-style-type: none;
317 padding-left: 1em;
318 }
319 .dot {
320 height: 0.9em; width: 0.9em;
321 border: 1px solid #888;
322 border-radius: 50%;
323 display: inline-block;
324 position: relative; left: -0.1em; top: 0.1em;
325 }
326 .bar {
327 width: 90%;
328 border: 1px solid #888;
329 border-radius: .5em;
330 margin-top: .2em;
331 margin-bottom: 1em;
332 position: relative; left: -0.2em;
333 }
334 .progress {
335 min-width:1.8em;
336 height: 1em;
337 border-radius: .45em;
338 color: white;
339 text-align: center;
340 padding-bottom: 2px;
341 }
342 .red {
343 background-color: #f33b2d;
344 }
345 .green {
346 background-color: #34c759;
347 }
348 .yellow {
349 background-color: #fdc625;
350 }
351 </style>
352 </head>
353 <body>"#
354 .to_string();
355
356 let lock = self.scheduler.inner.read().await;
361 let (folders_states, smtp) = match *lock {
362 InnerSchedulerState::Started(ref sched) => (
363 sched
364 .boxes()
365 .map(|b| (b.meaning, b.conn_state.state.connectivity.clone()))
366 .collect::<Vec<_>>(),
367 sched.smtp.state.connectivity.clone(),
368 ),
369 _ => {
370 ret += &format!(
371 "<h3>{}</h3>\n</body></html>\n",
372 stock_str::not_connected(self).await
373 );
374 return Ok(ret);
375 }
376 };
377 drop(lock);
378
379 let watched_folders = get_watched_folder_configs(self).await?;
387 let incoming_messages = stock_str::incoming_messages(self).await;
388 ret += &format!("<h3>{incoming_messages}</h3><ul>");
389 for (folder, state) in &folders_states {
390 let mut folder_added = false;
391
392 if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
393 let f = self.get_config(config).await.log_err(self).ok().flatten();
394
395 if let Some(foldername) = f {
396 let detailed = &state.get_detailed().await;
397 ret += "<li>";
398 ret += &*detailed.to_icon();
399 ret += " <b>";
400 ret += &*escaper::encode_minimal(&foldername);
401 ret += ":</b> ";
402 ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
403 ret += "</li>";
404
405 folder_added = true;
406 }
407 }
408
409 if !folder_added && folder == &FolderMeaning::Inbox {
410 let detailed = &state.get_detailed().await;
411 if let DetailedConnectivity::Error(_) = detailed {
412 ret += "<li>";
415 ret += &*detailed.to_icon();
416 ret += " ";
417 ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
418 ret += "</li>";
419 }
420 }
421 }
422 ret += "</ul>";
423
424 let outgoing_messages = stock_str::outgoing_messages(self).await;
431 ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
432 let detailed = smtp.get_detailed().await;
433 ret += &*detailed.to_icon();
434 ret += " ";
435 ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
436 ret += "</li></ul>";
437
438 let domain =
446 &deltachat_contact_tools::EmailAddress::new(&self.get_primary_self_addr().await?)?
447 .domain;
448 let storage_on_domain = stock_str::storage_on_domain(self, domain).await;
449 ret += &format!("<h3>{storage_on_domain}</h3><ul>");
450 let quota = self.quota.read().await;
451 if let Some(quota) = &*quota {
452 match "a.recent {
453 Ok(quota) => {
454 if !quota.is_empty() {
455 for (root_name, resources) in quota {
456 use async_imap::types::QuotaResourceName::*;
457 for resource in resources {
458 ret += "<li>";
459
460 if quota.len() > 1 && !root_name.is_empty() {
463 ret += &format!(
464 "<b>{}:</b> ",
465 &*escaper::encode_minimal(root_name)
466 );
467 } else {
468 info!(
469 self,
470 "connectivity: root name hidden: \"{}\"", root_name
471 );
472 }
473
474 let messages = stock_str::messages(self).await;
475 let part_of_total_used = stock_str::part_of_total_used(
476 self,
477 &resource.usage.to_string(),
478 &resource.limit.to_string(),
479 )
480 .await;
481 ret += &match &resource.name {
482 Atom(resource_name) => {
483 format!(
484 "<b>{}:</b> {}",
485 &*escaper::encode_minimal(resource_name),
486 part_of_total_used
487 )
488 }
489 Message => {
490 format!("<b>{part_of_total_used}:</b> {messages}")
491 }
492 Storage => {
493 let usage = &format_size(resource.usage * 1024, BINARY);
500 let limit = &format_size(resource.limit * 1024, BINARY);
501 stock_str::part_of_total_used(self, usage, limit).await
502 }
503 };
504
505 let percent = resource.get_usage_percentage();
506 let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
507 "red"
508 } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
509 "yellow"
510 } else {
511 "green"
512 };
513 let div_width_percent = min(100, percent);
514 ret += &format!(
515 "<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>"
516 );
517
518 ret += "</li>";
519 }
520 }
521 } else {
522 ret += format!("<li>Warning: {domain} claims to support quota but gives no information</li>").as_str();
523 }
524 }
525 Err(e) => {
526 ret += format!("<li>{e}</li>").as_str();
527 }
528 }
529 } else {
530 let not_connected = stock_str::not_connected(self).await;
531 ret += &format!("<li>{not_connected}</li>");
532 }
533 ret += "</ul>";
534
535 ret += "</body></html>\n";
538 Ok(ret)
539 }
540
541 async fn all_work_done(&self) -> bool {
543 let lock = self.scheduler.inner.read().await;
544 let stores: Vec<_> = match *lock {
545 InnerSchedulerState::Started(ref sched) => sched
546 .boxes()
547 .map(|b| &b.conn_state.state)
548 .chain(once(&sched.smtp.state))
549 .map(|state| state.connectivity.clone())
550 .collect(),
551 _ => return false,
552 };
553 drop(lock);
554
555 for s in &stores {
556 if !s.get_all_work_done().await {
557 return false;
558 }
559 }
560 true
561 }
562
563 pub async fn wait_for_all_work_done(&self) {
565 for _ in 0..10 {
570 if self.all_work_done().await {
571 break;
572 }
573 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
574 }
575
576 while !self.all_work_done().await {
578 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
579 }
580 }
581}