Compare commits

..
Author SHA1 Message Date
Hocuri b5acd0844a Add some basic tests 2026-10-08 16:22:47 +02:00
Hocuri af755bff58 Show device message about removal of unencrypted messages
In its stead, remove `recode_avatar` high-level migration. It was added
5 years ago, and even if someone updates Delta Chat after such a long
time, large avatars are not a problem anymore since we don't send them
in unencrypted messages.
2026-10-08 16:19:29 +02:00
Hocuri dce1690e10 Migrate unencrypted chats to be read-only 2026-10-08 16:19:24 +02:00
34 changed files with 430 additions and 531 deletions
+8 -10
View File
@@ -25,8 +25,6 @@ env:
# Minimum Supported Rust Version
MSRV: 1.89.0
PYTHON_VERSION: 3.15
jobs:
lint_rust:
name: Lint Rust
@@ -270,11 +268,11 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Python version.
# Currently used Rust version.
- os: ubuntu-latest
python: latest
python: 3.14
- os: macos-latest
python: latest
python: 3.14
# PyPy tests
- os: ubuntu-latest
@@ -305,7 +303,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
python-version: ${{ matrix.python }}
- name: Install tox
run: pip install tox
@@ -326,11 +324,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: latest
python: 3.14
- os: macos-latest
python: latest
python: 3.14
- os: windows-latest
python: latest
python: 3.14
# PyPy tests
- os: ubuntu-latest
@@ -353,7 +351,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
python-version: ${{ matrix.python }}
- name: Install tox
run: pip install tox
-80
View File
@@ -1,84 +1,5 @@
# Changelog
## [2.63.0] - 2026-10-09
### API-Changes
- add `PinnedMessagesChanged` event.
- [**breaking**]: remove unused `dc_chatlist_get_summary2()` ([#8812](https://github.com/chatmail/core/pull/8812))
- new `dc_msg_get_summary_text()` ([#8813](https://github.com/chatmail/core/pull/8813)).
### Features / Changes
- connect to the most recently successfully used SMTP transport first.
- re-add "second device added" message if deleted soon after transfer ([#8758](https://github.com/chatmail/core/pull/8758)).
- do not request MDNs for unencrypted messages.
- do not send unencrypted MDNs.
- add context to HTTP(S) proxy connection errors.
- deltachat-repl: remove "reset" command.
### Fixes
- Correctly percent-encode addresses in securejoin invite codes ([#8747](https://github.com/chatmail/core/pull/8747)).
- regenerate deltachat.pc if env changed ([#8752](https://github.com/chatmail/core/pull/8752)).
- delete handled securejoin messages on all relays.
- add headers to unencrypted MDNs.
- In SMTP loop, reset `transport_id` and `from` when disconnecting ([#8797](https://github.com/chatmail/core/pull/8797)).
- deltachat-rpc-client: shutdown all threads after RPC server crash.
- (linux-only) make sure large attachments return memory to kernel.
- do not reencode animated WebPs into JPEG.
### CI
- update Rust to 1.99.0.
- speedup lint job and the Rust test builds
### Refactor
- [**breaking**] stop saving legacy config values when configuring a transport.
- core does not set deprecated config values `addr`, `mail_pw` etc. Use `list_transports()` API instead.
- get rid of dead sticker-related code in check_or_recode_to_size().
- factor add_headers_to_part() out of mimefactory::add_headers_to_protected_part().
- replace MimeFactory::from_mdn() with a standalone mimefactory::mdn().
- take securejoin addresses from transport list.
- deltachat-repl: move all commands to cmdline.rs.
- do not guess image format from file extension on I/O error.
- reduce macro-generated lines by >70%, and drop tracing.
- stop excluding `COPYUID` responses from the logs.
### Tests
- move pinned messages tests into pinned_messages_tests module.
- Add basic tests for rendering unencrypted messages ([#8736](https://github.com/chatmail/core/pull/8736)).
- do not ignore errors in add_pseudo_transport().
- test sending and receiving long filename with emojis.
### Miscellaneous Tasks
- update rPGP from 0.20.0 to 0.21.0.
- reduce noise created by key-contact migration.
- enable `clippy::unnecessary_wraps`, fix `clippy::redundant_clone` and `clippy::string_lit_as_bytes` suggestions.
- add chatmail.au.
- add chatmail.cc ([#8756](https://github.com/chatmail/core/pull/8756)).
- remove some `unwrap()` calls.
- remove unused "sdp" dependency.
- cargo: bump smallvec from 1.15.2 to 1.16.1.
- cargo: bump syn from 3.0.4 to 3.0.6.
- deps: bump zizmorcore/zizmor-action from 0.6.2 to 0.6.4.
- cargo: bump mailparse from 0.16.1 to 0.17.0.
- cargo: bump uuid from 1.25.0 to 1.26.1.
- deps: bump cachix/install-nix-action from 31.11.0 to 31.11.1.
- cargo: bump hyper from 1.10.1 to 1.11.1.
- cargo: bump tokio-rustls from 0.26.4 to 0.26.5.
- cargo: bump thiserror from 2.0.20 to 2.0.21.
- deps: bump taiki-e/install-action from 2.86.7 to 2.87.20.
- cargo: bump hyper-util from 0.1.20 to 0.1.21.
- cargo: bump dirs from 6.0.0 to 7.0.0.
- move deltachat-time to dev dependencies.
- update astral-tokio-tar from 0.6.4 to 0.7.0.
- update async-imap to 0.12.0.
- update mail-builder to 1.0.
## [2.62.0] - 2026-09-22
### API-Changes
@@ -8982,4 +8903,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
[2.63.0]: https://github.com/chatmail/core/compare/v2.62.0..v2.63.0
Generated
+27 -34
View File
@@ -272,19 +272,19 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.12.0"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f97b87216c9f0ccc63c516263169085fa34bba25633f6d96c6a9b9ca1fd5f70a"
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
dependencies = [
"async-channel 2.5.0",
"async-compression",
"base64 0.23.1",
"base64",
"bytes",
"chrono",
"futures-util",
"futures",
"imap-proto",
"log",
"nom 8.0.0",
"nom 7.1.3",
"pin-project",
"pin-utils",
"self_cell",
@@ -323,7 +323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55219982f938e74491ba85dc4e49cefe8096b1e8f49348c67180a7d244988dca"
dependencies = [
"anyhow",
"base64 0.22.1",
"base64",
"futures",
"log",
"nom 8.0.0",
@@ -436,12 +436,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -839,7 +833,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
"base64 0.22.1",
"base64",
"encoding_rs",
]
@@ -1334,7 +1328,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1344,7 +1338,7 @@ dependencies = [
"async-native-tls",
"async-smtp",
"async_zip",
"base64 0.23.1",
"base64",
"blake3",
"brotli",
"bytes",
@@ -1440,11 +1434,11 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"base64 0.23.1",
"base64",
"deltachat",
"deltachat-contact-tools",
"futures",
@@ -1461,14 +1455,14 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc-bindings"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"deltachat-jsonrpc",
]
[[package]]
name = "deltachat-repl"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1484,13 +1478,12 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"anyhow",
"deltachat",
"deltachat-jsonrpc",
"futures-lite",
"libc",
"log",
"serde",
"serde_json",
@@ -1514,7 +1507,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.64.0-dev"
version = "2.63.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -2947,11 +2940,11 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.17.0"
version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ccf963d57074747b455398a1763d174da80bcaba6f51e3671a82252b531a68b"
checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856"
dependencies = [
"nom 8.0.0",
"nom 7.1.3",
]
[[package]]
@@ -3451,9 +3444,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "1.0.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
[[package]]
name = "mailparse"
@@ -4187,7 +4180,7 @@ version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64 0.22.1",
"base64",
"serde",
]
@@ -4262,7 +4255,7 @@ dependencies = [
"aes-gcm",
"aes-kw",
"argon2",
"base64 0.22.1",
"base64",
"bitfields",
"block-padding",
"blowfish",
@@ -4541,7 +4534,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
dependencies = [
"base64 0.22.1",
"base64",
"bytes",
"derive_more 1.0.0",
"futures-lite",
@@ -5097,7 +5090,7 @@ version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64 0.22.1",
"base64",
"bytes",
"futures-core",
"futures-util",
@@ -5691,7 +5684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
dependencies = [
"aes",
"base64 0.22.1",
"base64",
"blake3",
"byte_string",
"bytes",
@@ -5991,7 +5984,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac"
dependencies = [
"base64 0.22.1",
"base64",
"bounded-integer",
"byteorder",
"crc",
@@ -6406,7 +6399,7 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
dependencies = [
"base64 0.22.1",
"base64",
"bytes",
"futures-core",
"futures-sink",
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.64.0-dev"
version = "2.63.0-dev"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.89"
@@ -43,7 +43,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.12.0", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.6", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
@@ -69,7 +69,7 @@ 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 = "1.0.0", default-features = false }
mail-builder = { version = "0.5.0", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
@@ -178,7 +178,7 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.5.0"
base64 = { version = "0.23", default-features = false, features = ["std"] }
base64 = "0.22"
chrono = { version = "0.4.44", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.64.0-dev"
version = "2.63.0-dev"
description = "Deltachat FFI"
edition = "2024"
license = "MPL-2.0"
-29
View File
@@ -4291,7 +4291,6 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
* notifications.
*
* @memberof dc_msg_t
* @deprecated 2026-10-08, use dc_msg_get_summary_text(msg, 1, 1, approx_chars) instead
* @param msg The message object.
* @param approx_characters A rough length of the expected string.
* @return A summary for the given messages.
@@ -4301,34 +4300,6 @@ dc_lot_t* dc_msg_get_summary (const dc_msg_t* msg, const dc_cha
char* dc_msg_get_summarytext (const dc_msg_t* msg, int approx_characters);
/**
* Get a message summary.
* Used for notifications, pinned messages and for copying texts to clipboard.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param add_forwarded 1=add a hint if a message was forwarded.
* 0=do not add the hint;
* this is allowed only if the message is shown without a username, e.g. for pinned messages.
* @param add_type_emoji 1=add the type of the message as an emoji prefix.
* 0=do not add type emoji;
* this is allowed only if the message type is visible otherwise,
* close to where the emoji would be displayed otherwise
* (eg. images in pinned messages preceding summary)
* @param approx_chars >0=convert linebreaks to spaces and truncate the line at about the given number of characters;
* for single-line UI elements such as pinned messages or notifications;
* additionally, UIs will most times clip the text to the available space.
* 0=leave linebreaks as is and return the full text, useful e.g. for copying to clipboard.
* @return A summary for the given message.
* The returned string must be released using dc_str_unref().
* Returns an empty string on errors, never returns NULL.
*/
char* dc_msg_get_summary_text (const dc_msg_t* msg, int add_forwarded, int add_type_emoji, int approx_chars);
/**
* Get the name that should be shown over the message (in a group chat) instead of the contact
* display name, or NULL.
+8 -31
View File
@@ -12,6 +12,7 @@
extern crate human_panic;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::mem::ManuallyDrop;
@@ -3472,7 +3473,6 @@ pub unsafe extern "C" fn dc_msg_get_summary(
Box::into_raw(Box::new(summary.into()))
}
// deprecated, use dc_msg_get_summary_text instead
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_msg_get_summarytext(
msg: *mut dc_msg_t,
@@ -3484,37 +3484,14 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
}
let ffi_msg = unsafe { &mut *msg };
let add_forwarded = true;
let add_type_emoji = true;
block_on(ffi_msg.message.get_summary_text_ext(
&ffi_msg.context,
add_forwarded,
add_type_emoji,
usize::try_from(approx_characters).unwrap_or_default(),
))
.strdup()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn dc_msg_get_summary_text(
msg: *mut dc_msg_t,
add_forwarded: libc::c_int,
add_type_emoji: libc::c_int,
approx_chars: libc::c_int,
) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_summary_text()");
return "".strdup();
let summary = block_on(ffi_msg.message.get_summary(&ffi_msg.context, None))
.context("dc_msg_get_summarytext failed")
.log_err(&ffi_msg.context)
.unwrap_or_default();
match usize::try_from(approx_characters) {
Ok(chars) => summary.truncated_text(chars).strdup(),
Err(_) => summary.text.strdup(),
}
let ffi_msg = unsafe { &mut *msg };
block_on(ffi_msg.message.get_summary_text_ext(
&ffi_msg.context,
add_forwarded != 0,
add_type_emoji != 0,
usize::try_from(approx_chars).unwrap_or(0),
))
.strdup()
}
#[unsafe(no_mangle)]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc-bindings"
version = "2.64.0-dev"
version = "2.63.0-dev"
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
edition = "2024"
license = "MPL-2.0"
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.64.0-dev"
"version": "2.63.0-dev"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.64.0-dev"
version = "2.63.0-dev"
description = "DeltaChat JSON-RPC API"
edition = "2024"
license = "MPL-2.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.64.0-dev"
version = "2.63.0-dev"
license = "MPL-2.0"
edition = "2024"
repository = "https://github.com/chatmail/core"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.64.0-dev"
version = "2.63.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
-33
View File
@@ -1,33 +0,0 @@
import os
import sys
import pytest
def anonymous_mib(pid):
with open(f"/proc/{pid}/smaps_rollup") as f:
for line in f:
if line.startswith("Anonymous:"):
return int(line.split()[1]) // 1024
raise LookupError("Anonymous")
@pytest.mark.skipif(sys.platform != "linux", reason="reads /proc")
def test_attachment_memory_is_returned(acf, rpc, tmp_path):
# See also comments for `tune_malloc` in `deltachat-rpc-server/src/main.rs`
ac1, ac2 = acf.get_online_accounts(2)
chat1 = acf.get_accepted_chat(ac1, ac2)
chat2 = ac2.create_chat(ac1)
blob = tmp_path / "blob.bin"
blob.write_bytes(os.urandom(20 << 20))
before = anonymous_mib(rpc.process.pid)
for sender_chat, receiver in ((chat1, ac2), (chat2, ac1)):
sender_chat.send_file(str(blob))
event = receiver.wait_for_incoming_msg_event()
assert receiver.get_message_by_id(event.msg_id).get_snapshot().file_bytes == 20 << 20
for ac in (ac1, ac2):
rpc.wait_for_all_work_done(ac.id)
grown = anonymous_mib(rpc.process.pid) - before
assert grown < 64, f"the server kept {grown} MiB after two 20 MiB attachments"
+1 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.64.0-dev"
version = "2.63.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
license = "MPL-2.0"
@@ -14,7 +14,6 @@ deltachat = { workspace = true }
anyhow = { workspace = true }
futures-lite = { workspace = true }
libc = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.64.0-dev"
"version": "2.63.0-dev"
}
-24
View File
@@ -22,32 +22,8 @@ use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
/// Pins Linux glibc's mmap threshold so that freed message buffers go back to the kernel.
///
/// See M_MMAP_THRESHOLD in <https://man7.org/linux/man-pages/man3/mallopt.3.html>:
/// glibc by default starts with a M_MMAP_THRESHOLD threshold of 128 KiB
/// but raises it to the size of every freed block that exceeds it,
/// up to 32 MiB on 64-bit systems,
/// and trims the heap only from its top end once twice that much is free.
/// Fixating the threshold disables the adjustment: allocations at or above it
/// that the free list cannot satisfy are mmapped and unmapped on free,
/// at the price of the kernel zeroing each such buffer after unmap.
/// Large message processing (allocations above 128KiB) very slightly slows
/// down to the kernel zeroing the buffers, but it's hardly measurable,
/// while overall process memory allocation behaviour significantly improves.
#[cfg(all(target_os = "linux", target_env = "gnu"))]
fn tune_malloc() {
unsafe {
libc::mallopt(libc::M_MMAP_THRESHOLD, 128 * 1024);
}
}
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
fn tune_malloc() {}
#[tokio::main(flavor = "multi_thread")]
async fn main() {
tune_malloc();
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interfering with JSON-RPC using stdout.
-1
View File
@@ -63,7 +63,6 @@ ignore = [
# Please keep this list alphabetically sorted.
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "base64", version = "0.22.1" },
{ name = "bitflags", version = "1.3.2" },
{ name = "constant_time_eq", version = "0.3.1" },
{ name = "cpufeatures", version = "0.2.17" },
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.64.0-dev"
version = "2.63.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"
+1 -1
View File
@@ -188,7 +188,7 @@ class Message:
def get_summarytext(self, width: int) -> str:
"""Get a message summary as a single line of text. Typically used for notifications."""
return from_dc_charpointer(lib.dc_msg_get_summary_text(self._dc_msg, 1, 1, width))
return from_dc_charpointer(lib.dc_msg_get_summarytext(self._dc_msg, width))
@props.with_doc
def time_sent(self):
+1 -1
View File
@@ -1 +1 @@
2026-10-09
2026-09-22
+1 -18
View File
@@ -350,24 +350,7 @@ impl<'a> BlobObject<'a> {
*vt = Viewtype::Image;
return Ok(name);
}
let mut img = match fmt {
image::ImageFormat::WebP => {
// `with_guessed_format()` restores file position,
// so `buf_reader` is at the beginning of the file.
let buf_reader = imgreader.into_inner();
let webp_decoder = image::codecs::webp::WebPDecoder::new(buf_reader)
.context("Failed to create WebP decoder")?;
// If WebP has animation, do not try to recode it.
// Recoding into JPEG will result in losing the animation.
if !is_avatar && webp_decoder.has_animation() {
return Ok(name);
}
DynamicImage::from_decoder(webp_decoder)?
}
_ => imgreader.decode().context("Failed to decode image")?,
};
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif
.as_ref()
.map(|exif| exif_orientation(exif, context))
+1 -67
View File
@@ -4,9 +4,7 @@ use super::*;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::sql;
use crate::test_utils::{
self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
};
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
use crate::tools::SystemTime;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -740,70 +738,6 @@ async fn test_send_gif_as_sticker() -> Result<()> {
Ok(())
}
/// Tests that animated WebP is sent without reencoding.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_animated_webp_as_image() -> Result<()> {
let bytes = include_bytes!("../../test-data/image/animated.webp");
assert!(
bytes.len() > constants::WORSE_IMAGE_SIZE as usize,
"Animated WebP should be large enough to trigger reencoding"
);
let (width, height) = (900u32, 900u32);
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(
Config::MediaQuality,
Some(&(MediaQuality::Worse as i32).to_string()),
)
.await?;
let file = alice.get_blobdir().join("file").with_extension("webp");
fs::write(&file, &bytes)
.await
.context("Failed to write file")?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_and_deduplicate(alice, &file, Some("file.webp"), None)?;
let chat = alice.create_chat(bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, width);
assert_eq!(bob_msg.get_height() as u32, height);
assert_eq!(
bob_msg.get_filebytes(bob).await?.unwrap(),
bytes.len() as u64
);
Ok(())
}
/// Tests that if user sets animated WebP as an avatar, it may be recoded.
///
/// We don't want to recode animated WebPs into JPEG and lose animation,
/// but for avatars we don't want animation and transparency anyway.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_animated_webp_avatar() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let avatar_src = t.dir.path().join("avatar.webp");
let avatar_bytes = include_bytes!("../../test-data/image/animated.webp");
fs::write(&avatar_src, avatar_bytes).await.unwrap();
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await?;
let avatar_blob = t.get_config(Config::Selfavatar).await?.unwrap();
assert!(avatar_blob.ends_with(".jpg"));
let scaled_avatar_size = fs::metadata(&avatar_blob).await.unwrap().len();
assert!(
scaled_avatar_size < avatar_bytes.len() as u64,
"Animated WebP avatar must be recoded"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_deduplicate() -> Result<()> {
let t = TestContext::new().await;
+3 -1
View File
@@ -3,6 +3,7 @@
use std::cmp;
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::io::Cursor;
use std::marker::Sync;
use std::path::{Path, PathBuf};
use std::time::Duration;
@@ -1917,7 +1918,8 @@ impl Chat {
let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
let html_part = MimePart::new("text/html", s);
let mut buffer = Vec::new();
html_part.write_part(&mut buffer);
let cursor = Cursor::new(&mut buffer);
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 {
+21 -11
View File
@@ -1457,6 +1457,7 @@ impl Session {
fn drain_unsolicited_responses(&self, context: &Context) -> bool {
use UnsolicitedResponse::*;
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
let folder = self.selected_folder.as_deref().unwrap_or_default();
let mut should_refetch = false;
@@ -1471,19 +1472,28 @@ impl Session {
}
Expunge(_) | Recent(_) => {}
Other(ref response_data) => match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
Other(ref response_data) => {
match response_data.parsed() {
Response::Fetch { .. } => {
info!(
context,
"Need to refetch {folder:?}, got unsolicited FETCH {response:?}"
);
should_refetch = true;
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them.
Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
} => {}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
}
},
}
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
+15 -8
View File
@@ -1,6 +1,7 @@
//! # MIME message production.
use std::collections::{BTreeSet, HashSet};
use std::io::Cursor;
use anyhow::{Context as _, Result, bail, format_err};
use base64::Engine as _;
@@ -244,20 +245,25 @@ pub(crate) fn render_queued_mail(
let is_encrypted = encryption.is_encrypted();
fn add_header(name: &[u8], value: &impl mail_builder::headers::Header, headers: &mut Vec<u8>) {
fn add_header(
name: &[u8],
value: &impl mail_builder::headers::Header,
headers: &mut Vec<u8>,
) -> Result<()> {
headers.extend(name);
value.write_header(headers, name.len());
value.write_header(headers, name.len())?;
Ok(())
}
let from_header = new_address_with_name(&display_name, from_addr.clone());
add_header(b"From: ", &from_header, &mut inner_headers);
add_header(b"From: ", &from_header, &mut inner_headers)?;
if is_encrypted {
let unencrypted_from = Address::new_address(None::<&'static str>, from_addr.clone());
add_header(b"From: ", &unencrypted_from, &mut outer_headers);
add_header(b"HP-Outer: From: ", &unencrypted_from, &mut inner_headers);
add_header(b"From: ", &unencrypted_from, &mut outer_headers)?;
add_header(b"HP-Outer: From: ", &unencrypted_from, &mut inner_headers)?;
} else {
add_header(b"From: ", &from_header, &mut outer_headers);
add_header(b"From: ", &from_header, &mut outer_headers)?;
}
if is_encrypted {
@@ -328,7 +334,7 @@ pub(crate) fn render_queued_mail(
} else {
&mut outer_headers
},
);
)?;
}
if is_encrypted {
@@ -2427,7 +2433,8 @@ pub(crate) async fn keyupdate_message(
/// Renders MIME part into a vector of bytes.
pub(crate) fn part_to_bytes(message: MimePart<'static>) -> Vec<u8> {
let mut raw_message = Vec::new();
message.write_part(&mut raw_message);
let cursor = Cursor::new(&mut raw_message);
message.write_part(cursor).ok();
raw_message
}
+38 -36
View File
@@ -5,7 +5,7 @@ use pgp::armor;
use pgp::packet::{Packet, PacketParser};
use pretty_assertions::assert_eq;
use regex::regex;
use std::io::{BufReader, Cursor};
use std::io::BufReader;
use std::str;
use std::time::Duration;
@@ -30,7 +30,8 @@ fn render_email_address(display_name: &str, addr: &str) -> String {
let mut output = Vec::<u8>::new();
new_address_with_name(display_name, addr.to_string())
.unwrap_address()
.write_header(&mut output, 0);
.write_header(&mut output, 0)
.unwrap();
String::from_utf8(output).unwrap()
}
@@ -49,7 +50,9 @@ fn test_render_email_address() {
let s = render_email_address(display_name, addr);
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>\r\n");
println!("{s}");
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>");
}
#[test]
@@ -67,14 +70,14 @@ fn test_render_email_address_noescape() {
let s = render_email_address(display_name, addr);
// Addresses should not be unnecessarily be encoded, see <https://github.com/deltachat/deltachat-core-rust/issues/1575>:
assert_eq!(s, "\"a space\" <x@y.org>\r\n");
assert_eq!(s, r#""a space" <x@y.org>"#);
}
#[test]
fn test_render_email_address_duplicated_as_name() {
let addr = "x@y.org";
let s = render_email_address(addr, addr);
assert_eq!(s, "<x@y.org>\r\n");
assert_eq!(s, "<x@y.org>");
}
#[test]
@@ -99,7 +102,8 @@ fn render_header_text(text: &str) -> String {
// Some non-zero length of the header name.
let bytes_written = 20;
mail_builder::headers::text::Text::new(text.to_string())
.write_header(&mut output, bytes_written);
.write_header(&mut output, bytes_written)
.unwrap();
String::from_utf8(output).unwrap()
}
@@ -951,7 +955,7 @@ async fn test_no_empty_to_header() -> Result<()> {
assert!(
// It would be equally fine if the payload contained `To: alice@example.org` or similar,
// as long as it's a valid header
payload.contains("To: \"hidden-recipients\":;"),
payload.contains("To: \"hidden-recipients\": ;"),
"Payload doesn't contain correct To: header: {payload}"
);
@@ -1060,8 +1064,8 @@ MIME-Version: 1.0
To: "hidden-recipients": ;
Subject: [...]
Chat-Version: 1.0
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="BOUNDARY"
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
boundary="BOUNDARY"
--BOUNDARY
@@ -1102,18 +1106,17 @@ async fn test_render_unencrypted_msg_basic() -> Result<()> {
let expected = r#"From: <alice@example.org>
Message-ID: <MESSAGE_ID@localhost>
MIME-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: text/plain; charset="utf-8"
Date: DATE
To: <bob@example.net>
@@ -1123,7 +1126,7 @@ Chat-Version: 1.0
Content-Transfer-Encoding: 7bit
Hello!"#
.replace("\n", "\r\n");
.replace("\n", "\r\n");
assert_eq!(
unencrypted, expected,
"---------------- Actual: ----------------
@@ -1153,20 +1156,19 @@ async fn test_render_unencrypted_msg_with_attachment() -> Result<()> {
let expected = r#"From: <alice@example.org>
Message-ID: <MESSAGE_ID@localhost>
MIME-Version: 1.0
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual;
keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: multipart/mixed;
boundary="BOUNDARY"
Autocrypt: addr=alice@example.org; prefer-encrypt=mutual; keydata=mDMEXlh13RYJKwYBBAHaRw8BAQdAzfVIAleCXMJrq8VeLlEVof6ITCviMktKjmcBKAu4m5
DCtAQfFggAZgUCXlh13RYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDAhsDAh4JBAsJCAcFFQgJCgsDFgIB
AycJAgIZASwUgAAAAAASABFyZWxheXNAY2hhdG1haWwuYXRhbGljZUBleGFtcGxlLm9yZwAAb1QA/0
HbvPN3/Vn02Gk1dcQMEcyGyETld9dSsRo8uwHAyW35AQCrFJjAQFLTud7XK61uYt9BC/QHipCfIGbq
X1FjMbTUC80TPGFsaWNlQGV4YW1wbGUub3JnPsKRBBMWCAA5BQJeWHXdFiEELm+iyyO1MtcoY0tYZL
CPYantlEMCGwMCHgkECwkIBwUVCAkKCwMWAgEDJwkCAhkBAAoJEGSwj2Gp7ZRD1m4A/iOifEzIOiP8
wW0O8I/sg69gQtG8Czn4MsVV6Ea1EyIqAP4uByHaUJdy8MSQPfv/Usr09KsidNgy2Jh37yg82fKUBr
g4BF5Ydd0SCisGAQQBl1UBBQEBB0AG7cjWy2SFAU8KnltlubVW67rFiyfp01JrRe6Xqy22HQMBCAeI
eAQYFggAIBYhBC5vossjtTLXKGNLWGSwj2Gp7ZRDBQJeWHXdAhsMAAoJEGSwj2Gp7ZRDLo8BAObE8G
nsGVwKzNqCvHeWgJsqhjS3C6gvSlV3tEm9XmF6AQDXucIyVfoBwoyMh2h6cSn/ATn5QJb35pgo+ivp
3jsMAg==
Content-Type: multipart/mixed;
boundary="BOUNDARY"
Date: DATE
To: <bob@example.net>
Subject: Message from alice@example.org
-1
View File
@@ -2957,7 +2957,6 @@ async fn test_weird_and_duplicated_filenames() -> Result<()> {
"a.tar.gz",
"a.a..a.a.a.a.tar.gz",
"a. tar.tar.gz",
"very long filename with emoji \u{1F600} very long file name with emoji \u{1FAE0} very long file name with emoji.tar.gz",
] {
let attachment = alice.blobdir.join(filename_sent);
let content = "File content of tar.gz archive".to_string();
+49 -22
View File
@@ -8,8 +8,7 @@ use anyhow::{Context as _, Result, bail};
use rusqlite::{Connection, OpenFlags, Row, config::DbConfig, types::ValueRef};
use tokio::sync::RwLock;
use crate::blob::BlobObject;
use crate::chat::ChatId;
use crate::chat::{ChatId, add_device_msg};
use crate::config::Config;
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
@@ -214,32 +213,23 @@ impl Sql {
// this should be done before updates that use high-level objects that
// rely themselves on the low-level structure.
let recode_avatar = migrations::run(context, self)
let show_unencrypted_device_msg = migrations::run(context, self)
.await
.context("failed to run migrations")?;
// (2) updates that require high-level objects
// the structure is complete now and all objects are usable
if recode_avatar && let Some(avatar) = context.get_config(Config::Selfavatar).await? {
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
match blob.recode_to_avatar_size(context).await {
Ok(()) => {
if let Some(path) = blob.to_abs_path().to_str() {
context
.set_config_internal(Config::Selfavatar, Some(path))
.await?;
} else {
warn!(context, "Setting selfavatar failed: non-UTF-8 filename");
}
}
Err(e) => {
warn!(context, "Migrations can't recode avatar, removing. {:#}", e);
context
.set_config_internal(Config::Selfavatar, None)
.await?
}
}
if show_unencrypted_device_msg {
let txt = r#"To keep Delta Chat simpler and more secure, it no longer sends or receives messages that aren't end-to-end encrypted.
Nothing is lost: your encrypted chats work as before, and old unencrypted chats remain readable.
To keep sending and receiving unencrypted email, use a regular email app. You can find your email password in Delta Chat under "Settings → Advanced → Relays": tap (or right-click) your address and choose "Edit Relay".
More details: https://..."#;
let mut msg = crate::message::Message::new_text(txt.to_string());
add_device_msg(context, Some("unencrypted-device-msg"), Some(&mut msg)).await?;
}
Ok(())
@@ -684,6 +674,43 @@ impl Sql {
}
}
pub(crate) trait TransactionExt {
/// Used for executing `SELECT COUNT` statements only. Returns the resulting count.
fn count(&self, query: &str, params: impl rusqlite::Params + Send) -> Result<usize>;
/// Executes a query which is expected to return one row and one
/// column. If the query does not return any rows, returns `Ok(None)`.
fn query_get_value<T>(
&self,
query: &str,
params: impl rusqlite::Params + Send,
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql + Send + 'static;
}
impl TransactionExt for rusqlite::Transaction<'_> {
fn count(&self, query: &str, params: impl rusqlite::Params + Send) -> Result<usize> {
let count: isize = self.query_row(query, params, |row| row.get(0))?;
Ok(usize::try_from(count)?)
}
fn query_get_value<T>(
&self,
query: &str,
params: impl rusqlite::Params + Send,
) -> Result<Option<T>>
where
T: rusqlite::types::FromSql + Send + 'static,
{
match self.query_row(query, params, |row| row.get::<_, T>(0)) {
Ok(res) => Ok(Some(res)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(err) => Err(err.into()),
}
}
}
/// Creates a new SQLite connection.
///
/// `path` is the database path.
+122 -3
View File
@@ -16,6 +16,7 @@ use crate::key::DcKey;
use crate::log::warn;
use crate::sql::Sql;
use crate::sql::TransactionExt as _;
use crate::tools::{self, Time, inc_and_check, time_elapsed};
use crate::transport::ConfiguredLoginParam;
@@ -713,6 +714,111 @@ pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
Ok(())
}
fn unencrypted_chats_migration(
context: &Context,
transaction: &mut rusqlite::Transaction<'_>,
) -> Result<bool> {
let mut show_unencrypted_device_msg = false;
// Migrate:
// - all unencrypted (i.e. ad-hoc) groups to have 0 members
// - all chats of type Mailinglist into a group with 0 members
transaction.execute_batch(
"
CREATE TEMP TABLE temp.legacy_unencrypted_chats(chat_id INTEGER PRIMARY KEY, type INTEGER) STRICT;
INSERT INTO temp.legacy_unencrypted_chats(chat_id, type)
SELECT id, type FROM chats
WHERE ((type=120 AND grpid='') OR type=140) -- Ad-hoc groups and mailinglists
AND id>9;
DELETE FROM chats_contacts
WHERE chat_id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats);
UPDATE chats SET type=120
WHERE id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats);
",
)?;
// Rewrite all address-contacts to have "Hidden" origin.
// We still need the contacts because we want to keep the messages, and every message needs a sender.
// Make sure that the address is available in the name, so that the user can still see it.
transaction.execute_batch(
"
UPDATE contacts
SET origin=8 -- Origin::Hidden
WHERE fingerprint='' AND id>9;
UPDATE contacts
SET name=name || ' (' || addr || ')'
WHERE fingerprint='' AND id>9 AND name!='';
UPDATE contacts
SET authname=authname || ' (' || addr || ')'
WHERE fingerprint='' AND id>9 AND name='' AND authname!='';
UPDATE contacts
SET authname=addr
WHERE fingerprint='' AND id>9 AND name='' AND authname='';
-- Also update the names of the chats:
UPDATE chats
SET name = (
SELECT CASE
WHEN c.name != '' THEN c.name
WHEN c.authname != '' THEN c.authname
ELSE c.addr
END
FROM chats_contacts cc
JOIN contacts c ON c.id = cc.contact_id
WHERE cc.chat_id = chats.id
)
WHERE type = 100 AND id > 9
AND EXISTS (
SELECT 1 FROM chats_contacts cc
JOIN contacts c ON c.id = cc.contact_id
WHERE cc.chat_id = chats.id AND c.fingerprint = '' AND c.id > 9
);
",
)?;
let legacy_chats =
transaction.count("SELECT COUNT(*) FROM temp.legacy_unencrypted_chats", ())?;
let legacy_contacts = transaction.count(
"SELECT COUNT(*) FROM contacts WHERE fingerprint='' AND id>9",
(),
)?;
if legacy_chats > 0 || legacy_contacts > 0 {
// Set the gray letter avatar for all legacy chats:
let blob = crate::blob::BlobObject::create_and_deduplicate_from_bytes(
context,
include_bytes!("../../assets/icon-unencrypted.png"),
"icon-unencrypted.png",
)?;
let new_param = &format!("i={}", blob.as_name());
transaction.execute(
"UPDATE chats SET param=? WHERE id IN (SELECT chat_id FROM temp.legacy_unencrypted_chats)",
(new_param,),
)?;
transaction.execute(
"UPDATE contacts SET param=? WHERE fingerprint='' AND id>9",
(new_param,),
)?;
let force_encryption: Option<String> = transaction.query_get_value(
"SELECT value FROM config WHERE keyname='force_encryption'",
(),
)?;
if force_encryption == Some("0".to_string()) {
show_unencrypted_device_msg = true;
}
}
transaction.execute("DROP TABLE temp.legacy_unencrypted_chats", ())?;
Ok(show_unencrypted_device_msg)
}
impl Sql {
async fn set_db_version(&self, version: i32) -> Result<()> {
self.set_raw_config_int(VERSION_CFG, version).await?;
@@ -812,7 +918,7 @@ pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
}
let dbversion = dbversion_before_update;
let mut recode_avatar = false;
let mut show_unencrypted_device_msg = false;
if dbversion < 1 {
sql.execute_migration(
@@ -1187,7 +1293,7 @@ CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0,
.await?;
}
if dbversion < 77 {
recode_avatar = true;
// removed
sql.set_db_version(77).await?;
}
if dbversion < 78 {
@@ -2687,6 +2793,19 @@ CREATE TABLE smtp_success (
.await?;
}
inc_and_check(&mut migration_version, 169)?;
if dbversion < migration_version {
sql.execute_migration_transaction(
|transaction| {
show_unencrypted_device_msg = unencrypted_chats_migration(context, transaction)?;
Ok(())
},
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?
@@ -2701,7 +2820,7 @@ CREATE TABLE smtp_success (
}
info!(context, "Database version: v{new_version}.");
Ok(recode_avatar)
Ok(show_unencrypted_device_msg)
}
#[cfg(test)]
+101 -2
View File
@@ -1,8 +1,10 @@
use super::*;
use crate::chat;
use crate::chat::Chat;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants;
use crate::constants::Chattype;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::contact::Origin;
@@ -145,7 +147,8 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo);
// All email address contacts are hidden now:
assert_eq!(email_bob.origin, Origin::Hidden);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -178,7 +181,8 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
.unwrap();
let email_bob = Contact::get_by_id(&t, email_bob_id).await?;
assert_eq!(email_bob.is_key_contact(), false);
assert_eq!(email_bob.origin, Origin::OutgoingTo);
// All email address contacts are hidden now:
assert_eq!(email_bob.origin, Origin::Hidden);
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
assert_eq!(email_bob.fingerprint(), None);
@@ -227,3 +231,98 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
Ok(())
}
/// Creates a context right before the unencrypted-chats migration (v169).
async fn context_before_unencrypted_migration() -> TestContext {
STOP_MIGRATIONS_AT
.scope(168, async move { TestContext::new_alice().await })
.await
}
/// Adds legacy data: a 1:1 chat (chat 10), an ad-hoc group (chat 11),
/// a mailing list (chat 12) and an address-contact (contact 10).
async fn add_legacy_data(t: &TestContext) -> Result<()> {
t.sql
.call_write(|conn| {
conn.execute_batch(
r#"
INSERT INTO contacts (id, name, addr, origin, fingerprint)
VALUES (10, 'Bob', 'bob@example.net', 16384, '');
INSERT INTO chats (id, type, name, grpid) VALUES
(10, 100, 'Bob', ''),
(11, 120, 'Thread', ''),
(12, 140, 'List', 'list.example.org');
INSERT INTO chats_contacts (chat_id, contact_id) VALUES
(10, 10), (11, 1), (11, 10), (12, 10);"#,
)?;
Ok(())
})
.await
}
/// Legacy unencrypted 1:1 chats get the email address put into the name,
/// and get the "unencrypted" avatar.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_chats_migration_1to1_chat() -> Result<()> {
let t = &context_before_unencrypted_migration().await;
add_legacy_data(t).await?;
t.sql.run_migrations(t).await?;
let bob = Contact::get_by_id(t, ContactId::new(10)).await?;
assert_eq!(bob.get_display_name(), "Bob (bob@example.net)");
assert!(bob.get_profile_image(t).await?.unwrap().exists());
let chat = Chat::load_from_db(t, ChatId::new(10)).await?;
assert_eq!(chat.get_name(), "Bob (bob@example.net)");
assert!(chat.get_profile_image(t).await?.unwrap().exists());
Ok(())
}
/// Legacy ad-hoc groups and mailing lists become read-only groups without members
/// and get the "unencrypted" avatar.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_chats_migration_readonly_groups() -> Result<()> {
let t = &context_before_unencrypted_migration().await;
add_legacy_data(t).await?;
t.sql.run_migrations(t).await?;
for chat_id in [11, 12] {
let chat_id = ChatId::new(chat_id);
let chat = Chat::load_from_db(t, chat_id).await?;
assert_eq!(chat.typ, Chattype::Group);
assert!(chat::get_chat_contacts(t, chat_id).await?.is_empty());
assert_eq!(chat.can_send(t).await?, false);
assert!(chat.get_profile_image(t).await?.unwrap().exists());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unencrypted_chats_migration_device_msg() -> Result<()> {
for (force_encryption, has_legacy_data, expect_device_msg) in [
(Some("0"), true, true),
(Some("1"), true, false),
(None, true, false),
(Some("0"), false, false),
] {
let t = &context_before_unencrypted_migration().await;
t.sql
.set_raw_config("force_encryption", force_encryption)
.await?;
if has_legacy_data {
add_legacy_data(t).await?;
}
t.sql.run_migrations(t).await?;
let shown = chat::was_device_msg_ever_added(t, "unencrypted-device-msg").await?;
assert_eq!(shown, expect_device_msg);
// This assert will need to be removed once we remove the ForceEncryption config,
// but it is useful for now to check that the logic is implemented correctly:
let allow_unencrypted = !t.get_config_bool(Config::ForceEncryption).await?;
assert_eq!(shown, allow_unencrypted && has_legacy_data);
}
Ok(())
}
+18 -98
View File
@@ -10,7 +10,6 @@ use num_traits::FromPrimitive;
use crate::calls::{CallState, call_state};
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::constants::DC_DESIRED_TEXT_LEN;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, Viewtype};
@@ -118,9 +117,15 @@ impl Summary {
None
};
let mut text = msg.get_summary_text(context).await;
if text.is_empty() && msg.quoted_text().is_some() {
text = stock_str::reply_noun(context)
}
Ok(Summary {
prefix,
text: msg.get_summary_text(context).await,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
})
@@ -133,32 +138,19 @@ impl Summary {
}
impl Message {
/// Returns a summary text with emoji and "Forwarded:" prefixes.
/// This is the standard summary to be used in chatlists, notifications etc.
/// Returns a summary text.
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
let add_forwarded = true;
let add_type_emoji = true;
self.get_summary_text_ext(context, add_forwarded, add_type_emoji, DC_DESIRED_TEXT_LEN)
.await
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context), summary)
} else {
summary
}
}
/// Returns a summary text with emoji prefixes but without "Forwarded:" prefix.
/// Used for shorter reaction summaries as "USER reacts 👋 to SUMMARY"
/// Returns a summary text without "Forwarded:" prefix.
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
let add_forwarded = false;
let add_type_emoji = true;
self.get_summary_text_ext(context, add_forwarded, add_type_emoji, DC_DESIRED_TEXT_LEN)
.await
}
/// Returns a summary text with optional "Forwarded:" and emoji prefixes and optionally converting to one line.
pub async fn get_summary_text_ext(
&self,
context: &Context,
add_forwarded: bool,
add_type_emoji: bool,
approx_chars: usize,
) -> String {
let (emoji, type_name, type_file, append_text);
let viewtype = match self
.param
@@ -272,11 +264,7 @@ impl Message {
}
};
let text = if approx_chars > 0 {
truncate(self.text.trim(), approx_chars).to_string()
} else {
self.text.trim().to_string()
};
let text = self.text.clone();
let summary = if let Some(type_file) = type_file {
if append_text && !text.is_empty() {
@@ -298,30 +286,13 @@ impl Message {
"".to_string()
};
let emoji = emoji.filter(|_| add_type_emoji);
let summary = if let Some(emoji) = emoji {
format!("{emoji} {summary}")
} else {
summary
};
let summary = if summary.is_empty() && self.quoted_text().is_some() {
stock_str::reply_noun(context)
} else {
summary
};
let summary = if add_forwarded && self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context), summary)
} else {
summary
};
if approx_chars > 0 {
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
} else {
summary
}
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
}
}
@@ -343,13 +314,6 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let forwarded = true;
let emoji = true;
let one_line = 2000;
let no_forwarded = false;
let no_emoji = false;
let multi_line = 0;
let d = TestContext::new_alice().await;
let ctx = &d.ctx;
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
@@ -366,22 +330,12 @@ mod tests {
let msg = Message::new_text(some_text.to_string());
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, emoji, multi_line)
.await,
"bla \t\n\tbla" // lineends are preserved, but text is still trimmed
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Image);
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
.unwrap();
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"Image"
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Image);
@@ -389,16 +343,6 @@ mod tests {
msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
.unwrap();
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"bla bla"
);
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, emoji, multi_line)
.await,
"📷 bla \t\n\tbla"
);
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::Video);
@@ -529,30 +473,6 @@ mod tests {
msg.get_summary_text_without_prefix(ctx).await,
"📎 foo.bar \u{2013} bla bla"
); // skipping prefix used for reactions summaries
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, one_line)
.await,
"foo.bar \u{2013} bla bla"
);
assert_eq!(
msg.get_summary_text_ext(ctx, forwarded, no_emoji, one_line)
.await,
"Forwarded: foo.bar \u{2013} bla bla"
);
d.assert_warn("Not a valid DeltaChat vCard").await;
// If nothing else is present, but the message is a reply, we say so. needed for summary of draft
let mut msg = Message::new_text("".to_string());
msg.set_quote_text(Some(("blubb".to_string(), true)));
assert_summary_texts(&msg, ctx, "Reply").await;
assert_eq!(
msg.get_summary_text_ext(ctx, no_forwarded, no_emoji, multi_line)
.await,
"Reply"
);
// If there is nothing, the summary is empty
let msg = Message::new_text("".to_string());
assert_summary_texts(&msg, ctx, "").await;
}
}
+1 -4
View File
@@ -105,10 +105,7 @@ async fn check_aeap_transition(chat_for_transition: ChatForTransition) {
check_that_transition_worked(bob, &groups, alice_contact, ALICE_NEW_ADDR).await;
tcm.section("Test switching back");
alice
.set_primary_self_addr("alice@example.org")
.await
.unwrap();
tcm.change_addr(alice, "alice@example.org").await;
let sent = alice
.send_text(chat_to_send, "Hello from my old addr!")
.await;
+2 -2
View File
@@ -740,14 +740,14 @@ pub(crate) fn maybe_update_sending_transport(
pub async fn add_pseudo_transport(context: &Context, addr: &str) -> Result<()> {
context.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
addr,
serde_json::to_string(&EnteredLoginParam{addr: addr.to_string(), ..Default::default()})?,
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
)
.await.with_context(|| format!("Failed to insert pseudo transport for {addr:?}"))?;
.await?;
Ok(())
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 676 KiB