Compare commits

..

7 Commits

Author SHA1 Message Date
holger krekel
92de732686 try to make cargo happy 2026-03-19 01:52:51 +00:00
holger krekel
aa36cfd581 remove iroh underscore domain support for now. 2026-03-19 01:52:51 +00:00
holger krekel
1a04180ef6 fix the check, it's now always automat3ic 2026-03-19 01:52:51 +00:00
holger krekel
58db2d41ee revert ice server test 2026-03-19 01:52:51 +00:00
holger krekel
c2790521c6 i guess this core-foundation thing can be prevented from duplication
and the webpki-root-certs needs the license admission
2026-03-19 01:52:51 +00:00
holger krekel
e0768f5f37 fix: use Rustls NoCertificateVerification for underscore domains instead of AcceptInvalidCertificates
Remove AcceptInvalidCertificates overrides in configure.rs and qr.rs that
caused a fallback to OpenSSL/native-tls. The upstream Rustls TLS layer now
handles underscore-prefixed domains via NoCertificateVerification directly.
Also fix clippy lint in peer_channels.rs (map_or -> is_some_and).
2026-03-19 01:52:51 +00:00
holger krekel
1b860372cc feat: support underscore-prefixed domains with self-signed TLS certificates
Allow Delta Chat core to work with chatmail servers running on
underscore-prefixed domains (e.g. _alice.localchat) which use
self-signed TLS certificates. This is mirroring related work
on chatmail relays: https://github.com/chatmail/relay/pull/855
Underscore domains with self-signed TLS certs can be used by LXC test
containers where obtaining real certificates is not practical.

When the domain starts with '_', certificate verification is
automatically relaxed for IMAP/SMTP connections, dcaccount QR
code handling, and iroh relay endpoints. The Python test suite
is adapted to also work against such underscore-domain servers,
including cross-core tests with older Delta Chat versions.

Note: this PR does not support HTTPS requests with underscore
domains. They are not currently needed for working with LXC test
containers.

14 files changed, +102/-31 lines (excluding Cargo.lock).
Cargo.lock: +606/-11 lines from enabling iroh features
needed for connecting to iroh relay endpoint on underscore domains.
The added dependencies are unfortunate but best considered
when finally upgrading to iroh 1.0 (tm).
2026-03-19 01:52:51 +00:00
33 changed files with 188 additions and 212 deletions

View File

@@ -1,47 +1,5 @@
# Changelog
## [2.46.0] - 2026-03-19
### API-Changes
- [**breaking**] remove functions for sending and receiving Autocrypt Setup Message.
- Add `list_transports_ex()` and `set_transport_unpublished()` functions.
### Features / Changes
- add `IncomingCallAccepted.from_this_device`.
- mark messages as "fresh".
- decode `dcaccount://` URLs and error out on empty URLs early.
- enable anonymous OpenPGP key IDs.
- tls: do not verify TLS certificates for hostnames starting with `_`.
### Fixes
- Mark call message as seen when accepting/declining a call ([#7842](https://github.com/chatmail/core/pull/7842)).
- do not send MDNs for hidden messages.
- call sync_all() instead of sync_data() when writing accounts.toml.
- fsync() the rename() of accounts.toml.
- count recipients by Intended Recipient Fingerprints.
### Miscellaneous Tasks
- deps: bump zizmorcore/zizmor-action from 0.5.0 to 0.5.2.
- cargo: bump astral-tokio-tar from 0.5.6 to 0.6.0.
- deps: bump actions/upload-artifact from 6 to 7.
- cargo: bump blake3 from 1.8.2 to 1.8.3.
- add constant_time_eq 0.3.1 to deny.toml.
### Refactor
- use re-exported rustls::pki_types.
- import tokio_rustls::rustls.
- Move transport_tests to their own file.
### Tests
- Shift time even more in flaky test_sync_broadcast_and_send_message.
- test markfresh_chat()
## [2.45.0] - 2026-03-14
### API-Changes
@@ -7949,4 +7907,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.43.0]: https://github.com/chatmail/core/compare/v2.42.0..v2.43.0
[2.44.0]: https://github.com/chatmail/core/compare/v2.43.0..v2.44.0
[2.45.0]: https://github.com/chatmail/core/compare/v2.44.0..v2.45.0
[2.46.0]: https://github.com/chatmail/core/compare/v2.45.0..v2.46.0

67
Cargo.lock generated
View File

@@ -470,9 +470,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bitvec"
@@ -497,16 +497,15 @@ dependencies = [
[[package]]
name = "blake3"
version = "1.8.3"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq 0.4.2",
"cpufeatures",
"constant_time_eq",
]
[[package]]
@@ -964,12 +963,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "convert_case"
version = "0.5.0"
@@ -1123,7 +1116,7 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"crossterm_winapi",
"parking_lot",
"rustix 0.38.44",
@@ -1307,7 +1300,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.47.0-dev"
version = "2.46.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1416,7 +1409,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.47.0-dev"
version = "2.46.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1437,7 +1430,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.47.0-dev"
version = "2.46.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1453,7 +1446,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.47.0-dev"
version = "2.46.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1482,7 +1475,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.47.0-dev"
version = "2.46.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -3032,7 +3025,7 @@ dependencies = [
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq 0.3.1",
"constant_time_eq",
]
[[package]]
@@ -3276,7 +3269,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"libc",
"redox_syscall 0.5.12",
]
@@ -3596,7 +3589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2"
dependencies = [
"anyhow",
"bitflags 2.11.0",
"bitflags 2.9.1",
"byteorder",
"libc",
"log",
@@ -3691,7 +3684,7 @@ version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"cfg-if",
"cfg_aliases",
"libc",
@@ -3866,7 +3859,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
]
[[package]]
@@ -3937,7 +3930,7 @@ version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"cfg-if",
"foreign-types",
"libc",
@@ -4399,7 +4392,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"crc32fast",
"fdeflate",
"flate2",
@@ -4615,11 +4608,11 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.10.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"num-traits",
"rand 0.9.2",
"rand_chacha 0.9.0",
@@ -4903,7 +4896,7 @@ version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
]
[[package]]
@@ -5090,7 +5083,7 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -5134,7 +5127,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.4.14",
@@ -5147,7 +5140,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.12.1",
@@ -5211,7 +5204,7 @@ version = "16.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62fd9ca5ebc709e8535e8ef7c658eb51457987e48c98ead2be482172accc408d"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"cfg-if",
"clipboard-win",
"fd-lock",
@@ -5338,7 +5331,7 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -5657,7 +5650,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
]
[[package]]
@@ -5942,7 +5935,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
"core-foundation",
"system-configuration-sys",
]
@@ -7225,7 +7218,7 @@ version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags 2.11.0",
"bitflags 2.9.1",
]
[[package]]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.47.0-dev"
version = "2.46.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.47.0-dev"
version = "2.46.0-dev"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.47.0-dev"
version = "2.46.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -23,9 +23,8 @@ use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::{
self, delete_msgs_ex, dont_truncate_long_messages, get_existing_msg_ids,
get_msg_read_receipt_count, get_msg_read_receipts, markseen_msgs, Message, MessageState, MsgId,
Viewtype,
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -1435,15 +1434,6 @@ impl CommandApi {
MsgId::new(message_id).get_html(&ctx).await
}
/// Opt out of truncating long messages when loading.
///
/// Should be used by the UIs that can handle long text messages.
async fn dont_truncate_long_messages(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
dont_truncate_long_messages(&ctx);
Ok(())
}
/// get multiple messages in one call,
/// if loading one message fails the error is stored in the result object in it's place.
///

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.47.0-dev"
"version": "2.46.0-dev"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.47.0-dev"
version = "2.46.0-dev"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.47.0-dev"
version = "2.46.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [

View File

@@ -9,6 +9,7 @@ import platform
import random
import subprocess
import sys
import urllib.parse
from typing import AsyncGenerator, Optional
import execnet
@@ -283,8 +284,16 @@ def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# Build a dclogin QR code for the remote bob account.
# Using dclogin scheme with ic=3&sc=3 allows old cores
# to accept invalid certificates for underscore-prefixed domains.
addr, password = acfactory.get_credentials()
dclogin_qr = f"dclogin://{urllib.parse.quote(addr, safe='@')}?p={urllib.parse.quote(password)}&v=1"
if cm and cm.startswith("_"):
dclogin_qr += "&ic=3&sc=3"
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), cm))
channel.send((accounts_dir, str(rpc_server_path), dclogin_qr))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
@@ -316,10 +325,8 @@ def remote_bob_loop(channel):
import os
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
accounts_dir, rpc_server_path, dclogin_qr = channel.receive()
# older core versions don't support specifying rpc_server_path
# so we can't just pass `rpc_server_path` argument to Rpc constructor
@@ -330,8 +337,14 @@ def remote_bob_loop(channel):
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
acfactory = ACFactory(dc)
bob = acfactory.get_online_account()
# Configure account using dclogin scheme directly,
# avoiding the old ACFactory which doesn't handle
# underscore-prefixed domains' TLS on older cores.
bob = dc.add_account()
bob.set_config_from_qr(dclogin_qr)
bob.bring_online()
alice_vcard = channel.receive()
[alice_contact] = bob.import_vcard(alice_vcard)
ns = {"bob": bob, "bob_contact_alice": alice_contact}

View File

@@ -37,7 +37,11 @@ class DirectImap:
host = user.rsplit("@")[-1]
pw = self.account.get_config("mail_pw")
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
ssl_context = ssl.create_default_context()
if host.startswith("_"):
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")

View File

@@ -109,7 +109,7 @@ def test_delivery_status_failed(acfactory: ACFactory) -> None:
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory) -> None:
def test_download_on_demand(acfactory: ACFactory, data) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
@@ -127,7 +127,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
file="../test-data/image/screenshot.jpg",
file=data.get_path("image/screenshot.jpg"),
)
message = alice.wait_for_incoming_msg()

View File

@@ -36,7 +36,7 @@ def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
@@ -46,7 +46,7 @@ def test_send_and_receive_message(alice_and_remote_bob) -> None:
def test_second_device(acfactory, alice_and_remote_bob) -> None:
"""Test setting up current version as a second device for old version."""
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
_alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.24.0")
remote_eval("locals().setdefault('future', bob._rpc.provide_backup.future(bob.id))")
qr = remote_eval("bob._rpc.get_backup_qr(bob.id)")

View File

@@ -17,6 +17,13 @@ import pytest
from deltachat_rpc_client import EventType
@pytest.fixture(autouse=True)
def _xfail_underscore_domain():
domain = os.environ.get("CHATMAIL_DOMAIN", "")
if domain.startswith("_"):
pytest.xfail("Iroh tests are expected to fail on underscore domains (self-signed TLS certificates)")
@pytest.fixture
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")

View File

@@ -120,7 +120,7 @@ def test_change_address(acfactory) -> None:
assert sender_addr2 == new_alice_addr
def test_download_on_demand(acfactory) -> None:
def test_download_on_demand(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice.set_config("download_limit", "1")
@@ -131,7 +131,7 @@ def test_download_on_demand(acfactory) -> None:
alice.create_chat(bob)
chat_bob_alice = bob.create_chat(alice)
chat_bob_alice.send_message(file="../test-data/image/screenshot.jpg")
chat_bob_alice.send_message(file=data.get_path("image/screenshot.jpg"))
msg = alice.wait_for_incoming_msg()
snapshot = msg.get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE

View File

@@ -141,10 +141,9 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
def wait_for_broadcast_messages(ac):
snapshot1 = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot1.text == "You joined the channel."
snapshot2 = ac.wait_for_incoming_msg().get_snapshot()
assert snapshot2.text == "Hello everyone!"
texts = {snapshot1.text, snapshot2.text}
assert texts == {"You joined the channel.", "Hello everyone!"}
chat = get_broadcast(ac)
assert snapshot1.chat_id == chat.id

View File

@@ -97,8 +97,11 @@ def test_lowercase_address(acfactory) -> None:
def test_configure_ip(acfactory) -> None:
addr, password = acfactory.get_credentials()
domain = addr.rsplit("@")[-1]
if domain.startswith("_"):
pytest.skip("Underscore domains accept invalid certificates")
account = acfactory.get_unconfigured_account()
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
ip_address = socket.gethostbyname(domain)
with pytest.raises(JsonRpcError):
account.add_or_update_transport(
@@ -1012,7 +1015,8 @@ def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
# Certificate checks should be configured (not None)
assert "cert_strict" in alice.get_info().used_transport_settings
info = alice.get_info()
assert "cert_automatic" in info.used_transport_settings
# "cert_old_automatic" is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
@@ -1361,7 +1365,7 @@ def test_synchronize_member_list_on_group_rejoin(acfactory, log):
assert msg.get_snapshot().chat.num_contacts() == 2
def test_large_message(acfactory) -> None:
def test_large_message(acfactory, data) -> None:
"""
Test sending large message without download limit set,
so it is sent with pre-message but downloaded without user interaction.
@@ -1371,7 +1375,7 @@ def test_large_message(acfactory) -> None:
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_message(
"Hello World, this message is bigger than 5 bytes",
file="../test-data/image/screenshot.jpg",
file=data.get_path("image/screenshot.jpg"),
)
msg = bob.wait_for_incoming_msg()

View File

@@ -1,9 +1,9 @@
def test_webxdc(acfactory) -> None:
def test_webxdc(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
@@ -41,12 +41,12 @@ def test_webxdc(acfactory) -> None:
]
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
def test_webxdc_insert_lots_of_updates(acfactory, data) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
message = alice_chat_bob.send_message(text="Let's play chess!", file=data.get_path("webxdc/chess.xdc"))
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.47.0-dev"
version = "2.46.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.47.0-dev"
"version": "2.46.0-dev"
}

View File

@@ -27,7 +27,6 @@ ignore = [
skip = [
{ name = "async-channel", version = "1.9.0" },
{ 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" },

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.47.0-dev"
version = "2.46.0-dev"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"

View File

@@ -1 +1 @@
2026-03-19
2026-03-14

View File

@@ -50,7 +50,7 @@ use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
IsNoneOrEmpty, SystemTime, buf_compress, create_broadcast_secret, create_id,
create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps, get_abs_path,
gm2local_offset, normalize_text, smeared_time, time,
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
@@ -1742,6 +1742,7 @@ impl Chat {
///
/// If `update_msg_id` is set, that record is reused;
/// if `update_msg_id` is None, a new record is created.
#[expect(clippy::arithmetic_side_effects)]
async fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -1886,8 +1887,7 @@ impl Chat {
EphemeralTimer::Enabled { duration } => time().saturating_add(duration.into()),
};
let msg_text = msg.text.clone();
let (msg_text, was_truncated) = truncate_msg_text(context, msg.text.clone()).await?;
let new_mime_headers = if msg.has_html() {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
} else {
@@ -1900,6 +1900,13 @@ impl Chat {
html_part.write_part(cursor).ok();
String::from_utf8_lossy(&buffer).to_string()
});
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
// We need to add some headers so that they are stripped before formatting HTML by
// `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
// anyway a useful metadata about the stored text.
true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text),
false => None,
});
let new_mime_headers = match new_mime_headers {
Some(h) => Some(tokio::task::block_in_place(move || {
buf_compress(h.as_bytes())

View File

@@ -227,18 +227,7 @@ impl WeakContext {
pub struct InnerContext {
/// Blob directory path
pub(crate) blobdir: PathBuf,
pub(crate) sql: Sql,
/// True if long text messages should be truncated
/// and full message HTML added.
///
/// This should be set by the UIs that cannot handle
/// long messages but can display HTML messages.
///
/// Ignored for bots, bots never get truncated messages.
pub(crate) truncate_long_messages: AtomicBool,
pub(crate) smeared_timestamp: SmearedTimestamp,
/// The global "ongoing" process state.
///
@@ -502,7 +491,6 @@ impl Context {
blobdir,
running_state: RwLock::new(Default::default()),
sql: Sql::new(dbfile),
truncate_long_messages: AtomicBool::new(true),
smeared_timestamp: SmearedTimestamp::new(),
oauth2_mutex: Mutex::new(()),
wrong_pw_warning_mutex: Mutex::new(()),

View File

@@ -30,7 +30,7 @@ impl Message {
/// The corresponding ffi-function is `dc_msg_has_html()`.
/// To get the HTML-code of the message, use `MsgId.get_html()`.
pub fn has_html(&self) -> bool {
self.mime_modified || self.full_text.is_some()
self.mime_modified
}
/// Set HTML-part part of a message that is about to be sent.
@@ -279,19 +279,8 @@ impl MsgId {
Ok((parser, _)) => Ok(Some(parser.html)),
}
} else {
let msg = Message::load_from_db(context, self).await?;
if let Some(full_text) = &msg.full_text {
let html = PlainText {
text: full_text.clone(),
flowed: false,
delsp: false,
}
.to_html();
Ok(Some(html))
} else {
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
}
}

View File

@@ -4,7 +4,6 @@ use std::collections::BTreeSet;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::str;
use std::sync::atomic::Ordering;
use anyhow::{Context as _, Result, ensure, format_err};
use deltachat_contact_tools::{VcardContact, parse_vcard};
@@ -35,9 +34,10 @@ use crate::reaction::get_msg_reactions;
use crate::sql;
use crate::summary::Summary;
use crate::sync::SyncData;
use crate::tools::create_outgoing_rfc724_mid;
use crate::tools::{
buf_compress, buf_decompress, create_outgoing_rfc724_mid, get_filebytes, get_filemeta,
gm2local_offset, read_file, sanitize_filename, time, timestamp_to_str, truncate_msg_text,
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
sanitize_filename, time, timestamp_to_str,
};
/// Message ID, including reserved IDs.
@@ -431,13 +431,7 @@ pub struct Message {
pub(crate) timestamp_rcvd: i64,
pub(crate) ephemeral_timer: EphemeralTimer,
pub(crate) ephemeral_timestamp: i64,
/// Message text, possibly truncated if the message is large.
pub(crate) text: String,
/// Full text if the message text is truncated.
pub(crate) full_text: Option<String>,
/// Text that is added to the end of Message.text
///
/// Currently used for adding the download information on pre-messages
@@ -562,7 +556,6 @@ impl Message {
}
_ => String::new(),
};
let msg = Message {
id: row.get("id")?,
rfc724_mid: row.get::<_, String>("rfc724mid")?,
@@ -587,7 +580,6 @@ impl Message {
original_msg_id: row.get("original_msg_id")?,
mime_modified: row.get("mime_modified")?,
text,
full_text: None,
additional_text: String::new(),
subject: row.get("subject")?,
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
@@ -605,15 +597,6 @@ impl Message {
.with_context(|| format!("failed to load message {id} from the database"))?;
if let Some(msg) = &mut msg {
if !msg.mime_modified {
let (truncated_text, was_truncated) =
truncate_msg_text(context, msg.text.clone()).await?;
if was_truncated {
msg.full_text = Some(msg.text.clone());
msg.text = truncated_text;
}
}
msg.additional_text =
Self::get_additional_text(context, msg.download_state, &msg.param).await?;
}
@@ -2399,22 +2382,5 @@ impl Viewtype {
}
}
/// Opt out of truncating long messages.
///
/// After calling this function, long messages
/// will not be truncated during loading.
///
/// UIs should call this function if they
/// can handle long messages by cutting them
/// and displaying "Show full message" option.
///
/// Has no effect for bots which never
/// truncate messages when loading.
pub fn dont_truncate_long_messages(context: &Context) {
context
.truncate_long_messages
.store(false, Ordering::Relaxed);
}
#[cfg(test)]
mod message_tests;

View File

@@ -32,7 +32,9 @@ use crate::message::{self, Message, MsgId, Viewtype, get_vcard_summary, set_msg_
use crate::param::{Param, Params};
use crate::simplify::{SimplifiedText, simplify};
use crate::sync::SyncItems;
use crate::tools::{get_filemeta, parse_receive_headers, smeared_time, time, validate_id};
use crate::tools::{
get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id,
};
use crate::{chatlist_events, location, tools};
/// Public key extracted from `Autocrypt-Gossip`
@@ -1470,6 +1472,12 @@ impl MimeMessage {
(simplified_txt, top_quote)
};
let (simplified_txt, was_truncated) =
truncate_msg_text(context, simplified_txt).await?;
if was_truncated {
self.is_mime_modified = was_truncated;
}
if !simplified_txt.is_empty() || simplified_quote.is_some() {
let mut part = Part {
dehtml_failed,

View File

@@ -1286,12 +1286,12 @@ async fn test_mime_modified_large_plain() -> Result<()> {
{
let mimemsg = MimeMessage::from_bytes(&t, long_txt.as_ref()).await?;
assert!(!mimemsg.is_mime_modified);
assert!(mimemsg.parts[0].msg.matches("just repeated").count() == REPEAT_CNT);
assert_eq!(
mimemsg.parts[0].msg.len() + 1,
REPEAT_TXT.len() * REPEAT_CNT
assert!(mimemsg.is_mime_modified);
assert!(
mimemsg.parts[0].msg.matches("just repeated").count()
<= DC_DESIRED_TEXT_LEN / REPEAT_TXT.len()
);
assert!(mimemsg.parts[0].msg.len() <= DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len());
}
for draft in [false, true] {

View File

@@ -829,7 +829,7 @@ pub(crate) async fn login_param_from_account_qr(
..Default::default()
},
smtp: Default::default(),
certificate_checks: EnteredCertificateChecks::Strict,
certificate_checks: EnteredCertificateChecks::Automatic,
oauth2: false,
};
return Ok(param);

View File

@@ -754,6 +754,39 @@ async fn test_decode_empty_account() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account_underscore_domain() -> Result<()> {
let ctx = TestContext::new().await;
// Underscore domain is kept as-is.
let qr = check_qr(&ctx.ctx, "dcaccount:_example.org").await?;
assert_eq!(
qr,
Qr::Account {
domain: "_example.org".to_string()
}
);
// Verify login params use Automatic for underscore domain.
// The TLS layer handles underscore domains via NoCertificateVerification in Rustls.
let param = login_param_from_account_qr(&ctx.ctx, "dcaccount:_example.org").await?;
assert!(param.addr.ends_with("@_example.org"));
assert_eq!(
param.certificate_checks,
EnteredCertificateChecks::Automatic
);
// Regular domain also uses Automatic.
let param = login_param_from_account_qr(&ctx.ctx, "dcaccount:example.org").await?;
assert!(param.addr.ends_with("@example.org"));
assert_eq!(
param.certificate_checks,
EnteredCertificateChecks::Automatic
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_tg_socks_proxy() -> Result<()> {
let t = TestContext::new().await;

View File

@@ -3585,17 +3585,39 @@ async fn test_big_forwarded_with_big_attachment() -> Result<()> {
.starts_with("this text with 42 chars is just repeated.")
);
assert!(msg.get_text().ends_with("[...]"));
assert!(msg.has_html());
let html = msg.id.get_html(t).await?.unwrap();
assert_eq!(
html.matches("this text with 42 chars is just repeated.")
.count(),
128
);
assert!(!msg.has_html());
let msg = Message::load_from_db(t, rcvd.msg_ids[2]).await?;
assert_eq!(msg.get_viewtype(), Viewtype::File);
assert!(!msg.has_html());
assert!(msg.has_html());
let html = msg.id.get_html(t).await?.unwrap();
let tail = html
.split_once("Hello!")
.unwrap()
.1
.split_once("From: AAA")
.unwrap()
.1
.split_once("aaa@example.org")
.unwrap()
.1
.split_once("To: Alice")
.unwrap()
.1
.split_once("alice@example.org")
.unwrap()
.1
.split_once("Subject: Some subject")
.unwrap()
.1
.split_once("Date: Fri, 2 Jun 2023 12:29:17 +0000")
.unwrap()
.1;
assert_eq!(
tail.matches("this text with 42 chars is just repeated.")
.count(),
128
);
Ok(())
}

View File

@@ -9,7 +9,6 @@ use std::mem;
use std::ops::{AddAssign, Deref};
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::sync::atomic::Ordering;
// If a time value doesn't need to be sent to another host, saved to the db or otherwise used across
// program restarts, a monotonically nondecreasing clock (`Instant`) should be used. But as
// `Instant` may use `libc::clock_gettime(CLOCK_MONOTONIC)`, e.g. on Android, and does not advance
@@ -140,9 +139,7 @@ pub(crate) fn truncate_by_lines(
///
/// Returns the resulting text and a bool telling whether a truncation was done.
pub(crate) async fn truncate_msg_text(context: &Context, text: String) -> Result<(String, bool)> {
if !context.truncate_long_messages.load(Ordering::Relaxed)
|| context.get_config_bool(Config::Bot).await?
{
if context.get_config_bool(Config::Bot).await? {
return Ok((text, false));
}
// Truncate text if it has too many lines