use core::fmt;
use std::cmp::min;
use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::Result;
use humansize::{BINARY, format_size};
use crate::context::Context;
use crate::events::EventType;
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
use crate::stock_str;
use super::InnerSchedulerState;
/// Rough connectivity status for display in the status bar in the UI.
#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumProperty, PartialOrd, Ord)]
pub enum Connectivity {
/// Not connected.
///
/// This may be because we just started,
/// because we lost connection and
/// were not able to connect and log in yet
/// or because I/O is not started.
NotConnected = 1000,
/// Attempting to connect and log in.
Connecting = 2000,
/// Fetching or sending messages.
Working = 3000,
/// We are connected but not doing anything.
///
/// This is the most common state,
/// so mobile UIs display the profile name
/// instead of connectivity status in this state.
/// Desktop UI displays "Connected" in the tooltip,
/// which signals that no more messages
/// are coming in.
Connected = 4000,
}
// The order of the connectivities is important: worse connectivities (i.e. those at
// the top) take priority. This means that e.g. if any folder has an error - usually
// because there is no internet connection - the connectivity for the whole
// account will be `Notconnected`.
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumProperty, PartialOrd)]
enum DetailedConnectivity {
Error(String),
#[default]
Uninitialized,
/// Attempting to connect,
/// until we successfully log in.
Connecting,
/// Connection is just established,
/// there may be work to do.
Preparing,
/// There is actual work to do, e.g. there are messages in SMTP queue
/// or we detected a message on IMAP server that should be downloaded.
Working,
InterruptingIdle,
/// Connection is established and is idle.
Idle,
}
impl DetailedConnectivity {
fn to_basic(&self) -> Connectivity {
match self {
DetailedConnectivity::Error(_) => Connectivity::NotConnected,
DetailedConnectivity::Uninitialized => Connectivity::NotConnected,
DetailedConnectivity::Connecting => Connectivity::Connecting,
DetailedConnectivity::Working => Connectivity::Working,
DetailedConnectivity::InterruptingIdle => Connectivity::Working,
// At this point IMAP has just connected,
// but does not know yet if there are messages to download.
// We still convert this to Working state
// so user can see "Updating..." and not "Connected"
// which is reserved for idle state.
DetailedConnectivity::Preparing => Connectivity::Working,
DetailedConnectivity::Idle => Connectivity::Connected,
}
}
fn to_icon(&self) -> String {
match self {
DetailedConnectivity::Error(_) | DetailedConnectivity::Uninitialized => {
"".to_string()
}
DetailedConnectivity::Connecting => "".to_string(),
DetailedConnectivity::Preparing
| DetailedConnectivity::Working
| DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Idle => "".to_string(),
}
}
fn to_string_imap(&self, context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => stock_str::error(context, e),
DetailedConnectivity::Uninitialized => "Not started".to_string(),
DetailedConnectivity::Connecting => stock_str::connecting(context),
DetailedConnectivity::Preparing | DetailedConnectivity::Working => {
stock_str::updating(context)
}
DetailedConnectivity::InterruptingIdle | DetailedConnectivity::Idle => {
stock_str::connected(context)
}
}
}
fn to_string_smtp(&self, context: &Context) -> String {
match self {
DetailedConnectivity::Error(e) => stock_str::error(context, e),
DetailedConnectivity::Uninitialized => {
"You did not try to send a message recently.".to_string()
}
DetailedConnectivity::Connecting => stock_str::connecting(context),
DetailedConnectivity::Working => stock_str::sending(context),
// We don't know any more than that the last message was sent successfully;
// since sending the last message, connectivity could have changed, which we don't notice
// until another message is sent
DetailedConnectivity::InterruptingIdle
| DetailedConnectivity::Preparing
| DetailedConnectivity::Idle => stock_str::last_msg_sent_successfully(context),
}
}
fn all_work_done(&self) -> bool {
match self {
DetailedConnectivity::Error(_) => true,
DetailedConnectivity::Uninitialized => false,
DetailedConnectivity::Connecting => false,
DetailedConnectivity::Working => false,
DetailedConnectivity::InterruptingIdle => false,
DetailedConnectivity::Preparing => false, // Just connected, there may still be work to do.
DetailedConnectivity::Idle => true,
}
}
}
#[derive(Clone, Default)]
pub(crate) struct ConnectivityStore(Arc>);
impl ConnectivityStore {
fn set(&self, context: &Context, v: DetailedConnectivity) {
{
*self.0.lock() = v;
}
context.emit_event(EventType::ConnectivityChanged);
}
pub(crate) fn set_err(&self, context: &Context, e: impl ToString) {
self.set(context, DetailedConnectivity::Error(e.to_string()));
}
pub(crate) fn set_connecting(&self, context: &Context) {
self.set(context, DetailedConnectivity::Connecting);
}
pub(crate) fn set_working(&self, context: &Context) {
self.set(context, DetailedConnectivity::Working);
}
pub(crate) fn set_preparing(&self, context: &Context) {
self.set(context, DetailedConnectivity::Preparing);
}
pub(crate) fn set_idle(&self, context: &Context) {
self.set(context, DetailedConnectivity::Idle);
}
fn get_detailed(&self) -> DetailedConnectivity {
self.0.lock().deref().clone()
}
fn get_basic(&self) -> Connectivity {
self.0.lock().to_basic()
}
fn get_all_work_done(&self) -> bool {
self.0.lock().all_work_done()
}
}
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) fn idle_interrupted(inboxes: Vec) {
for inbox in inboxes {
let mut connectivity_lock = inbox.0.lock();
if *connectivity_lock == DetailedConnectivity::Idle {
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
}
// No need to send ConnectivityChanged, the user-facing connectivity doesn't change because
// of what we do here.
}
/// Set the connectivity to "Not connected" after a call to dc_maybe_network_lost().
/// If we did not do this, the connectivity would stay "Connected" for quite a long time
/// after `maybe_network_lost()` was called.
pub(crate) fn maybe_network_lost(context: &Context, stores: Vec) {
for store in &stores {
let mut connectivity_lock = store.0.lock();
if !matches!(
*connectivity_lock,
DetailedConnectivity::Uninitialized | DetailedConnectivity::Error(_)
) {
*connectivity_lock = DetailedConnectivity::Error("Connection lost".to_string());
}
}
context.emit_event(EventType::ConnectivityChanged);
}
impl fmt::Debug for ConnectivityStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(guard) = self.0.try_lock() {
write!(f, "ConnectivityStore {:?}", &*guard)
} else {
write!(f, "ConnectivityStore [LOCKED]")
}
}
}
impl Context {
/// Get the current connectivity, i.e. whether the device is connected to the IMAP server.
/// One of:
/// - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
/// - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
/// - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Updating…" or a spinning wheel
/// - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
///
/// We don't use exact values but ranges here so that we can split up
/// states into multiple states in the future.
///
/// Meant as a rough overview that can be shown
/// e.g. in the title of the main screen.
///
/// If the connectivity changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
pub fn get_connectivity(&self) -> Connectivity {
let stores = self.connectivities.lock().clone();
let mut connectivities = Vec::new();
for s in stores {
let connectivity = s.get_basic();
connectivities.push(connectivity);
}
connectivities
.into_iter()
.min()
.unwrap_or(Connectivity::NotConnected)
}
pub(crate) fn update_connectivities(&self, sched: &InnerSchedulerState) {
let stores: Vec<_> = match sched {
InnerSchedulerState::Started(sched) => sched
.boxes()
.map(|b| b.conn_state.state.connectivity.clone())
.collect(),
_ => Vec::new(),
};
*self.connectivities.lock() = stores;
}
/// Get an overview of the current connectivity, and possibly more statistics.
/// Meant to give the user more insight about the current status than
/// the basic connectivity info returned by dc_get_connectivity(); show this
/// e.g., if the user taps on said basic connectivity info.
///
/// If this page changes, a DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_connectivity_html(&self) -> Result {
let mut ret = r#"
"#
.to_string();
// =============================================================================================
// Get proxy state
// =============================================================================================
if self
.get_config_bool(crate::config::Config::ProxyEnabled)
.await?
{
let proxy_enabled = stock_str::proxy_enabled(self);
let proxy_description = stock_str::proxy_description(self);
ret += &format!("{proxy_enabled}
");
}
// =============================================================================================
// Get the states from the RwLock
// =============================================================================================
let lock = self.scheduler.inner.read().await;
let (folders_states, smtp) = match *lock {
InnerSchedulerState::Started(ref sched) => (
sched
.boxes()
.map(|b| {
(
b.addr.clone(),
b.folder.clone(),
b.conn_state.state.connectivity.clone(),
)
})
.collect::>(),
sched.smtp.state.connectivity.clone(),
),
_ => {
ret += &format!(
"{}
\n\n",
stock_str::not_connected(self)
);
return Ok(ret);
}
};
drop(lock);
// =============================================================================================
// Add e.g.
// Incoming messages
// - [X] nine.testrun.org: Connected
// 1.34 GiB of 2 GiB used
// [======67%===== ]
// =============================================================================================
let incoming_messages = stock_str::incoming_messages(self);
ret += &format!("{incoming_messages}
");
let transports = self
.sql
.query_map_vec("SELECT id, addr FROM transports", (), |row| {
let transport_id: u32 = row.get(0)?;
let addr: String = row.get(1)?;
Ok((transport_id, addr))
})
.await?;
let quota = self.quota.read().await;
for (transport_id, transport_addr) in transports {
let domain = &deltachat_contact_tools::EmailAddress::new(&transport_addr)
.map_or(transport_addr.clone(), |email| email.domain);
let domain_escaped = escaper::encode_minimal(domain);
ret += "- ";
let folders = folders_states
.iter()
.filter(|(folder_addr, ..)| *folder_addr == transport_addr);
for (_addr, _folder, state) in folders {
let detailed = &state.get_detailed();
ret += &*detailed.to_icon();
ret += " ";
ret += &*domain_escaped;
ret += ": ";
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self));
ret += "
";
}
let Some(quota) = quota.get(&transport_id) else {
ret += " ";
continue;
};
match "a.recent {
Err(e) => {
ret += &escaper::encode_minimal(&e.to_string());
}
Ok(quota) => {
if quota.is_empty() {
ret += &format!(
"Warning: {domain_escaped} claims to support quota but gives no information"
);
} else {
ret += "";
for (root_name, resources) in quota {
use async_imap::types::QuotaResourceName::*;
for resource in resources {
ret += "- ";
// root name is empty eg. for gmail and redundant eg. for riseup.
// therefore, use it only if there are really several roots.
if quota.len() > 1 && !root_name.is_empty() {
ret += &format!(
"{}: ",
&*escaper::encode_minimal(root_name)
);
} else {
info!(
self,
"connectivity: root name hidden: \"{}\"", root_name
);
}
let messages = stock_str::messages(self);
let part_of_total_used = stock_str::part_of_total_used(
self,
&resource.usage.to_string(),
&resource.limit.to_string(),
);
ret += &match &resource.name {
Atom(resource_name) => {
format!(
"{}: {}",
&*escaper::encode_minimal(resource_name),
part_of_total_used
)
}
Message => {
format!("{part_of_total_used}: {messages}")
}
Storage => {
// do not use a special title needed for "Storage":
// - it is usually shown directly under the "Storage" headline
// - by the units "1 MB of 10 MB used" there is some difference to eg. "Messages: 1 of 10 used"
// - the string is not longer than the other strings that way (minus title, plus units) -
// additional linebreaks on small displays are unlikely therefore
// - most times, this is the only item anyway
let usage = &format_size(resource.usage * 1024, BINARY);
let limit = &format_size(resource.limit * 1024, BINARY);
stock_str::part_of_total_used(self, usage, limit)
}
};
let percent = resource.get_usage_percentage();
let color = if percent >= QUOTA_ERROR_THRESHOLD_PERCENTAGE {
"red"
} else if percent >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
"yellow"
} else {
"grey"
};
let div_width_percent = min(100, percent);
ret += &format!(
""
);
ret += "
";
}
}
ret += "
";
}
}
}
ret += "";
}
ret += "
";
// =============================================================================================
// Add e.g.
// Outgoing messages
// Your last message was sent successfully
// =============================================================================================
let outgoing_messages = stock_str::outgoing_messages(self);
ret += &format!("{outgoing_messages}
- ");
let detailed = smtp.get_detailed();
ret += &*detailed.to_icon();
ret += " ";
ret += &*escaper::encode_minimal(&detailed.to_string_smtp(self));
ret += "
";
// =============================================================================================
ret += "