Compare commits

...

6 Commits

Author SHA1 Message Date
iequidoo
5083e2f067 feat: Allow sending unencrypted for chatmail if SignUnencrypted is set
If a chatmail user somehow can send unencrypted, setting `SignUnencrypted` is a simplest workaround,
it has no meaning for chatmail anyway. Setting `IsChatmail` to 0 has more side effects.
2025-04-16 15:31:29 -03:00
Hocuri
f311cae5ad fix: Parse login scheme in add_transport_from_qr() (#6802)
fix https://github.com/chatmail/core/issues/6801
2025-04-15 10:23:49 +02:00
Hocuri
7e8e4d2f39 api: Rename add_transport() -> add_or_update_transport() (#6800)
cc @nicodh
2025-04-15 10:19:25 +02:00
Hocuri
1379821b03 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>
2025-04-13 19:06:41 +02:00
link2xt
1722cb8851 test: fix mismatch between the contact and the account in securejoin tests 2025-04-13 05:48:58 +00:00
iequidoo
49c300d2ac test: Check headers absense straightforwardly
In the `test` cfg, introduce `MimeMessage::headers_removed` hash set and `header_exists()` function
returning whether the header exists in any part of the parsed message. `get_header()` shouldn't be
used in tests for checking absense of headers because it returns `None` for removed ("ignored")
headers.
2025-04-12 23:24:54 -03:00
20 changed files with 473 additions and 198 deletions

View File

@@ -439,7 +439,7 @@ impl CommandApi {
/// Setup the credential config before calling this.
///
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// or `add_or_update_transport()` instead.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
@@ -483,21 +483,30 @@ impl CommandApi {
/// 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<()> {
async fn add_or_update_transport(
&self,
account_id: u32,
param: EnteredLoginParam,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport(&param.try_into()?).await
ctx.add_or_update_transport(&param.try_into()?).await
}
/// Deprecated 2025-04. Alias for [Self::add_or_update_transport()].
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
self.add_or_update_transport(account_id, param).await
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
/// See [Self::add_or_update_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
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;

View File

@@ -111,9 +111,9 @@ class Account:
yield self._rpc.configure.future(self.id)
@futuremethod
def add_transport(self, params):
def add_or_update_transport(self, params):
"""Add a new transport."""
yield self._rpc.add_transport.future(self.id, params)
yield self._rpc.add_or_update_transport.future(self.id, params)
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""

View File

@@ -34,7 +34,7 @@ class ACFactory:
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
yield account.add_transport.future(params)
yield account.add_or_update_transport.future(params)
assert account.is_configured()
return account

View File

@@ -17,7 +17,7 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
account = acfactory.get_unconfigured_account()
account.clear_all_events()
assert not account.is_configured()
future = account.add_transport.future({"addr": addr, "password": password})
future = account.add_or_update_transport.future({"addr": addr, "password": password})
while True:
event = account.wait_for_event()
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:

View File

@@ -63,7 +63,7 @@ def test_acfactory(acfactory) -> None:
def test_configure_starttls(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -80,7 +80,7 @@ def test_configure_ip(acfactory) -> None:
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
with pytest.raises(JsonRpcError):
account.add_transport(
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -94,7 +94,7 @@ def test_configure_alternative_port(acfactory) -> None:
"""Test that configuration with alternative port 443 works."""
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -108,7 +108,7 @@ def test_configure_alternative_port(acfactory) -> None:
def test_list_transports(acfactory) -> None:
addr, password = acfactory.get_credentials()
account = acfactory.get_unconfigured_account()
account.add_transport(
account.add_or_update_transport(
{
"addr": addr,
"password": password,
@@ -420,7 +420,7 @@ def test_wait_next_messages(acfactory) -> None:
addr, password = acfactory.get_credentials()
bot = acfactory.get_unconfigured_account()
bot.set_config("bot", "1")
bot.add_transport({"addr": addr, "password": password})
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
# There are no old messages and the call returns immediately.
@@ -603,7 +603,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
addr, password = acfactory.get_credentials()
ac2 = acfactory.get_unconfigured_account()
ac2.add_transport({"addr": addr, "password": password})
ac2.add_or_update_transport({"addr": addr, "password": password})
ac2.set_config("mvbox_move", "1")
assert ac2.is_configured()
@@ -713,12 +713,11 @@ def test_get_http_response(acfactory):
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
assert configured_certificate_checks
assert "cert_automatic" in alice.get_info().used_account_settings
# 0 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)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
@@ -729,7 +728,7 @@ def test_configured_imap_certificate_checks(acfactory):
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert configured_certificate_checks != "0"
assert "cert_old_automatic" not in alice.get_info().used_account_settings
def test_no_old_msg_is_fresh(acfactory):

View File

@@ -482,12 +482,8 @@ class ACFactory:
addr = f"{acname}@offline.org"
ac.update_config(
{
"addr": addr,
"displayname": acname,
"mail_pw": "123",
"configured_addr": addr,
"configured_mail_pw": "123",
"configured": "1",
"displayname": acname,
},
)
self._preconfigure_key(ac)

View File

@@ -1502,15 +1502,6 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,

View File

@@ -2553,10 +2553,8 @@ async fn test_broadcast() -> Result<()> {
let sent_msg = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent_msg).await;
assert!(msg.was_encrypted());
assert!(msg
.get_header(HeaderDef::ChatGroupMemberTimestamps)
.is_none());
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_none());
assert!(!msg.header_exists(HeaderDef::ChatGroupMemberTimestamps));
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
let msg = bob.recv_msg(&sent_msg).await;
assert_eq!(msg.get_text(), "ola!");
assert_eq!(msg.subject, "Broadcast list");

View File

@@ -4,7 +4,7 @@ use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
@@ -13,10 +13,12 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
@@ -525,21 +527,22 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
false => Some("1"),
true => Some("0"),
false => Some("1".to_string()),
true => Some("0".to_string()),
},
Config::ConfiguredInboxFolder => Some("INBOX"),
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
&& Box::pin(self.is_chatmail()).await?
{
true => Some("1"),
false => Some("0"),
true => Some("1".to_string()),
false => Some("0".to_string()),
}
}
_ => key.get_str("default"),
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
_ => key.get_str("default").map(|s| s.to_string()),
};
Ok(val.map(|s| s.to_string()))
Ok(val)
}
/// Returns Some(T) if a value for the given key is set and was successfully parsed.
@@ -805,6 +808,19 @@ impl Context {
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
.await?;
}
Config::ConfiguredAddr => {
if self.is_configured().await? {
bail!("Cannot change ConfiguredAddr");
}
if let Some(addr) = value {
info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
}
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -891,6 +907,7 @@ impl Context {
/// primary address (if exists) as a secondary address.
///
/// This should only be used by test code and during configure.
#[cfg(test)] // AEAP is disabled, but there are still tests for it
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
self.quota.write().await.take();
@@ -904,7 +921,8 @@ impl Context {
)
.await?;
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
.await?;
self.emit_event(EventType::ConnectivityChanged);
Ok(())

View File

@@ -35,8 +35,7 @@ use crate::login_param::{
};
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::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
@@ -63,13 +62,13 @@ macro_rules! progress {
impl Context {
/// Checks if the context is already configured.
pub async fn is_configured(&self) -> Result<bool> {
self.sql.get_raw_config_bool("configured").await
self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
}
/// Configures this account with the currently provided parameters.
///
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// or `add_or_update_transport()` instead.
pub async fn configure(&self) -> Result<()> {
let param = EnteredLoginParam::load(self).await?;
@@ -105,7 +104,7 @@ impl Context {
/// 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<()> {
pub async fn add_or_update_transport(&self, param: &EnteredLoginParam) -> Result<()> {
self.stop_io().await;
let result = self.add_transport_inner(param).await;
if result.is_err() {
@@ -156,12 +155,23 @@ impl Context {
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
/// See [Self::add_or_update_transport].
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
self.stop_io().await;
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
// and then calls configure(), which loads them again.
// At some point, we will remove configure()
// and then simplify the code
// to directly create an EnteredLoginParam.
let result = async move {
set_account_from_qr(self, qr).await?;
match crate::qr::check_qr(self, qr).await? {
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
crate::qr::Qr::Login { address, options } => {
crate::qr::configure_from_login_qr(self, &address, options).await?
}
_ => bail!("QR code does not contain account"),
}
self.configure().await?;
Ok(())
}
@@ -178,12 +188,24 @@ impl Context {
}
/// 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
/// Use [Self::add_or_update_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let param = EnteredLoginParam::load(self).await?;
let transports = self
.sql
.query_map(
"SELECT entered_param FROM transports",
(),
|row| row.get::<_, String>(0),
|rows| {
rows.flatten()
.map(|s| Ok(serde_json::from_str(&s)?))
.collect::<Result<Vec<EnteredLoginParam>>>()
},
)
.await?;
Ok(vec![param])
Ok(transports)
}
/// Removes the transport with the specified email address
@@ -197,20 +219,20 @@ impl Context {
info!(self, "Configure ...");
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param = configure(self, param).await?;
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
on_configure_completed(self, provider, old_addr).await?;
Ok(())
}
}
async fn on_configure_completed(
context: &Context,
param: ConfiguredLoginParam,
provider: Option<&'static Provider>,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
for def in config_defaults {
if !context.config_exists(def.key).await? {
@@ -446,7 +468,7 @@ async fn get_configured_param(
Ok(configured_login_param)
}
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
progress!(ctx, 1);
let ctx2 = ctx.clone();
@@ -556,7 +578,11 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
}
}
configured_param.save_as_configured_params(ctx).await?;
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)
.await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
@@ -572,7 +598,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
ctx.sql.set_raw_config_bool("configured", true).await?;
ctx.emit_event(EventType::AccountsItemChanged);
Ok(configured_param)
Ok(provider)
}
/// Retrieve available autoconfigurations.

View File

@@ -48,12 +48,13 @@ impl EncryptHelper {
context: &Context,
peerstates: &[(Option<Peerstate>, String)],
) -> Result<bool> {
let is_chatmail = context.is_chatmail().await?;
// For chatmail we ignore the encryption preference,
// because we can either send encrypted or not at all.
let encrypt_if_possible = context.is_chatmail().await?
&& !context.get_config_bool(Config::SignUnencrypted).await?;
for (peerstate, _addr) in peerstates {
if let Some(peerstate) = peerstate {
// For chatmail we ignore the encryption preference,
// because we can either send encrypted or not at all.
if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
if encrypt_if_possible || peerstate.prefer_encrypt != EncryptPreference::Reset {
continue;
}
}

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(())
}
}

View File

@@ -893,10 +893,7 @@ async fn test_dont_remove_self() -> Result<()> {
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
.await
.unwrap();
assert_eq!(
mime_message.get_header(HeaderDef::ChatGroupPastMembers),
None
);
assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers));
assert_eq!(
mime_message.chat_group_member_timestamps().unwrap().len(),
1 // There is a timestamp for Bob, not for Alice

View File

@@ -57,6 +57,10 @@ pub(crate) struct MimeMessage {
/// Message headers.
headers: HashMap<String, String>,
#[cfg(test)]
/// Names of removed (ignored) headers. Used by `header_exists()` needed for tests.
headers_removed: HashSet<String>,
/// List of addresses from the `To` and `Cc` headers.
///
/// Addresses are normalized and lowercase.
@@ -236,6 +240,7 @@ impl MimeMessage {
let mut hop_info = parse_receive_headers(&mail.get_headers());
let mut headers = Default::default();
let mut headers_removed = HashSet::<String>::new();
let mut recipients = Default::default();
let mut past_members = Default::default();
let mut from = Default::default();
@@ -253,7 +258,12 @@ impl MimeMessage {
&mut chat_disposition_notification_to,
&mail.headers,
);
headers.retain(|k, _| !is_hidden(k));
headers.retain(|k, _| {
!is_hidden(k) || {
headers_removed.insert(k.clone());
false
}
});
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -298,9 +308,11 @@ impl MimeMessage {
// Overwrite Message-ID with X-Microsoft-Original-Message-ID.
// However if we later find Message-ID in the protected part,
// it will overwrite both.
if let Some(microsoft_message_id) =
headers.remove(HeaderDef::XMicrosoftOriginalMessageId.get_headername())
{
if let Some(microsoft_message_id) = remove_header(
&mut headers,
HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
&mut headers_removed,
) {
headers.insert(
HeaderDef::MessageId.get_headername().to_string(),
microsoft_message_id,
@@ -309,7 +321,7 @@ impl MimeMessage {
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
Self::remove_secured_headers(&mut headers);
Self::remove_secured_headers(&mut headers, &mut headers_removed);
let mut from = from.context("No from in message")?;
let private_keyring = load_self_secret_keyring(context).await?;
@@ -442,7 +454,7 @@ impl MimeMessage {
HeaderDef::ChatEdit,
HeaderDef::ChatUserAvatar,
] {
headers.remove(h.get_headername());
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
}
}
@@ -506,7 +518,7 @@ impl MimeMessage {
}
}
if signatures.is_empty() {
Self::remove_secured_headers(&mut headers);
Self::remove_secured_headers(&mut headers, &mut headers_removed);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
@@ -530,6 +542,9 @@ impl MimeMessage {
let mut parser = MimeMessage {
parts: Vec::new(),
headers,
#[cfg(test)]
headers_removed,
recipients,
past_members,
list_post,
@@ -931,6 +946,16 @@ impl MimeMessage {
.map(|s| s.as_str())
}
#[cfg(test)]
/// Returns whether the header exists in any part of the parsed message.
///
/// Use this to check for header absense. Header presense should be checked using
/// `get_header(...).is_some()` as it also checks that the header isn't ignored.
pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
let hname = headerdef.get_headername();
self.headers.contains_key(hname) || self.headers_removed.contains(hname)
}
/// Returns `Chat-Group-ID` header value if it is a valid group ID.
pub fn get_chat_group_id(&self) -> Option<&str> {
self.get_header(HeaderDef::ChatGroupId)
@@ -1526,14 +1551,17 @@ impl MimeMessage {
.and_then(|msgid| parse_message_id(msgid).ok())
}
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
headers.remove("secure-join-fingerprint");
headers.remove("secure-join-auth");
headers.remove("chat-verified");
headers.remove("autocrypt-gossip");
fn remove_secured_headers(
headers: &mut HashMap<String, String>,
removed: &mut HashSet<String>,
) {
remove_header(headers, "secure-join-fingerprint", removed);
remove_header(headers, "secure-join-auth", removed);
remove_header(headers, "chat-verified", removed);
remove_header(headers, "autocrypt-gossip", removed);
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = headers.remove("secure-join") {
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
@@ -1861,6 +1889,19 @@ impl MimeMessage {
}
}
fn remove_header(
headers: &mut HashMap<String, String>,
key: &str,
removed: &mut HashSet<String>,
) -> Option<String> {
if let Some((k, v)) = headers.remove_entry(key) {
removed.insert(k);
Some(v)
} else {
None
}
}
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
/// Params:
/// from: The address which sent the message currently being parsed

View File

@@ -5,6 +5,7 @@ pub(crate) mod data;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use hickory_resolver::{config, Resolver, TokioResolver};
use serde::{Deserialize, Serialize};
use crate::config::Config;
use crate::context::Context;
@@ -37,7 +38,19 @@ pub enum Protocol {
}
/// Socket security.
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
#[derive(
Debug,
Default,
Display,
PartialEq,
Eq,
Copy,
Clone,
FromPrimitive,
ToPrimitive,
Serialize,
Deserialize,
)]
#[repr(u8)]
pub enum Socket {
/// Unspecified socket security, select automatically.

View File

@@ -10,7 +10,7 @@ use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress}
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use self::dclogin_scheme::configure_from_login_qr;
pub(crate) use self::dclogin_scheme::configure_from_login_qr;
use crate::chat::ChatIdBlocked;
use crate::config::Config;
use crate::constants::Blocked;

View File

@@ -3113,7 +3113,6 @@ Message with references."#;
async fn test_rfc1847_encapsulation() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice.configure_addr("alice@example.org").await;
// Alice sends an Autocrypt message to Bob so Bob gets Alice's key.
let chat_alice = alice.create_chat(&bob).await;

View File

@@ -120,7 +120,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
// Step 3: Alice receives vc-request, sends vc-auth-required
alice.recv_msg_trash(&sent).await;
@@ -452,7 +452,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
// Step 7: Bob receives vc-contact-confirm
bob.recv_msg_trash(&sent).await;
@@ -523,7 +523,7 @@ async fn test_secure_join() -> Result<()> {
assert!(!msg.was_encrypted());
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
// but it was only used by Alice in `vg-request-with-auth`.
@@ -531,7 +531,7 @@ async fn test_secure_join() -> Result<()> {
// and it is deprecated.
// Now `Secure-Join-Group` header
// is only sent in `vg-request-with-auth` for compatibility.
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
assert!(!msg.header_exists(HeaderDef::SecureJoinGroup));
// Step 3: Alice receives vg-request, sends vg-auth-required
alice.recv_msg_trash(&sent).await;
@@ -606,7 +606,7 @@ async fn test_secure_join() -> Result<()> {
// Formally this message is auto-submitted, but as the member addition is a result of an
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
// be strange to have it in "member-added" messages of verified groups only.
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
@@ -636,7 +636,7 @@ async fn test_secure_join() -> Result<()> {
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
// Step 7: Bob receives vg-member-added
bob.recv_msg(&sent).await;

View File

@@ -5,9 +5,11 @@ use deltachat_contact_tools::EmailAddress;
use rusqlite::OptionalExtension;
use crate::config::Config;
use crate::configure::EnteredLoginParam;
use crate::constants::ShowEmails;
use crate::context::Context;
use crate::imap;
use crate::login_param::ConfiguredLoginParam;
use crate::message::MsgId;
use crate::provider::get_provider_by_domain;
use crate::sql::Sql;
@@ -1186,6 +1188,42 @@ CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
.await?;
}
inc_and_check(&mut migration_version, 131)?;
if dbversion < migration_version {
let entered_param = EnteredLoginParam::load(context).await?;
let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
sql.execute_migration_transaction(
|transaction| {
transaction.execute(
"CREATE TABLE transports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
addr TEXT NOT NULL,
entered_param TEXT NOT NULL,
configured_param TEXT NOT NULL,
UNIQUE(addr)
)",
(),
)?;
if let Some(configured_param) = configured_param {
transaction.execute(
"INSERT INTO transports (addr, entered_param, configured_param)
VALUES (?, ?, ?)",
(
configured_param.addr.clone(),
serde_json::to_string(&entered_param)?,
configured_param.into_json()?,
),
)?;
}
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -1230,6 +1268,21 @@ impl Sql {
}
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
self.execute_migration_transaction(
|transaction| {
transaction.execute_batch(query)?;
Ok(())
},
version,
)
.await
}
async fn execute_migration_transaction(
&self,
migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>,
version: i32,
) -> Result<()> {
self.transaction(move |transaction| {
let curr_version: String = transaction.query_row(
"SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
@@ -1239,7 +1292,7 @@ impl Sql {
let curr_version: i32 = curr_version.parse()?;
ensure!(curr_version < version, "Db version must be increased");
Self::set_db_version_trans(transaction, version)?;
transaction.execute_batch(query)?;
migration(transaction)?;
Ok(())
})

View File

@@ -477,15 +477,11 @@ impl TestContext {
/// The context will be configured but the key will not be pre-generated so if a key is
/// used the fingerprint will be different every time.
pub async fn configure_addr(&self, addr: &str) {
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
self.ctx
.set_config(Config::ConfiguredAddr, Some(addr))
.await
.unwrap();
self.ctx
.set_config(Config::Configured, Some("1"))
.await
.unwrap();
if let Some(name) = addr.split('@').next() {
self.set_name(name);
}