mirror of
https://github.com/chatmail/core.git
synced 2026-09-22 13:01:21 +03:00
imap_send table is not unused anymore and should eventually be deleted. It only exists for compatibility and no other SQL statements use it anymore.
999 lines
34 KiB
Rust
999 lines
34 KiB
Rust
//! # 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::sync::{self, Sync::*, SyncData};
|
|
use crate::tools::{get_abs_path, time};
|
|
use crate::transport::{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 {
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
|
|
/// or add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Email address, used in the `From:` field.
|
|
Addr,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// IMAP server hostname.
|
|
MailServer,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// IMAP server username.
|
|
MailUser,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// IMAP server password.
|
|
MailPw,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// IMAP server port.
|
|
MailPort,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// IMAP server security (e.g. TLS, STARTTLS).
|
|
MailSecurity,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// How to check TLS certificates.
|
|
///
|
|
/// "IMAP" in the name is for compatibility,
|
|
/// this actually applies to both IMAP and SMTP connections.
|
|
ImapCertificateChecks,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// SMTP server hostname.
|
|
SendServer,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// SMTP server username.
|
|
SendUser,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// SMTP server password.
|
|
SendPw,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// SMTP server port.
|
|
SendPort,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use EnteredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// SMTP server security (e.g. TLS, STARTTLS).
|
|
SendSecurity,
|
|
|
|
/// 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,
|
|
|
|
/// Quality of the media files to send.
|
|
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
|
MediaQuality,
|
|
|
|
/// 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,
|
|
|
|
/// The primary email address.
|
|
ConfiguredAddr,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// List of configured IMAP servers as a JSON array.
|
|
ConfiguredImapServers,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured IMAP server hostname.
|
|
///
|
|
/// This is replaced by `configured_imap_servers` for new configurations.
|
|
ConfiguredMailServer,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured IMAP server port.
|
|
ConfiguredMailPort,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
|
ConfiguredMailSecurity,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured IMAP server username.
|
|
ConfiguredMailUser,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured IMAP server password.
|
|
ConfiguredMailPw,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// 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,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// List of configured SMTP servers as a JSON array.
|
|
ConfiguredSmtpServers,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured SMTP server hostname.
|
|
///
|
|
/// This is replaced by `configured_smtp_servers` for new configurations.
|
|
ConfiguredSendServer,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured SMTP server port.
|
|
///
|
|
/// This is replaced by `configured_smtp_servers` for new configurations.
|
|
ConfiguredSendPort,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured SMTP server security (e.g. TLS, STARTTLS).
|
|
///
|
|
/// This is replaced by `configured_smtp_servers` for new configurations.
|
|
ConfiguredSendSecurity,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured SMTP server username.
|
|
///
|
|
/// This is set if user has configured username manually.
|
|
ConfiguredSendUser,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use ConfiguredLoginParam and add_transport{from_qr}()/list_transports() instead.
|
|
///
|
|
/// Configured SMTP server password.
|
|
ConfiguredSendPw,
|
|
|
|
/// Configured folder for incoming messages.
|
|
ConfiguredInboxFolder,
|
|
|
|
/// Unix timestamp of the last successful configuration.
|
|
ConfiguredTimestamp,
|
|
|
|
/// Deprecated(2026-04).
|
|
/// Use [`Context::is_configured()`] instead.
|
|
///
|
|
/// True if account is configured.
|
|
Configured,
|
|
|
|
/// Deprecated, we are trying to get rid of this global setting.
|
|
/// It is possible to configure a profile with both chatmail relays
|
|
/// and classical email servers.
|
|
///
|
|
/// Most usages in UIs can be replaced by `force_encryption`.
|
|
///
|
|
/// 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,
|
|
|
|
/// Timestamp of the last time housekeeping was run
|
|
LastHousekeeping,
|
|
|
|
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
|
|
LastCantDecryptOutgoingMsgs,
|
|
|
|
/// 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.
|
|
///
|
|
/// For messages with large attachments, two messages are sent:
|
|
/// a Pre-Message containing metadata and text and a Post-Message additionally
|
|
/// containing the attachment. NB: Some "extra" metadata like avatars and gossiped
|
|
/// encryption keys is stripped from post-messages to save traffic.
|
|
/// Pre-Messages are shown as placeholder messages. They can be downloaded fully using
|
|
/// `MsgId::download_full()` later. Post-Messages are automatically downloaded if they are
|
|
/// smaller than the download_limit. Other messages are always auto-downloaded.
|
|
///
|
|
/// 0 = no limit.
|
|
/// Changes only affect future messages.
|
|
#[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,
|
|
|
|
/// 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,
|
|
|
|
/// Return an error from `receive_imf_inner()`. For tests.
|
|
SimulateReceiveImfError,
|
|
|
|
/// 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,
|
|
|
|
/// Force encryption.
|
|
///
|
|
/// When enabled, unencrypted messages cannot be sent
|
|
/// and incoming unencrypted messages are not fetched and not processed.
|
|
#[strum(props(default = "1"))]
|
|
ForceEncryption,
|
|
}
|
|
|
|
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::Selfavatar
|
|
| Self::Selfstatus
|
|
| Self::ForceEncryption,
|
|
)
|
|
}
|
|
|
|
/// Whether the config option needs an IO scheduler restart to take effect.
|
|
pub(crate) fn needs_io_restart(&self) -> bool {
|
|
matches!(self, 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<bool> {
|
|
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<Option<String>> {
|
|
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<Option<String>> {
|
|
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::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<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
|
|
self.get_config_opt(key)
|
|
.await
|
|
.map(|s: Option<String>| 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<T: FromStr>(&self, key: Config) -> Result<Option<T>> {
|
|
self.get_config(key)
|
|
.await
|
|
.map(|s: Option<String>| 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<i32> {
|
|
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<u32> {
|
|
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<i64> {
|
|
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<u64> {
|
|
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<Option<bool>> {
|
|
Ok(self
|
|
.get_config_opt_parsed::<i32>(key)
|
|
.await?
|
|
.map(|x| x != 0))
|
|
}
|
|
|
|
/// Returns boolean configuration value for the given key.
|
|
pub async fn get_config_bool(&self, key: Config) -> Result<bool> {
|
|
Ok(self
|
|
.get_config(key)
|
|
.await?
|
|
.and_then(|s| s.parse::<i32>().ok())
|
|
.is_some_and(|x| x != 0))
|
|
}
|
|
|
|
/// Returns true if sync messages should be sent.
|
|
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
|
|
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<bool> {
|
|
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<bool> {
|
|
self.get_config_bool(Config::MdnsEnabled).await
|
|
}
|
|
|
|
/// 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<Option<i64>> {
|
|
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::Configured
|
|
| Config::Bot
|
|
| Config::NotifyAboutWrongPw
|
|
| Config::SyncMsgs
|
|
| 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 _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::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,),
|
|
)?;
|
|
|
|
// Update the timestamp for the primary transport
|
|
// so it becomes the first in `get_all_self_addrs()` list
|
|
// and the list of relays distributed in the public key.
|
|
// This ensures that messages will be sent
|
|
// to the primary relay by the contacts
|
|
// and will be fetched in background_fetch()
|
|
// which only fetches from the primary transport.
|
|
transaction
|
|
.execute(
|
|
"UPDATE transports SET add_timestamp=?, is_published=1 WHERE addr=?",
|
|
(time(), addr),
|
|
)
|
|
.context(
|
|
"Failed to update add_timestamp for the new primary transport",
|
|
)?;
|
|
|
|
// Clean up SMTP 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", ())?;
|
|
|
|
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<Option<String>> {
|
|
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::<i32>().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<bool> {
|
|
// Employ the config cache to optimize for `ConfiguredAddr` passed.
|
|
if !addr.is_empty()
|
|
&& addr_cmp(
|
|
addr,
|
|
&self
|
|
.get_config(Config::ConfiguredAddr)
|
|
.await?
|
|
.unwrap_or_default(),
|
|
)
|
|
{
|
|
return Ok(true);
|
|
}
|
|
Ok(self
|
|
.get_all_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 self addresses, newest first.
|
|
pub(crate) async fn get_all_self_addrs(&self) -> Result<Vec<String>> {
|
|
self.sql
|
|
.query_map_vec(
|
|
"SELECT addr FROM transports ORDER BY add_timestamp DESC, id DESC",
|
|
(),
|
|
|row| {
|
|
let addr: String = row.get(0)?;
|
|
Ok(addr)
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Returns all published self addresses, newest first.
|
|
/// See `[Context::set_transport_unpublished]`
|
|
pub(crate) async fn get_published_self_addrs(&self) -> Result<Vec<String>> {
|
|
self.sql
|
|
.query_map_vec(
|
|
"SELECT addr FROM transports WHERE is_published=1 ORDER BY add_timestamp DESC, id DESC",
|
|
(),
|
|
|row| {
|
|
let addr: String = row.get(0)?;
|
|
Ok(addr)
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
/// Returns all published secondary self addresses.
|
|
/// See `[Context::set_transport_unpublished]`
|
|
pub(crate) async fn get_published_secondary_self_addrs(&self) -> Result<Vec<String>> {
|
|
self.sql
|
|
.query_map_vec(
|
|
"SELECT addr FROM transports
|
|
WHERE is_published
|
|
AND addr NOT IN (SELECT value FROM config WHERE keyname='configured_addr')
|
|
ORDER BY add_timestamp DESC, id DESC",
|
|
(),
|
|
|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<String> {
|
|
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<Vec<String>> {
|
|
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;
|