diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 8ccbe7ead..7cafd4454 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -39,6 +39,7 @@ use deltachat::{imex, info}; use sanitize_filename::is_sanitized; use tokio::fs; use tokio::sync::{watch, Mutex, RwLock}; +use types::login_param::EnteredLoginParam; use walkdir::WalkDir; use yerpc::rpc; @@ -431,6 +432,9 @@ impl CommandApi { /// Configures this account with the currently set parameters. /// Setup the credential config before calling this. + /// + /// Deprecated as of 2025-02; use `add_transport_from_qr()` + /// or `add_transport()` instead. async fn configure(&self, account_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; ctx.stop_io().await; @@ -445,6 +449,69 @@ impl CommandApi { Ok(()) } + /// Configures a new email account using the provided parameters + /// and adds it as a transport. + /// + /// If the email address is the same as an existing transport, + /// then this existing account will be reconfigured instead of a new one being added. + /// + /// This function stops and starts IO as needed. + /// + /// Usually it will be enough to only set `addr` and `imap.password`, + /// and all the other settings will be autoconfigured. + /// + /// During configuration, ConfigureProgress events are emitted; + /// they indicate a successful configuration as well as errors + /// and may be used to create a progress bar. + /// This function will return after configuration is finished. + /// + /// If configuration is successful, + /// the working server parameters will be saved + /// and used for connecting to the server. + /// The parameters entered by the user will be saved separately + /// so that they can be prefilled when the user opens the server-configuration screen again. + /// + /// See also: + /// - [Self::is_configured()] to check whether there is + /// at least one working transport. + /// - [Self::add_transport_from_qr()] to add a transport + /// from a server encoded in a QR code. + /// - [Self::list_transports()] to get a list of all configured transports. + /// - [Self::delete_transport()] to remove a transport. + async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.add_transport(¶m.try_into()?).await + } + + /// Adds a new email account as a transport + /// using the server encoded in the QR code. + /// See [Self::add_transport]. + async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.add_transport_from_qr(&qr).await + } + + /// Returns the list of all email accounts that are used as a transport in the current profile. + /// Use [Self::add_transport()] to add or change a transport + /// and [Self::delete_transport()] to delete a transport. + async fn list_transports(&self, account_id: u32) -> Result> { + let ctx = self.get_context(account_id).await?; + let res = ctx + .list_transports() + .await? + .into_iter() + .map(|t| t.into()) + .collect(); + Ok(res) + } + + /// Removes the transport with the specified email address + /// (i.e. [EnteredLoginParam::addr]). + async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.delete_transport(&addr).await + } + /// Signal an ongoing process to stop. async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; diff --git a/deltachat-jsonrpc/src/api/types/login_param.rs b/deltachat-jsonrpc/src/api/types/login_param.rs new file mode 100644 index 000000000..5164e7649 --- /dev/null +++ b/deltachat-jsonrpc/src/api/types/login_param.rs @@ -0,0 +1,179 @@ +use anyhow::Result; +use deltachat::login_param as dc; +use serde::Deserialize; +use serde::Serialize; +use yerpc::TypeDef; + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EnteredServerLoginParam { + /// Server hostname or IP address. + pub server: String, + + /// Server port. + /// + /// 0 if not specified. + pub port: u16, + + /// Socket security. + pub security: Socket, + + /// Username. + /// + /// Empty string if not specified. + pub user: String, + + /// Password. + pub password: String, +} + +impl From for EnteredServerLoginParam { + fn from(param: dc::EnteredServerLoginParam) -> Self { + Self { + server: param.server, + port: param.port, + security: param.security.into(), + user: param.user, + password: param.password, + } + } +} + +impl From for dc::EnteredServerLoginParam { + fn from(param: EnteredServerLoginParam) -> Self { + Self { + server: param.server, + port: param.port, + security: param.security.into(), + user: param.user, + password: param.password, + } + } +} + +/// Login parameters entered by the user. + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct EnteredLoginParam { + /// Email address. + pub addr: String, + + /// IMAP settings. + pub imap: EnteredServerLoginParam, + + /// SMTP settings. + pub smtp: EnteredServerLoginParam, + + /// TLS options: whether to allow invalid certificates and/or + /// invalid hostnames + pub certificate_checks: EnteredCertificateChecks, + + /// If true, login via OAUTH2 (not recommended anymore) + pub oauth2: bool, +} + +impl From for EnteredLoginParam { + fn from(param: dc::EnteredLoginParam) -> Self { + Self { + addr: param.addr, + imap: param.imap.into(), + smtp: param.smtp.into(), + certificate_checks: param.certificate_checks.into(), + oauth2: param.oauth2, + } + } +} + +impl TryFrom for dc::EnteredLoginParam { + type Error = anyhow::Error; + + fn try_from(param: EnteredLoginParam) -> Result { + Ok(Self { + addr: param.addr, + imap: param.imap.into(), + smtp: param.smtp.into(), + certificate_checks: param.certificate_checks.into(), + oauth2: param.oauth2, + }) + } +} + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum Socket { + /// Unspecified socket security, select automatically. + Automatic, + + /// TLS connection. + Ssl, + + /// STARTTLS connection. + Starttls, + + /// No TLS, plaintext connection. + Plain, +} + +impl From for Socket { + fn from(value: dc::Socket) -> Self { + match value { + dc::Socket::Automatic => Self::Automatic, + dc::Socket::Ssl => Self::Ssl, + dc::Socket::Starttls => Self::Starttls, + dc::Socket::Plain => Self::Plain, + } + } +} + +impl From for dc::Socket { + fn from(value: Socket) -> Self { + match value { + Socket::Automatic => Self::Automatic, + Socket::Ssl => Self::Ssl, + Socket::Starttls => Self::Starttls, + Socket::Plain => Self::Plain, + } + } +} + +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum EnteredCertificateChecks { + /// `Automatic` means that provider database setting should be taken. + /// If there is no provider database setting for certificate checks, + /// check certificates strictly. + Automatic, + + /// Ensure that TLS certificate is valid for the server hostname. + Strict, + + /// Accept certificates that are expired, self-signed + /// or otherwise not valid for the server hostname. + AcceptInvalidCertificates, +} + +impl From for EnteredCertificateChecks { + fn from(value: dc::EnteredCertificateChecks) -> Self { + match value { + dc::EnteredCertificateChecks::Automatic => Self::Automatic, + dc::EnteredCertificateChecks::Strict => Self::Strict, + dc::EnteredCertificateChecks::AcceptInvalidCertificates => { + Self::AcceptInvalidCertificates + } + dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => { + Self::AcceptInvalidCertificates + } + } + } +} + +impl From for dc::EnteredCertificateChecks { + fn from(value: EnteredCertificateChecks) -> Self { + match value { + EnteredCertificateChecks::Automatic => Self::Automatic, + EnteredCertificateChecks::Strict => Self::Strict, + EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates, + } + } +} diff --git a/deltachat-jsonrpc/src/api/types/mod.rs b/deltachat-jsonrpc/src/api/types/mod.rs index 8143be73d..995e931bf 100644 --- a/deltachat-jsonrpc/src/api/types/mod.rs +++ b/deltachat-jsonrpc/src/api/types/mod.rs @@ -5,6 +5,7 @@ pub mod contact; pub mod events; pub mod http; pub mod location; +pub mod login_param; pub mod message; pub mod provider_info; pub mod qr; diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 5103f4f9d..73299c48b 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -463,6 +463,7 @@ def test_qr_new_group_unblocked(acfactory): assert ac2_msg.chat.get_basic_snapshot().is_contact_request +@pytest.mark.skip(reason="AEAP is disabled for now") def test_aeap_flow_verified(acfactory): """Test that a new address is added to a contact when it changes its address.""" ac1, ac2 = acfactory.get_online_accounts(2) diff --git a/src/configure.rs b/src/configure.rs index 1b17cf22f..004833826 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -28,13 +28,15 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT; use crate::context::Context; use crate::imap::Imap; use crate::log::LogExt; +pub use crate::login_param::EnteredLoginParam; use crate::login_param::{ ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam, - ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam, + ConnectionCandidate, EnteredCertificateChecks, ProxyConfig, }; use crate::message::Message; use crate::oauth2::get_oauth2_addr; use crate::provider::{Protocol, Socket, UsernamePattern}; +use crate::qr::set_account_from_qr; use crate::smtp::Smtp; use crate::sync::Sync::*; use crate::tools::time; @@ -64,8 +66,59 @@ impl Context { self.sql.get_raw_config_bool("configured").await } - /// Configures this account with the currently set parameters. + /// Configures this account with the currently provided parameters. + /// + /// Deprecated since 2025-02; use `add_transport_from_qr()` + /// or `add_transport()` instead. pub async fn configure(&self) -> Result<()> { + let param = EnteredLoginParam::load(self).await?; + + self.add_transport_inner(¶m).await + } + + /// Configures a new email account using the provided parameters + /// and adds it as a transport. + /// + /// If the email address is the same as an existing transport, + /// then this existing account will be reconfigured instead of a new one being added. + /// + /// This function stops and starts IO as needed. + /// + /// Usually it will be enough to only set `addr` and `imap.password`, + /// and all the other settings will be autoconfigured. + /// + /// During configuration, ConfigureProgress events are emitted; + /// they indicate a successful configuration as well as errors + /// and may be used to create a progress bar. + /// This function will return after configuration is finished. + /// + /// If configuration is successful, + /// the working server parameters will be saved + /// and used for connecting to the server. + /// The parameters entered by the user will be saved separately + /// so that they can be prefilled when the user opens the server-configuration screen again. + /// + /// See also: + /// - [Self::is_configured()] to check whether there is + /// at least one working transport. + /// - [Self::add_transport_from_qr()] to add a transport + /// from a server encoded in a QR code. + /// - [Self::list_transports()] to get a list of all configured transports. + /// - [Self::delete_transport()] to remove a transport. + pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> { + self.stop_io().await; + let result = self.add_transport_inner(param).await; + if result.is_err() { + if let Ok(true) = self.is_configured().await { + self.start_io().await; + } + return result; + } + self.start_io().await; + Ok(()) + } + + async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> { ensure!( !self.scheduler.is_running().await, "cannot configure, already running" @@ -74,42 +127,63 @@ impl Context { self.sql.is_open().await, "cannot configure, database not opened." ); + let old_addr = self.get_config(Config::ConfiguredAddr).await?; + if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) { + bail!("Adding a new transport is not supported right now. Check back in a few months!"); + } let cancel_channel = self.alloc_ongoing().await?; let res = self - .inner_configure() + .inner_configure(param) .race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled")))) .await; self.free_ongoing().await; if let Err(err) = res.as_ref() { - progress!( - self, - 0, - Some( - stock_str::configuration_failed( - self, - // We are using Anyhow's .context() and to show the - // inner error, too, we need the {:#}: - &format!("{err:#}"), - ) - .await - ) - ); + // We are using Anyhow's .context() and to show the + // inner error, too, we need the {:#}: + let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await; + progress!(self, 0, Some(error_msg)); } else { + param.save(self).await?; progress!(self, 1000); } res } - async fn inner_configure(&self) -> Result<()> { + /// Adds a new email account as a transport + /// using the server encoded in the QR code. + /// See [Self::add_transport]. + pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> { + set_account_from_qr(self, qr).await?; + self.configure().await?; + + Ok(()) + } + + /// Returns the list of all email accounts that are used as a transport in the current profile. + /// Use [Self::add_transport()] to add or change a transport + /// and [Self::delete_transport()] to delete a transport. + pub async fn list_transports(&self) -> Result> { + let param = EnteredLoginParam::load(self).await?; + + Ok(vec![param]) + } + + /// Removes the transport with the specified email address + /// (i.e. [EnteredLoginParam::addr]). + #[expect(clippy::unused_async)] + pub async fn delete_transport(&self, _addr: &str) -> Result<()> { + bail!("Adding and removing additional transports is not supported yet. Check back in a few months!") + } + + async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> { info!(self, "Configure ..."); - let param = EnteredLoginParam::load(self).await?; let old_addr = self.get_config(Config::ConfiguredAddr).await?; - let configured_param = configure(self, ¶m).await?; + let configured_param = configure(self, param).await?; self.set_config_internal(Config::NotifyAboutWrongPw, Some("1")) .await?; on_configure_completed(self, configured_param, old_addr).await?; @@ -185,8 +259,7 @@ async fn get_configured_param( param.smtp.password.clone() }; - let proxy_config = param.proxy_config.clone(); - let proxy_enabled = proxy_config.is_some(); + let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?; let mut addr = param.addr.clone(); if param.oauth2 { @@ -345,7 +418,7 @@ async fn get_configured_param( .collect(), smtp_user: param.smtp.user.clone(), smtp_password, - proxy_config: param.proxy_config.clone(), + proxy_config: ProxyConfig::load(ctx).await?, provider, certificate_checks: match param.certificate_checks { EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic, diff --git a/src/lib.rs b/src/lib.rs index 803bb8fe1..9e9cc49cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,7 @@ mod imap; pub mod imex; pub mod key; pub mod location; -mod login_param; +pub mod login_param; pub mod message; mod mimefactory; pub mod mimeparser; diff --git a/src/login_param.rs b/src/login_param.rs index 7ee7887a9..2314dc1a4 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -4,6 +4,7 @@ use std::fmt; use anyhow::{format_err, Context as _, Result}; use deltachat_contact_tools::EmailAddress; +use num_traits::ToPrimitive as _; use serde::{Deserialize, Serialize}; use crate::config::Config; @@ -11,9 +12,11 @@ use crate::configure::server_params::{expand_param_vector, ServerParams}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; use crate::context::Context; use crate::net::load_connection_timestamp; -use crate::net::proxy::ProxyConfig; -use crate::provider::{Protocol, Provider, Socket, UsernamePattern}; +pub use crate::net::proxy::ProxyConfig; +pub use crate::provider::Socket; +use crate::provider::{Protocol, Provider, UsernamePattern}; use crate::sql::Sql; +use crate::tools::ToOption; /// User-entered setting for certificate checks. /// @@ -44,7 +47,7 @@ pub enum EnteredCertificateChecks { #[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] #[repr(u32)] #[strum(serialize_all = "snake_case")] -pub enum ConfiguredCertificateChecks { +pub(crate) enum ConfiguredCertificateChecks { /// Use configuration from the provider database. /// If there is no provider database setting for certificate checks, /// accept invalid certificates. @@ -116,15 +119,13 @@ pub struct EnteredLoginParam { /// invalid hostnames pub certificate_checks: EnteredCertificateChecks, - /// Proxy configuration. - pub proxy_config: Option, - + /// If true, login via OAUTH2 (not recommended anymore) pub oauth2: bool, } impl EnteredLoginParam { /// Loads entered account settings. - pub async fn load(context: &Context) -> Result { + pub(crate) async fn load(context: &Context) -> Result { let addr = context .get_config(Config::Addr) .await? @@ -196,8 +197,6 @@ impl EnteredLoginParam { .unwrap_or_default(); let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2); - let proxy_config = ProxyConfig::load(context).await?; - Ok(EnteredLoginParam { addr, imap: EnteredServerLoginParam { @@ -215,10 +214,71 @@ impl EnteredLoginParam { password: send_pw, }, certificate_checks, - proxy_config, oauth2, }) } + + /// Saves entered account settings, + /// so that they can be prefilled if the user wants to configure the server again. + pub(crate) async fn save(&self, context: &Context) -> Result<()> { + context.set_config(Config::Addr, Some(&self.addr)).await?; + + context + .set_config(Config::MailServer, self.imap.server.to_option()) + .await?; + context + .set_config(Config::MailPort, self.imap.port.to_option().as_deref()) + .await?; + context + .set_config( + Config::MailSecurity, + self.imap.security.to_i32().to_option().as_deref(), + ) + .await?; + context + .set_config(Config::MailUser, self.imap.user.to_option()) + .await?; + context + .set_config(Config::MailPw, self.imap.password.to_option()) + .await?; + + context + .set_config(Config::SendServer, self.smtp.server.to_option()) + .await?; + context + .set_config(Config::SendPort, self.smtp.port.to_option().as_deref()) + .await?; + context + .set_config( + Config::SendSecurity, + self.smtp.security.to_i32().to_option().as_deref(), + ) + .await?; + context + .set_config(Config::SendUser, self.smtp.user.to_option()) + .await?; + context + .set_config(Config::SendPw, self.smtp.password.to_option()) + .await?; + + context + .set_config( + Config::ImapCertificateChecks, + self.certificate_checks.to_i32().to_option().as_deref(), + ) + .await?; + + let server_flags = if self.oauth2 { + Some(DC_LP_AUTH_OAUTH2.to_string()) + } else { + None + }; + context + .set_config(Config::ServerFlags, server_flags.as_deref()) + .await?; + + Ok(()) + } } impl fmt::Display for EnteredLoginParam { @@ -319,7 +379,7 @@ impl TryFrom for ConnectionSecurity { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct ConfiguredServerLoginParam { +pub(crate) struct ConfiguredServerLoginParam { pub connection: ConnectionCandidate, /// Username. @@ -357,7 +417,7 @@ pub(crate) async fn prioritize_server_login_params( /// Login parameters saved to the database /// after successful configuration. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConfiguredLoginParam { +pub(crate) struct ConfiguredLoginParam { /// `From:` address that was used at the time of configuration. pub addr: String, @@ -390,6 +450,7 @@ pub struct ConfiguredLoginParam { /// invalid hostnames pub certificate_checks: ConfiguredCertificateChecks, + /// If true, login via OAUTH2 (not recommended anymore) pub oauth2: bool, } @@ -428,7 +489,7 @@ impl ConfiguredLoginParam { /// Load configured account settings from the database. /// /// Returns `None` if account is not configured. - pub async fn load(context: &Context) -> Result> { + pub(crate) async fn load(context: &Context) -> Result> { if !context.get_config_bool(Config::Configured).await? { return Ok(None); } @@ -699,7 +760,7 @@ impl ConfiguredLoginParam { } /// Save this loginparam to the database. - pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> { + pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> { context.set_primary_self_addr(&self.addr).await?; context @@ -776,7 +837,7 @@ impl ConfiguredLoginParam { Ok(()) } - pub fn strict_tls(&self) -> bool { + pub(crate) fn strict_tls(&self) -> bool { let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls); match self.certificate_checks { ConfiguredCertificateChecks::OldAutomatic => { @@ -839,6 +900,42 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_save_entered_login_param() -> Result<()> { + let t = TestContext::new().await; + let param = EnteredLoginParam { + addr: "alice@example.org".to_string(), + imap: EnteredServerLoginParam { + server: "".to_string(), + port: 0, + security: Socket::Starttls, + user: "".to_string(), + password: "foobar".to_string(), + }, + smtp: EnteredServerLoginParam { + server: "".to_string(), + port: 2947, + security: Socket::default(), + user: "".to_string(), + password: "".to_string(), + }, + certificate_checks: Default::default(), + oauth2: false, + }; + param.save(&t).await?; + assert_eq!( + t.get_config(Config::Addr).await?.unwrap(), + "alice@example.org" + ); + assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar"); + assert_eq!(t.get_config(Config::SendPw).await?, None); + assert_eq!(t.get_config_int(Config::SendPort).await?, 2947); + + assert_eq!(EnteredLoginParam::load(&t).await?, param); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_save_load_login_param() -> Result<()> { let t = TestContext::new().await; diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 30e2fd510..fb7697972 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -154,18 +154,21 @@ impl Socks5Config { } } +/// Configuration for the proxy through which all traffic +/// (except for iroh p2p connections) +/// will be sent. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProxyConfig { - // HTTP proxy. + /// HTTP proxy. Http(HttpConfig), - // HTTPS proxy. + /// HTTPS proxy. Https(HttpConfig), - // SOCKS5 proxy. + /// SOCKS5 proxy. Socks5(Socks5Config), - // Shadowsocks proxy. + /// Shadowsocks proxy. Shadowsocks(ShadowsocksConfig), } @@ -246,7 +249,7 @@ where impl ProxyConfig { /// Creates a new proxy configuration by parsing given proxy URL. - pub(crate) fn from_url(url: &str) -> Result { + pub fn from_url(url: &str) -> Result { let url = Url::parse(url).context("Cannot parse proxy URL")?; match url.scheme() { "http" => { @@ -305,7 +308,7 @@ impl ProxyConfig { /// /// This function can be used to normalize proxy URL /// by parsing it and serializing back. - pub(crate) fn to_url(&self) -> String { + pub fn to_url(&self) -> String { match self { Self::Http(http_config) => http_config.to_url("http"), Self::Https(http_config) => http_config.to_url("https"), @@ -391,7 +394,7 @@ impl ProxyConfig { /// If `load_dns_cache` is true, loads cached DNS resolution results. /// Use this only if the connection is going to be protected with TLS checks. - pub async fn connect( + pub(crate) async fn connect( &self, context: &Context, target_host: &str, diff --git a/src/qr.rs b/src/qr.rs index 4d91df894..27117fe83 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -695,7 +695,7 @@ struct CreateAccountErrorResponse { /// take a qr of the type DC_QR_ACCOUNT, parse it's parameters, /// download additional information from the contained url and set the parameters. /// on success, a configure::configure() should be able to log in to the account -async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { +pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = qr .get(DCACCOUNT_SCHEME.len()..) .context("Invalid DCACCOUNT scheme")?; diff --git a/src/tools.rs b/src/tools.rs index 25f953720..579df342a 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -603,6 +603,36 @@ where } } +pub(crate) trait ToOption { + fn to_option(self) -> Option; +} +impl<'a> ToOption<&'a str> for &'a String { + fn to_option(self) -> Option<&'a str> { + if self.is_empty() { + None + } else { + Some(self) + } + } +} +impl ToOption for u16 { + fn to_option(self) -> Option { + if self == 0 { + None + } else { + Some(self.to_string()) + } + } +} +impl ToOption for Option { + fn to_option(self) -> Option { + match self { + None | Some(0) => None, + Some(v) => Some(v.to_string()), + } + } +} + pub fn remove_subject_prefix(last_subject: &str) -> String { let subject_start = if last_subject.starts_with("Chat:") { 0