//! # Key-value configuration management. use std::env; use std::path::Path; use std::str::FromStr; use anyhow::{Context as _, Result, bail, ensure}; use base64::Engine as _; use deltachat_contact_tools::{addr_cmp, sanitize_single_line}; use serde::{Deserialize, Serialize}; use strum::{EnumProperty, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; use tokio::fs; use crate::blob::BlobObject; use crate::context::Context; use crate::events::EventType; use crate::log::LogExt; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::provider::Provider; use crate::sync::{self, Sync::*, SyncData}; use crate::tools::get_abs_path; use crate::transport::{ConfiguredLoginParam, add_pseudo_transport, send_sync_transports}; use crate::{constants, stats}; /// The available configuration keys. #[derive( Debug, Clone, Copy, PartialEq, Eq, Display, EnumString, AsRefStr, EnumIter, EnumProperty, PartialOrd, Ord, Serialize, Deserialize, )] #[strum(serialize_all = "snake_case")] pub enum Config { /// Email address, used in the `From:` field. Addr, /// IMAP server hostname. MailServer, /// IMAP server username. MailUser, /// IMAP server password. MailPw, /// IMAP server port. MailPort, /// IMAP server security (e.g. TLS, STARTTLS). MailSecurity, /// How to check TLS certificates. /// /// "IMAP" in the name is for compatibility, /// this actually applies to both IMAP and SMTP connections. ImapCertificateChecks, /// SMTP server hostname. SendServer, /// SMTP server username. SendUser, /// SMTP server password. SendPw, /// SMTP server port. SendPort, /// SMTP server security (e.g. TLS, STARTTLS). SendSecurity, /// Deprecated option for backwards compatibility. /// /// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config. SmtpCertificateChecks, /// Whether to use OAuth 2. /// /// Historically contained other bitflags, which are now deprecated. /// Should not be extended in the future, create new config keys instead. ServerFlags, /// True if proxy is enabled. /// /// Can be used to disable proxy without erasing known URLs. ProxyEnabled, /// Proxy URL. /// /// Supported URLs schemes are `http://` (HTTP), `https://` (HTTPS), /// `socks5://` (SOCKS5) and `ss://` (Shadowsocks). /// /// May contain multiple URLs separated by newline, in which case the first one is used. ProxyUrl, /// True if SOCKS5 is enabled. /// /// Can be used to disable SOCKS5 without erasing SOCKS5 configuration. /// /// Deprecated in favor of `ProxyEnabled`. Socks5Enabled, /// SOCKS5 proxy server hostname or address. /// /// Deprecated in favor of `ProxyUrl`. Socks5Host, /// SOCKS5 proxy server port. /// /// Deprecated in favor of `ProxyUrl`. Socks5Port, /// SOCKS5 proxy server username. /// /// Deprecated in favor of `ProxyUrl`. Socks5User, /// SOCKS5 proxy server password. /// /// Deprecated in favor of `ProxyUrl`. Socks5Password, /// Own name to use in the `From:` field when sending messages. Displayname, /// Own status to display, sent in message footer. Selfstatus, /// Own avatar filename. Selfavatar, /// Send BCC copy to self. /// /// Should be enabled for multi-device setups. /// /// This is automatically enabled when importing/exporting a backup, /// setting up a second device, or receiving a sync message. #[strum(props(default = "0"))] BccSelf, /// True if Message Delivery Notifications (read receipts) should /// be sent and requested. #[strum(props(default = "1"))] MdnsEnabled, /// True if chat messages should be moved to a separate folder. Auto-sent messages like sync /// ones are moved there anyway. #[strum(props(default = "1"))] MvboxMove, /// Watch for new messages in the "Mvbox" (aka DeltaChat folder) only. /// /// This will not entirely disable other folders, e.g. the spam folder will also still /// be watched for new messages. #[strum(props(default = "0"))] OnlyFetchMvbox, /// Whether to show classic emails or only chat messages. #[strum(props(default = "2"))] // also change ShowEmails.default() on changes ShowEmails, /// Quality of the media files to send. #[strum(props(default = "0"))] // also change MediaQuality.default() on changes MediaQuality, /// If set to "1", then existing messages are considered to be already fetched. /// This flag is reset after successful configuration. #[strum(props(default = "1"))] FetchedExistingMsgs, /// Timer in seconds after which the message is deleted from the /// server. /// /// 0 means messages are never deleted by Delta Chat. /// /// Value 1 is treated as "delete at once": messages are deleted /// immediately, without moving to DeltaChat folder. /// /// Default is 1 for chatmail accounts without `BccSelf`, 0 otherwise. DeleteServerAfter, /// Timer in seconds after which the message is deleted from the /// device. /// /// Equals to 0 by default, which means the message is never /// deleted. #[strum(props(default = "0"))] DeleteDeviceAfter, /// Move messages to the Trash folder instead of marking them "\Deleted". Overrides /// `ProviderOptions::delete_to_trash`. DeleteToTrash, /// The primary email address. ConfiguredAddr, /// List of configured IMAP servers as a JSON array. ConfiguredImapServers, /// Configured IMAP server hostname. /// /// This is replaced by `configured_imap_servers` for new configurations. ConfiguredMailServer, /// Configured IMAP server port. /// /// This is replaced by `configured_imap_servers` for new configurations. ConfiguredMailPort, /// Configured IMAP server security (e.g. TLS, STARTTLS). /// /// This is replaced by `configured_imap_servers` for new configurations. ConfiguredMailSecurity, /// Configured IMAP server username. /// /// This is set if user has configured username manually. ConfiguredMailUser, /// Configured IMAP server password. ConfiguredMailPw, /// Configured TLS certificate checks. /// This option is saved on successful configuration /// and should not be modified manually. /// /// This actually applies to both IMAP and SMTP connections, /// but has "IMAP" in the name for backwards compatibility. ConfiguredImapCertificateChecks, /// List of configured SMTP servers as a JSON array. ConfiguredSmtpServers, /// Configured SMTP server hostname. /// /// This is replaced by `configured_smtp_servers` for new configurations. ConfiguredSendServer, /// Configured SMTP server port. /// /// This is replaced by `configured_smtp_servers` for new configurations. ConfiguredSendPort, /// Configured SMTP server security (e.g. TLS, STARTTLS). /// /// This is replaced by `configured_smtp_servers` for new configurations. ConfiguredSendSecurity, /// Configured SMTP server username. /// /// This is set if user has configured username manually. ConfiguredSendUser, /// Configured SMTP server password. ConfiguredSendPw, /// Deprecated, stored for backwards compatibility. /// /// ConfiguredImapCertificateChecks is actually used. ConfiguredSmtpCertificateChecks, /// Whether OAuth 2 is used with configured provider. ConfiguredServerFlags, /// Configured folder for incoming messages. ConfiguredInboxFolder, /// Configured folder for chat messages. ConfiguredMvboxFolder, /// Configured "Trash" folder. ConfiguredTrashFolder, /// Unix timestamp of the last successful configuration. ConfiguredTimestamp, /// ID of the configured provider from the provider database. ConfiguredProvider, /// True if account is configured. Configured, /// True if account is a chatmail account. IsChatmail, /// True if `IsChatmail` mustn't be autoconfigured. For tests. FixIsChatmail, /// True if account is muted. IsMuted, /// Optional tag as "Work", "Family". /// Meant to help profile owner to differ between profiles with similar names. PrivateTag, /// Read-only core version string. #[strum(serialize = "sys.version")] SysVersion, /// Maximal recommended attachment size in bytes. #[strum(serialize = "sys.msgsize_max_recommended")] SysMsgsizeMaxRecommended, /// Space separated list of all config keys available. #[strum(serialize = "sys.config_keys")] SysConfigKeys, /// True if it is a bot account. Bot, /// True when to skip initial start messages in groups. #[strum(props(default = "0"))] SkipStartMessages, /// Whether we send a warning if the password is wrong (set to false when we send a warning /// because we do not want to send a second warning) #[strum(props(default = "0"))] NotifyAboutWrongPw, /// If a warning about exceeding quota was shown recently, /// this is the percentage of quota at the time the warning was given. /// Unset, when quota falls below minimal warning threshold again. QuotaExceeding, /// Timestamp of the last time housekeeping was run LastHousekeeping, /// Timestamp of the last `CantDecryptOutgoingMsgs` notification. LastCantDecryptOutgoingMsgs, /// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely. #[strum(props(default = "60"))] ScanAllFoldersDebounceSecs, /// Whether to avoid using IMAP IDLE even if the server supports it. /// /// This is a developer option for testing "fake idle". #[strum(props(default = "0"))] DisableIdle, /// Timestamp of the next check for donation request need. DonationRequestNextCheck, /// Defines the max. size (in bytes) of messages downloaded automatically. /// 0 = no limit. #[strum(props(default = "0"))] DownloadLimit, /// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set /// and `Bot` unset. /// /// On real devices, this is usually always enabled and `BccSelf` is the only setting /// that controls whether sync messages are sent. /// /// In tests, this is usually disabled. #[strum(props(default = "1"))] SyncMsgs, /// Space-separated list of all the authserv-ids which we believe /// may be the one of our email server. /// /// See `crate::authres::update_authservid_candidates`. AuthservIdCandidates, /// Make all outgoing messages with Autocrypt header "multipart/signed". SignUnencrypted, /// Let the core save all events to the database. /// This value is used internally to remember the MsgId of the logging xdc #[strum(props(default = "0"))] DebugLogging, /// Last message processed by the bot. LastMsgId, /// How often to gossip Autocrypt keys in chats with multiple recipients, in seconds. 2 days by /// default. /// /// This is not supposed to be changed by UIs and only used for testing. #[strum(props(default = "172800"))] GossipPeriod, /// Row ID of the key in the `keypairs` table /// used for signatures, encryption to self and included in `Autocrypt` header. KeyId, /// Send statistics to Delta Chat's developers. /// Can be exposed to the user as a setting. StatsSending, /// Last time statistics were sent to Delta Chat's developers StatsLastSent, /// Last time `update_message_stats()` was called StatsLastUpdate, /// This key is sent to the statistics bot so that the bot can recognize the user /// without storing the email address StatsId, /// The last contact id that already existed when statistics-sending was enabled for the first time. StatsLastOldContactId, /// MsgId of webxdc map integration. WebxdcIntegration, /// Enable webxdc realtime features. #[strum(props(default = "1"))] WebxdcRealtimeEnabled, /// Last device token stored on the chatmail server. /// /// If it has not changed, we do not store /// the device token again. DeviceToken, /// Device token encrypted with OpenPGP. /// /// We store encrypted token next to `device_token` /// to avoid encrypting it differently and /// storing the same token multiple times on the server. EncryptedDeviceToken, /// Enables running test hooks, e.g. see `InnerContext::pre_encrypt_mime_hook`. /// This way is better than conditional compilation, i.e. `#[cfg(test)]`, because tests not /// using this still run unmodified code. TestHooks, /// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests. FailOnReceivingFullMsg, /// Enable composing emails with Header Protection as defined in /// "Header Protection for Cryptographically /// Protected Email". #[strum(props(default = "1"))] StdHeaderProtectionComposing, /// Who can call me. /// /// The options are from the `WhoCanCallMe` enum. #[strum(props(default = "1"))] WhoCanCallMe, /// Experimental option denoting that the current profile is shared between multiple team members. /// For now, the only effect of this option is that seen flags are not synchronized. TeamProfile, } impl Config { /// Whether the config option is synced across devices. /// /// This must be checked on both sides so that if there are different client versions, the /// synchronisation of a particular option is either done or not done in both directions. /// Moreover, receivers of a config value need to check if a key can be synced because if it is /// a file path, it could otherwise lead to exfiltration of files from a receiver's /// device if we assume an attacker to have control of a device in a multi-device setting or if /// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which /// mustn't be controlled by other devices. pub(crate) fn is_synced(&self) -> bool { matches!( self, Self::Displayname | Self::MdnsEnabled | Self::MvboxMove | Self::ShowEmails | Self::Selfavatar | Self::Selfstatus, ) } /// Whether the config option needs an IO scheduler restart to take effect. pub(crate) fn needs_io_restart(&self) -> bool { matches!( self, Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr ) } } impl Context { /// Returns true if configuration value is set in the db for the given key. /// /// NB: Don't use this to check if the key is configured because this doesn't look into /// environment. The proper use of this function is e.g. checking a key before setting it. pub(crate) async fn config_exists(&self, key: Config) -> Result { Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some()) } /// Get a config key value. Returns `None` if no value is set. pub(crate) async fn get_config_opt(&self, key: Config) -> Result> { let env_key = format!("DELTACHAT_{}", key.as_ref().to_uppercase()); if let Ok(value) = env::var(env_key) { return Ok(Some(value)); } let value = match key { Config::Selfavatar => { let rel_path = self.sql.get_raw_config(key.as_ref()).await?; rel_path.map(|p| { get_abs_path(self, Path::new(&p)) .to_string_lossy() .into_owned() }) } Config::SysVersion => Some(constants::DC_VERSION_STR.to_string()), Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")), Config::SysConfigKeys => Some(get_config_keys_string()), _ => self.sql.get_raw_config(key.as_ref()).await?, }; Ok(value) } /// Get a config key value if set, or a default value. Returns `None` if no value exists. pub async fn get_config(&self, key: Config) -> Result> { let value = self.get_config_opt(key).await?; if value.is_some() { return Ok(value); } // Default values let val = match key { Config::ConfiguredInboxFolder => Some("INBOX".to_string()), Config::DeleteServerAfter => { match !Box::pin(self.get_config_bool(Config::BccSelf)).await? && Box::pin(self.is_chatmail()).await? { true => Some("1".to_string()), false => Some("0".to_string()), } } Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?, _ => key.get_str("default").map(|s| s.to_string()), }; Ok(val) } /// Returns Some(T) if a value for the given key is set and was successfully parsed. /// Returns None if could not parse. pub(crate) async fn get_config_opt_parsed(&self, key: Config) -> Result> { self.get_config_opt(key) .await .map(|s: Option| s.and_then(|s| s.parse().ok())) } /// Returns Some(T) if a value for the given key exists (incl. default value) and was /// successfully parsed. /// Returns None if could not parse. pub async fn get_config_parsed(&self, key: Config) -> Result> { self.get_config(key) .await .map(|s: Option| s.and_then(|s| s.parse().ok())) } /// Returns 32-bit signed integer configuration value for the given key. pub async fn get_config_int(&self, key: Config) -> Result { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) } /// Returns 32-bit unsigned integer configuration value for the given key. pub async fn get_config_u32(&self, key: Config) -> Result { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) } /// Returns 64-bit signed integer configuration value for the given key. pub async fn get_config_i64(&self, key: Config) -> Result { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) } /// Returns 64-bit unsigned integer configuration value for the given key. pub async fn get_config_u64(&self, key: Config) -> Result { Ok(self.get_config_parsed(key).await?.unwrap_or_default()) } /// Returns boolean configuration value (if set) for the given key. pub(crate) async fn get_config_bool_opt(&self, key: Config) -> Result> { Ok(self .get_config_opt_parsed::(key) .await? .map(|x| x != 0)) } /// Returns boolean configuration value for the given key. pub async fn get_config_bool(&self, key: Config) -> Result { Ok(self .get_config(key) .await? .and_then(|s| s.parse::().ok()) .map(|x| x != 0) .unwrap_or_default()) } /// Returns true if movebox ("DeltaChat" folder) should be watched. pub(crate) async fn should_watch_mvbox(&self) -> Result { Ok(self.get_config_bool(Config::MvboxMove).await? || self.get_config_bool(Config::OnlyFetchMvbox).await? || !self.get_config_bool(Config::IsChatmail).await?) } /// Returns true if sync messages should be sent. pub(crate) async fn should_send_sync_msgs(&self) -> Result { Ok(self.get_config_bool(Config::SyncMsgs).await? && self.get_config_bool(Config::BccSelf).await? && !self.get_config_bool(Config::Bot).await?) } /// Returns whether MDNs should be requested. pub(crate) async fn should_request_mdns(&self) -> Result { match self.get_config_bool_opt(Config::MdnsEnabled).await? { Some(val) => Ok(val), None => Ok(!self.get_config_bool(Config::Bot).await?), } } /// Returns whether MDNs should be sent. pub(crate) async fn should_send_mdns(&self) -> Result { self.get_config_bool(Config::MdnsEnabled).await } /// Gets configured "delete_server_after" value. /// /// `None` means never delete the message, `Some(0)` means delete /// at once, `Some(x)` means delete after `x` seconds. pub async fn get_config_delete_server_after(&self) -> Result> { let val = match self .get_config_parsed::(Config::DeleteServerAfter) .await? .unwrap_or(0) { 0 => None, 1 => Some(0), x => Some(x), }; Ok(val) } /// Gets the configured provider. /// /// The provider is determined by the current primary transport. pub async fn get_configured_provider(&self) -> Result> { let provider = ConfiguredLoginParam::load(self) .await? .and_then(|(_transport_id, param)| param.provider); Ok(provider) } /// Gets configured "delete_device_after" value. /// /// `None` means never delete the message, `Some(x)` means delete /// after `x` seconds. pub async fn get_config_delete_device_after(&self) -> Result> { match self.get_config_int(Config::DeleteDeviceAfter).await? { 0 => Ok(None), x => Ok(Some(i64::from(x))), } } /// Executes [`SyncData::Config`] item sent by other device. pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> { let config_value; let value = match key { Config::Selfavatar if value.is_empty() => None, Config::Selfavatar => { config_value = BlobObject::store_from_base64(self, value)?; config_value.as_deref() } _ => Some(value), }; match key.is_synced() { true => self.set_config_ex(Nosync, *key, value).await, false => Ok(()), } } fn check_config(key: Config, value: Option<&str>) -> Result<()> { match key { Config::Socks5Enabled | Config::ProxyEnabled | Config::BccSelf | Config::MdnsEnabled | Config::MvboxMove | Config::OnlyFetchMvbox | Config::DeleteToTrash | Config::Configured | Config::Bot | Config::NotifyAboutWrongPw | Config::SyncMsgs | Config::SignUnencrypted | Config::DisableIdle => { ensure!( matches!(value, None | Some("0") | Some("1")), "Boolean value must be either 0 or 1" ); } _ => (), } Ok(()) } /// Set the given config key and make it effective. /// This may restart the IO scheduler. If `None` is passed as a value the value is cleared and /// set to the default if there is one. pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> { Self::check_config(key, value)?; let n_transports = self.count_transports().await?; if n_transports > 1 && matches!( key, Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails ) { bail!("Cannot reconfigure {key} when multiple transports are configured"); } let _pause = match key.needs_io_restart() { true => self.scheduler.pause(self).await?, _ => Default::default(), }; if key == Config::StatsSending { let old_value = self.get_config(key).await?; let old_value = bool_from_config(old_value.as_deref()); let new_value = bool_from_config(value); stats::pre_sending_config_change(self, old_value, new_value).await?; } self.set_config_internal(key, value).await?; if key == Config::StatsSending { stats::maybe_send_stats(self).await?; } Ok(()) } pub(crate) async fn set_config_internal(&self, key: Config, value: Option<&str>) -> Result<()> { self.set_config_ex(Sync, key, value).await } pub(crate) async fn set_config_ex( &self, sync: sync::Sync, key: Config, mut value: Option<&str>, ) -> Result<()> { Self::check_config(key, value)?; let sync = sync == Sync && key.is_synced() && self.is_configured().await?; let better_value; match key { Config::Selfavatar => { self.sql .execute("UPDATE contacts SET selfavatar_sent=0;", ()) .await?; match value { Some(path) => { let path = get_abs_path(self, Path::new(path)); let mut blob = BlobObject::create_and_deduplicate(self, &path, &path)?; blob.recode_to_avatar_size(self).await?; self.sql .set_raw_config(key.as_ref(), Some(blob.as_name())) .await?; if sync { let buf = fs::read(blob.to_abs_path()).await?; better_value = base64::engine::general_purpose::STANDARD.encode(buf); value = Some(&better_value); } } None => { self.sql.set_raw_config(key.as_ref(), None).await?; if sync { better_value = String::new(); value = Some(&better_value); } } } self.emit_event(EventType::SelfavatarChanged); } Config::DeleteDeviceAfter => { let ret = self.sql.set_raw_config(key.as_ref(), value).await; // Interrupt ephemeral loop to delete old messages immediately. self.scheduler.interrupt_ephemeral_task().await; ret? } Config::Displayname => { if let Some(v) = value { better_value = sanitize_single_line(v); value = Some(&better_value); } self.sql.set_raw_config(key.as_ref(), value).await?; } Config::Addr => { self.sql .set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref()) .await?; } Config::MvboxMove => { self.sql.set_raw_config(key.as_ref(), value).await?; self.sql .set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None) .await?; } Config::ConfiguredAddr => { let Some(addr) = value else { bail!("Cannot unset configured_addr"); }; if !self.is_configured().await? { info!( self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!" ); add_pseudo_transport(self, addr).await?; self.sql .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr)) .await?; } else { self.sql .transaction(|transaction| { if transaction.query_row( "SELECT COUNT(*) FROM transports WHERE addr=?", (addr,), |row| { let res: i64 = row.get(0)?; Ok(res) }, )? == 0 { bail!("Address does not belong to any transport."); } transaction.execute( "UPDATE config SET value=? WHERE keyname='configured_addr'", (addr,), )?; // Clean up SMTP and IMAP APPEND queue. // // The messages in the queue have a different // From address so we cannot send them over // the new SMTP transport. transaction.execute("DELETE FROM smtp", ())?; transaction.execute("DELETE FROM imap_send", ())?; Ok(()) }) .await?; send_sync_transports(self).await?; self.sql.uncache_raw_config("configured_addr").await; } } _ => { self.sql.set_raw_config(key.as_ref(), value).await?; } } if matches!( key, Config::Displayname | Config::Selfavatar | Config::PrivateTag ) { self.emit_event(EventType::AccountsItemChanged); } if key.is_synced() { self.emit_event(EventType::ConfigSynced { key }); } if !sync { return Ok(()); } let Some(val) = value else { return Ok(()); }; let val = val.to_string(); if self .add_sync_item(SyncData::Config { key, val }) .await .log_err(self) .is_err() { return Ok(()); } self.scheduler.interrupt_smtp().await; Ok(()) } /// Set the given config to an unsigned 32-bit integer value. pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> { self.set_config(key, Some(&value.to_string())).await?; Ok(()) } /// Set the given config to a boolean value. pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> { self.set_config(key, from_bool(value)).await?; Ok(()) } /// Sets an ui-specific key-value pair. /// Keys must be prefixed by `ui.` /// and should be followed by the name of the system and maybe subsystem, /// eg. `ui.desktop.linux.foo`, `ui.desktop.macos.bar`, `ui.ios.foobar`. pub async fn set_ui_config(&self, key: &str, value: Option<&str>) -> Result<()> { ensure!(key.starts_with("ui."), "set_ui_config(): prefix missing."); self.sql.set_raw_config(key, value).await } /// Gets an ui-specific value set by set_ui_config(). pub async fn get_ui_config(&self, key: &str) -> Result> { ensure!(key.starts_with("ui."), "get_ui_config(): prefix missing."); self.sql.get_raw_config(key).await } } /// Returns a value for use in `Context::set_config_*()` for the given `bool`. pub(crate) fn from_bool(val: bool) -> Option<&'static str> { Some(if val { "1" } else { "0" }) } pub(crate) fn bool_from_config(config: Option<&str>) -> bool { config.is_some_and(|v| v.parse::().unwrap_or_default() != 0) } // Separate impl block for self address handling impl Context { /// Determine whether the specified addr maps to the/a self addr. /// Returns `false` if no addresses are configured. pub(crate) async fn is_self_addr(&self, addr: &str) -> Result { Ok(self .get_config(Config::ConfiguredAddr) .await? .iter() .any(|a| addr_cmp(addr, a)) || self .get_secondary_self_addrs() .await? .iter() .any(|a| addr_cmp(addr, a))) } /// Sets `primary_new` as the new primary self address and saves the old /// primary address (if exists) as a secondary address. /// /// This should only be used by test code and during configure. #[cfg(test)] // AEAP is disabled, but there are still tests for it pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> { self.quota.write().await.clear(); self.sql .set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new)) .await?; self.emit_event(EventType::ConnectivityChanged); Ok(()) } /// Returns all primary and secondary self addresses. pub(crate) async fn get_all_self_addrs(&self) -> Result> { let primary_addrs = self.get_config(Config::ConfiguredAddr).await?.into_iter(); let secondary_addrs = self.get_secondary_self_addrs().await?.into_iter(); Ok(primary_addrs.chain(secondary_addrs).collect()) } /// Returns all secondary self addresses. pub(crate) async fn get_secondary_self_addrs(&self) -> Result> { self.sql.query_map_vec("SELECT addr FROM transports WHERE addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')", (), |row| { let addr: String = row.get(0)?; Ok(addr) }).await } /// Returns the primary self address. /// Returns an error if no self addr is configured. pub async fn get_primary_self_addr(&self) -> Result { self.get_config(Config::ConfiguredAddr) .await? .context("No self addr configured") } } /// Returns all available configuration keys concated together. fn get_config_keys_string() -> String { let keys = Config::iter().fold(String::new(), |mut acc, key| { acc += key.as_ref(); acc += " "; acc }); format!(" {keys} ") } /// Returns all `ui.*` config keys that were set by the UI. pub async fn get_all_ui_config_keys(context: &Context) -> Result> { let ui_keys = context .sql .query_map_vec( "SELECT keyname FROM config WHERE keyname GLOB 'ui.*' ORDER BY config.id", (), |row| Ok(row.get::<_, String>(0)?), ) .await?; Ok(ui_keys) } #[cfg(test)] mod config_tests;