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::context::Context;
9use crate::events::EventType;
10use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
11use crate::stock_str;
12
13use super::InnerSchedulerState;
14
15/// Rough connectivity status for display in the status bar in the UI.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
17pub enum Connectivity {
18    /// Not connected.
19    ///
20    /// This may be because we just started,
21    /// because we lost connection and
22    /// were not able to connect and log in yet
23    /// or because I/O is not started.
24    NotConnected = 1000,
25
26    /// Attempting to connect and log in.
27    Connecting = 2000,
28
29    /// Fetching or sending messages.
30    Working = 3000,
31
32    /// We are connected but not doing anything.
33    ///
34    /// This is the most common state,
35    /// so mobile UIs display the profile name
36    /// instead of connectivity status in this state.
37    /// Desktop UI displays "Connected" in the tooltip,
38    /// which signals that no more messages
39    /// are coming in.
40    Connected = 4000,
41}
42
43// The order of the connectivities is important: worse connectivities (i.e. those at
44// the top) take priority. This means that e.g. if any folder has an error - usually
45// because there is no internet connection - the connectivity for the whole
46// account will be `Notconnected`.
47#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
48enum DetailedConnectivity {
49    Error(String),
50    #[default]
51    Uninitialized,
52
53    /// Attempting to connect,
54    /// until we successfully log in.
55    Connecting,
56
57    /// Connection is just established,
58    /// there may be work to do.
59    Preparing,
60
61    /// There is actual work to do, e.g. there are messages in SMTP queue
62    /// or we detected a message on IMAP server that should be downloaded.
63    Working,
64
65    InterruptingIdle,
66
67    /// Connection is established and is idle.
68    Idle,
69}
70
71impl DetailedConnectivity {
72    fn to_basic(&self) -> Connectivity {
73        match self {
74            DetailedConnectivity::Error(_) => Connectivity::NotConnected,
75            DetailedConnectivity::Uninitialized => Connectivity::NotConnected,
76            DetailedConnectivity::Connecting => Connectivity::Connecting,
77            DetailedConnectivity::Working => Connectivity::Working,
78            DetailedConnectivity::InterruptingIdle => Connectivity::Working,
79
80            // At this point IMAP has just connected,
81            // but does not know yet if there are messages to download.
82            // We still convert this to Working state
83            // so user can see "Updating..." and not "Connected"
84            // which is reserved for idle state.
85            DetailedConnectivity::Preparing => Connectivity::Working,
86
87            DetailedConnectivity::Idle => Connectivity::Connected,
88        }
89    }
90
91    fn to_icon(&self) -> String {
92        match self {
93            DetailedConnectivity::Error(_) | DetailedConnectivity::Uninitialized => {
94                "<span class=\"red dot\"></span>".to_string()
95            }
96            DetailedConnectivity::Connecting => "<span class=\"yellow dot\"></span>".to_string(),
97            DetailedConnectivity::Preparing
98            | DetailedConnectivity::Working
99            | DetailedConnectivity::InterruptingIdle
100            | DetailedConnectivity::Idle => "<span class=\"green dot\"></span>".to_string(),
101        }
102    }
103
104    fn to_string_imap(&self, context: &Context) -> String {
105        match self {
106            DetailedConnectivity::Error(e) => stock_str::error(context, e),
107            DetailedConnectivity::Uninitialized => "Not started".to_string(),
108            DetailedConnectivity::Connecting => stock_str::connecting(context),
109            DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
110                stock_str::updating(context)
111            }
112            DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
113                stock_str::connected(context)
114            }
115        }
116    }
117
118    fn to_string_smtp(&self, context: &Context) -> String {
119        match self {
120            DetailedConnectivity::Error(e) => stock_str::error(context, e),
121            DetailedConnectivity::Uninitialized => {
122                "You did not try to send a message recently.".to_string()
123            }
124            DetailedConnectivity::Connecting => stock_str::connecting(context),
125            DetailedConnectivity::Working => stock_str::sending(context),
126
127            // We don't know any more than that the last message was sent successfully;
128            // since sending the last message, connectivity could have changed, which we don't notice
129            // until another message is sent
130            DetailedConnectivity::InterruptingIdle
131            | DetailedConnectivity::Preparing
132            | DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context),
133        }
134    }
135
136    fn all_work_done(&self) -> bool {
137        match self {
138            DetailedConnectivity::Error(_) => true,
139            DetailedConnectivity::Uninitialized => false,
140            DetailedConnectivity::Connecting => false,
141            DetailedConnectivity::Working => false,
142            DetailedConnectivity::InterruptingIdle => false,
143            DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do.
144            DetailedConnectivity::Idle => true,
145        }
146    }
147}
148
149#[derive(Clone, Default)]
150pub(crate) struct ConnectivityStore(Arc<parking_lot::Mutex<DetailedConnectivity>>);
151
152impl ConnectivityStore {
153    fn set(&self, context: &Context, v: DetailedConnectivity) {
154        {
155            *self.0.lock() = v;
156        }
157        context.emit_event(EventType::ConnectivityChanged);
158    }
159
160    pub(crate) fn set_err(&self, context: &Context, e: String) {
161        self.set(context, DetailedConnectivity::Error(e));
162    }
163    pub(crate) fn set_connecting(&self, context: &Context) {
164        self.set(context, DetailedConnectivity::Connecting);
165    }
166    pub(crate) fn set_working(&self, context: &Context) {
167        self.set(context, DetailedConnectivity::Working);
168    }
169    pub(crate) fn set_preparing(&self, context: &Context) {
170        self.set(context, DetailedConnectivity::Preparing);
171    }
172    pub(crate) fn set_idle(&self, context: &Context) {
173        self.set(context, DetailedConnectivity::Idle);
174    }
175
176    fn get_detailed(&self) -> DetailedConnectivity {
177        self.0.lock().deref().clone()
178    }
179    fn get_basic(&self) -> Connectivity {
180        self.0.lock().to_basic()
181    }
182    fn get_all_work_done(&self) -> bool {
183        self.0.lock().all_work_done()
184    }
185}
186
187/// Set all folder states to InterruptingIdle in case they were `Idle` before.
188/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
189/// returns false immediately after `dc_maybe_network()`.
190pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>) {
191    for inbox in inboxes {
192        let mut connectivity_lock = inbox.0.lock();
193        if *connectivity_lock == DetailedConnectivity::Idle {
194            *connectivity_lock = DetailedConnectivity::InterruptingIdle;
195        }
196    }
197
198    // No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
199    // of what we do here.
200}
201
202/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
203/// If we did not do this, the connectivity would stay "Connected" for quite a long time
204/// after `maybe_network_lost()` was called.
205pub(crate) fn maybe_network_lost(context: &Context, stores: Vec<ConnectivityStore>) {
206    for store in &stores {
207        let mut connectivity_lock = store.0.lock();
208        if !matches!(
209            *connectivity_lock,
210            DetailedConnectivity::Uninitialized | DetailedConnectivity::Error(_)
211        ) {
212            *connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
213        }
214    }
215    context.emit_event(EventType::ConnectivityChanged);
216}
217
218impl fmt::Debug for ConnectivityStore {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        if let Some(guard) = self.0.try_lock() {
221            write!(f, "ConnectivityStore {:?}", &*guard)
222        } else {
223            write!(f, "ConnectivityStore [LOCKED]")
224        }
225    }
226}
227
228impl Context {
229    /// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
230    /// One of:
231    /// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
232    /// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
233    /// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Updating…" or a spinning wheel
234    /// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
235    ///
236    /// We don't use exact values but ranges here so that we can split up
237    /// states into multiple states in the future.
238    ///
239    /// Meant as a rough overview that can be shown
240    /// e.g. in the title of the main screen.
241    ///
242    /// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
243    pub fn get_connectivity(&self) -> Connectivity {
244        let stores = self.connectivities.lock().clone();
245        let mut connectivities = Vec::new();
246        for s in stores {
247            let connectivity = s.get_basic();
248            connectivities.push(connectivity);
249        }
250        connectivities
251            .into_iter()
252            .min()
253            .unwrap_or(Connectivity::NotConnected)
254    }
255
256    pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
257        let stores: Vec<_> = match sched {
258            InnerSchedulerState::Started(sched) => sched
259                .boxes()
260                .map(|b| b.conn_state.state.connectivity.clone())
261                .collect(),
262            _ => Vec::new(),
263        };
264        *self.connectivities.lock() = stores;
265    }
266
267    /// Get an overview of the current connectivity, and possibly more statistics.
268    /// Meant to give the user more insight about the current status than
269    /// the basic connectivity info returned by dc_get_connectivity(); show this
270    /// e.g., if the user taps on said basic connectivity info.
271    ///
272    /// If this page changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
273    ///
274    /// This comes as an HTML from the core so that we can easily improve it
275    /// and the improvement instantly reaches all UIs.
276    #[expect(clippy::arithmetic_side_effects)]
277    pub async fn get_connectivity_html(&self) -> Result<String> {
278        let mut ret = r#"<!DOCTYPE html>
279            <html>
280            <head>
281                <meta charset="UTF-8" />
282                <meta name="viewport" content="initial-scale=1.0; user-scalable=no" />
283                <style>
284                    ul {
285                        list-style-type: none;
286                        padding-left: 1em;
287                    }
288                    .dot {
289                        height: 0.9em; width: 0.9em;
290                        border: 1px solid #888;
291                        border-radius: 50%;
292                        display: inline-block;
293                        position: relative; left: -0.1em; top: 0.1em;
294                    }
295                    .bar {
296                        width: 90%;
297                        border: 1px solid #888;
298                        border-radius: .5em;
299                        margin-top: .2em;
300                        margin-bottom: 1em;
301                        position: relative; left: -0.2em;
302                    }
303                    .progress {
304                        min-width:1.8em;
305                        height: 1em;
306                        border-radius: .45em;
307                        color: white;
308                        text-align: center;
309                        padding-bottom: 2px;
310                    }
311                    .red {
312                        background-color: #f33b2d;
313                    }
314                    .green {
315                        background-color: #34c759;
316                    }
317                    .grey {
318                        background-color: #808080;
319                    }
320                    .yellow {
321                        background-color: #fdc625;
322                    }
323                    .transport {
324                        margin-bottom: 1em;
325                    }
326                    .quota-list {
327                        padding-left: 0;
328                    }
329                </style>
330            </head>
331            <body>"#
332            .to_string();
333
334        // =============================================================================================
335        //                              Get proxy state
336        // =============================================================================================
337
338        if self
339            .get_config_bool(crate::config::Config::ProxyEnabled)
340            .await?
341        {
342            let proxy_enabled = stock_str::proxy_enabled(self);
343            let proxy_description = stock_str::proxy_description(self);
344            ret += &format!("<h3>{proxy_enabled}</h3><ul><li>{proxy_description}</li></ul>");
345        }
346
347        // =============================================================================================
348        //                              Get the states from the RwLock
349        // =============================================================================================
350
351        let lock = self.scheduler.inner.read().await;
352        let (folders_states, smtp) = match *lock {
353            InnerSchedulerState::Started(ref sched) => (
354                sched
355                    .boxes()
356                    .map(|b| {
357                        (
358                            b.addr.clone(),
359                            b.folder.clone(),
360                            b.conn_state.state.connectivity.clone(),
361                        )
362                    })
363                    .collect::<Vec<_>>(),
364                sched.smtp.state.connectivity.clone(),
365            ),
366            _ => {
367                ret += &format!(
368                    "<h3>{}</h3>\n</body></html>\n",
369                    stock_str::not_connected(self)
370                );
371                return Ok(ret);
372            }
373        };
374        drop(lock);
375
376        // =============================================================================================
377        // Add e.g.
378        //                              Incoming messages
379        //                               - [X] nine.testrun.org: Connected
380        //                                     1.34 GiB of 2 GiB used
381        //                                     [======67%=====       ]
382        // =============================================================================================
383
384        let incoming_messages = stock_str::incoming_messages(self);
385        ret += &format!("<h3>{incoming_messages}</h3><ul>");
386
387        let transports = self
388            .sql
389            .query_map_vec("SELECT id, addr FROM transports", (), |row| {
390                let transport_id: u32 = row.get(0)?;
391                let addr: String = row.get(1)?;
392                Ok((transport_id, addr))
393            })
394            .await?;
395        let quota = self.quota.read().await;
396        for (transport_id, transport_addr) in transports {
397            let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
398                .map_or(transport_addr.clone(), |email| email.domain);
399            let domain_escaped = escaper::encode_minimal(domain);
400
401            ret += "<li class=\"transport\">";
402            let folders = folders_states
403                .iter()
404                .filter(|(folder_addr, ..)| *folder_addr == transport_addr);
405            for (_addr, _folder, state) in folders {
406                let detailed = &state.get_detailed();
407                ret += &*detailed.to_icon();
408                ret += " <b>";
409                ret += &*domain_escaped;
410                ret += ":</b> ";
411                ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
412                ret += "<br />";
413            }
414
415            let Some(quota) = quota.get(&transport_id) else {
416                ret += "</li>";
417                continue;
418            };
419            match &quota.recent {
420                Err(e) => {
421                    ret += &escaper::encode_minimal(&e.to_string());
422                }
423                Ok(quota) => {
424                    if quota.is_empty() {
425                        ret += &format!(
426                            "Warning: {domain_escaped} claims to support quota but gives no information"
427                        );
428                    } else {
429                        ret += "<ul class=\"quota-list\">";
430                        for (root_name, resources) in quota {
431                            use async_imap::types::QuotaResourceName::*;
432                            for resource in resources {
433                                ret += "<li>";
434
435                                // root name is empty eg. for gmail and redundant eg. for riseup.
436                                // therefore, use it only if there are really several roots.
437                                if quota.len() > 1 && !root_name.is_empty() {
438                                    ret += &format!(
439                                        "<b>{}:</b> ",
440                                        &*escaper::encode_minimal(root_name)
441                                    );
442                                } else {
443                                    info!(
444                                        self,
445                                        "connectivity: root name hidden: \"{}\"", root_name
446                                    );
447                                }
448
449                                let messages = stock_str::messages(self);
450                                let part_of_total_used = stock_str::part_of_total_used(
451                                    self,
452                                    &resource.usage.to_string(),
453                                    &resource.limit.to_string(),
454                                );
455                                ret += &match &resource.name {
456                                    Atom(resource_name) => {
457                                        format!(
458                                            "<b>{}:</b> {}",
459                                            &*escaper::encode_minimal(resource_name),
460                                            part_of_total_used
461                                        )
462                                    }
463                                    Message => {
464                                        format!("<b>{part_of_total_used}:</b> {messages}")
465                                    }
466                                    Storage => {
467                                        // do not use a special title needed for "Storage":
468                                        // - it is usually shown directly under the "Storage" headline
469                                        // - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
470                                        // - the string is not longer than the other strings that way (minus title, plus units) -
471                                        //   additional linebreaks on small displays are unlikely therefore
472                                        // - most times, this is the only item anyway
473                                        let usage = &format_size(resource.usage * 1024, BINARY);
474                                        let limit = &format_size(resource.limit * 1024, BINARY);
475                                        stock_str::part_of_total_used(self, usage, limit)
476                                    }
477                                };
478
479                                let percent = resource.get_usage_percentage();
480                                let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
481                                    "red"
482                                } else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
483                                    "yellow"
484                                } else {
485                                    "grey"
486                                };
487                                let div_width_percent = min(100, percent);
488                                ret += &format!(
489                                    "<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>"
490                                );
491
492                                ret += "</li>";
493                            }
494                        }
495                        ret += "</ul>";
496                    }
497                }
498            }
499            ret += "</li>";
500        }
501        ret += "</ul>";
502
503        // =============================================================================================
504        // Add e.g.
505        //                              Outgoing messages
506        //                                Your last message was sent successfully
507        // =============================================================================================
508
509        let outgoing_messages = stock_str::outgoing_messages(self);
510        ret += &format!("<h3>{outgoing_messages}</h3><ul><li>");
511        let detailed = smtp.get_detailed();
512        ret += &*detailed.to_icon();
513        ret += " ";
514        ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
515        ret += "</li></ul>";
516
517        // =============================================================================================
518
519        ret += "</body></html>\n";
520        Ok(ret)
521    }
522
523    /// Returns true if all background work is done.
524    async fn all_work_done(&self) -> bool {
525        let lock = self.scheduler.inner.read().await;
526        let stores: Vec<_> = match *lock {
527            InnerSchedulerState::Started(ref sched) => sched
528                .boxes()
529                .map(|b| &b.conn_state.state)
530                .chain(once(&sched.smtp.state))
531                .map(|state| state.connectivity.clone())
532                .collect(),
533            _ => return false,
534        };
535        drop(lock);
536
537        for s in &stores {
538            if !s.get_all_work_done() {
539                return false;
540            }
541        }
542        true
543    }
544
545    /// Waits until background work is finished.
546    pub async fn wait_for_all_work_done(&self) {
547        // Ideally we could wait for connectivity change events,
548        // but sleep loop is good enough.
549
550        // First 100 ms sleep in chunks of 10 ms.
551        for _ in 0..10 {
552            if self.all_work_done().await {
553                break;
554            }
555            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
556        }
557
558        // If we are not finished in 100 ms, keep waking up every 100 ms.
559        while !self.all_work_done().await {
560            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
561        }
562    }
563}