Compare commits

..

3 Commits

Author SHA1 Message Date
d2weber
1d69a547df fmt 2026-06-19 19:12:40 +02:00
d2weber
65d72f052a fixup: renames 2026-06-19 18:49:36 +02:00
d2weber
0c064a52fc feat: generate jsonrpc headers at build time 2026-06-19 18:39:33 +02:00
60 changed files with 274 additions and 457 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

@@ -33,7 +33,7 @@ jobs:
run: npm install -g npm@latest
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm install --ignore-scripts
- name: Package
@@ -43,5 +43,5 @@ jobs:
npm pack .
- name: Publish
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public

View File

@@ -30,16 +30,16 @@ jobs:
save-if: ${{ github.ref == 'refs/heads/main' }}
cache-bin: false
- name: npm install
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm install
- name: Build TypeScript, run Rust tests, generate bindings
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm run build
- name: Run integration tests
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
- name: Run linter
working-directory: deltachat-jsonrpc/typescript
working-directory: deltachat-jsonrpc-bindings/typescript
run: npm run prettier:check

View File

@@ -81,7 +81,7 @@ jobs:
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
working-directory: ./deltachat-jsonrpc-bindings/typescript
steps:
- uses: actions/checkout@v6
@@ -104,7 +104,7 @@ jobs:
mkdir -p "$HOME/.ssh"
echo "${{ secrets.JS_JSONRPC_DOCS_SSH_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
rsync -avzh --delete -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc-bindings/typescript/docs/ "${{ secrets.JS_JSONRPC_DOCS_SSH_USER }}@js.jsonrpc.delta.chat:/var/www/html/js.jsonrpc.delta.chat/"
build-cffi:
runs-on: ubuntu-latest

90
Cargo.lock generated
View File

@@ -124,9 +124,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
version = "1.0.103"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "argon2"
@@ -194,15 +194,15 @@ dependencies = [
[[package]]
name = "astral-tokio-tar"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd"
checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277"
dependencies = [
"filetime",
"futures-core",
"libc",
"portable-atomic",
"rustc-hash",
"rustix 0.38.44",
"tokio",
"tokio-stream",
]
@@ -1478,6 +1478,13 @@ dependencies = [
"yerpc",
]
[[package]]
name = "deltachat-jsonrpc-bindings"
version = "2.54.0-dev"
dependencies = [
"deltachat-jsonrpc",
]
[[package]]
name = "deltachat-repl"
version = "2.54.0-dev"
@@ -1710,7 +1717,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.1",
"windows-sys 0.59.0",
]
[[package]]
@@ -2079,6 +2086,18 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys 0.59.0",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -2721,7 +2740,7 @@ dependencies = [
"hyper",
"libc",
"pin-project-lite",
"socket2 0.6.3",
"socket2 0.5.9",
"tokio",
"tower-service",
"tracing",
@@ -3142,7 +3161,7 @@ dependencies = [
"iroh-metrics-derive",
"itoa",
"serde",
"snafu 0.8.5",
"snafu",
"tracing",
]
@@ -3365,6 +3384,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.11.0",
"libc",
"redox_syscall 0.5.12",
]
[[package]]
@@ -3511,9 +3531,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.8.2"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
@@ -3779,7 +3799,7 @@ dependencies = [
"netlink-proto",
"netlink-sys",
"serde",
"snafu 0.8.5",
"snafu",
"socket2 0.5.9",
"time",
"tokio",
@@ -3867,7 +3887,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.1",
"windows-sys 0.59.0",
]
[[package]]
@@ -4271,9 +4291,9 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.20.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1"
checksum = "eaffe1ec22db286599c30ae6be75b37493b558735d86c8e59ec5c38794415fe4"
dependencies = [
"aead",
"aes",
@@ -4312,7 +4332,6 @@ dependencies = [
"k256",
"log",
"md-5",
"memchr",
"ml-dsa",
"ml-kem",
"nom 8.0.0",
@@ -4324,6 +4343,7 @@ dependencies = [
"p384",
"p521",
"rand 0.8.6",
"regex",
"replace_with",
"ripemd",
"rsa",
@@ -4334,8 +4354,7 @@ dependencies = [
"signature",
"slh-dsa",
"smallvec",
"snafu 0.9.1",
"subtle",
"snafu",
"twofish",
"x25519-dalek",
"zeroize",
@@ -4574,7 +4593,7 @@ dependencies = [
"rand 0.8.6",
"serde",
"smallvec",
"snafu 0.8.5",
"snafu",
"socket2 0.5.9",
"time",
"tokio",
@@ -5269,7 +5288,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
"windows-sys 0.61.1",
"windows-sys 0.52.0",
]
[[package]]
@@ -5837,16 +5856,7 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019"
dependencies = [
"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",
"snafu-derive",
]
[[package]]
@@ -5861,18 +5871,6 @@ 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"
@@ -6156,7 +6154,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.1.4",
"windows-sys 0.61.1",
"windows-sys 0.52.0",
]
[[package]]
@@ -7516,8 +7514,7 @@ dependencies = [
[[package]]
name = "yerpc"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
source = "git+https://github.com/chatmail/yerpc.git?branch=gen_fns#f92899f570ba07c0e6ff548d8162e2118f938d43"
dependencies = [
"anyhow",
"async-channel 1.9.0",
@@ -7535,9 +7532,8 @@ dependencies = [
[[package]]
name = "yerpc_derive"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d8560d021437420316370db865e44c000bf86380b47cf05e49be9d652042bf5"
version = "0.6.4"
source = "git+https://github.com/chatmail/yerpc.git?branch=gen_fns#f92899f570ba07c0e6ff548d8162e2118f938d43"
dependencies = [
"convert_case",
"darling",

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.20.0", features = ["draft-pqc"], default-features = false }
pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.39", features = ["escape-html"] }
@@ -103,7 +103,7 @@ thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6.3", default-features = false }
astral-tokio-tar = { version = "0.6.2", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
@@ -130,6 +130,7 @@ members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc",
"deltachat-jsonrpc-bindings",
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
@@ -184,6 +185,7 @@ base64 = "0.22"
chrono = { version = "0.4.44", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat-jsonrpc-bindings = { path = "deltachat-jsonrpc-bindings", default-features = false }
deltachat = { path = ".", default-features = false }
futures = "0.3.32"
futures-lite = "2.6.1"
@@ -203,7 +205,7 @@ thiserror = "2"
tokio = "1"
tokio-util = "0.7.18"
tracing-subscriber = "0.3"
yerpc = "0.6.4"
yerpc = { git="https://github.com/chatmail/yerpc.git", branch="gen_fns" }
[features]
default = ["vendored"]

View File

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

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

@@ -0,0 +1,16 @@
[package]
name = "deltachat-jsonrpc-bindings"
version = "2.54.0-dev"
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
edition = "2024"
license = "MPL-2.0"
repository = "https://github.com/chatmail/core"
[build-dependencies]
deltachat-jsonrpc = { workspace = true }
[dependencies]
[features]
default = ["vendored"]
vendored = ["deltachat-jsonrpc/vendored"]

View File

@@ -0,0 +1,6 @@
use deltachat_jsonrpc::api::write_ts_bindings;
use std::path::Path;
fn main() {
write_ts_bindings(Path::new("typescript/generated"));
}

View File

@@ -0,0 +1 @@

View File

@@ -157,7 +157,7 @@ impl CommandApi {
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
#[rpc(all_positional)]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {

View File

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

View File

@@ -704,25 +704,3 @@ 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)
@@ -380,7 +356,7 @@ def test_receive_imf_failure(acfactory) -> None:
snapshot.text == "❌ Failed to receive a message:"
" Condition failed: `!context.get_config_bool(Config::SimulateReceiveImfError).await?`."
f" Core version {version}."
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/"
" Please report this bug to delta@merlinux.eu or https://support.delta.chat/."
)
# The failed message doesn't break the IMAP loop.

View File

@@ -56,7 +56,7 @@ for (const { folder_name, package_name } of platform_package_names) {
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
`file:${join(expected_cwd, "/../../deltachat-jsonrpc-bindings/typescript")}`;
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
}

View File

@@ -83,8 +83,6 @@ 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

@@ -51,6 +51,7 @@
./deltachat-contact-tools
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-jsonrpc-bindings
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client

View File

@@ -67,13 +67,14 @@ def main():
parser.add_argument("newversion")
json_list = [
"deltachat-jsonrpc/typescript/package.json",
"deltachat-jsonrpc-bindings/typescript/package.json",
"deltachat-rpc-server/npm-package/package.json",
]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
"deltachat-jsonrpc/Cargo.toml",
"deltachat-jsonrpc-bindings/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
"deltachat-repl/Cargo.toml",
"python/pyproject.toml",

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,
@@ -461,6 +455,12 @@ pub enum Config {
/// Return an error from `receive_imf_inner()`. For tests.
SimulateReceiveImfError,
/// Enable composing emails with Header Protection as defined in
/// <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for Cryptographically
/// Protected Email".
#[strum(props(default = "1"))]
StdHeaderProtectionComposing,
/// Who can call me.
///
/// The options are from the `WhoCanCallMe` enum.

View File

@@ -721,12 +721,12 @@ async fn get_autoconfig(
}
progress!(ctx, 300);
// `?emailaddress=` query string is excluded on purpose.
// It is not part of the URL according to <https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/06/>.
// Related discussion confirming this is at <https://github.com/benbucksch/autoconfig-spec/issues/17>.
if let Ok(res) = moz_autoconfigure(
ctx,
&format!("https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml"),
// the doc does not mention `emailaddress=`, however, Thunderbird adds it, see <https://releases.mozilla.org/pub/thunderbird/>, which makes some sense
&format!(
"https://{param_domain}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
),
&param.addr,
accept_invalid_certificates,
)

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

@@ -6,16 +6,13 @@ 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;
@@ -51,15 +48,6 @@ 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
@@ -73,15 +61,9 @@ pub(crate) async fn decrypt(
expected_sender_fingerprint = fingerprint;
tokio::task::spawn_blocking(move || -> Result<Message<'_>> {
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 = msg
.decrypt_with_session_key(psk)
.context("decrypt_with_session_key")?;
let plain: Message<'static> = plain.decompress()?;
Ok(plain)
@@ -93,15 +75,11 @@ 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 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 = msg
.decrypt_with_keys(vec![&empty_pw], secret_keys)
.context("decrypt_with_keys")?;
let plain: Message<'static> = plain.decompress()?;
Ok(plain)

View File

@@ -83,20 +83,29 @@ impl EncryptHelper {
None
};
let shared_secret = shared_secret.to_string();
let mut raw_message = Vec::new();
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let ctext = tokio::task::spawn_blocking(move || {
pgp::symm_encrypt_message(raw_message, sign_key, shared_secret, compress)
})
.await??;
let ctext =
pgp::symm_encrypt_message(raw_message, sign_key, shared_secret, compress).await?;
Ok(ctext)
}
}
/// 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 +115,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
@@ -1284,9 +1284,7 @@ impl Session {
warn!(context, "receive_imf error: {err:#}.");
let text = format!(
// No trailing '.' to avoid from the Android UI treating it as a part of
// URL.
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/",
"❌ Failed to receive a message: {err:#}. Core version v{DC_VERSION_STR}. Please report this bug to delta@merlinux.eu or https://support.delta.chat/.",
);
let mut msg = Message::new_text(text);
add_device_msg(context, None, Some(&mut msg)).await?;

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

@@ -618,7 +618,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,
}
}
@@ -1028,12 +1030,16 @@ impl MimeFactory {
is_securejoin_message,
);
let use_std_header_protection = context
.get_config_bool(Config::StdHeaderProtectionComposing)
.await?;
let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys {
let mut message = add_headers_to_encrypted_part(
message,
&unprotected_headers,
hidden_headers,
protected_headers,
use_std_header_protection,
);
// Add gossip headers in chats with multiple recipients
@@ -1935,6 +1941,7 @@ fn add_headers_to_encrypted_part(
unprotected_headers: &[(&'static str, HeaderType<'static>)],
hidden_headers: Vec<(&'static str, HeaderType<'static>)>,
protected_headers: Vec<(&'static str, HeaderType<'static>)>,
use_std_header_protection: bool,
) -> MimePart<'static> {
// Store protected headers in the inner message.
let message = protected_headers
@@ -1950,19 +1957,21 @@ fn add_headers_to_encrypted_part(
message.header(header, value)
});
message = unprotected_headers
.iter()
// Structural headers shouldn't be added as "HP-Outer". They are defined in
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
.filter(|(name, _)| {
!(name.eq_ignore_ascii_case("mime-version")
|| name.eq_ignore_ascii_case("content-type")
|| name.eq_ignore_ascii_case("content-transfer-encoding")
|| name.eq_ignore_ascii_case("content-disposition"))
})
.fold(message, |message, (name, value)| {
message.header(format!("HP-Outer: {name}"), value.clone())
});
if use_std_header_protection {
message = unprotected_headers
.iter()
// Structural headers shouldn't be added as "HP-Outer". They are defined in
// <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
.filter(|(name, _)| {
!(name.eq_ignore_ascii_case("mime-version")
|| name.eq_ignore_ascii_case("content-type")
|| name.eq_ignore_ascii_case("content-transfer-encoding")
|| name.eq_ignore_ascii_case("content-disposition"))
})
.fold(message, |message, (name, value)| {
message.header(format!("HP-Outer: {name}"), value.clone())
});
}
// Set the appropriate Content-Type for the inner message
for (h, v) in &mut message.headers {
@@ -1971,7 +1980,9 @@ fn add_headers_to_encrypted_part(
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
ct_new = ct_new.attribute("hp", "cipher");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
}
@@ -2313,11 +2324,13 @@ pub(crate) async fn render_symm_encrypted_securejoin_message(
);
let outer_message = {
let use_std_header_protection = true;
let message = add_headers_to_encrypted_part(
message,
&unprotected_headers,
hidden_headers,
protected_headers,
use_std_header_protection,
);
// Disable compression for SecureJoin to ensure

View File

@@ -730,14 +730,21 @@ async fn test_hp_outer_headers() -> Result<()> {
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
let msg = MimeMessage::from_bytes(t, sent_msg.payload.as_bytes()).await?;
assert!(msg.header_exists(HeaderDef::HpOuter));
for hdr in ["Date", "From", "Message-ID"] {
assert!(msg.decoded_data_contains(&format!("HP-Outer: {hdr}:")),);
for std_hp_composing in [false, true] {
t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
.await?;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
let msg = MimeMessage::from_bytes(t, sent_msg.payload.as_bytes()).await?;
assert_eq!(msg.header_exists(HeaderDef::HpOuter), std_hp_composing);
for hdr in ["Date", "From", "Message-ID"] {
assert_eq!(
msg.decoded_data_contains(&format!("HP-Outer: {hdr}:")),
std_hp_composing,
);
}
assert!(!msg.decoded_data_contains("HP-Outer: Content-Type"));
}
assert!(!msg.decoded_data_contains("HP-Outer: Content-Type"));
Ok(())
}

View File

@@ -1404,17 +1404,23 @@ async fn test_extra_imf_headers() -> Result<()> {
let t = &tcm.alice().await;
let chat_id = t.get_self_chat().await.id;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check that extra headers are ignored with RFC 9788 Header Protection.
let payload = sent_msg.payload.replace(
"Message-ID:",
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
);
let msg = MimeMessage::from_bytes(t, payload.as_bytes()).await?;
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
assert!(!msg.headers.contains_key("forty-two"));
for std_hp_composing in [false, true] {
t.set_config_bool(Config::StdHeaderProtectionComposing, std_hp_composing)
.await?;
chat::send_text_msg(t, chat_id, "hi!".to_string()).await?;
let sent_msg = t.pop_sent_msg().await;
// Check removal of some nonexistent "Chat-*" header to protect the code from future
// breakages. But headers not prefixed with "Chat-" remain unless a message has standard
// Header Protection.
let payload = sent_msg.payload.replace(
"Message-ID:",
"Chat-Forty-Two: 42\r\nForty-Two: 42\r\nMessage-ID:",
);
let msg = MimeMessage::from_bytes(t, payload.as_bytes()).await?;
assert!(msg.headers.contains_key("chat-version"));
assert!(!msg.headers.contains_key("chat-forty-two"));
assert_ne!(msg.headers.contains_key("forty-two"), std_hp_composing);
}
Ok(())
}

View File

@@ -48,9 +48,10 @@ async fn test_shared_secret_decryption_ex(
let encrypted_msg = pgp::symm_encrypt_message(
plain_text.as_bytes().to_vec(),
signer_key,
secret_for_encryption.to_string(),
secret_for_encryption,
true,
)?;
)
.await?;
let boundary = "boundary123";
let rcvd_mail = format!(

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

@@ -589,7 +589,6 @@ mod tests {
EventType,
chat::{self, ChatId, add_contact_to_chat, resend_msgs, send_msg},
message::{Message, Viewtype},
receive_imf::receive_imf,
test_utils::{TestContext, TestContextManager},
};
@@ -760,67 +759,6 @@ mod tests {
assert!(alice.iroh.read().await.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_duplicated_out_of_order_advertisement() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
let bob = &mut tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let mut instance = Message::new(Viewtype::File);
instance.set_file_from_bytes(
alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
None,
)?;
send_msg(alice, alice_chat.id, &mut instance).await?;
let alice_webxdc = alice.get_last_msg().await;
assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
let webxdc = alice.pop_sent_msg().await;
// Imagine that at this point Alice learns about Bob's new transport...
send_webxdc_realtime_advertisement(alice, alice_webxdc.id).await?;
let advertisement = alice.pop_sent_msg().await;
// Bob receives an out-of-order advertisement from his new transport.
receive_imf(bob, advertisement.payload().as_bytes(), false).await?;
let bob_webxdc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
bob_webxdc.chat_id.accept(bob).await?;
bob.recv_msg_trash(&advertisement).await;
loop {
let event = bob.evtracker.recv().await.unwrap();
if let EventType::WebxdcRealtimeAdvertisementReceived { msg_id } = event.typ {
assert!(msg_id == bob_webxdc.id);
break;
}
}
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
.await?
.into_iter()
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
assert_eq!(
members,
vec![
alice
.get_or_try_init_peer_channel()
.await
.unwrap()
.get_node_addr()
.await
.unwrap()
.node_id
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_can_reconnect() {
let mut tcm = TestContextManager::new();

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::Curve25519Legacy);
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
let user_id = format!("<{addr}>");
let key_params = SecretKeyParamsBuilder::default()
@@ -254,41 +254,44 @@ pub fn pk_validate(
/// Symmetrically encrypt the message.
/// This is used for broadcast channels and for version 2 of the Securejoin protocol.
/// `shared secret` is the secret that will be used for symmetric encryption.
pub fn symm_encrypt_message(
pub async fn symm_encrypt_message(
plain: Vec<u8>,
private_key_for_signing: Option<SignedSecretKey>,
shared_secret: String,
shared_secret: &str,
compress: bool,
) -> Result<String> {
let shared_secret = Password::from(shared_secret);
let shared_secret = Password::from(shared_secret.to_string());
let msg = MessageBuilder::from_bytes("", plain);
let mut rng = thread_rng();
let mut salt = [0u8; 8];
rng.fill(&mut salt[..]);
let s2k = StringToKey::Salted {
hash_alg: HashAlgorithm::default(),
salt,
};
let mut msg = msg.seipd_v2(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
tokio::task::spawn_blocking(move || {
let msg = MessageBuilder::from_bytes("", plain);
let mut rng = thread_rng();
let mut salt = [0u8; 8];
rng.fill(&mut salt[..]);
let s2k = StringToKey::Salted {
hash_alg: HashAlgorithm::default(),
salt,
};
let mut msg = msg.seipd_v2(
&mut rng,
SYMMETRIC_KEY_ALGORITHM,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
if let Some(private_key_for_signing) = private_key_for_signing.as_deref() {
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(private_key_for_signing, Password::empty(), hash_algorithm);
}
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
if let Some(private_key_for_signing) = private_key_for_signing.as_deref() {
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(private_key_for_signing, Password::empty(), hash_algorithm);
}
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
Ok(encoded_msg)
Ok(encoded_msg)
})
.await?
}
/// Merges and minimizes OpenPGP certificates.
@@ -371,7 +374,10 @@ 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| x.created());
.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()));
let direct_signatures: Vec<Signature> = best_direct_key_signature.into_iter().collect();
// Select at most one User ID.
@@ -393,10 +399,12 @@ pub fn merge_openpgp_certificates(
.verify_certification(&old_primary_key, pgp::types::Tag::UserId, &id)
.is_ok()
})
.max_by_key(|signature: &Signature| signature.created());
.max_by_key(|signature: &Signature| {
signature.created().map_or(0, |ts| ts.as_secs())
});
best_user_signature.map(|signature| (id, signature))
})
.max_by_key(|(_id, signature)| signature.created())
.max_by_key(|(_id, signature)| signature.created().map_or(0, |ts| ts.as_secs()))
.map(|(id, signature)| SignedUser {
id,
signatures: vec![signature],
@@ -702,7 +710,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_the_ring: missing key"))
test_dont_decrypt_expensive_message_ex(s2k, true, Some("decrypt_with_keys: missing key"))
.await
}
@@ -785,7 +793,7 @@ mod tests {
.await
.unwrap_err();
assert_eq!(format!("{error:#}"), "decrypt_the_ring: missing key");
assert_eq!(format!("{error:#}"), "decrypt_with_keys: missing key");
Ok(())
}

View File

@@ -56,9 +56,6 @@ 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,
@@ -83,9 +80,6 @@ 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,
@@ -114,9 +108,6 @@ 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.
@@ -572,7 +563,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
invitenumber,
authcode,
is_v3,
@@ -609,7 +599,6 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
grpid,
contact_id,
fingerprint,
addrs: vec![addr.to_string()],
invitenumber,
authcode,
is_v3,
@@ -635,7 +624,6 @@ 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

@@ -61,7 +61,7 @@ use crate::{logged_debug_assert, mimeparser};
///
/// One email with multiple attachments can end up as multiple chat messages, but they
/// all have the same chat_id, state and sort_timestamp.
#[derive(Debug, Default)]
#[derive(Debug)]
pub struct ReceivedMsg {
/// Chat the message is assigned to.
pub chat_id: ChatId,
@@ -545,15 +545,8 @@ pub(crate) async fn receive_imf_inner(
if mime_parser.incoming {
return Ok(None);
}
// 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.
// For the case if we missed a successful SMTP response. Be optimistic that the message is
// delivered also.
let self_addr = context.get_primary_self_addr().await?;
context
.sql
@@ -2074,15 +2067,8 @@ async fn add_parts(
None => {
warn!(
context,
"Cannot add iroh peer because WebXDC instance {in_reply_to} does not exist."
"Cannot add iroh peer because WebXDC instance does not exist."
);
return Ok(ReceivedMsg {
chat_id,
state,
hidden: true,
sort_timestamp,
..Default::default()
});
}
},
None => {

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) => {
@@ -741,7 +741,7 @@ pub(crate) async fn handle_securejoin_handshake(
async fn insert_into_smtp(
context: &Context,
rfc724_mid: &str,
recipients: &str,
recipient: &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, &recipients, &rendered_message, msg_id),
(&rfc724_mid, &recipient, &rendered_message, msg_id),
)
.await?;
Ok(())

View File

@@ -1,21 +1,19 @@
//! 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::Origin;
use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey as _, self_fingerprint};
use crate::key::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,
};
@@ -60,28 +58,14 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Broadcast { .. } => {}
}
let public_key_bytes: Option<Vec<u8>> = context
let has_key = context
.sql
.query_get_value(
"SELECT public_key FROM public_keys WHERE fingerprint=?",
.exists(
"SELECT COUNT(*) 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
@@ -113,7 +97,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
progress: JoinerProgress::Succeeded.into_u16(),
});
return Ok(joining_chat_id);
} else if key_contains_all_invite_addrs
} else if has_key
&& verify_sender_by_fingerprint(context, invite.fingerprint(), invite.contact_id())
.await?
{
@@ -170,7 +154,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 !key_contains_all_invite_addrs {
if !has_key {
chat::add_info_msg_with_cmd(
context,
private_chat_id,
@@ -326,7 +310,8 @@ 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 recipients = invite.addrs().join(" ");
let contact = Contact::get_by_id(context, invite.contact_id()).await?;
let recipient = contact.get_addr();
let alice_fp = invite.fingerprint().hex();
let auth = invite.authcode();
let shared_secret = format!("securejoin/{alice_fp}/{auth}");
@@ -342,7 +327,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, &recipients, rendered_message, msg_id).await?;
insert_into_smtp(context, &rfc724_mid, recipient, rendered_message, msg_id).await?;
context.scheduler.interrupt_smtp().await;
} else {
let mut msg = Message {

View File

@@ -18,8 +18,6 @@ pub enum QrInvite {
Contact {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
invitenumber: String,
authcode: String,
#[serde(default)]
@@ -28,8 +26,6 @@ pub enum QrInvite {
Group {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String,
grpid: String,
invitenumber: String,
@@ -40,8 +36,6 @@ pub enum QrInvite {
Broadcast {
contact_id: ContactId,
fingerprint: Fingerprint,
#[serde(default)]
addrs: Vec<String>,
name: String,
grpid: String,
invitenumber: String,
@@ -98,14 +92,6 @@ 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 {
@@ -116,14 +102,12 @@ 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,
@@ -133,14 +117,12 @@ 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,
@@ -152,7 +134,6 @@ impl TryFrom<Qr> for QrInvite {
grpid,
contact_id,
fingerprint,
addrs,
authcode,
invitenumber,
is_v3,
@@ -161,7 +142,6 @@ 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,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;