mirror of
https://github.com/chatmail/core.git
synced 2026-10-10 23:20:26 +03:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b9fd5005 | ||
|
|
694ef8ba76 | ||
|
|
de6f469710 | ||
|
|
81140d514f | ||
|
|
a8b6246052 | ||
|
|
4ea5cddc09 | ||
|
|
f8edd9136a | ||
|
|
697304e98a | ||
|
|
40c0954e42 | ||
|
|
3a79324728 | ||
|
|
6ab0cd513a |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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,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,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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -1 +1 @@
|
||||
2026-09-22
|
||||
2026-10-09
|
||||
+18
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 |
Reference in New Issue
Block a user