Compare commits

..

10 Commits

Author SHA1 Message Date
Hocuri
f6e819d2a5 refactor: improve comment (#8366)
It's unclear whether this logic works, but I think we should at least
properly document why it exists (I already sent this text while figuring
out what the code does, so it was easy making this PR now)

Could add a XXX note that it's unsure whether this works
2026-06-25 17:06:22 +00:00
Hocuri
abb84efc37 fix: Rerun the full securejoin protocol if the address was outdated (#8358)
Alternative to https://github.com/chatmail/core/pull/8355/, fixes
https://github.com/chatmail/core/issues/8329

This fixes the following bug:

Scanning QR for existing contact doesn't update relay list

Two chat partners suddenly lost connectivity to all of their relays. They added another relay, which is accessible, and set it as main. Of course, they still couldn't communicate, because there was no way to announce a new relay list to each other (clients know only about old relays so they try to send updates there).

That's why the friends used a different channel (e.g. met IRL) to send their QR codes once again.

Expected behavior: They would expect that clients update their relay lists according to the contact info inside the QR codes, and they can continue talking in Delta.

Actual behavior: But Delta Chat client doesn't update a list of announced relays on scanning a QR in case a contact already exists. It simply opens the existing chat without altering any contact info.

---------

Co-authored-by: holger krekel <holger@merlinux.eu>
2026-06-25 16:59:08 +00:00
link2xt
09d5d0bddf chore: update rPGP from 0.19.0 to 0.20.0 2026-06-25 16:17:24 +00:00
link2xt
b38277c294 feat: log transport ID in store_seen_flags_on_imap 2026-06-23 05:05:19 +00:00
dependabot[bot]
975fd8aee3 chore(deps): bump taiki-e/install-action from 2.81.8 to 2.81.11
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.81.8 to 2.81.11.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](0631aa6515...15449e3094)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.81.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-23 04:41:22 +00:00
link2xt
5a54e18fee fix: recreate imap_markseen with PRIMARY KEY constraint
It is needed to speed up "DELETE FROM imap_markseen_new WHERE id = ?".
Otherwise if imap_markseen table grows large,
marking a lot of messages at once as seen and trying to delete them one by one
from imap_markseen table may have quadratic complexity.
2026-06-23 03:33:33 +00:00
iequidoo
8b80ac146f fix: Tombstone MDN before sending it (#8252)
Otherwise, when it appears on IMAP, it will mark chat messages as seen/noticed even if
markfresh_chat() is called meanwhile.
2026-06-22 13:52:04 -03:00
Hocuri
d1f4e59d82 api: Deprecate is_chatmail
BREAKING CHANGE: UIs should not behave differently for chatmail relays than for classical email servers; most usages of `is_chatmail` can be replaced by `force_encryption`
2026-06-22 17:48:56 +02:00
link2xt
0a82d73eb5 refactor: move ensure_secret_key_exists into key.rs 2026-06-21 04:18:39 +00:00
link2xt
c026910fa7 chore: deprecate unused SkipAutocrypt param
I renamed SkipAutocrypt into DeprecatedSkipAutocrypt
instead of removing to make sure the letter is not reused.
2026-06-21 04:18:20 +00:00
31 changed files with 279 additions and 121 deletions

View File

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

52
Cargo.lock generated
View File

@@ -1710,7 +1710,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -2733,7 +2733,7 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.5.9",
"socket2 0.6.3",
"tokio",
"tower-service",
"tracing",
@@ -3154,7 +3154,7 @@ dependencies = [
"iroh-metrics-derive",
"itoa",
"serde",
"snafu",
"snafu 0.8.5",
"tracing",
]
@@ -3524,9 +3524,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "mime"
@@ -3792,7 +3792,7 @@ dependencies = [
"netlink-proto",
"netlink-sys",
"serde",
"snafu",
"snafu 0.8.5",
"socket2 0.5.9",
"time",
"tokio",
@@ -3880,7 +3880,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -4284,9 +4284,9 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4"
checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1"
dependencies = [
"aead",
"aes",
@@ -4325,6 +4325,7 @@ dependencies = [
"k256",
"log",
"md-5",
"memchr",
"ml-dsa",
"ml-kem",
"nom 8.0.0",
@@ -4336,7 +4337,6 @@ dependencies = [
"p384",
"p521",
"rand 0.8.6",
"regex",
"replace_with",
"ripemd",
"rsa",
@@ -4347,7 +4347,8 @@ dependencies = [
"signature",
"slh-dsa",
"smallvec",
"snafu",
"snafu 0.9.1",
"subtle",
"twofish",
"x25519-dalek",
"zeroize",
@@ -4586,7 +4587,7 @@ dependencies = [
"rand 0.8.6",
"serde",
"smallvec",
"snafu",
"snafu 0.8.5",
"socket2 0.5.9",
"time",
"tokio",
@@ -5281,7 +5282,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.52.0",
"windows-sys 0.61.1",
]
[[package]]
@@ -5849,7 +5850,16 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019"
dependencies = [
"snafu-derive",
"snafu-derive 0.8.5",
]
[[package]]
name = "snafu"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1a012328be2e3f5d5f6f3218147ca02588cea4cb865e876849ab6debcf36522"
dependencies = [
"snafu-derive 0.9.1",
]
[[package]]
@@ -5864,6 +5874,18 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "snafu-derive"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f103c50866b8743da9429b8a581d81a27c2d3a9c4ac7df8f8571c1dd7896eda"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "socket2"
version = "0.5.9"
@@ -6147,7 +6169,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.52.0",
"windows-sys 0.61.1",
]
[[package]]

View File

@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
pgp = { version = "0.20.0", features = ["draft-pqc"], default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.39", features = ["escape-html"] }

View File

@@ -39,7 +39,7 @@ mod vcard;
pub use vcard::{make_vcard, parse_vcard, VcardContact};
/// Valid contact address.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContactAddress(String);
impl Deref for ContactAddress {

View File

@@ -462,7 +462,10 @@ 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` = 1 if the the server is a chatmail server, 0 otherwise.
* - `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_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

@@ -236,6 +236,7 @@ impl From<Qr> for QrObject {
invitenumber,
authcode,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();
@@ -255,6 +256,7 @@ impl From<Qr> for QrObject {
invitenumber,
authcode,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();
@@ -276,6 +278,7 @@ impl From<Qr> for QrObject {
authcode,
invitenumber,
is_v3,
..
} => {
let contact_id = contact_id.to_u32();
let fingerprint = fingerprint.human_readable();

View File

@@ -221,6 +221,30 @@ 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

@@ -83,6 +83,8 @@ skip = [
{ name = "rustix", version = "0.38.44" },
{ name = "rustls-webpki", version = "0.102.8" },
{ name = "serdect", version = "0.2.0" },
{ name = "snafu-derive", version = "0.8.5" },
{ name = "snafu", version = "0.8.5" },
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },

View File

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

View File

@@ -566,6 +566,10 @@ 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

@@ -6,13 +6,16 @@ use std::io::Cursor;
use anyhow::{Context as _, Result, bail};
use mailparse::ParsedMail;
use pgp::composed::DecryptionOptions;
use pgp::composed::Esk;
use pgp::composed::Message;
use pgp::composed::PlainSessionKey;
use pgp::composed::SignedSecretKey;
use pgp::composed::TheRing;
use pgp::composed::decrypt_session_key_with_password;
use pgp::packet::SymKeyEncryptedSessionKey;
use pgp::types::Password;
use pgp::types::Seipdv1ReadMode;
use pgp::types::StringToKey;
use crate::chat::ChatId;
@@ -48,6 +51,15 @@ pub(crate) async fn decrypt(
};
let expected_sender_fingerprint: Option<String>;
let abort_early = true;
// Use streaming mode for SEIPDv1 decryption to save memory.
// This was the default in rPGP 0.19.0
// and requires explicitly changing the mode in rPGP 0.20.0.
// SEPIDv2 is decrypted in streaming mode in any case.
let decrypt_options =
DecryptionOptions::new().set_seipdv1_read_mode(Seipdv1ReadMode::Streaming);
let plain = if let Message::Encrypted { esk, .. } = &*msg
// We only allow one ESK for symmetrically encrypted messages
// to avoid dealing with messages that are encrypted to multiple symmetric keys
@@ -61,9 +73,15 @@ pub(crate) async fn decrypt(
expected_sender_fingerprint = fingerprint;
tokio::task::spawn_blocking(move || -> Result<Message<'_>> {
let plain = msg
.decrypt_with_session_key(psk)
.context("decrypt_with_session_key")?;
let ring = TheRing {
session_keys: vec![psk],
decrypt_options,
..Default::default()
};
let (plain, _ring_result) = msg
.decrypt_the_ring(ring, abort_early)
.context("decrypt_the_ring")?;
let plain: Message<'static> = plain.decompress()?;
Ok(plain)
@@ -75,11 +93,15 @@ pub(crate) async fn decrypt(
expected_sender_fingerprint = None;
tokio::task::spawn_blocking(move || -> Result<Message<'_>> {
let empty_pw = Password::empty();
let secret_keys: Vec<&SignedSecretKey> = secret_keys.iter().collect();
let plain = msg
.decrypt_with_keys(vec![&empty_pw], secret_keys)
.context("decrypt_with_keys")?;
let ring = TheRing {
secret_keys,
decrypt_options,
..Default::default()
};
let (plain, _ring_result) = msg
.decrypt_the_ring(ring, abort_early)
.context("decrypt_the_ring")?;
let plain: Message<'static> = plain.decompress()?;
Ok(plain)

View File

@@ -97,18 +97,6 @@ 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::*;
@@ -118,23 +106,7 @@ mod tests {
use crate::message::Message;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
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());
}
}
use crate::test_utils::TestContextManager;
#[test]
fn test_mailmime_parse() {

View File

@@ -1117,7 +1117,7 @@ impl Session {
Err(err) => {
warn!(
context,
"store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}."
"store_seen_flags_on_imap: Transport {transport_id}: 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,
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
"Transport {transport_id}: Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}."
);
continue;
} else {
info!(
context,
"Marked messages {} in folder {} as seen.", uid_set, folder
"Transport {transport_id}: Marked messages {uid_set} in folder {folder} as seen."
);
}
context

View File

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

View File

@@ -17,7 +17,6 @@ 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};
@@ -170,7 +169,7 @@ async fn imex_inner(
if what == ImexMode::ExportBackup || what == ImexMode::ExportSelfKeys {
// before we export anything, make sure the private key exists
e2ee::ensure_secret_key_exists(context)
key::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;

View File

@@ -38,15 +38,16 @@ 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};
@@ -112,7 +113,7 @@ impl BackupProvider {
.context("Context dir not found")?;
// before we export, make sure the private key exists
e2ee::ensure_secret_key_exists(context)
key::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;

View File

@@ -320,6 +320,17 @@ 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.
@@ -898,4 +909,20 @@ 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,7 +271,9 @@ impl MimeFactory {
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
recipients.extend(relay_addrs(&public_key, &addr));
let relays =
addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
recipients.extend(relays);
to.push((authname, addr.clone()));
encryption_pubkeys = Some(vec![(addr, public_key)]);
@@ -351,7 +353,7 @@ impl MimeFactory {
};
if add_timestamp >= remove_timestamp {
let relays = if let Some(public_key) = public_key_opt {
let addrs = relay_addrs(&public_key, &addr);
let addrs = addresses_from_public_key(&public_key);
keys.push((addr.clone(), public_key));
addrs
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
@@ -359,10 +361,10 @@ impl MimeFactory {
if is_encrypted {
warn!(context, "Missing key for {addr}");
}
vec![addr.clone()]
None
} else {
vec![addr.clone()]
};
None
}.unwrap_or_else(|| vec![addr.clone()]);
if !recipients_contain_addr(&to, &addr) {
if id != ContactId::SELF {
@@ -391,7 +393,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 = relay_addrs(&public_key, &addr);
let addrs = addresses_from_public_key(&public_key);
keys.push((addr.clone(), public_key));
addrs
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
@@ -399,10 +401,10 @@ impl MimeFactory {
if is_encrypted {
warn!(context, "Missing key for {addr}");
}
vec![addr.clone()]
None
} else {
vec![addr.clone()]
};
None
}.unwrap_or_else(|| vec![addr.clone()]);
// This is a "member removed" message,
// we need to notify removed member
@@ -616,9 +618,7 @@ impl MimeFactory {
fn should_skip_autocrypt(&self) -> bool {
match &self.loaded {
Loaded::Message { msg, .. } => {
msg.param.get_bool(Param::SkipAutocrypt).unwrap_or_default()
}
Loaded::Message { .. } => false,
Loaded::Mdn { .. } => true,
}
}
@@ -2201,14 +2201,6 @@ 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,7 +64,8 @@ pub enum Param {
ForcePlaintext = b'u',
/// For Messages: do not include Autocrypt header.
SkipAutocrypt = b'o',
/// Deprecated on 2026-06-20
DeprecatedSkipAutocrypt = b'o',
/// For Messages
WantsMdn = b'r',

View File

@@ -34,7 +34,7 @@ const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AE
/// as [described in the Autocrypt standard](https://autocrypt.org/level1.html#openpgp-based-key-data).
pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
let signing_key_type = PgpKeyType::Ed25519Legacy;
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519Legacy);
let user_id = format!("<{addr}>");
let key_params = SecretKeyParamsBuilder::default()
@@ -371,10 +371,7 @@ pub fn merge_openpgp_certificates(
.into_iter()
.chain(new_direct_signatures)
.filter(|x: &Signature| x.verify_key(&old_primary_key).is_ok())
.max_by_key(|x: &Signature|
// Converting to seconds because `Ord` is not derived for `Timestamp`:
// <https://github.com/rpgp/rpgp/issues/737>
x.created().map_or(0, |ts| ts.as_secs()));
.max_by_key(|x: &Signature| x.created());
let direct_signatures: Vec<Signature> = best_direct_key_signature.into_iter().collect();
// Select at most one User ID.
@@ -396,12 +393,10 @@ pub fn merge_openpgp_certificates(
.verify_certification(&old_primary_key, pgp::types::Tag::UserId, &id)
.is_ok()
})
.max_by_key(|signature: &Signature| {
signature.created().map_or(0, |ts| ts.as_secs())
});
.max_by_key(|signature: &Signature| signature.created());
best_user_signature.map(|signature| (id, signature))
})
.max_by_key(|(_id, signature)| signature.created().map_or(0, |ts| ts.as_secs()))
.max_by_key(|(_id, signature)| signature.created())
.map(|(id, signature)| SignedUser {
id,
signatures: vec![signature],
@@ -707,7 +702,7 @@ mod tests {
// This error message is actually not great,
// but grepping for it will lead to the correct code
test_dont_decrypt_expensive_message_ex(s2k, true, Some("decrypt_with_keys: missing key"))
test_dont_decrypt_expensive_message_ex(s2k, true, Some("decrypt_the_ring: missing key"))
.await
}
@@ -790,7 +785,7 @@ mod tests {
.await
.unwrap_err();
assert_eq!(format!("{error:#}"), "decrypt_with_keys: missing key");
assert_eq!(format!("{error:#}"), "decrypt_the_ring: missing key");
Ok(())
}

View File

@@ -56,6 +56,9 @@ pub enum Qr {
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number.
invitenumber: String,
@@ -80,6 +83,9 @@ pub enum Qr {
/// Fingerprint of the contact key as scanned from the QR code.
fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number.
invitenumber: String,
@@ -108,6 +114,9 @@ pub enum Qr {
/// Fingerprint of the contact's key as scanned from the QR code.
fingerprint: Fingerprint,
/// The inviter's addresses.
addrs: Vec<String>,
/// Invite number.
invitenumber: String,
/// Authentication code.
@@ -563,6 +572,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
invitenumber,
authcode,
is_v3,
@@ -599,6 +609,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
invitenumber,
authcode,
is_v3,
@@ -624,6 +635,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
Ok(Qr::AskVerifyContact {
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
invitenumber,
authcode,
is_v3,

View File

@@ -545,8 +545,15 @@ pub(crate) async fn receive_imf_inner(
if mime_parser.incoming {
return Ok(None);
}
// For the case if we missed a successful SMTP response. Be optimistic that the message is
// delivered also.
// It sometimes happens that a slow server (usually a classical email server)
// receives a message via SMTP,
// but then the connection to the server dies before it sends the OK response.
// In order to handle this case, we delete the SMTP send jobs if we receive our own message via IMAP.
//
// Now, messages with long recipient lists are split into multiple SMTP jobs.
// In this case, we only want to delete the SMTP job that was sent to self
// because this is the only chunk we can be sure was sent out.
let self_addr = context.get_primary_self_addr().await?;
context
.sql

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 ====
=======================================================*/
ensure_secret_key_exists(context).await.ok();
key::ensure_secret_key_exists(context).await.ok();
let chat = match chat {
Some(id) => {
@@ -741,7 +741,7 @@ pub(crate) async fn handle_securejoin_handshake(
async fn insert_into_smtp(
context: &Context,
rfc724_mid: &str,
recipient: &str,
recipients: &str,
rendered_message: String,
msg_id: MsgId,
) -> Result<(), Error> {
@@ -750,7 +750,7 @@ async fn insert_into_smtp(
.execute(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
(&rfc724_mid, &recipient, &rendered_message, msg_id),
(&rfc724_mid, &recipients, &rendered_message, msg_id),
)
.await?;
Ok(())

View File

@@ -1,19 +1,21 @@
//! Bob's side of SecureJoin handling, the joiner-side.
use anyhow::{Context as _, Result};
use pgp::composed::SignedPublicKey;
use super::HandshakeMessage;
use super::qrinvite::QrInvite;
use crate::chat::{self, ChatId, is_contact_in_chat};
use crate::constants::{Blocked, Chattype};
use crate::contact::{Contact, Origin};
use crate::contact::Origin;
use crate::context::Context;
use crate::events::EventType;
use crate::key::self_fingerprint;
use crate::key::{DcKey as _, self_fingerprint};
use crate::log::LogExt;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::{Param, Params};
use crate::pgp::addresses_from_public_key;
use crate::securejoin::{
ContactId, encrypted_and_signed, insert_into_smtp, verify_sender_by_fingerprint,
};
@@ -58,14 +60,28 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Broadcast { .. } => {}
}
let has_key = context
let public_key_bytes: Option<Vec<u8>> = context
.sql
.exists(
"SELECT COUNT(*) FROM public_keys WHERE fingerprint=?",
.query_get_value(
"SELECT public_key FROM public_keys WHERE fingerprint=?",
(invite.fingerprint().hex(),),
)
.await?;
let key_contains_all_invite_addrs = if let Some(public_key_bytes) = public_key_bytes {
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
if let Some(addrs_in_key) = addresses_from_public_key(&public_key) {
invite.addrs().iter().all(|a| addrs_in_key.contains(a))
} else {
// This can happen if the inviter is using an old version of Delta Chat
// that doesn't put the relay list into the key.
// In this case, we never take the securejoin protocol shortcut, which is fine.
false
}
} else {
false
};
// Now start the protocol and initialise the state.
{
// `joining_chat_id` is `Some` if group chat
@@ -97,7 +113,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
progress: JoinerProgress::Succeeded.into_u16(),
});
return Ok(joining_chat_id);
} else if has_key
} else if key_contains_all_invite_addrs
&& verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
.await?
{
@@ -154,7 +170,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it is
// used to send the handshake messages.
if !has_key {
if !key_contains_all_invite_addrs {
chat::add_info_msg_with_cmd(
context,
private_chat_id,
@@ -310,8 +326,7 @@ pub(crate) async fn send_handshake_message(
if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
// Send a minimal symmetrically-encrypted vc-request-pubkey message
let rfc724_mid = create_outgoing_rfc724_mid();
let contact = Contact::get_by_id(context, invite.contact_id()).await?;
let recipient = contact.get_addr();
let recipients = invite.addrs().join(" ");
let alice_fp = invite.fingerprint().hex();
let auth = invite.authcode();
let shared_secret = format!("securejoin/{alice_fp}/{auth}");
@@ -327,7 +342,7 @@ pub(crate) async fn send_handshake_message(
.await?;
let msg_id = message::insert_tombstone(context, &rfc724_mid).await?;
insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message, msg_id).await?;
context.scheduler.interrupt_smtp().await;
} else {
let mut msg = Message {

View File

@@ -18,6 +18,8 @@ pub enum QrInvite {
Contact {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
invitenumber: String,
authcode: String,
#[serde(default)]
@@ -26,6 +28,8 @@ pub enum QrInvite {
Group {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String,
grpid: String,
invitenumber: String,
@@ -36,6 +40,8 @@ pub enum QrInvite {
Broadcast {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String,
grpid: String,
invitenumber: String,
@@ -92,6 +98,14 @@ impl QrInvite {
QrInvite::Broadcast { is_v3, .. } => is_v3,
}
}
pub(crate) fn addrs(&self) -> &Vec<String> {
match self {
QrInvite::Contact { addrs, .. } => addrs,
QrInvite::Group { addrs, .. } => addrs,
QrInvite::Broadcast { addrs, .. } => addrs,
}
}
}
impl TryFrom<Qr> for QrInvite {
@@ -102,12 +116,14 @@ impl TryFrom<Qr> for QrInvite {
Qr::AskVerifyContact {
contact_id,
fingerprint,
addrs,
invitenumber,
authcode,
is_v3,
} => Ok(QrInvite::Contact {
contact_id,
fingerprint,
addrs,
invitenumber,
authcode,
is_v3,
@@ -117,12 +133,14 @@ impl TryFrom<Qr> for QrInvite {
grpid,
contact_id,
fingerprint,
addrs,
invitenumber,
authcode,
is_v3,
} => Ok(QrInvite::Group {
contact_id,
fingerprint,
addrs,
name: grpname,
grpid,
invitenumber,
@@ -134,6 +152,7 @@ impl TryFrom<Qr> for QrInvite {
grpid,
contact_id,
fingerprint,
addrs,
authcode,
invitenumber,
is_v3,
@@ -142,6 +161,7 @@ impl TryFrom<Qr> for QrInvite {
grpid,
contact_id,
fingerprint,
addrs,
authcode,
invitenumber,
is_v3,

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,6 +2434,28 @@ 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,9 +232,7 @@ impl TestContextManager {
test_context.set_primary_self_addr(new_addr).await.unwrap();
// ensure_secret_key_exists() is called during configure
crate::e2ee::ensure_secret_key_exists(test_context)
.await
.unwrap();
key::ensure_secret_key_exists(test_context).await.unwrap();
assert_eq!(
test_context.get_primary_self_addr().await.unwrap(),

View File

@@ -6,7 +6,9 @@ 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;
@@ -18,7 +20,6 @@ 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() {
@@ -126,7 +127,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
let fiona_new = tcm.unconfigured().await;
fiona_new.configure_addr("fiona@example.net").await;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
key::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
@@ -428,7 +429,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;
e2ee::ensure_secret_key_exists(&bob_new).await?;
key::ensure_secret_key_exists(&bob_new).await?;
tcm.execute_securejoin(&bob_new, &alice).await;
assert_verified(&alice, &bob_new).await;