feat: allow plain domain in dcaccount: scheme

This is similar to old `dcaccount:` with URL,
but creates a 9-character username on the client
and avoids making an HTTPS request.

The scheme is reused to avoid the apps
needing to register for the new scheme.

`http` support is removed because it was
not working already, there is a check
that the scheme is `https` when the URL
is actually used and the core has
no way to make HTTP requests without TLS.
This commit is contained in:
link2xt
2025-10-27 17:41:39 +00:00
committed by l
parent 9f0d106818
commit 05ba206c5a
4 changed files with 54 additions and 58 deletions

View File

@@ -45,8 +45,8 @@ class ACFactory:
"""Create a new configured account.""" """Create a new configured account."""
addr, password = self.get_credentials() addr, password = self.get_credentials()
account = self.get_unconfigured_account() account = self.get_unconfigured_account()
params = {"addr": addr, "password": password} domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_or_update_transport.future(params) yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
assert account.is_configured() assert account.is_configured()
return account return account

View File

@@ -816,7 +816,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account() alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None) # Certificate checks should be configured (not None)
assert "cert_automatic" in alice.get_info().used_account_settings assert "cert_strict" in alice.get_info().used_account_settings
# "cert_old_automatic" is the value old Delta Chat core versions used # "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic) # to mean user entered "imap_certificate_checks=0" (Automatic)

View File

@@ -9,6 +9,8 @@ pub use dclogin_scheme::LoginOptions;
pub(crate) use dclogin_scheme::login_param_from_login_qr; pub(crate) use dclogin_scheme::login_param_from_login_qr;
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr}; use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode}; use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
use rand::TryRngCore as _;
use rand::distr::{Alphanumeric, SampleString};
use serde::Deserialize; use serde::Deserialize;
use crate::config::Config; use crate::config::Config;
@@ -543,21 +545,29 @@ async fn decode_ideltachat(context: &Context, prefix: &str, qr: &str) -> Result<
.with_context(|| format!("failed to decode {prefix} QR code")) .with_context(|| format!("failed to decode {prefix} QR code"))
} }
/// scheme: `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3` /// scheme: `DCACCOUNT:example.org`
/// or `DCACCOUNT:https://example.org/new`
/// or `DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3`
fn decode_account(qr: &str) -> Result<Qr> { fn decode_account(qr: &str) -> Result<Qr> {
let payload = qr let payload = qr
.get(DCACCOUNT_SCHEME.len()..) .get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT payload")?; .context("Invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?; if payload.starts_with("https://") {
if url.scheme() == "http" || url.scheme() == "https" { let url = url::Url::parse(payload).context("Invalid account URL")?;
Ok(Qr::Account { if url.scheme() == "https" {
domain: url Ok(Qr::Account {
.host_str() domain: url
.context("can't extract account setup domain")? .host_str()
.to_string(), .context("can't extract account setup domain")?
}) .to_string(),
})
} else {
bail!("Bad scheme for account URL: {:?}.", url.scheme());
}
} else { } else {
bail!("Bad scheme for account URL: {:?}.", url.scheme()); Ok(Qr::Account {
domain: payload.to_string(),
})
} }
} }
@@ -659,15 +669,30 @@ pub(crate) async fn login_param_from_account_qr(
context: &Context, context: &Context,
qr: &str, qr: &str,
) -> Result<EnteredLoginParam> { ) -> Result<EnteredLoginParam> {
let url_str = qr let payload = qr
.get(DCACCOUNT_SCHEME.len()..) .get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT scheme")?; .context("Invalid DCACCOUNT scheme")?;
if !url_str.starts_with(HTTPS_SCHEME) { if !payload.starts_with(HTTPS_SCHEME) {
bail!("DCACCOUNT QR codes must use HTTPS scheme"); let rng = &mut rand::rngs::OsRng.unwrap_err();
let username = Alphanumeric.sample_string(rng, 9);
let addr = username + "@" + payload;
let password = Alphanumeric.sample_string(rng, 50);
let param = EnteredLoginParam {
addr,
imap: EnteredServerLoginParam {
password,
..Default::default()
},
smtp: Default::default(),
certificate_checks: EnteredCertificateChecks::Strict,
oauth2: false,
};
return Ok(param);
} }
let (response_text, response_success) = post_empty(context, url_str).await?; let (response_text, response_success) = post_empty(context, payload).await?;
if response_success { if response_success {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text) let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| { .with_context(|| {

View File

@@ -643,30 +643,20 @@ async fn test_decode_dclogin_advanced_options() -> Result<()> {
async fn test_decode_account() -> Result<()> { async fn test_decode_account() -> Result<()> {
let ctx = TestContext::new().await; let ctx = TestContext::new().await;
let qr = check_qr( for text in [
&ctx.ctx, "DCACCOUNT:example.org",
"dcaccount:example.org",
"DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "DCACCOUNT:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await?;
assert_eq!(
qr,
Qr::Account {
domain: "example.org".to_string()
}
);
// Test it again with lowercased "dcaccount:" uri scheme
let qr = check_qr(
&ctx.ctx,
"dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3", "dcaccount:https://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
) ] {
.await?; let qr = check_qr(&ctx.ctx, text).await?;
assert_eq!( assert_eq!(
qr, qr,
Qr::Account { Qr::Account {
domain: "example.org".to_string() domain: "example.org".to_string()
} }
); );
}
Ok(()) Ok(())
} }
@@ -734,25 +724,6 @@ async fn test_decode_tg_socks_proxy() -> Result<()> {
Ok(()) Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account_bad_scheme() {
let ctx = TestContext::new().await;
let res = check_qr(
&ctx.ctx,
"DCACCOUNT:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await;
assert!(res.is_err());
// Test it again with lowercased "dcaccount:" uri scheme
let res = check_qr(
&ctx.ctx,
"dcaccount:ftp://example.org/new_email?t=1w_7wDjgjelxeX884x96v3",
)
.await;
assert!(res.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_proxy_config_from_qr() -> Result<()> { async fn test_set_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await; let t = TestContext::new().await;