Compare commits

..

13 Commits

Author SHA1 Message Date
Hocuri
298814d6db feat: Remove detached signature validation 2026-04-30 17:10:34 +02:00
Hocuri
4c01802982 feat: Remove mostly-unused SignUnencrypted option (#8190) 2026-04-30 13:59:30 +02:00
Hocuri
4b528e426b docs: Discourage into(), try_into() and parse() (#8180)
Follow-up to
https://github.com/chatmail/core/pull/8178#issuecomment-4322738959

In a previous version, I added a note that the JsonRPC API is a notable exception, but I removed it.
2026-04-30 13:58:19 +02:00
link2xt
585de7d18b docs: update echobot_no_hooks.py example
Use add_or_update_transport() instead of deprecated configure(),
do not use deprecated get_next_messages(),
make use of AttrDict,
print invite link,
do not use % formatting for logging.
2026-04-29 17:14:45 +00:00
link2xt
0598fdcab3 chore: update astral-tokio-tar from 0.6.0 to 0.6.1
Fixes https://rustsec.org/advisories/RUSTSEC-2026-0112
2026-04-29 17:05:38 +00:00
Nico de Haen
903e736fa2 fix: default value for imap folder (#8193)
resolves #8192
2026-04-29 08:24:13 +02:00
iequidoo
f20907d597 feat: is_self_addr(): Employ the config cache to optimize for ConfiguredAddr passed 2026-04-28 23:41:25 -03:00
B. Petersen
804590c7f3 api: jsonrpc: remove unused set_draft_vcard() 2026-04-28 17:12:16 +02:00
dependabot[bot]
62d4cf4ed8 chore(deps): bump taiki-e/install-action from 2.75.10 to 2.75.19
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.75.10 to 2.75.19.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](85b24a67ef...5f57d6cb7c)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 14:16:35 +00:00
link2xt
0d772d4dba api!(deltachat-rpc-client): remove deprecated get_fresh_messages_in_arrival_order() 2026-04-28 14:16:10 +00:00
dependabot[bot]
408afa5656 chore(deps): bump cachix/install-nix-action from 31.9.1 to 31.10.5
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.9.1 to 31.10.5.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](2126ae7fc5...ab739621df)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.10.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 14:15:29 +00:00
link2xt
1a6249c10f build: increase MSRV to 1.89
This is required by iroh 0.98.1,
so we will need to update MSRV eventually
when iroh 1.0 is released.
2026-04-28 14:14:47 +00:00
Nico de Haen
daea820fe5 chore(json-rpc): deprecate send_sticker (#8189)
send_sticker is not needed anymore since
https://github.com/chatmail/core/pull/8162/changes/4dbbd4d8e
2026-04-28 15:59:51 +02:00
29 changed files with 1506 additions and 1890 deletions

View File

@@ -137,7 +137,7 @@ jobs:
uses: swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4
- name: Install nextest
uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754
uses: taiki-e/install-action@5f57d6cb7cd20b14a8a27f522884c4bc8a187458
with:
tool: nextest

View File

@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
@@ -82,7 +82,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -106,7 +106,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
@@ -157,7 +157,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -181,7 +181,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
@@ -208,7 +208,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v7

View File

@@ -25,7 +25,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix fmt flake.nix -- --check
build:
@@ -84,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +105,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- run: nix build .#${{ matrix.installable }}

View File

@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -41,7 +41,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -63,7 +63,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
- uses: cachix/install-nix-action@ab739621df7a23f52766f9ccc97f38da6b7af14f # v31.10.5
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

2613
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,8 +66,8 @@ humansize = "2"
hyper = "1"
hyper-util = "0.1.16"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.98", default-features = false, features = ["net"] }
iroh = { version = "0.98", default-features = false, features = ["tls-ring"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.4", default-features = false }
@@ -101,9 +101,9 @@ tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false, features = ["ring"] }
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
astral-tokio-tar = { version = "0.6", default-features = false }
astral-tokio-tar = { version = "0.6.1", default-features = false }
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"

View File

@@ -161,3 +161,16 @@ are documented.
Follow Rust guidelines for the documentation comments:
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
## Do not use `into()`, `try_into()` or `parse()`
For internal types, implementing `From`, `TryFrom` or `FromStr` is discouraged.
Instead, a `new()` function is recommended.
For external types, prefer using `Type::from()`, `Type::try_from()` or `Type::from_str()`
over `into()`, `try_into()` or `parse()`.
Calling `into()`, `try_into()` or `parse()`
creates an indirection,
which is hard to follow for people who are not familiar with Rust,
or who are not using rust-analyzer.

View File

@@ -1882,20 +1882,6 @@ impl CommandApi {
deltachat::contact::make_vcard(&ctx, &contacts).await
}
/// Sets vCard containing the given contacts to the message draft.
async fn set_draft_vcard(
&self,
account_id: u32,
msg_id: u32,
contacts: Vec<u32>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
msg.make_vcard(&ctx, &contacts).await?;
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -2421,6 +2407,7 @@ impl CommandApi {
chat::resend_msgs(&ctx, &message_ids).await
}
/// @deprecated as of 2026-04; use `send_msg` with `Viewtype::Sticker` instead.
async fn send_sticker(
&self,
account_id: u32,

View File

@@ -13,7 +13,7 @@ def main():
with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
logging.info(f"Running deltachat core {system_info['deltachat_core_version']}")
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
@@ -21,36 +21,30 @@ def main():
account.set_config("bot", "1")
if not account.is_configured():
logging.info("Account is not configured, configuring")
account.set_config("addr", sys.argv[1])
account.set_config("mail_pw", sys.argv[2])
account.configure()
account.add_or_update_transport({"addr": sys.argv[1], "password": sys.argv[2]})
logging.info("Configured")
else:
logging.info("Account is already configured")
deltachat.start_io()
def process_messages():
for message in account.get_next_messages():
qr = account.get_qr_code()
logging.info(f"Invite link: {qr}")
while True:
event = account.wait_for_event()
if event.kind == EventType.INFO:
logging.info(event["msg"])
elif event.kind == EventType.WARNING:
logging.warning(event["msg"])
elif event.kind == EventType.ERROR:
logging.error(event["msg"])
elif event.kind == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
message = account.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
snapshot.chat.send_text(snapshot.text)
snapshot.message.mark_seen()
# Process old messages.
process_messages()
while True:
event = account.wait_for_event()
if event["kind"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["kind"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["kind"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["kind"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
from ._utils import AttrDict, futuremethod
from .chat import Chat
@@ -392,8 +391,7 @@ class Account:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
If you are writing a bot, process "incoming message" events instead.
"""
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
@@ -463,16 +461,6 @@ class Account:
"""Wait for reaction change event."""
return self.wait_for_event(EventType.REACTIONS_CHANGED)
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
self._rpc.export_backup(self.id, str(path), passphrase)

View File

@@ -164,7 +164,7 @@ class Chat:
return Message(self.account, msg_id)
def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
"""Deprecated as of 2026-04; use `send_message` with `Viewtype.STICKER` instead."""
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)

View File

@@ -18,7 +18,7 @@ futures-lite = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["io-std", "signal"] }
tokio = { workspace = true, features = ["io-std"] }
tokio-util = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }

View File

@@ -7,11 +7,33 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Archived repository
"RUSTSEC-2023-0089",
# Unmaintained instant
"RUSTSEC-2024-0384",
# Unmaintained paste
"RUSTSEC-2024-0436",
# Unmaintained rustls-pemfile
# It is a transitive dependency of iroh 0.35.0,
# this should be fixed by upgrading to iroh 1.0 once it is released.
"RUSTSEC-2025-0134",
# rustls-webpki v0.102.8
# We cannot upgrade to >=0.103.10 because
# it is a transitive dependency of iroh 0.35.0
# which depends on ^0.102.
# <https://rustsec.org/advisories/RUSTSEC-2026-0049>
# <https://rustsec.org/advisories/RUSTSEC-2026-0098>
# <https://rustsec.org/advisories/RUSTSEC-2026-0099>
"RUSTSEC-2026-0049",
"RUSTSEC-2026-0098",
"RUSTSEC-2026-0099",
# Panic in CRL signature checks.
# We do not check CRL and cannot update rustls-webpki 0.102.8
# which is a dependency of iroh 0.35.0.
# <https://rustsec.org/advisories/RUSTSEC-2026-0104>
"RUSTSEC-2026-0104"
]
[bans]
@@ -21,51 +43,33 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "block-buffer", version = "0.10.4" },
{ name = "chacha20", version = "0.9.1" },
{ name = "const-oid", version = "0.9.6" },
{ name = "convert_case", version = "0.5.0" },
{ name = "core-foundation", version = "0.9.4" },
{ name = "cpufeatures", version = "0.2.17" },
{ name = "crypto-common", version = "0.1.6" },
{ name = "curve25519-dalek", version = "4.1.3" },
{ name = "der", version = "0.7.9" },
{ name = "digest", version = "0.10.7" },
{ name = "ed25519-dalek", version = "2.1.1" },
{ name = "ed25519", version = "2.2.3" },
{ name = "bitflags", version = "1.3.2" },
{ name = "constant_time_eq", version = "0.3.1" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "fiat-crypto", version = "0.2.9" },
{ name = "foldhash", version = "0.1.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "getrandom", version = "0.3.3" },
{ name = "hashbrown", version = "0.15.4" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "netlink-packet-route", version = "0.29.0" },
{ name = "lru", version = "0.12.5" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },
{ name = "openssl-probe", version = "0.1.6" },
{ name = "pem-rfc7468", version = "0.7.0" },
{ name = "pkcs8", version = "0.10.2" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand_core", version = "0.9.3" },
{ name = "rand", version = "0.8.5" },
{ name = "rand", version = "0.9.4" },
{ name = "r-efi", version = "5.2.0" },
{ name = "rustix", version = "0.38.44" },
{ name = "security-framework", version = "2.11.1" },
{ name = "rustls-webpki", version = "0.102.8" },
{ name = "serdect", version = "0.2.0" },
{ name = "sha2", version = "0.10.9"},
{ name = "signature", version = "2.2.0"},
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "spki", version = "0.7.3"},
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "vergen-lib", version = "0.1.6" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "webpki-roots", version = "0.26.8" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
{ name = "windows_aarch64_msvc" },
@@ -82,7 +86,6 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "wit-bindgen", version = "0.51.0" },
]
@@ -94,7 +97,6 @@ allow = [
"BSD-3-Clause",
"BSL-1.0", # Boost Software License 1.0
"CC0-1.0",
"CDLA-Permissive-2.0",
"ISC",
"MIT",
"MPL-2.0",

View File

@@ -407,9 +407,6 @@ pub enum Config {
#[strum(props(default = "1"))]
SyncMsgs,
/// Make all outgoing messages with Autocrypt header "multipart/signed".
SignUnencrypted,
/// Let the core save all events to the database.
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
@@ -710,7 +707,6 @@ impl Context {
| Config::Bot
| Config::NotifyAboutWrongPw
| Config::SyncMsgs
| Config::SignUnencrypted
| Config::DisableIdle => {
ensure!(
matches!(value, None | Some("0") | Some("1")),
@@ -944,6 +940,18 @@ impl Context {
/// Determine whether the specified addr maps to the/a self addr.
/// Returns `false` if no addresses are configured.
pub(crate) async fn is_self_addr(&self, addr: &str) -> Result<bool> {
// Employ the config cache to optimize for `ConfiguredAddr` passed.
if !addr.is_empty()
&& addr_cmp(
addr,
&self
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default(),
)
{
return Ok(true);
}
Ok(self
.get_all_self_addrs()
.await?

View File

@@ -991,12 +991,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"sign_unencrypted",
self.get_config_int(Config::SignUnencrypted)
.await?
.to_string(),
);
res.insert(
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),

View File

@@ -1,7 +1,6 @@
//! Helper functions for decryption.
//! The actual decryption is done in the [`crate::pgp`] module.
use std::collections::HashSet;
use std::io::Cursor;
use anyhow::{Context as _, Result, bail};
@@ -19,8 +18,8 @@ use crate::chat::ChatId;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::key::load_self_secret_keyring;
use crate::key::self_fingerprint;
use crate::key::{Fingerprint, SignedPublicKey, load_self_secret_keyring};
use crate::token::Namespace;
/// Tries to decrypt the message,
@@ -335,36 +334,6 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
}
}
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
///
/// Returns the signed part and the set of key
/// fingerprints for which there is a valid signature.
///
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
pub(crate) fn validate_detached_signature<'a, 'b>(
mail: &'a ParsedMail<'b>,
public_keyring_for_validate: &[SignedPublicKey],
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
if mail.ctype.mimetype != "multipart/signed" {
return None;
}
if let [first_part, second_part] = &mail.subparts[..] {
// First part is the content, second part is the signature.
let content = first_part.raw_bytes;
let ret_valid_signatures = match second_part.get_body_raw() {
Ok(signature) => {
crate::pgp::pk_validate(content, &signature, public_keyring_for_validate)
.unwrap_or_default()
}
Err(_) => Default::default(),
};
Some((first_part, ret_valid_signatures))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -79,16 +79,6 @@ impl EncryptHelper {
Ok(ctext)
}
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
let sign_key = load_self_secret_key(context).await?;
let mut buffer = Vec::new();
mail.clone().write_part(&mut buffer)?;
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
Ok(signature)
}
}
/// Ensures a private key exists for the configured user.

View File

@@ -69,7 +69,7 @@ pub struct BackupProvider {
_endpoint: Endpoint,
/// iroh address.
node_addr: iroh::EndpointAddr,
node_addr: iroh::NodeAddr,
/// Authentication token that should be submitted
/// to retrieve the backup.
@@ -95,12 +95,13 @@ impl BackupProvider {
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
pub async fn prepare(context: &Context) -> Result<Self> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.alpns(vec![BACKUP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind()
.await?;
let node_addr = endpoint.addr();
let node_addr = endpoint.node_addr().await?;
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
@@ -167,7 +168,7 @@ impl BackupProvider {
async fn handle_connection(
context: Context,
conn: iroh::endpoint::Accepting,
conn: iroh::endpoint::Connecting,
auth_token: String,
dbfile: Arc<TempPathGuard>,
) -> Result<()> {
@@ -298,12 +299,13 @@ impl Future for BackupProvider {
pub async fn get_backup2(
context: &Context,
node_addr: iroh::EndpointAddr,
node_addr: iroh::NodeAddr,
auth_token: String,
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.relay_mode(relay_mode)
.bind()
.await?;
@@ -351,7 +353,7 @@ pub async fn get_backup2(
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
/// does avoid having [`iroh::EndpointAddr`] in the primary API however, without
/// does avoid having [`iroh::NodeAddr`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {

View File

@@ -70,6 +70,7 @@ pub struct EnteredImapLoginParam {
/// Folder to watch.
///
/// If empty, user has not entered anything and it shuold expand to "INBOX" later.
#[serde(default)]
pub folder: String,
/// Socket security.

View File

@@ -1227,53 +1227,18 @@ impl MimeFactory {
message.header(header, value)
});
let message = MimePart::new("multipart/mixed", vec![message]);
let mut message = protected_headers
let message = protected_headers
.iter()
.fold(message, |message, (header, value)| {
message.header(*header, value.clone())
});
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
// Deduplicate unprotected headers that also are in the protected headers:
let protected: HashSet<&str> =
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
// Deduplicate unprotected headers that also are in the protected headers:
let protected: HashSet<&str> =
HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
unprotected_headers.retain(|(header, _value)| !protected.contains(header));
message
} else {
for (h, v) in &mut message.headers {
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
}
}
let signature = encrypt_helper.sign(context, &message).await?;
MimePart::new(
"multipart/signed; protocol=\"application/pgp-signature\"; protected",
vec![
message,
MimePart::new(
"application/pgp-signature; name=\"signature.asc\"",
signature,
)
.header(
"Content-Description",
mail_builder::headers::raw::Raw::<'static>::new(
"OpenPGP digital signature",
),
)
.attachment("signature"),
],
)
}
message
};
let MimeFactory {
@@ -1625,7 +1590,7 @@ impl MimeFactory {
// We should not send `null` as relay URL
// as this is the only way to reach the node.
debug_assert_eq!(node_addr.relay_urls().count(), 1);
debug_assert!(node_addr.relay_url().is_some());
headers.push((
HeaderDef::IrohNodeAddr.into(),
mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
@@ -2192,10 +2157,6 @@ fn group_headers_by_confidentiality(
}
}
} else {
// Copy the header to the protected headers
// in case of signed-only message.
// If the message is not signed, this value will not be used.
protected_headers.push(header.clone());
unprotected_headers.push(header.clone())
}
}

View File

@@ -601,70 +601,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_unencrypted_signed() {
// create chat with bob, set selfavatar
let t = TestContext::new_alice().await;
t.set_config(Config::SignUnencrypted, Some("1"))
.await
.unwrap();
let chat = t.create_chat_with_contact("bob", "bob@example.org").await;
let file = t.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await.unwrap();
t.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await
.unwrap();
// send message to bob: that should get multipart/signed.
// `Subject:` is protected by copying it.
// make sure, `Subject:` stays in the outer header (imf header)
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/signed").count(), 1);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(part.match_indices("From:").count(), 0);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
let bob = TestContext::new_bob().await;
bob.recv_msg(&sent_msg).await;
let alice_id = Contact::lookup_id_by_addr(&bob.ctx, "alice@example.org", Origin::Unknown)
.await
.unwrap()
.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
}
/// Test that removed member address does not go into the `To:` field.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_remove_member_bcc() -> Result<()> {

View File

@@ -20,7 +20,7 @@ use crate::config::Config;
use crate::constants;
use crate::contact::{ContactId, import_public_key};
use crate::context::Context;
use crate::decrypt::{self, validate_detached_signature};
use crate::decrypt::{self};
use crate::dehtml::dehtml;
use crate::download::PostMsgMetadata;
use crate::events::EventType;
@@ -304,37 +304,9 @@ impl MimeMessage {
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
let (part, mimetype) =
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
if let Some(part) = mail.subparts.first() {
// We don't remove "subject" from `headers` because currently just signed
// messages are shown as unencrypted anyway.
timestamp_sent =
Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
MimeMessage::merge_headers(
context,
&mut headers,
&mut headers_removed,
&mut recipients,
&mut past_members,
&mut from,
&mut list_post,
&mut chat_disposition_notification_to,
part,
);
(part, part.ctype.mimetype.parse::<Mime>()?)
} else {
// Not a valid signed message, handle it as plaintext.
(&mail, mimetype)
}
} else {
// Currently we do not sign unencrypted messages by default.
(&mail, mimetype)
};
if mimetype.type_() == mime::MULTIPART
&& mimetype.subtype().as_str() == "mixed"
&& let Some(part) = part.subparts.first()
&& let Some(part) = mail.subparts.first()
{
for field in &part.headers {
let key = field.get_key().to_lowercase();
@@ -358,8 +330,7 @@ impl MimeMessage {
);
}
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
// Remove headers that are allowed _only_ in the encrypted+signed part
let encrypted = false;
Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
@@ -516,17 +487,6 @@ impl MimeMessage {
HashMap::new()
};
let mail = mail.as_ref().map(|mail| {
let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
.unwrap_or((mail, Default::default()));
let signatures_detached = signatures_detached
.into_iter()
.map(|fp| (fp, Vec::new()))
.collect::<HashMap<_, _>>();
signatures.extend(signatures_detached);
content
});
if let Some(expected_sender_fingerprint) = expected_sender_fingerprint {
ensure!(
!signatures.is_empty(),
@@ -542,7 +502,7 @@ impl MimeMessage {
);
}
if let (Ok(mail), true) = (mail, is_encrypted) {
if let (Ok(mail), true) = (&mail, is_encrypted) {
if !signatures.is_empty() {
// Unsigned "Subject" mustn't be prepended to messages shown as encrypted
// (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
@@ -567,7 +527,7 @@ impl MimeMessage {
&mut inner_from,
&mut list_post,
&mut chat_disposition_notification_to,
mail,
&mail,
);
if !signatures.is_empty() {
@@ -611,7 +571,7 @@ impl MimeMessage {
signatures.clear();
}
if let (Ok(mail), true) = (mail, is_encrypted)
if let (Ok(mail), true) = (&mail, is_encrypted)
&& let Some(post_msg_rfc724_mid) =
mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
{
@@ -669,7 +629,7 @@ impl MimeMessage {
from,
incoming,
chat_disposition_notification_to,
decryption_error: mail.err().map(|err| format!("{err:#}")),
decryption_error: mail.as_ref().err().map(|err| format!("{err:#}")),
// only non-empty if it was a valid autocrypt message
signature,
@@ -695,9 +655,9 @@ impl MimeMessage {
pre_message,
};
match mail {
match &mail {
Ok(mail) => {
parser.parse_mime_recursive(context, mail, false).await?;
parser.parse_mime_recursive(context, &mail, false).await?;
}
Err(err) => {
let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
@@ -2217,9 +2177,6 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
/// Returns whether the outer header value must be ignored if the message contains a signed (and
/// optionally encrypted) part. This is independent from the modern Header Protection defined in
/// <https://www.rfc-editor.org/rfc/rfc9788.html>.
///
/// NB: There are known cases when Subject and List-ID only appear in the outer headers of
/// signed-only messages. Such messages are shown as unencrypted anyway.
fn is_protected(key: &str) -> bool {
key.starts_with("chat-")
|| matches!(

View File

@@ -7,6 +7,7 @@ use crate::{
chat,
chatlist::Chatlist,
constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
contact::Contact,
key,
message::{MessageState, MessengerMessage},
receive_imf::receive_imf,
@@ -2041,32 +2042,24 @@ async fn test_multiple_autocrypt_hdrs() -> Result<()> {
Ok(())
}
/// Tests that timestamp of signed but not encrypted message is protected.
/// Tests receiving a simple signed-unencrypted message
/// that was generated by an old version of Core that supported sending such messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_date() -> Result<()> {
async fn test_receive_signed_only() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice.set_config(Config::SignUnencrypted, Some("1")).await?;
let imf_raw = include_bytes!("../../test-data/message/unencrypted_signed_simple.eml");
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
assert_eq!(msg.msg_ids.len(), 1);
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
assert_eq!(msg.get_text(), "Hello!");
assert_eq!(msg.viewtype, Viewtype::Text);
assert_eq!(msg.get_timestamp(), 1615987853);
let alice_chat = alice.create_email_chat(bob).await;
let alice_msg_id = chat::send_text_msg(alice, alice_chat.id, "Hello!".to_string()).await?;
let alice_msg = Message::load_from_db(alice, alice_msg_id).await?;
assert_eq!(alice_msg.get_showpadlock(), false);
let alice_contact = Contact::get_by_id(bob, msg.from_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
let mut sent_msg = alice.pop_sent_msg().await;
sent_msg.payload = sent_msg.payload.replacen(
"Date:",
"Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)\r\nX-Not-Date:",
1,
);
let bob_msg = bob.recv_msg(&sent_msg).await;
assert_eq!(alice_msg.get_text(), bob_msg.get_text());
// Timestamp that the sender has put into the message
// should always be displayed as is on the receiver.
assert_eq!(alice_msg.get_timestamp(), bob_msg.get_timestamp());
Ok(())
}

View File

@@ -19,22 +19,18 @@
//! This message contains the users relay-server and public key.
//! Direct IP address is not included as this information can be persisted by email providers.
//! 4. After the announcement, the sending peer joins the gossip swarm with an empty list of peer IDs (as they don't know anyone yet).
//! 5. Upon receiving an announcement message, other peers store the sender's [EndpointAddr] in the database
//! 5. Upon receiving an announcement message, other peers store the sender's [NodeAddr] in the database
//! (scoped per WebXDC app instance/message-id). The other peers can then join the gossip with `joinRealtimeChannel().setListener()`
//! and `joinRealtimeChannel().send()` just like the other peers.
use anyhow::{Context as _, Result, anyhow, bail};
use data_encoding::BASE32_NOPAD;
use futures_lite::StreamExt;
use iroh::address_lookup::MemoryLookup;
use iroh::{
Endpoint, EndpointAddr, EndpointId, PublicKey, RelayMode, RelayUrl, SecretKey, TransportAddr,
};
use iroh_gossip::api::{Event as GossipEvent, GossipReceiver, GossipSender, JoinOptions};
use iroh_gossip::net::{GOSSIP_ALPN, Gossip};
use iroh::{Endpoint, NodeAddr, NodeId, PublicKey, RelayMode, RelayUrl, SecretKey};
use iroh_gossip::net::{Event, GOSSIP_ALPN, Gossip, GossipEvent, JoinOptions};
use iroh_gossip::proto::TopicId;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::env;
use tokio::sync::{RwLock, oneshot};
use tokio::task::JoinHandle;
@@ -58,9 +54,6 @@ pub struct Iroh {
/// Iroh router needed for Iroh peer channels.
pub(crate) router: iroh::protocol::Router,
/// Address lookup, called "Discovery service" before Iroh 0.96.0.
pub(crate) address_lookup: MemoryLookup,
/// [Gossip] needed for Iroh peer channels.
pub(crate) gossip: Gossip,
@@ -112,7 +105,7 @@ impl Iroh {
}
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
let node_ids = peers.iter().map(|p| p.id).collect::<Vec<_>>();
let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
info!(
ctx,
@@ -122,7 +115,7 @@ impl Iroh {
// Inform iroh of potentially new node addresses
for node_addr in &peers {
if !node_addr.is_empty() {
self.address_lookup.add_endpoint_info(node_addr.clone());
self.router.endpoint().add_node_addr(node_addr.clone())?;
}
}
@@ -131,7 +124,6 @@ impl Iroh {
let (gossip_sender, gossip_receiver) = self
.gossip
.subscribe_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
.await?
.split();
let ctx = ctx.clone();
@@ -147,10 +139,10 @@ impl Iroh {
}
/// Add gossip peer to realtime channel if it is already active.
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: EndpointAddr) -> Result<()> {
pub async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
if self.iroh_channels.read().await.get(&topic).is_some() {
self.address_lookup.add_endpoint_info(peer.clone());
self.gossip.subscribe(topic, vec![peer.id]).await?;
self.router.endpoint().add_node_addr(peer.clone())?;
self.gossip.subscribe(topic, vec![peer.node_id])?;
}
Ok(())
}
@@ -192,20 +184,16 @@ impl Iroh {
*entry
}
/// Get the iroh [EndpointAddr] without direct IP addresses.
/// Get the iroh [NodeAddr] without direct IP addresses.
///
/// The address is guaranteed to have home relay URL set
/// as it is the only way to reach the node
/// without global discovery mechanisms.
pub(crate) async fn get_node_addr(&self) -> Result<EndpointAddr> {
// Wait until home relay connection is established.
self.router.endpoint().online().await;
let mut endpoint_addr = self.router.endpoint().addr();
endpoint_addr
.addrs
.retain(|addr| matches!(addr, TransportAddr::Relay(_)));
debug_assert_eq!(endpoint_addr.addrs.len(), 1);
Ok(endpoint_addr)
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
let mut addr = self.router.endpoint().node_addr().await?;
addr.direct_addresses = BTreeSet::new();
debug_assert!(addr.relay_url().is_some());
Ok(addr)
}
/// Leave the realtime channel for a given topic.
@@ -231,11 +219,11 @@ pub(crate) struct ChannelState {
/// The subscribe loop handle.
subscribe_loop: JoinHandle<()>,
sender: GossipSender,
sender: iroh_gossip::net::GossipSender,
}
impl ChannelState {
fn new(subscribe_loop: JoinHandle<()>, sender: GossipSender) -> Self {
fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
Self {
subscribe_loop,
sender,
@@ -247,7 +235,7 @@ impl Context {
/// Create iroh endpoint and gossip.
async fn init_peer_channels(&self) -> Result<Iroh> {
info!(self, "Initializing peer channels.");
let secret_key = SecretKey::generate();
let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
let public_key = secret_key.public();
let relay_mode = if let Some(relay_url) = self
@@ -264,9 +252,8 @@ impl Context {
RelayMode::Default
};
let address_lookup = MemoryLookup::new();
let endpoint = Endpoint::builder(iroh::endpoint::presets::Minimal)
.address_lookup(address_lookup.clone())
let endpoint = Endpoint::builder()
.tls_x509() // For compatibility with iroh <0.34.0
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
@@ -280,7 +267,8 @@ impl Context {
let gossip = Gossip::builder()
.max_message_size(128 * 1024)
.spawn(endpoint.clone());
.spawn(endpoint.clone())
.await?;
let router = iroh::protocol::Router::builder(endpoint)
.accept(GOSSIP_ALPN, gossip.clone())
@@ -288,7 +276,6 @@ impl Context {
Ok(Iroh {
router,
address_lookup,
gossip,
sequence_numbers: Mutex::new(HashMap::new()),
iroh_channels: RwLock::new(HashMap::new()),
@@ -335,15 +322,11 @@ impl Context {
}
}
pub(crate) async fn maybe_add_gossip_peer(
&self,
topic: TopicId,
peer: EndpointAddr,
) -> Result<()> {
pub(crate) async fn maybe_add_gossip_peer(&self, topic: TopicId, peer: NodeAddr) -> Result<()> {
if let Some(iroh) = &*self.iroh.read().await {
info!(
self,
"Adding (maybe existing) peer with id {} to {topic}.", peer.id
"Adding (maybe existing) peer with id {} to {topic}.", peer.node_id
);
iroh.maybe_add_gossip_peer(topic, peer).await?;
}
@@ -351,12 +334,12 @@ impl Context {
}
}
/// Cache a peers [EndpointId] for one topic.
/// Cache a peers [NodeId] for one topic.
pub(crate) async fn iroh_add_peer_for_topic(
ctx: &Context,
msg_id: MsgId,
topic: TopicId,
peer: EndpointId,
peer: NodeId,
relay_server: Option<&str>,
) -> Result<()> {
ctx.sql
@@ -382,11 +365,11 @@ pub async fn add_gossip_peer_from_header(
}
let node_addr =
serde_json::from_str::<EndpointAddr>(node_addr).context("Failed to parse node address")?;
serde_json::from_str::<NodeAddr>(node_addr).context("Failed to parse node address")?;
info!(
context,
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.id
"Adding iroh peer with node id {} to the topic of {instance_id}.", node_addr.node_id
);
context.emit_event(EventType::WebxdcRealtimeAdvertisementReceived {
@@ -401,8 +384,8 @@ pub async fn add_gossip_peer_from_header(
return Ok(());
};
let node_id = node_addr.id;
let relay_server = node_addr.relay_urls().map(|relay| relay.as_str()).next();
let node_id = node_addr.node_id;
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
context.maybe_add_gossip_peer(topic, node_addr).await?;
@@ -420,8 +403,8 @@ pub(crate) async fn insert_topic_stub(ctx: &Context, msg_id: MsgId, topic: Topic
Ok(())
}
/// Get a list of [EndpointAddr]s for one webxdc.
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<EndpointAddr>> {
/// Get a list of [NodeAddr]s for one webxdc.
async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeAddr>> {
ctx.sql
.query_map(
"SELECT public_key, relay_server FROM iroh_gossip_peers WHERE msg_id = ? AND public_key != ?",
@@ -434,11 +417,11 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<Endpo
|g| {
g.map(|data| {
let (key, server) = data?;
let server: Option<TransportAddr> = server.map(|data| Ok::<_, url::ParseError>(TransportAddr::Relay(RelayUrl::from(Url::parse(&data)?)))).transpose()?;
let id = EndpointId::from_bytes(&key.try_into()
let server = server.map(|data| Ok::<_, url::ParseError>(RelayUrl::from(Url::parse(&data)?))).transpose()?;
let id = NodeId::from_bytes(&key.try_into()
.map_err(|_| anyhow!("Can't convert sql data to [u8; 32]"))?)?;
Ok::<_, anyhow::Error>(EndpointAddr::from_parts(
id, server
Ok::<_, anyhow::Error>(NodeAddr::from_parts(
id, server, vec![]
))
})
.collect::<std::result::Result<Vec<_>, _>>()
@@ -553,39 +536,45 @@ pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
#[expect(clippy::arithmetic_side_effects)]
async fn subscribe_loop(
context: &Context,
mut stream: GossipReceiver,
mut stream: iroh_gossip::net::GossipReceiver,
topic: TopicId,
msg_id: MsgId,
join_tx: oneshot::Sender<()>,
) -> Result<()> {
stream.joined().await?;
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
for node in stream.neighbors() {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
let mut join_tx = Some(join_tx);
while let Some(event) = stream.try_next().await? {
match event {
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
GossipEvent::Lagged => {
Event::Gossip(event) => match event {
GossipEvent::Joined(nodes) => {
if let Some(join_tx) = join_tx.take() {
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
}
for node in nodes {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
}
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
},
Event::Lagged => {
warn!(context, "Gossip lost some messages");
}
};
@@ -650,7 +639,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.id)
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
assert_eq!(
@@ -663,7 +652,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.id
.node_id
]
);
@@ -726,7 +715,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.id)
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
assert_eq!(
@@ -738,7 +727,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.id
.node_id
]
);
@@ -816,7 +805,7 @@ mod tests {
.await
.unwrap()
.into_iter()
.map(|addr| addr.id)
.map(|addr| addr.node_id)
.collect::<Vec<_>>();
assert_eq!(
@@ -829,7 +818,7 @@ mod tests {
.get_node_addr()
.await
.unwrap()
.id
.node_id
]
);

View File

@@ -6,15 +6,15 @@ use std::io::Cursor;
use anyhow::{Context as _, Result, ensure};
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
use pgp::composed::{
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey,
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType, MessageBuilder,
SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
SubkeyParamsBuilder, SubpacketConfig,
};
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::packet::{Signature, Subpacket, SubpacketData};
use pgp::types::{
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
StringToKey,
@@ -202,47 +202,6 @@ pub async fn pk_encrypt(
.await?
}
/// Produces a detached signature for `plain` text using `private_key_for_signing`.
pub fn pk_calc_signature(
plain: Vec<u8>,
private_key_for_signing: &SignedSecretKey,
) -> Result<String> {
let rng = thread_rng();
let mut config = SignatureConfig::from_key(
rng,
&private_key_for_signing.primary_key,
SignatureType::Binary,
)?;
config.hashed_subpackets = vec![
Subpacket::regular(SubpacketData::IssuerFingerprint(
private_key_for_signing.fingerprint(),
))?,
Subpacket::critical(SubpacketData::SignatureCreationTime(
pgp::types::Timestamp::now(),
))?,
];
config.unhashed_subpackets = vec![];
if private_key_for_signing.version() <= KeyVersion::V4 {
config
.unhashed_subpackets
.push(Subpacket::regular(SubpacketData::IssuerKeyId(
private_key_for_signing.legacy_key_id(),
))?);
}
let signature = config.sign(
&private_key_for_signing.primary_key,
&Password::empty(),
plain.as_slice(),
)?;
let sig = DetachedSignature::new(signature);
Ok(sig.to_armored_string(ArmorOptions::default())?)
}
/// Returns fingerprints
/// of all keys from the `public_keys_for_validation` keyring that
/// have valid signatures in `msg` and corresponding intended recipient fingerprints

View File

@@ -146,7 +146,7 @@ pub enum Qr {
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
node_addr: iroh::EndpointAddr,
node_addr: iroh::NodeAddr,
/// Authentication token.
auth_token: String,
@@ -781,7 +781,7 @@ fn decode_backup2(qr: &str) -> Result<Qr> {
.split_once('&')
.context("Backup QR code has no separator")?;
let auth_token = auth_token.to_string();
let node_addr = serde_json::from_str::<iroh::EndpointAddr>(node_addr)
let node_addr = serde_json::from_str::<iroh::NodeAddr>(node_addr)
.context("Invalid node addr in backup QR code")?;
Ok(Qr::Backup2 {

View File

@@ -955,3 +955,25 @@ async fn test_decode_socks5() -> Result<()> {
Ok(())
}
/// Ensure that `DCBACKUP2` QR code does not fail to deserialize
/// because iroh changes the format of `NodeAddr`
/// as happened between iroh 0.29 and iroh 0.30 before.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_backup() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx, r#"DCBACKUP2:TWSv6ZjDPa5eoxkocj7xMi8r&{"node_id":"9afc1ea5b4f543e5cdd7b7a21cd26aee7c0b1e1c2af26790896fbd8932a06e1e","relay_url":null,"direct_addresses":["192.168.1.10:12345"]}"#).await?;
assert!(matches!(qr, Qr::Backup2 { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?;
assert!(matches!(qr, Qr::Backup2 { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP9:from-the-future"#).await?;
assert!(matches!(qr, Qr::BackupTooNew { .. }));
let qr = check_qr(&ctx, r#"DCBACKUP99:far-from-the-future"#).await?;
assert!(matches!(qr, Qr::BackupTooNew { .. }));
Ok(())
}

View File

@@ -0,0 +1,70 @@
Content-Type: multipart/signed; protocol="application/pgp-signature"; protected;
boundary="18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0"
MIME-Version: 1.0
From: <alice@example.org>
To: <bob@example.net>
Subject: Message from alice@example.org
Date: Wed, 17 Mar 2021 14:30:53 +0100 (CET)
X-Not-Date: Tue, 28 Apr 2026 20:20:34 +0000
Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL
DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM
Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+
+XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0
Content-Type: multipart/mixed; protected-headers="v1"; hp="clear";
boundary="18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0"
From: <alice@example.org>
To: <bob@example.net>
Subject: Message from alice@example.org
Date: Tue, 28 Apr 2026 20:20:34 +0000
References: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
Chat-Version: 1.0
Chat-Disposition-Notification-To: alice@example.org
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCAAAAABYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAA57ABAL
DeNEB8l86SrqNKbUhDl5e7Q46VN+k/jxPEbIAs506MAQDXxgFEO2xAE19ykJI4JqU8+Zj+dwld9rXM
Bh98UTnEBs0TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQIAAAAAFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD4e8BAKrOvjAu/Zd+
+XeYCfN00mA7Vb6FtLlvVb0gT0hzv/rBAP0dYE736fa81MseX1PdUeN2Lf9SyNOVw3eW8W0nKXEbDr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0
Content-Type: text/plain; charset="utf-8"
Message-ID: <13140637-3c00-4553-8b76-fdbbbe3cc117@localhost>
Content-Transfer-Encoding: 7bit
Hello!
--18aa9ed357004185_2007cbc2d36c354a_6b26de88a99ef0a0--
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0
Content-Type: application/pgp-signature; name="signature.asc";
charset="utf-8"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="signature"
Content-Transfer-Encoding: quoted-printable
-----BEGIN PGP SIGNATURE-----=0A=0AwnUEABYIAB0WIQQub6LLI7Uy1yhjS1hksI9hqe2UQ=
wWCafEWkQAKCRBksI9hqe2U=0AQ4qaAQCFSLVDANIjaXswP8V5zIwUSvGnUwsMD+ruozO0mG2AqA=
D9EqpWeD6cc+is=0Av9/nvp6uHi35pUmDX0s1XKu3xbSTWg8=3D=0A=3Dr9hO=0A-----END PGP=
SIGNATURE-----=0A
--18aa9ed356ff9321_81d052095421b935_6b26de88a99ef0a0--