Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ab08a47
fix: Handle the case that the user starts a securejoin, and then dele…
Hocuri Mar 2, 2026
e7625ca
chore(cargo): bump proptest from 1.9.0 to 1.10.0
dependabot[bot] Mar 2, 2026
bfae229
test: Fix flaky test_qr_securejoin_broadcast (#7937)
Hocuri Mar 3, 2026
b947927
feat: Don't depend on cleartext Chat-Version, In-Reply-To, and Refere…
Hocuri Mar 3, 2026
b10acd1
Add support to gif stickers (#7941)
fcr-- Mar 3, 2026
c928015
fix: Use the correct chat description stock string again (#7939)
Hocuri Mar 3, 2026
0622289
fix(vcard): Improve property value escaping (#7931)
j-g00da Mar 3, 2026
3c4ce17
feat: Remove QR code tokens sync compatibility code
iequidoo Feb 9, 2026
a1eb376
feat: Don't send unencrypted In-Reply-To and References headers (#7935)
Hocuri Mar 4, 2026
964bbad
api: add createQrSvg to jsonrpc (#7949)
nicodh Mar 4, 2026
e3a7d55
docs: Fix documentation for membership change stock strings (#7944)
Hocuri Mar 5, 2026
8ff8ba7
refactor: `use super::*` in qr::dclogin_scheme
link2xt Mar 4, 2026
89b5675
fix: percent-decode the address in `dclogin://` URLs
link2xt Mar 4, 2026
0c4e323
fix: Make broadcast owner and subscriber hidden contacts for each oth…
iequidoo Mar 4, 2026
d1c3a67
ci: allow non-hash references for actions/* and dependabot/*
link2xt Mar 3, 2026
5f84be7
ci: update zizmor workflow to use zizmorcore/zizmor-action
link2xt Mar 3, 2026
abb93cd
fix: Set proper placeholder texts for system messages (#7953)
j-g00da Mar 5, 2026
1e20055
feat: Don't send unencrypted Auto-Submitted header (#7938)
Hocuri Mar 6, 2026
cce8e3b
fix: do not run more than one housekeeping at a time
link2xt Mar 6, 2026
874e38c
refactor: move WAL checkpointing into `sql::pool` submodule
link2xt Mar 6, 2026
53acfaa
fix: add mutex around wal_checkpoint()
link2xt Mar 6, 2026
1b43aac
chore(cargo): bump strum from 0.27.2 to 0.28.0
dependabot[bot] Mar 1, 2026
d26fa71
chore(cargo): bump strum_macros from 0.27.2 to 0.28.0
dependabot[bot] Mar 1, 2026
4be35e1
refactor: use re-exported rustls::pki_types
link2xt Mar 2, 2026
8113520
refactor: import tokio_rustls::rustls
link2xt Mar 2, 2026
98068f5
feat(tls): do not verify TLS certificates for hostnames starting with…
link2xt Mar 2, 2026
a5e875e
feat: support underscore-prefixed domains with self-signed TLS certif…
hpk42 Mar 1, 2026
429ba11
fix: use Rustls NoCertificateVerification for underscore domains inst…
hpk42 Mar 2, 2026
77bfc8c
i guess this core-foundation thing can be prevented from duplication
hpk42 Mar 2, 2026
ff2482c
revert ice server test
hpk42 Mar 2, 2026
a6dbd38
fix the check, it's now always automat3ic
hpk42 Mar 2, 2026
6592aa8
remove iroh underscore domain support for now.
hpk42 Mar 4, 2026
1f9674b
try to make cargo happy
hpk42 Mar 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 7 additions & 12 deletions .github/workflows/zizmor-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,21 @@ on:
pull_request:
branches: ["**"]

permissions: {}

jobs:
zizmor:
name: zizmor latest via PyPI
name: Run zizmor
runs-on: ubuntu-latest
permissions:
security-events: write
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
contents: read
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Install the latest version of uv
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b

- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
category: zizmor
uses: zizmorcore/zizmor-action@0dce2577a4760a2749d8cfb7a84b7d5585ebcb7d # v0.5.0
6 changes: 6 additions & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
rules:
unpinned-uses:
config:
policies:
actions/*: ref-pin
dependabot/*: ref-pin
13 changes: 6 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ rand-old = { package = "rand", version = "0.8" }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
sanitize-filename = { workspace = true }
sdp = "0.10.0"
serde_json = { workspace = true }
Expand All @@ -96,8 +95,8 @@ sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
smallvec = "1.15.1"
strum = "0.27"
strum_macros = "0.27"
strum = "0.28"
strum_macros = "0.28"
tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
Expand Down
45 changes: 40 additions & 5 deletions deltachat-contact-tools/src/vcard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,45 @@ impl VcardContact {
}
}

fn escape(s: &str) -> String {
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
s
// backslash must be first!
.replace(r"\", r"\\")
.replace(',', r"\,")
.replace(';', r"\;")
.replace('\n', r"\n")
}

fn unescape(s: &str) -> String {
// https://www.rfc-editor.org/rfc/rfc6350.html#section-3.4
let mut out = String::new();

let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(next) = chars.next() {
match next {
'\\' | ',' | ';' => out.push(next),
'n' | 'N' => out.push('\n'),
_ => {
// Invalid escape sequence (keep unchanged)
out.push('\\');
out.push(next);
}
}
} else {
// Invalid escape sequence (keep unchanged)
out.push('\\');
}
} else {
out.push(c);
}
}

out
}

/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
Expand All @@ -46,10 +85,6 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}

fn escape(s: &str) -> String {
s.replace(',', "\\,")
}

let mut res = "".to_string();
for c in contacts {
// Mustn't contain ',', but it's easier to escape than to error out.
Expand Down Expand Up @@ -124,7 +159,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
let (params, value) = vcard_property_raw(line, property)?;
// Some fields can't contain commas, but unescape them everywhere for safety.
Some((params, value.replace("\\,", ",")))
Some((params, unescape(value)))
}
fn base64_key(line: &str) -> Option<&str> {
let (params, value) = vcard_property_raw(line, "key")?;
Expand Down
15 changes: 13 additions & 2 deletions deltachat-contact-tools/src/vcard/vcard_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ fn test_make_and_parse_vcard() {
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
biography: Some("Hi, I'm Alice".to_string()),
biography: Some("Hi,\nI'm Alice; and this is a backslash: \\".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
Expand All @@ -110,7 +110,7 @@ fn test_make_and_parse_vcard() {
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
NOTE:Hi\\, I'm Alice\r\n\
NOTE:Hi\\,\\nI'm Alice\\; and this is a backslash: \\\\\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
Expand Down Expand Up @@ -276,3 +276,14 @@ END:VCARD",
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
}

#[test]
fn test_vcard_value_escape_unescape() {
let original = "Text, with; chars and a \\ and a newline\nand a literal newline \\n";
let expected_escaped = r"Text\, with\; chars and a \\ and a newline\nand a literal newline \\n";

let escaped = escape(original);
assert_eq!(escaped, expected_escaped);
let unescaped = unescape(&escaped);
assert_eq!(original, unescaped);
}
16 changes: 14 additions & 2 deletions deltachat-jsonrpc/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use deltachat::peer_channels::{
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
Expand Down Expand Up @@ -864,6 +864,8 @@ impl CommandApi {
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
/// an out-of-band-verification can be joined using `secure_join()`
///
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
///
/// chat_id: If set to a group-chat-id,
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
Expand Down Expand Up @@ -1980,6 +1982,8 @@ impl CommandApi {
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 60 seconds to avoid deadlocks.
///
/// @deprecated as of 2026-03; use `create_qr_svg(get_backup_qr())` instead.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
Expand All @@ -1993,6 +1997,11 @@ impl CommandApi {
generate_backup_qr(&ctx, &qr).await
}

/// Renders the given text as a QR code SVG image.
async fn create_qr_svg(&self, text: String) -> Result<String> {
create_qr_svg(&text)
}

/// Gets a backup from a remote provider.
///
/// This retrieves the backup from a remote device over the network and imports it into
Expand Down Expand Up @@ -2506,7 +2515,10 @@ impl CommandApi {
continue;
}
let sticker_name = sticker_entry.file_name().into_string().unwrap_or_default();
if sticker_name.ends_with(".png") || sticker_name.ends_with(".webp") {
if sticker_name.ends_with(".png")
|| sticker_name.ends_with(".webp")
|| sticker_name.ends_with(".gif")
{
sticker_paths.push(
sticker_entry
.path()
Expand Down
25 changes: 19 additions & 6 deletions deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import random
import subprocess
import sys
import urllib.parse
from typing import AsyncGenerator, Optional

import execnet
Expand Down Expand Up @@ -283,8 +284,16 @@ def factory(core_version):
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()
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
6 changes: 5 additions & 1 deletion deltachat-rpc-client/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ def connect(self):
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")
Expand Down
4 changes: 2 additions & 2 deletions deltachat-rpc-client/tests/test_chatlist_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions deltachat-rpc-client/tests/test_cross_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')")

Expand All @@ -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)")
Expand Down
7 changes: 7 additions & 0 deletions deltachat-rpc-client/tests/test_iroh_webxdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
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")
Expand Down
4 changes: 2 additions & 2 deletions deltachat-rpc-client/tests/test_multitransport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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
Expand Down
Loading