deltachat/scheduler/
connectivity.rs

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/// Rough connectivity status for display in the status bar in the UI.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
19pub enum Connectivity {
20    /// Not connected.
21    ///
22    /// This may be because we just started,
23    /// because we lost connection and
24    /// were not able to connect and log in yet
25    /// or because I/O is not started.
26    NotConnected = 1000,
27
28    /// Attempting to connect and log in.
29    Connecting = 2000,
30
31    /// Fetching or sending messages.
32    Working = 3000,
33
34    /// We are connected but not doing anything.
35    ///
36    /// This is the most common state,
37    /// so mobile UIs display the profile name
38    /// instead of connectivity status in this state.
39    /// Desktop UI displays "Connected" in the tooltip,
40    /// which signals that no more messages
41    /// are coming in.
42    Connected = 4000,
43}
44
45// The order of the connectivities is important: worse connectivities (i.e. those at
46// the top) take priority. This means that e.g. if any folder has an error - usually
47// because there is no internet connection - the connectivity for the whole
48// account will be `Notconnected`.
49#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
50enum DetailedConnectivity {
51    Error(String),
52    #[default]
53    Uninitialized,
54
55    /// Attempting to connect,
56    /// until we successfully log in.
57    Connecting,
58
59    /// Connection is just established,
60    /// there may be work to do.
61    Preparing,
62
63    /// There is actual work to do, e.g. there are messages in SMTP queue
64    /// or we detected a message on IMAP server that should be downloaded.
65    Working,
66
67    InterruptingIdle,
68
69    /// Connection is established and is idle.
70    Idle,
71
72    /// The folder was configured not to be watched or configured_*_folder is not set
73    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            // At this point IMAP has just connected,
86            // but does not know yet if there are messages to download.
87            // We still convert this to Working state
88            // so user can see "Updating..." and not "Connected"
89            // which is reserved for idle state.
90            DetailedConnectivity::Preparing => Some(Connectivity::Working),
91
92            // Just don't return a connectivity, probably the folder is configured not to be
93            // watched or there is e.g. no "Sent" folder, so we are not interested in it
94            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            // We don't know any more than that the last message was sent successfully;
138            // since sending the last message, connectivity could have changed, which we don't notice
139            // until another message is sent
140            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, // Just connected, there may still be work to do.
155            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
203/// Set all folder states to InterruptingIdle in case they were `Idle` before.
204/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
205/// returns false immediately after `dc_maybe_network()`.
206pub(crate) async fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
207    let mut connectivity_lock = inbox.0.lock().await;
208    // For the inbox, we also have to set the connectivity to InterruptingIdle if it was
209    // NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
210    // returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
211    // return Connected until DC is completely done with fetching folders; this also
212    // includes scan_folders() which happens on the inbox thread.
213    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    // No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
227    // of what we do here.
228}
229
230/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
231/// If we did not do this, the connectivity would stay "Connected" for quite a long time
232/// after `maybe_network_lost()` was called.
233pub(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    /// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
261    /// One of:
262    /// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
263    /// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
264    /// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Updating…" or a spinning wheel
265    /// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
266    ///
267    /// We don't use exact values but ranges here so that we can split up
268    /// states into multiple states in the future.
269    ///
270    /// Meant as a rough overview that can be shown
271    /// e.g. in the title of the main screen.
272    ///
273    /// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
274    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    /// Get an overview of the current connectivity, and possibly more statistics.
298    /// Meant to give the user more insight about the current status than
299    /// the basic connectivity info returned by dc_get_connectivity(); show this
300    /// e.g., if the user taps on said basic connectivity info.
301    ///
302    /// If this page changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
303    ///
304    /// This comes as an HTML from the core so that we can easily improve it
305    /// and the improvement instantly reaches all UIs.
306    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        // =============================================================================================
355        //                              Get the states from the RwLock
356        // =============================================================================================
357
358        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        // =============================================================================================
378        // Add e.g.
379        //                              Incoming messages
380        //                               - "Inbox": Connected
381        //                               - "Sent": Connected
382        // =============================================================================================
383
384        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                    // On the inbox thread, we also do some other things like scan_folders and run jobs
411                    // so, maybe, the inbox is not watched, but something else went wrong
412                    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        // =============================================================================================
423        // Add e.g.
424        //                              Outgoing messages
425        //                                Your last message was sent successfully
426        // =============================================================================================
427
428        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        // =============================================================================================
437        // Add e.g.
438        //                              Storage on testrun.org
439        //                                1.34 GiB of 2 GiB used
440        //                                [======67%=====       ]
441        // =============================================================================================
442
443        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 &quota.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                                // root name is empty eg. for gmail and redundant eg. for riseup.
459                                // therefore, use it only if there are really several roots.
460                                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                                        // do not use a special title needed for "Storage":
492                                        // - it is usually shown directly under the "Storage" headline
493                                        // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
494                                        // - the string is not longer than the other strings that way (minus title, plus units) -
495                                        //   additional linebreaks on small displays are unlikely therefore
496                                        // - most times, this is the only item anyway
497                                        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        // =============================================================================================
532
533        ret += "</body></html>\n";
534        Ok(ret)
535    }
536
537    /// Returns true if all background work is done.
538    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    /// Waits until background work is finished.
560    pub async fn wait_for_all_work_done(&self) {
561        // Ideally we could wait for connectivity change events,
562        // but sleep loop is good enough.
563
564        // First 100 ms sleep in chunks of 10 ms.
565        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        // If we are not finished in 100 ms, keep waking up every 100 ms.
573        while !self.all_work_done().await {
574            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
575        }
576    }
577}