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};
7
8use crate::events::EventType;
9use crate::imap::{FolderMeaning, scan_folders::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/// Rough connectivity status for display in the status bar in the UI.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
18pub enum Connectivity {
19    /// Not connected.
20    ///
21    /// This may be because we just started,
22    /// because we lost connection and
23    /// were not able to connect and log in yet
24    /// or because I/O is not started.
25    NotConnected = 1000,
26
27    /// Attempting to connect and log in.
28    Connecting = 2000,
29
30    /// Fetching or sending messages.
31    Working = 3000,
32
33    /// We are connected but not doing anything.
34    ///
35    /// This is the most common state,
36    /// so mobile UIs display the profile name
37    /// instead of connectivity status in this state.
38    /// Desktop UI displays "Connected" in the tooltip,
39    /// which signals that no more messages
40    /// are coming in.
41    Connected = 4000,
42}
43
44// The order of the connectivities is important: worse connectivities (i.e. those at
45// the top) take priority. This means that e.g. if any folder has an error - usually
46// because there is no internet connection - the connectivity for the whole
47// account will be `Notconnected`.
48#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
49enum DetailedConnectivity {
50    Error(String),
51    #[default]
52    Uninitialized,
53
54    /// Attempting to connect,
55    /// until we successfully log in.
56    Connecting,
57
58    /// Connection is just established,
59    /// there may be work to do.
60    Preparing,
61
62    /// There is actual work to do, e.g. there are messages in SMTP queue
63    /// or we detected a message on IMAP server that should be downloaded.
64    Working,
65
66    InterruptingIdle,
67
68    /// Connection is established and is idle.
69    Idle,
70
71    /// The folder was configured not to be watched or configured_*_folder is not set
72    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            // At this point IMAP has just connected,
85            // but does not know yet if there are messages to download.
86            // We still convert this to Working state
87            // so user can see "Updating..." and not "Connected"
88            // which is reserved for idle state.
89            DetailedConnectivity::Preparing => Some(Connectivity::Working),
90
91            // Just don't return a connectivity, probably the folder is configured not to be
92            // watched, so we are not interested in it.
93            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            // We don't know any more than that the last message was sent successfully;
137            // since sending the last message, connectivity could have changed, which we don't notice
138            // until another message is sent
139            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, // Just connected, there may still be work to do.
154            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
201/// Set all folder states to InterruptingIdle in case they were `Idle` before.
202/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
203/// returns false immediately after `dc_maybe_network()`.
204pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) {
205    for inbox in inboxes {
206        let mut connectivity_lock = inbox.0.lock();
207        // For the inbox, we also have to set the connectivity to InterruptingIdle if it was
208        // NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
209        // returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
210        // return Connected until DC is completely done with fetching folders; this also
211        // includes scan_folders() which happens on the inbox thread.
212        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    // No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
226    // of what we do here.
227}
228
229/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
230/// If we did not do this, the connectivity would stay "Connected" for quite a long time
231/// after `maybe_network_lost()` was called.
232pub(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    /// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
259    /// One of:
260    /// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
261    /// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
262    /// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Updating…" or a spinning wheel
263    /// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
264    ///
265    /// We don't use exact values but ranges here so that we can split up
266    /// states into multiple states in the future.
267    ///
268    /// Meant as a rough overview that can be shown
269    /// e.g. in the title of the main screen.
270    ///
271    /// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
272    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    /// 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 proxy state
356        // =============================================================================================
357
358        if self
359            .get_config_bool(crate::config::Config::ProxyEnabled)
360            .await?
361        {
362            let proxy_enabled = stock_str::proxy_enabled(self).await;
363            let proxy_description = stock_str::proxy_description(self).await;
364            ret += &format!("<h3>{proxy_enabled}</h3><ul><li>{proxy_description}</li></ul>");
365        }
366
367        // =============================================================================================
368        //                              Get the states from the RwLock
369        // =============================================================================================
370
371        let lock = self.scheduler.inner.read().await;
372        let (folders_states, smtp) = match *lock {
373            InnerSchedulerState::Started(ref sched) => (
374                sched
375                    .boxes()
376                    .map(|b| {
377                        (
378                            b.host.clone(),
379                            b.meaning,
380                            b.conn_state.state.connectivity.clone(),
381                        )
382                    })
383                    .collect::<Vec<_>>(),
384                sched.smtp.state.connectivity.clone(),
385            ),
386            _ => {
387                ret += &format!(
388                    "<h3>{}</h3>\n</body></html>\n",
389                    stock_str::not_connected(self).await
390                );
391                return Ok(ret);
392            }
393        };
394        drop(lock);
395
396        // =============================================================================================
397        // Add e.g.
398        //                              Incoming messages
399        //                               - "Inbox": Connected
400        // =============================================================================================
401
402        let watched_folders = get_watched_folder_configs(self).await?;
403        let incoming_messages = stock_str::incoming_messages(self).await;
404        ret += &format!("<h3>{incoming_messages}</h3><ul>");
405        for (host, folder, state) in &folders_states {
406            let mut folder_added = false;
407
408            if let Some(config) = folder.to_config().filter(|c| watched_folders.contains(c)) {
409                let f = self.get_config(config).await.log_err(self).ok().flatten();
410
411                if let Some(foldername) = f {
412                    let detailed = &state.get_detailed();
413                    ret += "<li>";
414                    ret += &*detailed.to_icon();
415                    ret += " <b>";
416                    if folder == &FolderMeaning::Inbox {
417                        ret += &*escaper::encode_minimal(host);
418                    } else {
419                        ret += &*escaper::encode_minimal(&foldername);
420                    }
421                    ret += ":</b> ";
422                    ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
423                    ret += "</li>";
424
425                    folder_added = true;
426                }
427            }
428
429            if !folder_added && folder == &FolderMeaning::Inbox {
430                let detailed = &state.get_detailed();
431                if let DetailedConnectivity::Error(_) = detailed {
432                    // On the inbox thread, we also do some other things like scan_folders and run jobs
433                    // so, maybe, the inbox is not watched, but something else went wrong
434                    ret += "<li>";
435                    ret += &*detailed.to_icon();
436                    ret += " ";
437                    ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
438                    ret += "</li>";
439                }
440            }
441        }
442        ret += "</ul>";
443
444        // =============================================================================================
445        // Add e.g.
446        //                              Outgoing messages
447        //                                Your last message was sent successfully
448        // =============================================================================================
449
450        let outgoing_messages = stock_str::outgoing_messages(self).await;
451        ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
452        let detailed = smtp.get_detailed();
453        ret += &*detailed.to_icon();
454        ret += " ";
455        ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self).await);
456        ret += "</li></ul>";
457
458        // =============================================================================================
459        // Add e.g.
460        //                              Storage on testrun.org
461        //                                1.34 GiB of 2 GiB used
462        //                                [======67%=====       ]
463        // =============================================================================================
464
465        ret += "<h3>Message Buffers</h3>";
466        let transports = self
467            .sql
468            .query_map_vec("SELECT id, addr FROM transports", (), |row| {
469                let transport_id: u32 = row.get(0)?;
470                let addr: String = row.get(1)?;
471                Ok((transport_id, addr))
472            })
473            .await?;
474        let quota = self.quota.read().await;
475        ret += "<ul>";
476        for (transport_id, transport_addr) in transports {
477            let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
478                .map_or(transport_addr, |email| email.domain);
479            let domain_escaped = escaper::encode_minimal(domain);
480            let Some(quota) = quota.get(&transport_id) else {
481                let not_connected = stock_str::not_connected(self).await;
482                ret += &format!("<li>{domain_escaped} &middot; {not_connected}</li>");
483                continue;
484            };
485            match &quota.recent {
486                Err(e) => {
487                    let error_escaped = escaper::encode_minimal(&e.to_string());
488                    ret += &format!("<li>{domain_escaped} &middot; {error_escaped}</li>");
489                }
490                Ok(quota) => {
491                    if quota.is_empty() {
492                        ret += &format!(
493                            "<li>{domain_escaped} &middot; Warning: {domain_escaped} claims to support quota but gives no information</li>"
494                        );
495                    } else {
496                        for (root_name, resources) in quota {
497                            use async_imap::types::QuotaResourceName::*;
498                            for resource in resources {
499                                ret += &format!("<li>{domain_escaped} &middot; ");
500
501                                // root name is empty eg. for gmail and redundant eg. for riseup.
502                                // therefore, use it only if there are really several roots.
503                                if quota.len() > 1 && !root_name.is_empty() {
504                                    ret += &format!(
505                                        "<b>{}:</b> ",
506                                        &*escaper::encode_minimal(root_name)
507                                    );
508                                } else {
509                                    info!(
510                                        self,
511                                        "connectivity: root name hidden: \"{}\"", root_name
512                                    );
513                                }
514
515                                let messages = stock_str::messages(self).await;
516                                let part_of_total_used = stock_str::part_of_total_used(
517                                    self,
518                                    &resource.usage.to_string(),
519                                    &resource.limit.to_string(),
520                                )
521                                .await;
522                                ret += &match &resource.name {
523                                    Atom(resource_name) => {
524                                        format!(
525                                            "<b>{}:</b> {}",
526                                            &*escaper::encode_minimal(resource_name),
527                                            part_of_total_used
528                                        )
529                                    }
530                                    Message => {
531                                        format!("<b>{part_of_total_used}:</b> {messages}")
532                                    }
533                                    Storage => {
534                                        // do not use a special title needed for "Storage":
535                                        // - it is usually shown directly under the "Storage" headline
536                                        // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
537                                        // - the string is not longer than the other strings that way (minus title, plus units) -
538                                        //   additional linebreaks on small displays are unlikely therefore
539                                        // - most times, this is the only item anyway
540                                        let usage = &format_size(resource.usage * 1024, BINARY);
541                                        let limit = &format_size(resource.limit * 1024, BINARY);
542                                        stock_str::part_of_total_used(self, usage, limit).await
543                                    }
544                                };
545
546                                let percent = resource.get_usage_percentage();
547                                let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
548                                    "red"
549                                } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
550                                    "yellow"
551                                } else {
552                                    "green"
553                                };
554                                let div_width_percent = min(100, percent);
555                                ret += &format!(
556                                    "<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>"
557                                );
558
559                                ret += "</li>";
560                            }
561                        }
562                    }
563                }
564            }
565        }
566        ret += "</ul>";
567
568        // =============================================================================================
569
570        ret += "</body></html>\n";
571        Ok(ret)
572    }
573
574    /// Returns true if all background work is done.
575    async fn all_work_done(&self) -> bool {
576        let lock = self.scheduler.inner.read().await;
577        let stores: Vec<_> = match *lock {
578            InnerSchedulerState::Started(ref sched) => sched
579                .boxes()
580                .map(|b| &b.conn_state.state)
581                .chain(once(&sched.smtp.state))
582                .map(|state| state.connectivity.clone())
583                .collect(),
584            _ => return false,
585        };
586        drop(lock);
587
588        for s in &stores {
589            if !s.get_all_work_done() {
590                return false;
591            }
592        }
593        true
594    }
595
596    /// Waits until background work is finished.
597    pub async fn wait_for_all_work_done(&self) {
598        // Ideally we could wait for connectivity change events,
599        // but sleep loop is good enough.
600
601        // First 100 ms sleep in chunks of 10 ms.
602        for _ in 0..10 {
603            if self.all_work_done().await {
604                break;
605            }
606            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
607        }
608
609        // If we are not finished in 100 ms, keep waking up every 100 ms.
610        while !self.all_work_done().await {
611            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
612        }
613    }
614}