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