Compare commits

..
Author SHA1 Message Date
link2xt d8b9fd5005 ci: test with Python 3.15
Also moved python version to environment variable
so status checks will be named
"JSON-RPC Python tests (ubuntu-latest, latest)" instead of
"JSON-RPC Python tests (ubuntu-latest, 3.14)" and we don't have to
update the list of required status checks each time the version changes.
We already do the same for Rust.
2026-10-10 18:46:18 +00:00
link2xt 694ef8ba76 chore: bump version to 2.64.0-dev 2026-10-09 17:59:58 +00:00
link2xt de6f469710 chore(release): prepare for 2.63.0 2026-10-09 17:39:17 +00:00
link2xt 81140d514f fix: do not reencode animated WebPs into JPEG
Reencoding animated WebPs into JPEG loses all frames except the first one.
Avatars we still reencode because we don't want animated avatars anyway.

animated.webp is produced with this script using Pillow 12.3.0:
```
#!/usr/bin/env python3
from PIL import Image, ImageOps

with Image.open("avatar900x900.png") as im:
    frames = [im.rotate(x) for x in range(10, 360, 10)]
    im.save(
        "animated.webp",
        "webp",
        append_images=frames,
        lossless=True,
        duration=200,
        save_all=True,
    )
```

Resulting image is 677K, just slightly larger than the size needed to trigger reencoding.
2026-10-09 15:48:18 +00:00
holger krekel a8b6246052 fix: (linux-only) make sure large attachments return memory to kernel
The test fails on main with ~150MB memory remaining allocated
after sending/receiving two 20MB messages,
and stays well below when explicitely setting libc's M_MMAP_THRESHOLD.
2026-10-09 17:12:17 +02:00
biørnandHocuri 4ea5cddc09 api: new dc_msg_get_summary_text() (#8813)
this PR adds a new `msg_get_summary_text(add_forwarded, add_type_emoji,
approx_chars)` api, replacing the old `get_summarytext(approx_chars)`.

checking the concrete existing and new usecases, the new options are
required, eg. for:

- pinned messages: they do not show the user name (therefore, there
should not be a "forward" indicator), and may or may not not e.g.
display image left of string (this makes type emoji optional)

- notifications, quotes: they show user name (therefore, "forward"
indicator needed) and often display e.g. image a bit off (so type emoji
is potentially useful, cmp. whatsapp, but potentially also optional)

- copy to clipboard: clipboard should get forwarded and emoji, but text
should not be truncated and linends should be preserved - esp. the
latter was a silent bug in at least android. to support that better,
`approx_chars` can be set to `0` to skip truncation and unwrapping.

| Use case | `add_forwarded` | `add_type_emoji` | `approx_chars>0` |
| --- | :---: | :---: | :---: |
| Notification | ✅ | ✅ (potentially ❌) | ✅ |
| Quote | ✅  | ✅ (potentially ❌) | ✅ |
| Pinned message | ❌  | ❌ (rarely  ✅) | ✅ |
| Copy to clipboard | ✅ | ✅ | ❌  |
| Chatlist (core internal) | ✅ | ✅ | ✅ |
| Reaction summary (core internal) | ❌ | ✅ | ✅ |

(the get_summary() returning a "lot" can maybe be removed at some point,
in favor of adding the usernames directly to get_summary_text() and
changing add_forwarded to add_usernames. but that is out of scope)

jsonrpc can be added easily once we settle on name etc. 

references: https://github.com/chatmail/core/pull/8812 ,
https://github.com/chatmail/core/pull/8546

cc @Amzd , @adbenitez

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-10-09 13:49:59 +00:00
link2xt f8edd9136a chore: update mail-builder to 1.0 2026-10-09 12:49:55 +00:00
link2xt 697304e98a test: test sending and receiving long filename with emojis
mail-builder changed how such filename is encoded in Content-Disposition between versions 0.5.0 and 1.0.0.
We can parse both encodings already and the test was not failing, but it is worth adding this corner case to the tests.

mail-builder 0.5.0 encoded the filename like this:

Content-Disposition: attachment;
	filename="=?utf-8?Q?very_long_filename_with_emoji_=F0=9F=98=80_very_long_file_name_with_emoji_=F0=9F=AB=A0_very_long_file_name_with_emoji.tar.gz?="

mail-builder 1.0.0 encodes the filename like this:

Content-Disposition: attachment;
 filename*0*=UTF-8''very%20long%20filename%20with%20emoji%20%F0%9F%98%80%20;
 filename*1*=very%20long%20file%20name%20with%20emoji%20%F0%9F%AB%A0%20very;
 filename*2*=%20long%20file%20name%20with%20emoji.tar.gz
2026-10-09 12:49:55 +00:00
link2xt 40c0954e42 chore: update async-imap to 0.12.0
async-imap 0.12.0 uses imap-proto 0.17.0 which includes bugfixes
for parsing responses with unicode characters.

Also upgraded base64 to 0.23 since new async-imap depends on it anyway.
2026-10-08 22:19:32 +00:00
link2xt 3a79324728 refactor: stop excluding COPYUID responses from the logs
We do not move the messages out of the inbox anymore,
so should not get these responses anyway.
2026-10-08 22:19:32 +00:00
link2xt 6ab0cd513a test: do not ignore errors in add_pseudo_transport() 2026-10-08 17:09:55 +00:00
34 changed files with 516 additions and 430 deletions
+10 -8
View File
@@ -25,6 +25,8 @@ env:
# Minimum Supported Rust Version
MSRV: 1.89.0
PYTHON_VERSION: 3.15
jobs:
lint_rust:
name: Lint Rust
@@ -268,11 +270,11 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Rust version.
# Currently used Python version.
- os: ubuntu-latest
python: 3.14
python: latest
- os: macos-latest
python: 3.14
python: latest
# PyPy tests
- os: ubuntu-latest
@@ -303,7 +305,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
- name: Install tox
run: pip install tox
@@ -324,11 +326,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.14
python: latest
- os: macos-latest
python: 3.14
python: latest
- os: windows-latest
python: 3.14
python: latest
# PyPy tests
- os: ubuntu-latest
@@ -351,7 +353,7 @@ jobs:
- name: Install python
uses: actions/setup-python@v7.0.0
with:
python-version: ${{ matrix.python }}
python-version: ${{ matrix.python == 'latest' && env.PYTHON_VERSION || matrix.python }}
- name: Install tox
run: pip install tox
+80
View File
@@ -1,5 +1,84 @@
# 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
@@ -8903,3 +8982,4 @@ 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
+34 -27
View File
@@ -272,19 +272,19 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.11.3"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a6728e0f7931b36d725ac234fcb02539e9f7888dbeaaa8a18d9ea5792181570"
checksum = "f97b87216c9f0ccc63c516263169085fa34bba25633f6d96c6a9b9ca1fd5f70a"
dependencies = [
"async-channel 2.5.0",
"async-compression",
"base64",
"base64 0.23.1",
"bytes",
"chrono",
"futures",
"futures-util",
"imap-proto",
"log",
"nom 7.1.3",
"nom 8.0.0",
"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",
"base64 0.22.1",
"futures",
"log",
"nom 8.0.0",
@@ -436,6 +436,12 @@ 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"
@@ -833,7 +839,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
dependencies = [
"base64",
"base64 0.22.1",
"encoding_rs",
]
@@ -1328,7 +1334,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1338,7 +1344,7 @@ dependencies = [
"async-native-tls",
"async-smtp",
"async_zip",
"base64",
"base64 0.23.1",
"blake3",
"brotli",
"bytes",
@@ -1434,11 +1440,11 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"async-channel 2.5.0",
"base64",
"base64 0.23.1",
"deltachat",
"deltachat-contact-tools",
"futures",
@@ -1455,14 +1461,14 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc-bindings"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"deltachat-jsonrpc",
]
[[package]]
name = "deltachat-repl"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -1478,12 +1484,13 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
"deltachat-jsonrpc",
"futures-lite",
"libc",
"log",
"serde",
"serde_json",
@@ -1507,7 +1514,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.63.0-dev"
version = "2.64.0-dev"
dependencies = [
"anyhow",
"deltachat",
@@ -2940,11 +2947,11 @@ dependencies = [
[[package]]
name = "imap-proto"
version = "0.16.5"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de555d9526462b6f9ece826a26fb7c67eca9a0245bd9ff84fa91972a5d5d8856"
checksum = "5ccf963d57074747b455398a1763d174da80bcaba6f51e3671a82252b531a68b"
dependencies = [
"nom 7.1.3",
"nom 8.0.0",
]
[[package]]
@@ -3444,9 +3451,9 @@ checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]]
name = "mail-builder"
version = "0.5.0"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c942e8a4b83f9351236c1e531ea9fa0237913d63c7fc36818430e0128a1ddf3"
checksum = "ccbc27cb02a58a311a404769a22f5abf63fe3173b290d80be7c534f9c7023151"
[[package]]
name = "mailparse"
@@ -4180,7 +4187,7 @@ version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
dependencies = [
"base64",
"base64 0.22.1",
"serde",
]
@@ -4255,7 +4262,7 @@ dependencies = [
"aes-gcm",
"aes-kw",
"argon2",
"base64",
"base64 0.22.1",
"bitfields",
"block-padding",
"blowfish",
@@ -4534,7 +4541,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d6db66007eac4a0ec8331d0d20c734bd64f6445d64bbaf0d0a27fea7a054e36"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"derive_more 1.0.0",
"futures-lite",
@@ -5090,7 +5097,7 @@ version = "0.12.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
@@ -5684,7 +5691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78db9c9912c90ea7487f49bc149b329b535806bfa12b740fbade73f573a3d9f"
dependencies = [
"aes",
"base64",
"base64 0.22.1",
"blake3",
"byte_string",
"bytes",
@@ -5984,7 +5991,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb921f10397d5669e1af6455e9e2d367bf1f9cebcd6b1dd1dc50e19f6a9ac2ac"
dependencies = [
"base64",
"base64 0.22.1",
"bounded-integer",
"byteorder",
"crc",
@@ -6399,7 +6406,7 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fcaf159b4e7a376b05b5bfd77bfd38f3324f5fce751b4213bfc7eaa47affb4e"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-core",
"futures-sink",
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.63.0-dev"
version = "2.64.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.11.3", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.12.0", 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 = "0.5.0", default-features = false }
mail-builder = { version = "1.0.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 = "0.22"
base64 = { version = "0.23", default-features = false, features = ["std"] }
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.63.0-dev"
version = "2.64.0-dev"
description = "Deltachat FFI"
edition = "2024"
license = "MPL-2.0"
+29
View File
@@ -4291,6 +4291,7 @@ 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.
@@ -4300,6 +4301,34 @@ 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.
+31 -8
View File
@@ -12,7 +12,6 @@
extern crate human_panic;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::Write;
use std::future::Future;
use std::mem::ManuallyDrop;
@@ -3473,6 +3472,7 @@ 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,14 +3484,37 @@ pub unsafe extern "C" fn dc_msg_get_summarytext(
}
let ffi_msg = unsafe { &mut *msg };
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 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 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.63.0-dev"
version = "2.64.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.63.0-dev"
"version": "2.64.0-dev"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.63.0-dev"
version = "2.64.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.63.0-dev"
version = "2.64.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.63.0-dev"
version = "2.64.0-dev"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
+33
View File
@@ -0,0 +1,33 @@
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"
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.63.0-dev"
version = "2.64.0-dev"
description = "DeltaChat JSON-RPC server"
edition = "2024"
license = "MPL-2.0"
@@ -14,6 +14,7 @@ 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.63.0-dev"
"version": "2.64.0-dev"
}
+24
View File
@@ -22,8 +22,32 @@ 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,6 +63,7 @@ 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.63.0-dev"
version = "2.64.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_summarytext(self._dc_msg, width))
return from_dc_charpointer(lib.dc_msg_get_summary_text(self._dc_msg, 1, 1, width))
@props.with_doc
def time_sent(self):
+1 -1
View File
@@ -1 +1 @@
2026-09-22
2026-10-09
+18 -1
View File
@@ -350,7 +350,24 @@ impl<'a> BlobObject<'a> {
*vt = Viewtype::Image;
return Ok(name);
}
let mut img = imgreader.decode().context("image decode failure")?;
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 orientation = exif
.as_ref()
.map(|exif| exif_orientation(exif, context))
+67 -1
View File
@@ -4,7 +4,9 @@ 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};
use crate::test_utils::{
self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
};
use crate::tools::SystemTime;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -738,6 +740,70 @@ 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;
+1 -3
View File
@@ -3,7 +3,6 @@
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;
@@ -1918,8 +1917,7 @@ 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();
let cursor = Cursor::new(&mut buffer);
html_part.write_part(cursor).ok();
html_part.write_part(&mut buffer);
String::from_utf8_lossy(&buffer).to_string()
});
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
+12 -22
View File
@@ -1457,7 +1457,6 @@ 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;
@@ -1472,28 +1471,19 @@ 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;
}
// 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:?}")
}
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:?}")
}
},
_ => {
info!(context, "{folder:?}: got unsolicited response {response:?}")
}
+4 -1
View File
@@ -125,11 +125,14 @@ pub trait DcKey: Serialize + Deserializable + Clone {
/// Converts secret key to public key.
pub(crate) fn secret_key_to_public_key(
context: &Context,
mut signed_secret_key: SignedSecretKey,
timestamp: u32,
addr: &str,
relay_addrs: &str,
) -> Result<SignedPublicKey> {
info!(context, "Converting secret key to public key.");
// Make sure timestamp of created signatures
// is not in the past compared to the primary key timestamp.
let timestamp = std::cmp::max(
@@ -302,7 +305,7 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
let addr = context.get_primary_self_addr().await?;
let all_addrs = context.get_self_addrs().await?.join(",");
let signed_public_key =
secret_key_to_public_key(signed_secret_key, timestamp, &addr, &all_addrs)?;
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
*lock = Some(signed_public_key.clone());
Ok(Some(signed_public_key))
+8 -15
View File
@@ -1,7 +1,6 @@
//! # MIME message production.
use std::collections::{BTreeSet, HashSet};
use std::io::Cursor;
use anyhow::{Context as _, Result, bail, format_err};
use base64::Engine as _;
@@ -245,25 +244,20 @@ 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>,
) -> Result<()> {
fn add_header(name: &[u8], value: &impl mail_builder::headers::Header, headers: &mut Vec<u8>) {
headers.extend(name);
value.write_header(headers, name.len())?;
Ok(())
value.write_header(headers, name.len());
}
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 {
@@ -334,7 +328,7 @@ pub(crate) fn render_queued_mail(
} else {
&mut outer_headers
},
)?;
);
}
if is_encrypted {
@@ -2433,8 +2427,7 @@ 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();
let cursor = Cursor::new(&mut raw_message);
message.write_part(cursor).ok();
message.write_part(&mut raw_message);
raw_message
}
+37 -38
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;
use std::io::{BufReader, Cursor};
use std::str;
use std::time::Duration;
@@ -30,8 +30,7 @@ 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)
.unwrap();
.write_header(&mut output, 0);
String::from_utf8(output).unwrap()
}
@@ -50,9 +49,7 @@ fn test_render_email_address() {
let s = render_email_address(display_name, addr);
println!("{s}");
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>");
assert_eq!(s, "=?utf-8?B?w6Qgc3BhY2U=?= <x@y.org>\r\n");
}
#[test]
@@ -70,14 +67,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, r#""a space" <x@y.org>"#);
assert_eq!(s, "\"a space\" <x@y.org>\r\n");
}
#[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>");
assert_eq!(s, "<x@y.org>\r\n");
}
#[test]
@@ -102,8 +99,7 @@ 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)
.unwrap();
.write_header(&mut output, bytes_written);
String::from_utf8(output).unwrap()
}
@@ -366,6 +362,7 @@ async fn test_mdn_sent_to_all_relays() -> Result<()> {
// Bob's key gets a second relay address and Alice merges the newer key.
let bob_secret_key = load_self_secret_key(bob).await?;
let bob_public_key = secret_key_to_public_key(
bob,
bob_secret_key,
u32::try_from(time())? + 100,
"bob@example.net",
@@ -954,7 +951,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}"
);
@@ -1063,8 +1060,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
@@ -1105,17 +1102,18 @@ 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>
@@ -1125,7 +1123,7 @@ Chat-Version: 1.0
Content-Transfer-Encoding: 7bit
Hello!"#
.replace("\n", "\r\n");
.replace("\n", "\r\n");
assert_eq!(
unencrypted, expected,
"---------------- Actual: ----------------
@@ -1155,19 +1153,20 @@ 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
+6 -60
View File
@@ -1,6 +1,5 @@
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::io::Cursor;
@@ -84,64 +83,13 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
/// Selects a subkey of the public key to use for encryption.
///
/// The key is selected according to
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-03.html#section-4.3-4>.
/// If multiple keys are available, the one that will expire sooner is selected.
///
/// Returns `None` if the public key cannot be used for encryption.
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
///
/// TODO: take key flags and expiration dates into account
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
key.public_subkeys
.iter()
.filter(|subkey| subkey.algorithm().can_encrypt())
.filter_map(|subkey| {
// We take arbitrary signature here and don't try to select the newest one.
// Filtering out outdated and invalid signatures should happen during certificate merging.
let signature = subkey.signatures.first()?;
let key_flags = signature.key_flags();
if !key_flags.encrypt_comms() {
return None;
}
if let Some(expiration_duration) = signature
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0)
&& now
> subkey
.created_at()
.as_secs()
.saturating_add(expiration_duration.as_secs())
{
// Key is expired.
return None;
}
Some((subkey, signature))
})
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
match (
signature1
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
signature2
.key_expiration_time()
.filter(|duration| duration.as_secs() != 0),
) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Greater,
(Some(_), None) => Ordering::Less,
(Some(expiration1), Some(expiration2)) => (subkey1
.created_at()
.as_secs()
.saturating_add(expiration1.as_secs()))
.cmp(
&(subkey2
.created_at()
.as_secs()
.saturating_add(expiration2.as_secs())),
),
}
})
.map(|(subkey, _signature)| subkey)
.find(|subkey| subkey.algorithm().can_encrypt())
}
/// Version of SEIPD packet to use.
@@ -203,11 +151,10 @@ pub fn pk_encrypt(
) -> Result<String> {
tokio::task::block_in_place(|| {
let mut rng = thread_rng();
let now = pgp::types::Timestamp::now();
let pkeys = public_keys_for_encryption
.iter()
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
.filter_map(select_pk_for_encryption);
let msg = MessageBuilder::from_bytes("", plain);
let encoded_msg = match seipd_version {
@@ -538,8 +485,7 @@ pub(crate) fn relay_addrs(public_key: &SignedPublicKey, addr: &str) -> Vec<Strin
/// Returns true if the key can be encrypted to, i.e. has an encryption subkey.
pub(crate) fn pubkey_can_encrypt(public_key: &SignedPublicKey) -> bool {
let now = pgp::types::Timestamp::now();
select_pk_for_encryption(now.as_secs(), public_key).is_some()
select_pk_for_encryption(public_key).is_some()
}
/// Returns true if public key advertises SEIPDv2 feature.
-210
View File
@@ -11,7 +11,6 @@ use crate::{
test_utils::{TestContext, TestContextManager, alice_keypair, bob_keypair},
token,
};
use chrono::{TimeZone as _, Utc};
use pgp::composed::{Esk, Message};
use pgp::packet::PublicKeyEncryptedSessionKey;
@@ -424,212 +423,3 @@ async fn test_securejoin_pqc_joiner() {
tcm.execute_securejoin(bob, pqc).await;
}
/// Tests that public subkey selection for encryption respects key flags and prefers expring subkeys.
///
/// Non-encryption subkeys such as RSA subkey for authentication are ignored.
#[test]
fn test_select_pk_for_encryption() {
// Public key generated with GnuPG 2.4.9 with the following subkeys:
// 1. Auth-only RSA subkey (92E762B9084CA740).
// 2. Expired Curve25519 encryption subkey with 1-day expiration (C8F382BD0F35C49E)
// 3. Ed25519 signing subkey (F177AC3118F923CC).
// 4. Curve25519 encryption subkey with fingerprint (36188C6FFC8E267B)
// 5. Curve25519 encryption subkey with 1 year expiration, valid in the beginning of 2008, with key ID 9223FCEE7546CDE7
// 6. Curve25519 encryption subkey with no expiration (FD2C0567967223D8).
// Primary key is an Ed25519 not expiring key.
// Key 4 is the one that should be selected.
// Subkey 4 fingerprint.
let expected_fallback_fingerprint = "cdeb3ba3999bf7880f0ee1f536188c6ffc8e267b";
// Subkey 5 fingerprint, should be preferred to fallback when not expired.
let expected_expiring_fingerprint = "5133fab157c4a46ca41f6dc39223fcee7546cde7";
let alice_tpk_asc = "
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMERvfcPBYJKwYBBAHaRw8BAQdAimvPsr7NdJ4dBoFPySwhpTQqoYOoHL3AzfE7
mGWQOOC0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBCi19Yqv
ugVVkhyHgicqAms0FFoiBQJG99w8AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
AAoJECcqAms0FFoiUGEA/3VZMBCoRq0ZpHarzmvzgdZCoL3r3m9en/eZScFzxITx
AP42Mn27r0SOKwIln0VcPTdAQCk49mBW/EX3CMOlLPU3DLkBjQRG99w8AQwArsYe
Jkdl6sSM/hfoEw0vgx/RdUBQ6QRYi1uc0UUNlIGy8mlczLFdkD3JF/hGocjPvt45
XAQoK110zAkZlfpFRqNT1M/IC68Er8rLkYPC4OeFh6W4Iyn17fcUanP0lf8em/jh
Vffvgy8sFOMdO235lvFA3txNA98s4fHdmU3PScyd1hc3C4M0yP83LnYyWt4X59Xc
E/Om5Dm458eKCSeYkLI6752W0mXsBxSi3/dLn0XeuNRpgmKxSkm562FHOFaLbKtR
Y5hobAI9PkNcVgRxZZvWQls5PHTZWjqngF21lKlaLfdqZ/Uae1i2hzZOihgitL43
Le00qwNBi5hKYMeuDnHaQrLmb+A+0/IAEE4Ub+TkZhzI+2ZP2k1cAT0qZmZi0w+q
xqP9INk0hY/oZFCnV2wkHN7zvQmVlUIcQ2rmfbafK1yiEL1qeGT96zyjbdXeGPqJ
3O9h+YIG38JMRJBijsFujUN34Z546zS/kzOPXsz/WlGUMjwu8n5s5uf2TFyFABEB
AAGIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsgAAoJECcq
Ams0FFoiCOUBAPafRLDpWN9iT4hcCXjESf1Hw5KNVkJpwfzPfu2H9BkMAQCaHhKg
pq9ywH4pyOHZCPV8P2ywkyn+EsjBC3fG+GBBBbg4BEb33DwSCisGAQQBl1UBBQEB
B0DfI8AJFT3nWa6ZXLkHSf7W8W7S6AWIO7LAcjoyHwb8CwMBCAeIfgQYFgoAJhYh
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMBQkAAVGAAAoJECcqAms0FFoi
U5EA/3G74HRwIMJlNOEW5gkYYV5KJW2qgtMfxHCUjoHvNWU1AQCHt/bLU2aviAiS
of1R43qojxKUqzzoi8lYRQ+1sYhvB7gzBEb33DwWCSsGAQQB2kcPAQEHQKZXUJ7s
xqH3kVcMnhasw6DrFMwCxHDdj+qvkg8r/DvtiO8EGBYKACAWIQQotfWKr7oFVZIc
h4InKgJrNBRaIgUCRvfcPAIbAgCBCRAnKgJrNBRaInYgBBkWCgAdFiEEI8hstnVQ
9sgylIYg8XesMRj5I8wFAkb33DwACgkQ8XesMRj5I8xo6QEAu4o/TyEZwFcyqZpw
LEo9vTLCsc7fo0nx0ssiP6FyV5cBAOWal1DznDhsXWCNt+U8UaafXsU2DTV51KaD
VBOVFo4CyeQBAMirIjXV5PbUV674TNLhYl2s0jTtNz+GKtOjSdZuRm1mAPwPG6ya
K1b7iMRdBT92gNZMw30LbtcXmttCxpZAwr9lBLg4BEb33DwSCisGAQQBl1UBBQEB
B0C5fFb4WTHoIoI6ou/31+1N1wn8ghsSkUVzpbtv/aTkegMBCAeIeAQYFgoAIBYh
BCi19YqvugVVkhyHgicqAms0FFoiBQJG99w8AhsMAAoJECcqAms0FFoi8z8BALPL
7V0ICLEY5YSUa4lQ2rjiXOcVTlWkG3h4TATPrr08AP9tIAQIE0o50IGdQAcKJoTn
Lyxnf2wfjZ16vL3JLLjSBrg4BEb33DwSCisGAQQBl1UBBQEBB0DXDrcGrnuLjAUO
eo/t8MQNOe+ZKYSDPGTkO7iM5IloSAMBCAeIfgQYFgoAJhYhBCi19YqvugVVkhyH
gicqAms0FFoiBQJG99w8AhsMBQkB4TOAAAoJECcqAms0FFoivQIA/2PcZ1vcImAa
7ldPY00JkcW6WlSSd6yOIZsVa4TdA1FiAP42gOji+4RrLps2+NX6L1znSc8EJBXo
RMbND/CZQWXfA7g4BEb33DwSCisGAQQBl1UBBQEBB0BHxCvo5zuygw2XiluYNobx
7iFJqlmCkjekKyoVFquKHQMBCAeIeAQYFgoAIBYhBCi19YqvugVVkhyHgicqAms0
FFoiBQJG99w8AhsMAAoJECcqAms0FFoirSMA/0gVP98sPFga+UhQ3uJxJw5bO2Rs
7hxVk6aPREWgBYg1AQCT7AE8m7j17SP/1fl8OjpxsQQmCJyv2wNcP48OfKGOCA==
=AXAi
-----END PGP PUBLIC KEY BLOCK-----
";
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
let now = pgp::types::Timestamp::now();
let encryption_subkey = select_pk_for_encryption(now.as_secs(), &alice_tpk).unwrap();
assert_eq!(
encryption_subkey.fingerprint().to_string().as_str(),
expected_fallback_fingerprint
);
// If we pass 1199149200 as the `now` argument, which was in the beginning of 2008,
// then the expiring subkey is not yet expired and should be used.
let encryption_subkey = select_pk_for_encryption(1199149200, &alice_tpk).unwrap();
assert_eq!(
encryption_subkey.fingerprint().to_string().as_str(),
expected_expiring_fingerprint
);
}
/// Tests that key selection fails if there is only an expired subkey.
#[test]
fn test_select_only_expired_subkey() {
let alice_tpk_asc = "
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMERvfcPBYJKwYBBAHaRw8BAQdAUz6AZXIRE8T04Vh8RReFiP3tEV8UfSs2EiYs
8b8i4ce0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBNRG3w/A
qWyPitBaqkGLyqDx3CaUBQJG99w8AhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
AAoJEEGLyqDx3CaUMjIA/Rq9/iORLP360s6EsIDe9qSmlmSCggtivafH+uVBWa0X
AP0Wb+kailDwISq2O9Ef/jceZw7ozyzDLeDskKpCYiL3A7g4BEb33DwSCisGAQQB
l1UBBQEBB0BXhbrks9iskW1GfT3B022W3KhJCgz8gw81z9lAWv7OZgMBCAeIfgQY
FgoAJhYhBNRG3w/AqWyPitBaqkGLyqDx3CaUBQJG99w8AhsMBQkB4TOAAAoJEEGL
yqDx3CaU6/YA/jaYJsZMXvu5grrMq1wq3Z/yzGXd15zeWp+alY/6UYxoAQCmSrBC
SOtE3ODLZN9tC3F7k1N9clme1cHXyUiH3EliBQ==
=zWPq
-----END PGP PUBLIC KEY BLOCK-----
";
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
let now = pgp::types::Timestamp::now();
assert_eq!(select_pk_for_encryption(now.as_secs(), &alice_tpk), None);
}
/// Tests that the key with the closest expiration date is selected.
#[test]
fn test_expiring_subkey_selection() {
// The key has the following keys:
// 1. Primary non-expiring Ed25519 key created on 2025-01-01
// 2. Non-expiring subkey CE5FC3FD479E41F08069DCE6F6CAD4ADB9AF47F1 created on 2025-01-01
// 3. Subkey 10493EF4DBA7EE4D55C826A7634684D9BFEEB890 created on 2026-06-01 and expiring on 2026-07-01
// 4. Subkey 477650C13FA4842BAF2831BC41C2D560BE27B38D created on 2026-06-01 and expiring on 2026-06-15
// 5. Subkey AB92F4AFD46DDD7D31FB05151BE08A8089F67CA2 created on 2026-06-08 and expiring on 2026-06-22
let alice_tpk_asc = "
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZ3SFgBYJKwYBBAHaRw8BAQdAOrjYsxkYNvVtjbZwvglXk94Rd8S3F0YlIgcQ
SSp45ni0GUFsaWNlIDxhbGljZUBleGFtcGxlLm9yZz6IkAQTFgoAOBYhBGV3iWZU
bA6WJh3MR00wvB0X57PiBQJndIWAAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA
AAoJEE0wvB0X57PiAr0A/0qzulPRXZR1+D1fUeW6/C3BRP+8qRZTlOvF0XEnxPOV
AQD7oePZ/QI6AuDMFmnJ5HQJ6cQSSOp/FwyucD8yiAoABrg4BGd0hYASCisGAQQB
l1UBBQEBB0DBBk+BdzyTu3Hys6dewzd0L5AFeYMRIOC99XosMBLZYwMBCAeIeAQY
FgoAIBYhBGV3iWZUbA6WJh3MR00wvB0X57PiBQJndIWAAhsMAAoJEE0wvB0X57Pi
BxYA+gOtDVtb9p9vVaKyAuYCG7LdJ63Bqu0OtrVxmZEQvHQbAQDJmJX/yOX5QSkh
cRtM1qoiY7OUjcT8ERnaoAcw2suFBLg4BGocy4ASCisGAQQBl1UBBQEBB0AgEDSA
muuqS2iyi7aZeV608Xx2qdVqYKjREgiC3cu4YQMBCAeIfgQYFgoAJhYhBGV3iWZU
bA6WJh3MR00wvB0X57PiBQJqHMuAAhsMBQkAJ40AAAoJEE0wvB0X57Pi9KgA/2vW
4yNU1EF/WRCKmr87mJtmYR5Xf9Jps54Tv4BexNGgAQDxxx9lKvwW4U9/8UkrSF28
HrGdWES4wAa9N9HvJzuOD7g4BGocy4ASCisGAQQBl1UBBQEBB0CK1TifDqKtFzks
4E8gNmF90OkIYVriZxdY3QFSwowILQMBCAeIfgQYFgoAJhYhBGV3iWZUbA6WJh3M
R00wvB0X57PiBQJqHMuAAhsMBQkAEnUAAAoJEE0wvB0X57PiZ+UA/0+s3fM/8OOb
8Ft21QMFM/7Ce1P/ovKgkxsYWjk8q7zYAP9HYm/vcWBGzQw08mn3X26UFB9UfCF3
qc1yyltDEeERDrg4BGomBgASCisGAQQBl1UBBQEBB0AvGvgG8Wkwx/KklkUHdLuC
UKKSqTp4xsjocWXJs69rSwMBCAeIfgQYFgoAJhYhBGV3iWZUbA6WJh3MR00wvB0X
57PiBQJqJgYAAhsMBQkAEnUAAAoJEE0wvB0X57PibzQA/A0qT23pcdenNJJ5QZN/
ecs08p2pbiipv+adPeojVGZIAP47Pyr8Hj0o3qhrEhhFZkIhTxxDxS/jAsq2v4UG
JGp8Ag==
=RzZZ
-----END PGP PUBLIC KEY BLOCK-----
";
let alice_tpk = SignedPublicKey::from_asc(alice_tpk_asc).unwrap();
// Two-week subkey that was generated earlier is selected
// because it expires earlier, on 2026-06-15.
{
let ts = u32::try_from(
Utc.with_ymd_and_hms(2026, 6, 9, 0, 0, 0)
.unwrap()
.timestamp(),
)
.unwrap();
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
assert_eq!(
subkey.fingerprint().to_string().as_str(),
"477650c13fa4842baf2831bc41c2d560be27b38d",
);
}
// Now both 477650C13FA4842BAF2831BC41C2D560BE27B38D expires
// and we switch to another subkey that expires on 2026-06-22.
{
let ts = u32::try_from(
Utc.with_ymd_and_hms(2026, 6, 16, 0, 0, 0)
.unwrap()
.timestamp(),
)
.unwrap();
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
assert_eq!(
subkey.fingerprint().to_string().as_str(),
"ab92f4afd46ddd7d31fb05151be08a8089f67ca2",
);
}
// Now both two-week subkeys have expired and we switch to expiring subkey that expires on 2026-07-01.
{
let ts = u32::try_from(
Utc.with_ymd_and_hms(2026, 6, 23, 0, 0, 0)
.unwrap()
.timestamp(),
)
.unwrap();
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
assert_eq!(
subkey.fingerprint().to_string().as_str(),
"10493ef4dba7ee4d55c826a7634684d9bfeeb890",
);
}
// All expiring subkeys are expired, switching to non-expiring subkey.
{
let ts = u32::try_from(
Utc.with_ymd_and_hms(2026, 7, 2, 0, 0, 0)
.unwrap()
.timestamp(),
)
.unwrap();
let subkey = select_pk_for_encryption(ts, &alice_tpk).unwrap();
assert_eq!(
subkey.fingerprint().to_string().as_str(),
"ce5fc3fd479e41f08069dce6f6cad4adb9af47f1",
);
}
}
+1
View File
@@ -2957,6 +2957,7 @@ 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();
+98 -18
View File
@@ -10,6 +10,7 @@ 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};
@@ -117,15 +118,9 @@ 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,
text: msg.get_summary_text(context).await,
timestamp: msg.get_timestamp(),
state: msg.state,
})
@@ -138,19 +133,32 @@ impl Summary {
}
impl Message {
/// Returns a summary text.
/// Returns a summary text with emoji and "Forwarded:" prefixes.
/// This is the standard summary to be used in chatlists, notifications etc.
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context), summary)
} else {
summary
}
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
}
/// Returns a summary text without "Forwarded:" prefix.
/// Returns a summary text with emoji prefixes but without "Forwarded:" prefix.
/// Used for shorter reaction summaries as "USER reacts 👋 to SUMMARY"
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
@@ -264,7 +272,11 @@ impl Message {
}
};
let text = self.text.clone();
let text = if approx_chars > 0 {
truncate(self.text.trim(), approx_chars).to_string()
} else {
self.text.trim().to_string()
};
let summary = if let Some(type_file) = type_file {
if append_text && !text.is_empty() {
@@ -286,13 +298,30 @@ impl Message {
"".to_string()
};
let emoji = emoji.filter(|_| add_type_emoji);
let summary = if let Some(emoji) = emoji {
format!("{emoji} {summary}")
} else {
summary
};
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
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
}
}
}
@@ -314,6 +343,13 @@ 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)
@@ -330,12 +366,22 @@ 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);
@@ -343,6 +389,16 @@ 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);
@@ -473,6 +529,30 @@ 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;
}
}
+4 -1
View File
@@ -105,7 +105,10 @@ 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");
tcm.change_addr(alice, "alice@example.org").await;
alice
.set_primary_self_addr("alice@example.org")
.await
.unwrap();
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 OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
"INSERT 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?;
.await.with_context(|| format!("Failed to insert pseudo transport for {addr:?}"))?;
Ok(())
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 676 KiB