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