Compare commits

..

2 Commits

21 changed files with 94 additions and 132 deletions

View File

@@ -146,7 +146,7 @@ jobs:
cache-bin: false
- name: Install nextest
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595
uses: taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154
with:
tool: nextest

View File

@@ -462,10 +462,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
* This is deprecated, UIs should not behave differently
* for chatmail relays and classical email servers.
* Most usages in UIs can be replaced by `force_encryption`.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
* In contrast to `dc_set_chat_mute_duration()`,

View File

@@ -704,3 +704,25 @@ def test_withdraw_securejoin_qr(acfactory):
and "Ignoring RequestWithAuth message because of invalid auth code." in event.msg
):
break
def test_qr_scan_updates_new_relay_address(acfactory):
alice, bob = acfactory.get_online_accounts(2)
bob_alice_chat = bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
for ac in [alice, bob]:
old_addr = ac.get_config("configured_addr")
ac.add_transport_from_qr(acfactory.get_account_qr())
ac.set_config("configured_addr", ac.list_transports()[1]["addr"])
ac.delete_transport(old_addr)
bob.secure_join(alice.get_qr_code())
alice.wait_for_securejoin_inviter_success()
bob.wait_for_securejoin_joiner_success()
bob_alice_chat.send_text("hi")
snapshot = alice.wait_for_incoming_msg().get_snapshot()
assert snapshot.text == "hi"

View File

@@ -221,30 +221,6 @@ def test_account(acfactory) -> None:
alice.stop_io()
def test_mark_fresh_vs_self_mdn(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob.set_config("bcc_self", "1")
alice_contact_bob = alice.create_contact(bob)
alice_chat = alice_contact_bob.create_chat()
alice_chat.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
bob_chat = bob.get_chat_by_id(chat_id)
message = bob.get_message_by_id(msg_id)
bob_chat.accept()
bob.mark_seen_messages([message])
bob_chat.mark_fresh()
assert bob_chat.get_fresh_message_count() == 1
alice.wait_for_event(EventType.MSG_READ)
alice_chat.send_text("You've read 'Hello!'")
bob.wait_for_incoming_msg_event()
assert bob_chat.get_fresh_message_count() == 2
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)

View File

@@ -1374,18 +1374,6 @@ async fn test_markfresh_chat() -> Result<()> {
assert_eq!(bob_chat_id.get_fresh_msg_cnt(bob).await?, 0);
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
// Marking a message as seen results to sending an MDN to the contact and self.
message::markseen_msgs(bob, vec![bob_msg2.id]).await?;
assert_eq!(
bob.sql
.count(
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
(bob_msg2.from_id,)
)
.await?,
1
);
// bob marks the chat as fresh again, fresh count is 1 again
markfresh_chat(bob, bob_chat_id).await?;
let bob_msg1 = Message::load_from_db(bob, bob_msg1.id).await?;

View File

@@ -319,12 +319,6 @@ pub enum Config {
/// True if account is configured.
Configured,
/// Deprecated, we are trying to get rid of this global setting.
/// It is possible to configure a profile with both chatmail relays
/// and classical email servers.
///
/// Most usages in UIs can be replaced by `force_encryption`.
///
/// True if account is a chatmail account.
IsChatmail,

View File

@@ -1026,7 +1026,9 @@ impl Contact {
|| row_authname.is_empty());
row_id = id;
if origin >= row_origin && addr != row_addr {
let qr_with_fingerprint = !fingerprint.is_empty()
&& origin == Origin::UnhandledSecurejoinQrScan;
if (origin >= row_origin || qr_with_fingerprint) && addr != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {

View File

@@ -566,10 +566,6 @@ impl Context {
self.scheduler.maybe_network().await;
}
/// Deprecated, we are trying to get rid of this global setting.
/// It is possible to configure a profile with both chatmail relays
/// and classical email servers.
///
/// Returns true if an account is on a chatmail server.
pub async fn is_chatmail(&self) -> Result<bool> {
self.get_config_bool(Config::IsChatmail).await

View File

@@ -97,6 +97,18 @@ impl EncryptHelper {
}
}
/// Ensures a private key exists for the configured user.
///
/// Normally the private key is generated when the first message is
/// sent but in a few locations there are no such guarantees,
/// e.g. when exporting keys, and calling this function ensures a
/// private key will be present.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
load_self_public_key(context).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -106,7 +118,23 @@ mod tests {
use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContextManager;
use crate::test_utils::{TestContext, TestContextManager};
mod ensure_secret_key_exists {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert!(ensure_secret_key_exists(&t).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_not_configured() {
let t = TestContext::new().await;
assert!(ensure_secret_key_exists(&t).await.is_err());
}
}
#[test]
fn test_mailmime_parse() {

View File

@@ -1117,7 +1117,7 @@ impl Session {
Err(err) => {
warn!(
context,
"store_seen_flags_on_imap: Transport {transport_id}: Failed to select {folder}, will retry later: {err:#}."
"store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
);
continue;
}
@@ -1128,13 +1128,13 @@ impl Session {
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Transport {transport_id}: Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
);
continue;
} else {
info!(
context,
"Transport {transport_id}: Marked messages {uid_set} in folder {folder} as seen."
"Marked messages {} in folder {} as seen.", uid_set, folder
);
}
context

View File

@@ -111,7 +111,7 @@ impl Session {
}
// Returns true if IMAP server has `XCHATMAIL` capability.
pub(crate) fn is_chatmail(&self) -> bool {
pub fn is_chatmail(&self) -> bool {
self.capabilities.is_chatmail
}

View File

@@ -17,6 +17,7 @@ use crate::blob::BlobDirContents;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::config::Config;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, SignedSecretKey};
use crate::log::{LogExt, warn};
@@ -169,7 +170,7 @@ async fn imex_inner(
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
key::ensure_secret_key_exists(context)
e2ee::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;

View File

@@ -38,16 +38,15 @@ use tokio::fs;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use crate::EventType;
use crate::chat::add_device_msg;
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::key;
use crate::log::warn;
use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{TempPathGuard, create_id, time};
use crate::{EventType, e2ee};
use super::{DBFILE_BACKUP_NAME, export_backup_stream, export_database, import_backup_stream};
@@ -113,7 +112,7 @@ impl BackupProvider {
.context("Context dir not found")?;
// before we export, make sure the private key exists
key::ensure_secret_key_exists(context)
e2ee::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;

View File

@@ -320,17 +320,6 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
}
}
/// Ensures a private key exists for the configured user.
///
/// Normally the private key is generated when the first message is
/// sent but in a few locations there are no such guarantees,
/// e.g. when exporting keys, and calling this function ensures a
/// private key will be present.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
load_self_public_key(context).await?;
Ok(())
}
/// Returns our own public keyring.
///
/// No keys are generated and at most one key is returned.
@@ -909,20 +898,4 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
"0102 0408 1020 4080 FF01\n0204 0810 2040 80FF 1314"
);
}
mod ensure_secret_key_exists {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert!(ensure_secret_key_exists(&t).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_not_configured() {
let t = TestContext::new().await;
assert!(ensure_secret_key_exists(&t).await.is_err());
}
}
}

View File

@@ -271,9 +271,7 @@ impl MimeFactory {
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
let relays =
addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
recipients.extend(relays);
recipients.extend(relay_addrs(&public_key, &addr));
to.push((authname, addr.clone()));
encryption_pubkeys = Some(vec![(addr, public_key)]);
@@ -353,7 +351,7 @@ impl MimeFactory {
};
if add_timestamp >= remove_timestamp {
let relays = if let Some(public_key) = public_key_opt {
let addrs = addresses_from_public_key(&public_key);
let addrs = relay_addrs(&public_key, &addr);
keys.push((addr.clone(), public_key));
addrs
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
@@ -361,10 +359,10 @@ impl MimeFactory {
if is_encrypted {
warn!(context, "Missing key for {addr}");
}
None
vec![addr.clone()]
} else {
None
}.unwrap_or_else(|| vec![addr.clone()]);
vec![addr.clone()]
};
if !recipients_contain_addr(&to, &addr) {
if id != ContactId::SELF {
@@ -393,7 +391,7 @@ impl MimeFactory {
if let Some(email_to_remove) = email_to_remove
&& email_to_remove == addr {
let relays = if let Some(public_key) = public_key_opt {
let addrs = addresses_from_public_key(&public_key);
let addrs = relay_addrs(&public_key, &addr);
keys.push((addr.clone(), public_key));
addrs
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
@@ -401,10 +399,10 @@ impl MimeFactory {
if is_encrypted {
warn!(context, "Missing key for {addr}");
}
None
vec![addr.clone()]
} else {
None
}.unwrap_or_else(|| vec![addr.clone()]);
vec![addr.clone()]
};
// This is a "member removed" message,
// we need to notify removed member
@@ -618,7 +616,9 @@ impl MimeFactory {
fn should_skip_autocrypt(&self) -> bool {
match &self.loaded {
Loaded::Message { .. } => false,
Loaded::Message { msg, .. } => {
msg.param.get_bool(Param::SkipAutocrypt).unwrap_or_default()
}
Loaded::Mdn { .. } => true,
}
}
@@ -2201,6 +2201,14 @@ async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
Ok(encoded_body)
}
fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<String> {
let mut addrs = addresses_from_public_key(public_key).unwrap_or_default();
if !addrs.iter().any(|r| r == addr) {
addrs.push(addr.to_string());
}
addrs
}
fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool {
let addr_lc = addr.to_lowercase();
recipients

View File

@@ -64,8 +64,7 @@ pub enum Param {
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.
/// Deprecated on 2026-06-20
DeprecatedSkipAutocrypt = b'o',
SkipAutocrypt = b'o',
/// For Messages
WantsMdn = b'r',

View File

@@ -14,9 +14,9 @@ use crate::constants::{
use crate::contact::mark_contact_id_as_verified;
use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key;
use crate::key::{DcKey, Fingerprint, load_self_public_key, self_fingerprint};
use crate::log::LogExt as _;
use crate::log::warn;
@@ -92,7 +92,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
==== Step 1 in "Setup verified contact" protocol ====
=======================================================*/
key::ensure_secret_key_exists(context).await.ok();
ensure_secret_key_exists(context).await.ok();
let chat = match chat {
Some(id) => {

View File

@@ -599,7 +599,7 @@ async fn send_mdn_rfc724_mid(
.ok()
})
.collect();
message::insert_tombstone(context, &rendered_msg.rfc724_mid).await?;
match smtp_send(context, &recipients, &body, smtp, None).await {
SendResult::Success => {
if !recipients.is_empty() {

View File

@@ -2434,28 +2434,6 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
.await?;
}
inc_and_check(&mut migration_version, 154)?;
if dbversion < migration_version {
// Recreate imap_markseen with PRIMARY KEY and NOT NULL constraints.
// PRIMARY KEY is needed to turn
// "DELETE FROM imap_markseen_new WHERE id = ?"
// query from SCAN into SEARCH.
sql.execute_migration(
"
CREATE TABLE new_imap_markseen (
id INTEGER PRIMARY KEY NOT NULL,
FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO new_imap_markseen (id)
SELECT id FROM imap_markseen;
DROP TABLE imap_markseen;
ALTER TABLE new_imap_markseen RENAME TO imap_markseen;
",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -232,7 +232,9 @@ impl TestContextManager {
test_context.set_primary_self_addr(new_addr).await.unwrap();
// ensure_secret_key_exists() is called during configure
key::ensure_secret_key_exists(test_context).await.unwrap();
crate::e2ee::ensure_secret_key_exists(test_context)
.await
.unwrap();
assert_eq!(
test_context.get_primary_self_addr().await.unwrap(),

View File

@@ -6,9 +6,7 @@ use crate::chat::{self, Chat, add_contact_to_chat, remove_contact_from_chat, sen
use crate::config::Config;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
use crate::key;
use crate::key::self_fingerprint;
use crate::message;
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -20,6 +18,7 @@ use crate::test_utils::{
E2EE_INFO_MSGS, TestContext, TestContextManager, get_chat_msg, mark_as_verified,
};
use crate::tools::SystemTime;
use crate::{e2ee, message};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_not_broken_by_classical() {
@@ -127,7 +126,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
let fiona_new = tcm.unconfigured().await;
fiona_new.configure_addr("fiona@example.net").await;
key::ensure_secret_key_exists(&fiona_new).await?;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
@@ -429,7 +428,7 @@ async fn test_verify_then_verify_again() -> Result<()> {
drop(bob);
let bob_new = tcm.unconfigured().await;
bob_new.configure_addr("bob@example.net").await;
key::ensure_secret_key_exists(&bob_new).await?;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new).await;