diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 7cafd44541..0cab66d360 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -457,7 +457,7 @@ impl CommandApi { /// /// This function stops and starts IO as needed. /// - /// Usually it will be enough to only set `addr` and `imap.password`, + /// Usually it will be enough to only set `addr` and `password`, /// and all the other settings will be autoconfigured. /// /// During configuration, ConfigureProgress events are emitted; diff --git a/deltachat-jsonrpc/src/api/types/login_param.rs b/deltachat-jsonrpc/src/api/types/login_param.rs index 5164e7649b..5b94b4e90c 100644 --- a/deltachat-jsonrpc/src/api/types/login_param.rs +++ b/deltachat-jsonrpc/src/api/types/login_param.rs @@ -4,83 +4,75 @@ use serde::Deserialize; use serde::Serialize; use yerpc::TypeDef; +/// Login parameters entered by the user. + #[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] -pub struct EnteredServerLoginParam { - /// Server hostname or IP address. - pub server: String, +pub struct EnteredLoginParam { + /// Email address. + pub addr: String, - /// Server port. - /// - /// 0 if not specified. - pub port: u16, + /// Password. + pub password: String, - /// Socket security. - pub security: Socket, + /// Imap server hostname or IP address. + pub imap_server: Option, - /// Username. - /// - /// Empty string if not specified. - pub user: String, + /// Imap server port. + pub imap_port: Option, - /// Password. - pub password: String, -} + /// Imap socket security. + pub imap_security: Option, -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, - } - } -} + /// Imap username. + pub imap_user: Option, -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, - } - } -} + /// SMTP server hostname or IP address. + pub smtp_server: Option, -/// Login parameters entered by the user. + /// SMTP server port. + pub smtp_port: Option, -#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] -#[serde(rename_all = "camelCase")] -pub struct EnteredLoginParam { - /// Email address. - pub addr: String, + /// SMTP socket security. + pub smtp_security: Option, - /// IMAP settings. - pub imap: EnteredServerLoginParam, + /// SMTP username. + pub smtp_user: Option, - /// SMTP settings. - pub smtp: EnteredServerLoginParam, + /// SMTP Password. + /// + /// Only needs to be specified if different than IMAP password. + pub smtp_password: Option, /// TLS options: whether to allow invalid certificates and/or - /// invalid hostnames - pub certificate_checks: EnteredCertificateChecks, + /// invalid hostnames. + /// Default: Automatic + pub certificate_checks: Option, - /// If true, login via OAUTH2 (not recommended anymore) - pub oauth2: bool, + /// If true, login via OAUTH2 (not recommended anymore). + /// Default: false + pub oauth2: Option, } impl From for EnteredLoginParam { fn from(param: dc::EnteredLoginParam) -> Self { + let imap_security: Socket = param.imap.security.into(); + let smtp_security: Socket = param.smtp.security.into(); + let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into(); Self { addr: param.addr, - imap: param.imap.into(), - smtp: param.smtp.into(), - certificate_checks: param.certificate_checks.into(), - oauth2: param.oauth2, + password: param.imap.password, + imap_server: param.imap.server.into_option(), + imap_port: param.imap.port.into_option(), + imap_security: imap_security.into_option(), + imap_user: param.imap.user.into_option(), + smtp_server: param.smtp.server.into_option(), + smtp_port: param.smtp.port.into_option(), + smtp_security: smtp_security.into_option(), + smtp_user: param.smtp.user.into_option(), + smtp_password: param.smtp.password.into_option(), + certificate_checks: certificate_checks.into_option(), + oauth2: param.oauth2.into_option(), } } } @@ -91,18 +83,31 @@ impl TryFrom for dc::EnteredLoginParam { 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, + imap: dc::EnteredServerLoginParam { + server: param.imap_server.unwrap_or_default(), + port: param.imap_port.unwrap_or_default(), + security: param.imap_security.unwrap_or_default().into(), + user: param.imap_user.unwrap_or_default(), + password: param.password, + }, + smtp: dc::EnteredServerLoginParam { + server: param.smtp_server.unwrap_or_default(), + port: param.smtp_port.unwrap_or_default(), + security: param.smtp_security.unwrap_or_default().into(), + user: param.smtp_user.unwrap_or_default(), + password: param.smtp_password.unwrap_or_default(), + }, + certificate_checks: param.certificate_checks.unwrap_or_default().into(), + oauth2: param.oauth2.unwrap_or_default(), }) } } -#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)] #[serde(rename_all = "camelCase")] pub enum Socket { /// Unspecified socket security, select automatically. + #[default] Automatic, /// TLS connection. @@ -137,12 +142,13 @@ impl From for dc::Socket { } } -#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)] #[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. + #[default] Automatic, /// Ensure that TLS certificate is valid for the server hostname. @@ -177,3 +183,19 @@ impl From for dc::EnteredCertificateChecks { } } } + +trait IntoOption { + fn into_option(self) -> Option; +} +impl IntoOption for T +where + T: Default + std::cmp::PartialEq, +{ + fn into_option(self) -> Option { + if self == T::default() { + None + } else { + Some(self) + } + } +} diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 7776575606..ca74b18875 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -12,14 +12,6 @@ from .rpc import Rpc -def get_temp_credentials() -> dict: - domain = os.getenv("CHATMAIL_DOMAIN") - username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6)) - password = f"{username}${username}" - addr = f"{username}@{domain}" - return {"email": addr, "password": password} - - class ACFactory: def __init__(self, deltachat: DeltaChat) -> None: self.deltachat = deltachat @@ -32,26 +24,25 @@ def get_unconfigured_account(self) -> Account: def get_unconfigured_bot(self) -> Bot: return Bot(self.get_unconfigured_account()) - def new_preconfigured_account(self) -> Account: - """Make a new account with configuration options set, but configuration not started.""" - credentials = get_temp_credentials() - account = self.get_unconfigured_account() - account.set_config("addr", credentials["email"]) - account.set_config("mail_pw", credentials["password"]) - assert not account.is_configured() - return account + def get_credentials(self) -> (str, str): + domain = os.getenv("CHATMAIL_DOMAIN") + username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6)) + return f"{username}@{domain}", f"{username}${username}" @futuremethod def new_configured_account(self): - account = self.new_preconfigured_account() - yield account.configure.future() + addr, password = self.get_credentials() + account = self.get_unconfigured_account() + params = {"addr": addr, "password": password} + yield account._rpc.add_transport.future(account.id, params) + assert account.is_configured() return account def new_configured_bot(self) -> Bot: - credentials = get_temp_credentials() + addr, password = self.get_credentials() bot = self.get_unconfigured_bot() - bot.configure(credentials["email"], credentials["password"]) + bot.configure(addr, password) return bot @futuremethod diff --git a/deltachat-rpc-client/tests/test_account_events.py b/deltachat-rpc-client/tests/test_account_events.py index 4c4d5c4473..fb87fc6f27 100644 --- a/deltachat-rpc-client/tests/test_account_events.py +++ b/deltachat-rpc-client/tests/test_account_events.py @@ -13,10 +13,11 @@ def test_event_on_configuration(acfactory: ACFactory) -> None: Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure """ - account = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() account.clear_all_events() assert not account.is_configured() - future = account.configure.future() + future = account._rpc.add_transport.future(account.id, {"addr": addr, "password": password}) while True: event = account.wait_for_event() if event.kind == EventType.ACCOUNTS_ITEM_CHANGED: diff --git a/deltachat-rpc-client/tests/test_securejoin.py b/deltachat-rpc-client/tests/test_securejoin.py index 73299c48ba..afd36bbdd6 100644 --- a/deltachat-rpc-client/tests/test_securejoin.py +++ b/deltachat-rpc-client/tests/test_securejoin.py @@ -468,8 +468,7 @@ 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) - # ac1new is only used to get a new address. - ac1new = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() logging.info("ac1: create verified-group QR, ac2 scans and joins") chat = ac1.create_group("hello", protect=True) @@ -489,8 +488,8 @@ def test_aeap_flow_verified(acfactory): assert msg_in_1.text == msg_out.text logging.info("changing email account") - ac1.set_config("addr", ac1new.get_config("addr")) - ac1.set_config("mail_pw", ac1new.get_config("mail_pw")) + ac1.set_config("addr", addr) + ac1.set_config("mail_pw", password) ac1.stop_io() ac1.configure() ac1.start_io() @@ -503,11 +502,9 @@ def test_aeap_flow_verified(acfactory): msg_in_2_snapshot = msg_in_2.get_snapshot() assert msg_in_2_snapshot.text == msg_out.text assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id - assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr") + assert msg_in_2.get_sender_contact().get_snapshot().address == addr assert len(msg_in_2_snapshot.chat.get_contacts()) == 2 - assert ac1new.get_config("addr") in [ - contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts() - ] + assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()] def test_gossip_verification(acfactory) -> None: diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 177ecf19d4..2757a22f7e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -61,45 +61,70 @@ def test_acfactory(acfactory) -> None: def test_configure_starttls(acfactory) -> None: - account = acfactory.new_preconfigured_account() - - # Use STARTTLS - account.set_config("mail_security", "2") - account.set_config("send_security", "2") - account.configure() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account._rpc.add_transport( + account.id, + { + "addr": addr, + "password": password, + "imapSecurity": "starttls", + "smtpSecurity": "starttls", + }, + ) assert account.is_configured() def test_configure_ip(acfactory) -> None: - account = acfactory.new_preconfigured_account() - - domain = account.get_config("addr").rsplit("@")[-1] - ip_address = socket.gethostbyname(domain) + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + ip_address = socket.gethostbyname(addr.rsplit("@")[-1]) - # This should fail TLS check. - account.set_config("mail_server", ip_address) with pytest.raises(JsonRpcError): - account.configure() + account._rpc.add_transport( + account.id, + { + "addr": addr, + "password": password, + # This should fail TLS check. + "imapServer": ip_address, + }, + ) def test_configure_alternative_port(acfactory) -> None: """Test that configuration with alternative port 443 works.""" - account = acfactory.new_preconfigured_account() - - account.set_config("mail_port", "443") - account.set_config("send_port", "443") - - account.configure() - - -def test_configure_username(acfactory) -> None: - account = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account._rpc.add_transport( + account.id, + { + "addr": addr, + "password": password, + "imapPort": 443, + "smtpPort": 443, + }, + ) + assert account.is_configured() - addr = account.get_config("addr") - account.set_config("mail_user", addr) - account.configure() - assert account.get_config("configured_mail_user") == addr +def test_list_transports(acfactory) -> None: + addr, password = acfactory.get_credentials() + account = acfactory.get_unconfigured_account() + account._rpc.add_transport( + account.id, + { + "addr": addr, + "password": password, + "imapUser": addr, + }, + ) + transports = account._rpc.list_transports(account.id) + assert len(transports) == 1 + params = transports[0] + assert params["addr"] == addr + assert params["password"] == password + assert params["imapUser"] == addr def test_account(acfactory) -> None: @@ -415,9 +440,11 @@ def test_wait_next_messages(acfactory) -> None: alice = acfactory.new_configured_account() # Create a bot account so it does not receive device messages in the beginning. - bot = acfactory.new_preconfigured_account() + addr, password = acfactory.get_credentials() + bot = acfactory.get_unconfigured_account() bot.set_config("bot", "1") - bot.configure() + bot._rpc.add_transport(bot.id, {"addr": addr, "password": password}) + assert bot.is_configured() # There are no old messages and the call returns immediately. assert not bot.wait_next_messages() @@ -604,9 +631,13 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap): messages they refer to and thus dropped. """ (ac1,) = acfactory.get_online_accounts(1) - ac2 = acfactory.new_preconfigured_account() - ac2.configure() + + addr, password = acfactory.get_credentials() + ac2 = acfactory.get_unconfigured_account() + ac2._rpc.add_transport(ac2.id, {"addr": addr, "password": password}) ac2.set_config("mvbox_move", "1") + assert ac2.is_configured() + ac2.bring_online() chat1 = acfactory.get_accepted_chat(ac1, ac2) ac2.stop_io() diff --git a/src/configure.rs b/src/configure.rs index 004833826a..02bdea36b7 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -144,7 +144,8 @@ impl Context { // 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)); + progress!(self, 0, Some(error_msg.clone())); + bail!(error_msg); } else { param.save(self).await?; progress!(self, 1000); @@ -157,9 +158,22 @@ impl Context { /// 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?; + self.stop_io().await; + + let result = async move { + set_account_from_qr(self, qr).await?; + self.configure().await?; + Ok(()) + } + .await; + if result.is_err() { + if let Ok(true) = self.is_configured().await { + self.start_io().await; + } + return result; + } + self.start_io().await; Ok(()) }