refactor: Move logins into SQL table (#6724)

Move all `configured_*` parameters into a new SQL table `transports`.
All `configured_*` parameters are deprecated; the only exception is
`configured_addr`, which is used to store the address of the primary
transport. Currently, there can only ever be one primary transport (i.e.
the `transports` table only ever has one row); this PR is not supposed
to change DC's behavior in any meaningful way.

This is a preparation for mt.

---------

Co-authored-by: l <link2xt@testrun.org>
This commit is contained in:
Hocuri
2025-04-13 19:06:41 +02:00
committed by GitHub
parent 1722cb8851
commit 1379821b03
10 changed files with 364 additions and 145 deletions

View File

@@ -2,26 +2,38 @@
use std::fmt;
use anyhow::{format_err, Context as _, Result};
use deltachat_contact_tools::EmailAddress;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use deltachat_contact_tools::{addr_cmp, addr_normalize, EmailAddress};
use num_traits::ToPrimitive as _;
use serde::{Deserialize, Serialize};
use crate::config::Config;
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::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::net::load_connection_timestamp;
pub use crate::net::proxy::ProxyConfig;
pub use crate::provider::Socket;
use crate::provider::{Protocol, Provider, UsernamePattern};
use crate::provider::{get_provider_by_id, Protocol, Provider, UsernamePattern};
use crate::sql::Sql;
use crate::tools::ToOption;
/// User-entered setting for certificate checks.
///
/// Should be saved into `imap_certificate_checks` before running configuration.
#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(
Copy,
Clone,
Debug,
Default,
Display,
FromPrimitive,
ToPrimitive,
PartialEq,
Eq,
Serialize,
Deserialize,
)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum EnteredCertificateChecks {
@@ -44,7 +56,9 @@ pub enum EnteredCertificateChecks {
}
/// Values saved into `imap_certificate_checks`.
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(
Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum ConfiguredCertificateChecks {
@@ -81,7 +95,7 @@ pub(crate) enum ConfiguredCertificateChecks {
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnteredServerLoginParam {
/// Server hostname or IP address.
pub server: String,
@@ -104,7 +118,7 @@ pub struct EnteredServerLoginParam {
}
/// Login parameters entered by the user.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EnteredLoginParam {
/// Email address.
pub addr: String,
@@ -451,6 +465,22 @@ pub(crate) struct ConfiguredLoginParam {
pub oauth2: bool,
}
/// The representation of ConfiguredLoginParam in the database,
/// saved as Json.
#[derive(Debug, Serialize, Deserialize)]
struct ConfiguredLoginParamJson {
pub addr: String,
pub imap: Vec<ConfiguredServerLoginParam>,
pub imap_user: String,
pub imap_password: String,
pub smtp: Vec<ConfiguredServerLoginParam>,
pub smtp_user: String,
pub smtp_password: String,
pub provider_id: Option<String>,
pub certificate_checks: ConfiguredCertificateChecks,
pub oauth2: bool,
}
impl fmt::Display for ConfiguredLoginParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let addr = &self.addr;
@@ -487,6 +517,26 @@ impl ConfiguredLoginParam {
///
/// Returns `None` if account is not configured.
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
return Ok(None);
};
let json: Option<String> = context
.sql
.query_get_value(
"SELECT configured_param FROM transports WHERE addr=?",
(&self_addr,),
)
.await?;
if let Some(json) = json {
Ok(Some(Self::from_json(&json)?))
} else {
bail!("Self address {self_addr} doesn't have a corresponding transport");
}
}
/// Loads legacy configured param. Only used for tests and the migration.
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
if !context.get_config_bool(Config::Configured).await? {
return Ok(None);
}
@@ -753,84 +803,82 @@ impl ConfiguredLoginParam {
}))
}
/// Save this loginparam to the database.
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
context.set_primary_self_addr(&self.addr).await?;
pub(crate) async fn save_to_transports_table(
self,
context: &Context,
entered_param: &EnteredLoginParam,
) -> Result<()> {
let addr = addr_normalize(&self.addr);
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
if let Some(configured_addr) = configured_addr {
ensure!(
addr_cmp(&configured_addr, &addr,),
"Adding a second transport is not supported right now."
);
}
context
.set_config(
Config::ConfiguredImapServers,
Some(&serde_json::to_string(&self.imap)?),
)
.await?;
context
.set_config(
Config::ConfiguredSmtpServers,
Some(&serde_json::to_string(&self.smtp)?),
)
.await?;
context
.set_config(Config::ConfiguredMailUser, Some(&self.imap_user))
.await?;
context
.set_config(Config::ConfiguredMailPw, Some(&self.imap_password))
.await?;
context
.set_config(Config::ConfiguredSendUser, Some(&self.smtp_user))
.await?;
context
.set_config(Config::ConfiguredSendPw, Some(&self.smtp_password))
.await?;
context
.set_config_u32(
Config::ConfiguredImapCertificateChecks,
self.certificate_checks as u32,
)
.await?;
context
.set_config_u32(
Config::ConfiguredSmtpCertificateChecks,
self.certificate_checks as u32,
)
.await?;
// Remove legacy settings.
context
.set_config(Config::ConfiguredMailServer, None)
.await?;
context.set_config(Config::ConfiguredMailPort, None).await?;
context
.set_config(Config::ConfiguredMailSecurity, None)
.await?;
context
.set_config(Config::ConfiguredSendServer, None)
.await?;
context.set_config(Config::ConfiguredSendPort, None).await?;
context
.set_config(Config::ConfiguredSendSecurity, None)
.await?;
let server_flags = match self.oauth2 {
true => DC_LP_AUTH_OAUTH2,
false => DC_LP_AUTH_NORMAL,
};
context
.set_config_u32(Config::ConfiguredServerFlags, server_flags as u32)
.await?;
context
.set_config(
Config::ConfiguredProvider,
.sql
.set_raw_config(
Config::ConfiguredProvider.as_ref(),
self.provider.map(|provider| provider.id),
)
.await?;
context
.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param)
VALUES (?, ?, ?)
ON CONFLICT (addr)
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
(
self.addr.clone(),
serde_json::to_string(entered_param)?,
self.into_json()?,
),
)
.await?;
context
.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
Ok(())
}
pub(crate) fn from_json(json: &str) -> Result<Self> {
let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
Ok(ConfiguredLoginParam {
addr: json.addr,
imap: json.imap,
imap_user: json.imap_user,
imap_password: json.imap_password,
smtp: json.smtp,
smtp_user: json.smtp_user,
smtp_password: json.smtp_password,
provider,
certificate_checks: json.certificate_checks,
oauth2: json.oauth2,
})
}
pub(crate) fn into_json(self) -> Result<String> {
let json = ConfiguredLoginParamJson {
addr: self.addr,
imap: self.imap,
imap_user: self.imap_user,
imap_password: self.imap_password,
smtp: self.smtp,
smtp_user: self.smtp_user,
smtp_password: self.smtp_password,
provider_id: self.provider.map(|p| p.id.to_string()),
certificate_checks: self.certificate_checks,
oauth2: self.oauth2,
};
Ok(serde_json::to_string(&json)?)
}
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
match self.certificate_checks {
@@ -848,8 +896,10 @@ impl ConfiguredLoginParam {
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogExt as _;
use crate::provider::get_provider_by_id;
use crate::test_utils::TestContext;
use pretty_assertions::assert_eq;
#[test]
fn test_certificate_checks_display() {
@@ -961,18 +1011,33 @@ mod tests {
oauth2: false,
};
param.save_as_configured_params(&t).await?;
param
.clone()
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
assert_eq!(
t.get_config(Config::ConfiguredImapServers).await?.unwrap(),
r#"[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}]"#
t.sql
.query_get_value::<String>("SELECT configured_param FROM transports", ())
.await?
.unwrap(),
expected_param
);
t.set_config(Config::Configured, Some("1")).await?;
assert_eq!(t.is_configured().await?, true);
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(param, loaded);
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
// Legacy ConfiguredImapCertificateChecks config is ignored
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
.await?;
assert!(ConfiguredLoginParam::load(&t).await.is_ok());
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
let wrong_param = expected_param.replace("Strict", "Stricct");
assert_ne!(expected_param, wrong_param);
t.sql
.execute("UPDATE transports SET configured_param=?", (wrong_param,))
.await?;
assert!(ConfiguredLoginParam::load(&t).await.is_err());
Ok(())
@@ -989,7 +1054,8 @@ mod tests {
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some("posteo"))
.await?;
t.set_config(Config::ConfiguredAddr, Some("alice@posteo.at"))
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
.await?;
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
.await?;
@@ -1063,12 +1129,68 @@ mod tests {
oauth2: false,
};
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded, param);
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded, param);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list_legacy() -> Result<()> {
// Find a provider that does not have server list set.
//
// There is at least one such provider in the provider database.
let (domain, provider) = crate::provider::data::PROVIDER_DATA
.iter()
.find(|(_domain, provider)| provider.server.is_empty())
.unwrap();
let t = TestContext::new().await;
let addr = format!("alice@{domain}");
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some(provider.id))
.await?;
t.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
.await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
migrate_configured_login_param(&t).await;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
Ok(())
}
async fn migrate_configured_login_param(t: &TestContext) {
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
t.sql.run_migrations(t).await.log_err(t).ok();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_empty_server_list() -> Result<()> {
// Find a provider that does not have server list set.
@@ -1083,25 +1205,41 @@ mod tests {
let addr = format!("alice@{domain}");
t.set_config(Config::Configured, Some("1")).await?;
t.set_config(Config::ConfiguredProvider, Some(provider.id))
.await?;
t.set_config(Config::ConfiguredAddr, Some(&addr)).await?;
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
.await?;
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
.await?; // Strict
t.set_config(Config::ConfiguredServerFlags, Some("0"))
.await?;
ConfiguredLoginParam {
addr: addr.clone(),
imap: vec![ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "example.org".to_string(),
port: 100,
security: ConnectionSecurity::Tls,
},
user: addr.clone(),
}],
imap_user: addr.clone(),
imap_password: "foobarbaz".to_string(),
smtp: vec![ConfiguredServerLoginParam {
connection: ConnectionCandidate {
host: "example.org".to_string(),
port: 100,
security: ConnectionSecurity::Tls,
},
user: addr.clone(),
}],
smtp_user: addr.clone(),
smtp_password: "foobarbaz".to_string(),
provider: Some(provider),
certificate_checks: ConfiguredCertificateChecks::Automatic,
oauth2: false,
}
.save_to_transports_table(&t, &EnteredLoginParam::default())
.await?;
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
assert_eq!(loaded.provider, Some(*provider));
assert_eq!(loaded.imap.is_empty(), false);
assert_eq!(loaded.smtp.is_empty(), false);
assert_eq!(t.get_configured_provider().await?, Some(*provider));
Ok(())
}
}