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."""
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
yield account.add_or_update_transport.future(params)
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
assert account.is_configured()
return account

View File

@@ -816,7 +816,7 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# 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
# 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;
use deltachat_contact_tools::{ContactAddress, addr_normalize, may_be_valid_addr};
use percent_encoding::{NON_ALPHANUMERIC, percent_decode_str, percent_encode};
use rand::TryRngCore as _;
use rand::distr::{Alphanumeric, SampleString};
use serde::Deserialize;
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"))
}
/// 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> {
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT payload")?;
let url = url::Url::parse(payload).context("Invalid account URL")?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::Account {
domain: url
.host_str()
.context("can't extract account setup domain")?
.to_string(),
})
if payload.starts_with("https://") {
let url = url::Url::parse(payload).context("Invalid account URL")?;
if url.scheme() == "https" {
Ok(Qr::Account {
domain: url
.host_str()
.context("can't extract account setup domain")?
.to_string(),
})
} else {
bail!("Bad scheme for account URL: {:?}.", url.scheme());
}
} 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,
qr: &str,
) -> Result<EnteredLoginParam> {
let url_str = qr
let payload = qr
.get(DCACCOUNT_SCHEME.len()..)
.context("Invalid DCACCOUNT scheme")?;
if !url_str.starts_with(HTTPS_SCHEME) {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
if !payload.starts_with(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 {
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
.with_context(|| {

View File

@@ -643,30 +643,20 @@ async fn test_decode_dclogin_advanced_options() -> Result<()> {
async fn test_decode_account() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(
&ctx.ctx,
for text in [
"DCACCOUNT:example.org",
"dcaccount:example.org",
"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",
)
.await?;
assert_eq!(
qr,
Qr::Account {
domain: "example.org".to_string()
}
);
] {
let qr = check_qr(&ctx.ctx, text).await?;
assert_eq!(
qr,
Qr::Account {
domain: "example.org".to_string()
}
);
}
Ok(())
}
@@ -734,25 +724,6 @@ async fn test_decode_tg_socks_proxy() -> Result<()> {
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)]
async fn test_set_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await;