Compare commits

..

40 Commits

Author SHA1 Message Date
link2xt
47dbac9b50 chore(release): prepare for 1.124.1 2023-10-05 05:01:26 +00:00
link2xt
a49282727b ci: pin urllib3 version to <2
Otherwise it is impossible to build wheels.
2023-10-05 04:41:51 +00:00
iequidoo
0d22fc7ac1 fix: Remove footer from reactions on the receiver side (#4780)
Reactions do not have footer since 6d2ac30. However, mailing lists still add the footer to the
messages, and receiver interpreted words as a reaction.
2023-10-04 22:46:09 -03:00
link2xt
1040bc551f chore(release): prepare for 1.124.0 2023-10-04 21:12:38 +00:00
iequidoo
5aa0205c80 fix: Add protected-headers directive to Content-Type of encrypted/signed MIME (#2302)
Add protected-headers="v1" directive to Content-Type of an encrypted/signed MIME so that other MUAs
like Thunderbird display the true message Subject instead of "...".
2023-10-04 19:58:08 +00:00
link2xt
210a4ebcbe ci: test async python bindings with Python 3.11 2023-10-04 19:56:56 +00:00
link2xt
7fc2b06b3f ci(mypy): ignore distutils
Python 3.12 removed `distutils`.
`distutils` from `setuptools` work fine,
but have not typing stubs:
https://github.com/python/typeshed/issues/10255
2023-10-04 19:56:56 +00:00
link2xt
1177c19a43 bulid: build wheels for Python 3.12 and PyPy 3.10 2023-10-04 19:56:56 +00:00
link2xt
8a2417f32d ci: test with Python 3.12 and PyPy 3.10 2023-10-04 19:56:56 +00:00
link2xt
a154347834 fix(deltachat-rpc-client): increase stdio buffer to 64 MiB
Otherwise readline() gets stuck when JSON-RPC response
is longer that 64 KiB.
2023-10-04 16:08:15 +00:00
link2xt
d51adf2aa0 feat(deltachat-rpc-client): log exceptions when long-running tasks die
For example, reader_loop() may die
if readline() tries to read too large line
and thows an exception.
We want to at least log the exception in this case.
2023-10-04 08:22:50 +00:00
link2xt
a5f0c1613e fix: add Let's Encrypt root certificate to reqwest
This certificate does not exist on older Android phones.
It is already added manually for IMAP and SMTP,
this commit adds the same certificate for HTTP requests.
2023-10-03 19:35:39 +00:00
link2xt
7c4bcf9004 api!: deprecate get_next_media() 2023-10-03 16:08:25 +00:00
B. Petersen
2dd44d5f89 fix: cap percentage in connectivity layout to 100%
it may happen that percentages larger than 100% are reported by the provider,
eg. for some time a storage usage of 120% may be accepted.

while we should report the values "as is" to the user,
for the bar, percentages larger 100% will destroy the layout.
2023-10-01 14:54:37 +00:00
link2xt
f656cb29be fix: ignore special chats in get_similar_chat_ids()
For unknown reason trash chat contains members in some existing databases.
Workaround this by ignoring chats_contacts entries with special chat_id.
2023-10-01 07:38:13 +00:00
link2xt
53230b6eb0 chore(cargo): update webpki to fix RUSTSEC-2023-0052 2023-10-01 00:04:45 +00:00
iequidoo
80ca59f152 feat: Remove extra members from the local list in sake of group membership consistency (#3782)
9bd7ab72 brings a possibility of group membership inconsistency to the original Hocuri's algo
described and implemented in e12e026b in sake of security so that nobody can add themselves to a
group by forging "InReplyTo" and other headers. This commit fixes the problem by removing group
members locally if we see a discrepancy with the "To" list in the received message as it is better
for privacy than adding absent members locally. But it shouldn't be a big problem if somebody missed
a member addition, because they will likely recreate the member list from the next received
message. The problem occurs only if that "somebody" managed to reply earlier. Really, it's a problem
for big groups with high message rate, but let it be for now.

Also:
- Query chat contacts from the db only once.
- Update chat contacts in the only transaction, otherwise we can just break the chat contact list
  halfway.
- Allow classic MUA messages to remove group members if a parent message is missing. Currently it
  doesn't matter because unrelated messages go to new ad-hoc groups, but let this logic be outside
  of apply_group_changes(). Just in case if there will be a MUA preserving "Chat-Group-ID" header
  f.e.
2023-09-30 19:14:22 -03:00
link2xt
eb624e43c0 refactor: remove incomplete protected header code skipping Legacy Display Part
The code removed is an incomplete implementation of skipping
the Legacy Display Part specified in
https://www.ietf.org/archive/id/draft-autocrypt-lamps-protected-headers-02.html#section-5.2

The code does not fully implement the specification, e.g.
it does not check that there are exactly two parts.

Delta Chat and Thunderbird are not adding this part anyway,
and it is defined as "transitional" in the draft.

This also removes misplaced warning "Ignoring nested protected headers"
that is printed for every incoming Delta Chat message
since commit 5690c48863
which is part of the PR <https://github.com/deltachat/deltachat-core-rust/pull/982>.
2023-09-30 21:54:08 +00:00
link2xt
532e9cb09a refactor: ignore public key argument in dc_preconfigure_keypair()
Public key can be extracted from the secret key file.
2023-09-30 19:16:23 +00:00
link2xt
ef4d2a7ed0 api!(python): use dc_contact_get_verifier_id()
get_verifier() returns a Contact rather than an address now

dc_contact_get_verifier_addr() is unused.
2023-09-30 15:49:22 +00:00
link2xt
6d2ac30461 fix: do not put the status footer into reaction MIME parts 2023-09-29 16:38:55 +00:00
link2xt
33a203d56e fix: initialise last_msg_id to the highest known row id
Otherwise existing bots migrating to get_next_msgs()
are trying to process all the messages they have in the database.
2023-09-29 13:28:58 +00:00
link2xt
a19811f379 chore(cargo): update tungstenite to fix RUSTSEC-2023-0065
Used `cargo update -p axum`.
2023-09-29 13:08:04 +00:00
link2xt
f23023961e api!: return DC_CONTACT_ID_SELF from dc_contact_get_verifier_id() for directly verified contacts 2023-09-28 19:10:15 +00:00
link2xt
b463a0566e refactor: flatten create_or_lookup_mailinglist() 2023-09-28 15:20:51 +00:00
link2xt
38d5743c06 refactor: do not ignore errors in get_kml()
This removes unnecessary warning
"mimefactory: could not send location: No locations processed"
when there are no locations to send.
2023-09-28 15:19:33 +00:00
link2xt
6990312051 fix: trash only empty *text* parts when location.kml is attached
If the message contains other attachment parts
such as images, they should not go into trash.
2023-09-27 18:51:40 +00:00
link2xt
a7cf51868b test: test send_location 2023-09-27 18:51:40 +00:00
link2xt
815c1b9c49 refactor: resultify location::set() 2023-09-27 18:51:40 +00:00
link2xt
88bba83383 refactor: flatten process_report() 2023-09-26 16:02:14 +00:00
WofWca
b1d517398d refactor: improve comment about Ratelimit
A few people got the impression that if you send 6 messages
in a burst you'll only be able to send the next one in 60 seconds.
Hopefully this can resolve it.
2023-09-26 15:58:24 +00:00
link2xt
4e5b41f150 fix: require valid email addresses in dc_provider_new_from_email[_with_dns]() 2023-09-25 15:51:10 +00:00
B. Petersen
56b2361f01 reset document.update on forwarding
this fixes the test test_forward_webxdc_instance()
2023-09-25 15:20:57 +00:00
B. Petersen
968cc65323 test that update.document is not forwarded
the test is failing currently
2023-09-25 15:20:57 +00:00
link2xt
d0ee21e6dc refactor: flatten GENERATED_PREFIX check in receive_imf_inner 2023-09-25 10:35:07 +00:00
link2xt
a1345f2542 refactor: flatten lookup_chat_by_reply 2023-09-25 10:34:20 +00:00
link2xt
f290fe0871 fix: wrap base64-encoded parts to 76 characters
This is an RFC 2045 requirement for base64-encoded MIME parts.
Previously referenced RFC 5322 requirement
is a general Internet Message Format requirement
and is more generous.
2023-09-25 10:33:46 +00:00
link2xt
aa78e82fed chore(release): prepare for 1.123.0 2023-09-22 22:13:47 +00:00
link2xt
d4e670d5e9 chore(deps): update OpenSSL from 3.1.2 to 3.1.3 2023-09-22 21:57:36 +00:00
link2xt
4553c6521f api!: make dc_jsonrpc_blocking_call accept JSON-RPC request 2023-09-22 21:33:52 +00:00
40 changed files with 730 additions and 501 deletions

View File

@@ -182,15 +182,15 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
python: 3.12
- os: macos-latest
python: 3.11
python: 3.12
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
python: pypy3.10
- os: macos-latest
python: pypy3.9
python: pypy3.10
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
@@ -231,6 +231,10 @@ jobs:
fail-fast: false
matrix:
include:
# Async Python bindings do not depend on Python version,
# but are tested on Python 3.11 until Python 3.12 support
# is added to `aiohttp` dependency:
# https://github.com/aio-libs/aiohttp/issues/7646
- os: ubuntu-latest
python: 3.11
- os: macos-latest
@@ -238,9 +242,9 @@ jobs:
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
python: pypy3.10
- os: macos-latest
python: pypy3.9
python: pypy3.10
# Minimum Supported Python Version = 3.8
#

View File

@@ -1,5 +1,65 @@
# Changelog
## [1.124.1] - 2023-10-05
### Fixes
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
### CI
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
## [1.124.0] - 2023-10-04
### API-Changes
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
- Deprecate `dc_contact_get_verifier_addr`.
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
- Deprecate `get_next_media()`.
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
### Fixes
- Wrap base64-encoded parts to 76 characters.
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
- Do not put the status footer into reaction MIME parts.
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
### Features / Changes
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
- deltachat-rpc-client: Log exceptions when long-running tasks die.
### Build
- Build wheels for Python 3.12 and PyPy 3.10.
## [1.123.0] - 2023-09-22
### API-Changes
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
### Fixes
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- imex: Use "default" in the filename of the default key.
### Miscellaneous Tasks
- Update OpenSSL from 3.1.2 to 3.1.3.
## [1.122.0] - 2023-09-12
### API-Changes
@@ -2816,3 +2876,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1

34
Cargo.lock generated
View File

@@ -312,9 +312,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.18"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [
"async-trait",
"axum-core",
@@ -1103,7 +1103,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.122.0"
version = "1.124.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1179,7 +1179,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.122.0"
version = "1.124.1"
dependencies = [
"anyhow",
"async-channel",
@@ -1203,7 +1203,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.122.0"
version = "1.124.1"
dependencies = [
"ansi_term",
"anyhow",
@@ -1218,7 +1218,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.122.0"
version = "1.124.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1243,7 +1243,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.122.0"
version = "1.124.1"
dependencies = [
"anyhow",
"deltachat",
@@ -1605,7 +1605,7 @@ dependencies = [
[[package]]
name = "email"
version = "0.0.21"
source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
source = "git+https://github.com/deltachat/rust-email?branch=master#37778c89d5eb5a94b7983f3f37ff67769bde3cf9"
dependencies = [
"base64 0.11.0",
"chrono",
@@ -3110,9 +3110,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.1.3+3.1.2"
version = "300.1.5+3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107"
checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
dependencies = [
"cc",
]
@@ -4947,9 +4947,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.18.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
dependencies = [
"futures-util",
"log",
@@ -5159,13 +5159,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.18.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
@@ -5448,9 +5448,9 @@ dependencies = [
[[package]]
name = "webpki"
version = "0.22.1"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
dependencies = [
"ring",
"untrusted",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.122.0"
version = "1.124.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"

View File

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

View File

@@ -824,7 +824,7 @@ void dc_maybe_network (dc_context_t* context);
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data ASCII armored public key.
* @param public_data Ignored, actual public key is extracted from secret_data.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -1175,24 +1175,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
/**
* Replaces webxdc app with a new version.
*
* On the JavaScript side this API could be used like this:
* ```
* window.webxdc.replaceWebxdc(blob);
* ```
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the WebXDC message to be replaced.
* @param blob New blob to replace WebXDC with.
* @param n Blob size.
*/
void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n);
/**
* Save a draft for a chat in the database.
*
@@ -1499,6 +1481,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
@@ -5065,6 +5048,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
* A string containing the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is an empty string, we don't have verifier
* information or the contact is not verified.
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
*/
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
@@ -5077,7 +5061,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5773,12 +5757,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @param input JSON-RPC request.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* On error, NULL is returned.
* If there is no response, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
/**
* @class dc_event_emitter_t

View File

@@ -29,14 +29,14 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::DcKey;
use deltachat::key::{DcKey, DcSecretKey};
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use num_traits::{FromPrimitive, ToPrimitive};
@@ -805,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
_public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -815,8 +815,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
let ctx = &*context;
block_on(async move {
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let public = secret.split_public_key()?;
let keypair = key::KeyPair {
addr,
public,
@@ -1097,32 +1097,6 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_replace_webxdc(
context: *mut dc_context_t,
msg_id: u32,
blob: *const u8,
n: libc::size_t,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_replace_webxdc()");
return;
}
let msg_id = MsgId::new(msg_id);
let blob_slice = std::slice::from_raw_parts(blob, n);
let ctx = &*context;
block_on(async move {
replace_webxdc(ctx, msg_id, blob_slice)
.await
.context("Failed to replace WebXDC")
.log_err(ctx)
.ok();
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1457,6 +1431,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
}
#[no_mangle]
#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
@@ -1565,10 +1540,14 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
}
let ctx = &*context;
block_on(ChatId::new(chat_id).delete(ctx))
.context("Failed chat delete")
.log_err(ctx)
.ok();
block_on(async move {
ChatId::new(chat_id)
.delete(ctx)
.await
.context("Failed chat delete")
.log_err(ctx)
.ok();
})
}
#[no_mangle]
@@ -2589,7 +2568,12 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
}
#[no_mangle]
@@ -4553,7 +4537,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4580,11 +4571,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info(
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
socks5_enabled,
)) {
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -5032,7 +5026,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
@@ -5108,25 +5102,24 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
method: *const libc::c_char,
params: *const libc::c_char,
input: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.122.0"
version = "1.124.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -1427,6 +1427,10 @@ impl CommandApi {
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.122.0"
"version": "1.124.1"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.122.0"
version = "1.124.1"
license = "MPL-2.0"
edition = "2021"

View File

@@ -894,7 +894,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {

View File

@@ -1,5 +1,6 @@
import asyncio
import json
import logging
import os
from typing import Any, Dict, Optional
@@ -28,10 +29,16 @@ class Rpc:
self.events_task: asyncio.Task
async def start(self) -> None:
# Use buffer of 64 MiB.
# Default limit as of Python 3.11 is 2**16 bytes, this is too low for some JSON-RPC responses,
# such as loading large HTML message content.
limit = 2**26
self.process = await asyncio.create_subprocess_exec(
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
limit=limit,
**self._kwargs,
)
self.id = 0
@@ -57,16 +64,20 @@ class Rpc:
await self.close()
async def reader_loop(self) -> None:
while True:
line = await self.process.stdout.readline() # noqa
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
else:
print(response)
try:
while True:
line = await self.process.stdout.readline() # noqa
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
else:
print(response)
except Exception:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
@@ -75,13 +86,17 @@ class Rpc:
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
try:
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""

View File

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

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.122.0"
"version": "1.124.1"
}

View File

@@ -24,3 +24,5 @@ ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True
[mypy-distutils.*]
ignore_missing_imports = True

View File

@@ -195,7 +195,7 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -203,7 +203,7 @@ class Account:
res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
ffi.NULL,
as_dc_charpointer(secret),
)
if res == 0:

View File

@@ -75,9 +75,12 @@ class Contact:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact):
def get_verifier(self, contact) -> Optional["Contact"]:
"""Return the address of the contact that verified the contact."""
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
if verifier_id == 0:
return None
return Contact(self.account, verifier_id)
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.

View File

@@ -478,10 +478,9 @@ class ACFactory:
except IndexError:
pass
else:
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
if fname_sec:
account._preconfigure_keypair(addr, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")

View File

@@ -1,6 +1,7 @@
import sys
import pytest
import deltachat as dc
class TestGroupStressTests:
@@ -149,9 +150,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
# If we verified the contact ourselves then verifier addr == contact addr
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
@@ -176,9 +176,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")

View File

@@ -67,10 +67,9 @@ class TestOfflineAccountBasic:
def test_preconfigure_keypair(self, acfactory, data):
ac = acfactory.get_unconfigured_account()
alice_public = data.read_path("key/alice-public.asc")
alice_secret = data.read_path("key/alice-secret.asc")
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
assert alice_secret
ac._preconfigure_keypair("alice@example.org", alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -1,3 +1,4 @@
import json
from queue import Queue
import deltachat as dc
@@ -226,10 +227,26 @@ def test_jsonrpc_blocking_call(tmp_path):
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
).encode("utf-8"),
),
),
)
assert res == "true"
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
).encode("utf-8"),
),
),
)
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}

View File

@@ -25,6 +25,9 @@ deps =
pytest-xdist
pdbpp
requests
# urllib3 2.0 does not work in manylinux2014 containers.
# https://github.com/deltachat/deltachat-core-rust/issues/4788
urllib3<2
[testenv:.pkg]
passenv =

View File

@@ -1 +1 @@
2023-09-12
2023-10-05

View File

@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -918,9 +918,10 @@ impl ChatId {
.sql
.query_map(
"SELECT chat_id, count(*) AS n
FROM chats_contacts where contact_id > 9
FROM chats_contacts
WHERE contact_id > ? AND chat_id > ?
GROUP BY chat_id",
(),
(ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
|row| {
let chat_id: ChatId = row.get(0)?;
let size: f64 = row.get(1)?;
@@ -2897,6 +2898,9 @@ pub enum Direction {
}
/// Searches next/previous message based on the given message and list of types.
///
/// Deprecated since 2023-10-03.
#[deprecated(note = "use `get_chat_media` instead")]
pub async fn get_next_media(
context: &Context,
curr_msg_id: MsgId,
@@ -3528,6 +3532,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.param.remove(Param::WebxdcDocument);
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.in_reply_to = None;

View File

@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
const LAST_SPECIAL: ContactId = ContactId::new(9);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -1252,11 +1252,22 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let verifier_addr = self.get_verifier_addr(context).await?;
if let Some(addr) = verifier_addr {
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
} else {
Ok(None)
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
return Ok(None);
};
if verifier_addr == self.addr {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::AddressBook).await? {
Some(contact_id) => Ok(Some(contact_id)),
None => {
let addr = &self.addr;
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
Ok(None)
}
}
}

View File

@@ -382,7 +382,7 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
@@ -814,7 +814,22 @@ impl Context {
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
None => MsgId::new_unset(),
None => {
// If `last_msg_id` is not set yet,
// subtract 1 from the last id,
// so a single message is returned and can
// be marked as seen.
self.sql
.query_row(
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
(),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
)
.await?
}
};
let list = self

View File

@@ -328,13 +328,13 @@ pub async fn is_sending_locations_to_chat(
}
/// Sets current location of the user device.
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
if latitude == 0.0 && longitude == 0.0 {
return true;
return Ok(true);
}
let mut continue_streaming = false;
if let Ok(chats) = context
let chats = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
@@ -346,33 +346,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
.map_err(Into::into)
},
)
.await
{
for chat_id in chats {
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
(
latitude,
longitude,
accuracy,
time(),
chat_id,
ContactId::SELF,
)
).await {
warn!(context, "failed to store location {:#}", err);
} else {
info!(context, "stored location for chat {}", chat_id);
continue_streaming = true;
}
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
};
}
.await?;
continue_streaming
for chat_id in chats {
context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
(
latitude,
longitude,
accuracy,
time(),
chat_id,
ContactId::SELF,
)).await.context("Failed to store location")?;
info!(context, "Stored location for chat {chat_id}.");
continue_streaming = true;
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
};
Ok(continue_streaming)
}
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
@@ -464,7 +460,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
}
/// Returns `location.kml` contents.
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
let mut last_added_location_id = 0;
let self_addr = context.get_primary_self_addr().await?;
@@ -534,9 +530,11 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
ret += "</Document>\n</kml>";
}
ensure!(location_count > 0, "No locations processed");
Ok((ret, last_added_location_id))
if location_count > 0 {
Ok(Some((ret, last_added_location_id)))
} else {
Ok(None)
}
}
fn get_kml_timestamp(utc: i64) -> String {
@@ -928,4 +926,38 @@ Content-Disposition: attachment; filename="location.kml"
assert_eq!(locations.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_locations_to_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
let bob_chat_id = msg.chat_id;
assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
// Send image without text.
let file_name = "image.png";
let bytes = include_bytes!("../test-data/image/logo.png");
let file = alice.get_blobdir().join(file_name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg_opt(&sent).await.unwrap();
assert!(msg.chat_id == bob_chat_id);
assert_eq!(msg.msg_ids.len(), 1);
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.get(0).unwrap()).await?;
assert_eq!(bob_msg.chat_id, bob_chat_id);
assert_eq!(bob_msg.viewtype, Viewtype::Image);
Ok(())
}
}

View File

@@ -678,6 +678,12 @@ impl<'a> MimeFactory<'a> {
})
};
let get_content_type_directives_header = || {
(
"Content-Type-Deltachat-Directives".to_string(),
"protected-headers=\"v1\"".to_string(),
)
};
let outer_message = if is_encrypted {
headers.protected.push(from_header);
@@ -714,10 +720,7 @@ impl<'a> MimeFactory<'a> {
if !existing_ct.ends_with(';') {
existing_ct += ";";
}
let message = message.replace_header(Header::new(
"Content-Type".to_string(),
format!("{existing_ct} protected-headers=\"v1\";"),
));
let message = message.header(get_content_type_directives_header());
// Set the appropriate Content-Type for the outer message
let outer_message = PartBuilder::new().header((
@@ -786,11 +789,12 @@ impl<'a> MimeFactory<'a> {
{
message
} else {
let message = message.header(get_content_type_directives_header());
let (payload, signature) = encrypt_helper.sign(context, message).await?;
PartBuilder::new()
.header((
"Content-Type".to_string(),
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
"Content-Type",
"multipart/signed; protocol=\"application/pgp-signature\"",
))
.child(payload)
.child(
@@ -860,9 +864,13 @@ impl<'a> MimeFactory<'a> {
}
/// Returns MIME part with a `location.kml` attachment.
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
let (kml_content, last_added_location_id) =
location::get_kml(context, self.msg.chat_id).await?;
async fn get_location_kml_part(&mut self, context: &Context) -> Result<Option<PartBuilder>> {
let Some((kml_content, last_added_location_id)) =
location::get_kml(context, self.msg.chat_id).await?
else {
return Ok(None);
};
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
@@ -878,7 +886,7 @@ impl<'a> MimeFactory<'a> {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
Ok(part)
Ok(Some(part))
}
#[allow(clippy::cognitive_complexity)]
@@ -1168,7 +1176,10 @@ impl<'a> MimeFactory<'a> {
}
let flowed_text = format_flowed(final_text);
let footer = &self.selfstatus;
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
let footer = if is_reaction { "" } else { &self.selfstatus };
let message_text = format!(
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
@@ -1191,7 +1202,7 @@ impl<'a> MimeFactory<'a> {
))
.body(message_text);
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
if is_reaction {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
@@ -1230,11 +1241,8 @@ impl<'a> MimeFactory<'a> {
}
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
match self.get_location_kml_part(context).await {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not send location: {}", err);
}
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
}
}
@@ -1363,15 +1371,16 @@ impl<'a> MimeFactory<'a> {
}
}
/// Returns base64-encoded buffer `buf` split into 78-bytes long
/// Returns base64-encoded buffer `buf` split into 76-bytes long
/// chunks separated by CRLF.
///
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8)
/// says that "The encoded output stream must be represented in lines of no more than 76 characters each."
/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin.
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
let mut chars = base64.chars();
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
std::iter::repeat_with(|| chars.by_ref().take(76).collect::<String>())
.take_while(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\r\n")
@@ -1530,6 +1539,7 @@ fn maybe_encode_words(words: &str) -> String {
#[cfg(test)]
mod tests {
use mailparse::{addrparse_header, MailHeaderMap};
use std::str;
use super::*;
use crate::chat::ChatId;
@@ -1538,10 +1548,11 @@ mod tests {
ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::constants;
use crate::contact::{ContactAddress, Origin};
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1611,8 +1622,8 @@ mod tests {
fn test_wrapped_base64_encode() {
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let output =
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
FBQUFBQUFBQQ==";
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
QUFBQUFBQUFBQQ==";
assert_eq!(wrapped_base64_encode(input), output);
}
@@ -2190,7 +2201,11 @@ mod tests {
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/mixed").count(), 1);
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
@@ -2302,4 +2317,37 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_headers_directive() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = tcm
.send_recv_accept(&alice, &bob, "alice->bob")
.await
.chat_id;
// Now Bob can send an encrypted message to Alice.
let mut msg = Message::new(Viewtype::File);
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
// decoded_data to check presense of the necessary headers.
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
.await?;
let sent = bob.send_msg(chat, &mut msg).await;
assert!(msg.get_showpadlock());
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("Subject:").count(), 1);
Ok(())
}
}

View File

@@ -804,18 +804,6 @@ impl MimeMessage {
// Boxed future to deal with recursion
async move {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(context, "Ignoring nested protected headers");
}
enum MimeS {
Multiple,
Single,
@@ -852,7 +840,10 @@ impl MimeMessage {
self.parse_mime_recursive(context, &mail, is_related).await
}
MimeS::Single => self.add_single_part_if_known(context, mail, is_related).await,
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
}
}
}
.boxed()
@@ -1442,33 +1433,36 @@ impl MimeMessage {
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
.collect()
});
if report_fields
.get_header_value(HeaderDef::Disposition)
.is_none()
{
warn!(
context,
"Ignoring unknown disposition-notification, Message-Id: {:?}.",
report_fields.get_header_value(HeaderDef::MessageId)
);
return Ok(None);
};
return Ok(Some(Report {
original_message_id,
additional_message_ids,
}));
}
warn!(
context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId)
);
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
.collect()
});
Ok(None)
Ok(Some(Report {
original_message_id,
additional_message_ids,
}))
}
fn process_delivery_status(

View File

@@ -4,12 +4,20 @@ use std::time::Duration;
use anyhow::{anyhow, Result};
use mime::Mime;
use once_cell::sync::Lazy;
use crate::context::Context;
use crate::socks::Socks5Config;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
reqwest::tls::Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
/// HTTP(S) GET response.
#[derive(Debug)]
pub struct Response {
@@ -79,7 +87,10 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
}
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
let builder = reqwest::ClientBuilder::new()
.timeout(HTTP_TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)

View File

@@ -8,6 +8,7 @@ use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
use crate::config::Config;
use crate::context::Context;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
use crate::tools::EmailAddress;
/// Provider status according to manual testing.
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -175,21 +176,30 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
Ok(resolver)
}
/// Returns provider for the given an e-mail address.
///
/// Returns an error if provided address is not valid.
pub async fn get_provider_info_by_addr(
context: &Context,
addr: &str,
skip_mx: bool,
) -> Result<Option<&'static Provider>> {
let addr = EmailAddress::new(addr)?;
let provider = get_provider_info(context, &addr.domain, skip_mx).await;
Ok(provider)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database first. If not
/// found, it queries MX record for the domain and looks up offline
/// database for MX domains.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
pub async fn get_provider_info(
context: &Context,
domain: &str,
skip_mx: bool,
) -> Option<&'static Provider> {
let domain = domain.rsplit('@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
@@ -314,15 +324,25 @@ mod tests {
let t = TestContext::new().await;
assert!(get_provider_info(&t, "", false).await.is_none());
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
assert!(get_provider_info(&t, "example@google.com", false)
.await
.is_none());
}
// get_provider_info() accepts email addresses for backwards compatibility
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_provider_info_by_addr() -> Result<()> {
let t = TestContext::new().await;
assert!(get_provider_info_by_addr(&t, "google.com", false)
.await
.is_err());
assert!(
get_provider_info(&t, "example@google.com", false)
.await
get_provider_info_by_addr(&t, "example@google.com", false)
.await?
.unwrap()
.id
== "gmail"
);
Ok(())
}
#[test]

View File

@@ -431,6 +431,32 @@ Content-Disposition: reaction\n\
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
assert_eq!(bob_reaction.as_str(), "👍");
// Alice receives reaction to her message from Bob with a footer.
receive_imf(
&alice,
"To: alice@example.org\n\
From: bob@example.net\n\
Date: Today, 29 February 2021 00:00:10 -800\n\
Message-ID: 56790@example.net\n\
In-Reply-To: 12345@example.org\n\
Subject: Meeting\n\
Mime-Version: 1.0 (1.0)\n\
Content-Type: text/plain; charset=utf-8\n\
Content-Disposition: reaction\n\
\n\
😀\n\
\n\
--\n\
_______________________________________________\n\
Here's my footer -- bob@example.net"
.as_bytes(),
false,
)
.await?;
let reactions = get_msg_reactions(&alice, msg.id).await?;
assert_eq!(reactions.to_string(), "😀1");
Ok(())
}
@@ -464,6 +490,16 @@ Content-Disposition: reaction\n\
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Test that the status does not get mixed up into reactions.
alice
.set_config(
Config::Selfstatus,
Some("Buy Delta Chat today and make this banner go away!"),
)
.await?;
bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. 👍"))
.await?;
let chat_alice = alice.create_chat(&bob).await;
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
let bob_msg = bob.recv_msg(&alice_msg).await;

View File

@@ -35,6 +35,7 @@ use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
use crate::reaction::{set_msg_reaction, Reaction};
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
use crate::simplify;
use crate::sql;
use crate::stock_str;
use crate::tools::{
@@ -113,21 +114,20 @@ pub(crate) async fn receive_imf_inner(
{
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
let msg_ids;
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?;
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
} else {
return Ok(None);
if rfc724_mid.starts_with(GENERATED_PREFIX) {
// We don't have an rfc724_mid, there's no point in adding a trash entry
return Ok(None);
}
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?;
let msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::Undefined,
@@ -1093,12 +1093,13 @@ async fn add_parts(
for part in &mut mime_parser.parts {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
set_msg_reaction(
context,
&mime_in_reply_to,
orig_chat_id.unwrap_or_default(),
from_id,
Reaction::from(part.msg.as_str()),
Reaction::from(reaction_str.as_str()),
)
.await?;
}
@@ -1129,7 +1130,8 @@ async fn add_parts(
(&part.msg, part.typ)
};
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
let mime_modified = save_mime_modified && !part_is_empty;
if mime_modified {
// Avoid setting mime_modified for more than one part.
@@ -1154,7 +1156,8 @@ async fn add_parts(
// If you change which information is skipped if the message is trashed,
// also change `MsgId::trash()` and `delete_expired_messages()`
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
let trash =
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
let row_id = context
.sql
@@ -1408,56 +1411,53 @@ async fn lookup_chat_by_reply(
) -> Result<Option<(ChatId, Blocked)>> {
// Try to assign message to the same chat as the parent message.
if let Some(parent) = parent {
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
let Some(parent) = parent else {
return Ok(None);
};
if parent.download_state != DownloadState::Done
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
// `DownloadState::Undecipherable`. Remove eventually with the comment in
// `MimeMessage::from_bytes()`.
|| parent
.error
.as_ref()
.filter(|e| e.starts_with("Decrypting failed:"))
.is_some()
{
// If the parent msg is not fully downloaded or undecipherable, it may have been
// assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender).
return Ok(None);
}
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
if parent_chat.id == DC_CHAT_ID_TRASH {
return Ok(None);
}
// If this was a private message just to self, it was probably a private reply.
// It should not go into the group then, but into the private chat.
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
return Ok(None);
}
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
// newly created ad-hoc group.
if parent_chat.typ == Chattype::Single
&& !mime_parser.has_chat_version()
&& to_ids.len() > 1
{
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
chat_contacts.push(ContactId::SELF);
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
return Ok(None);
}
}
info!(
context,
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
);
return Ok(Some((parent_chat.id, parent_chat.blocked)));
if parent.download_state != DownloadState::Done
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
// `DownloadState::Undecipherable`. Remove eventually with the comment in
// `MimeMessage::from_bytes()`.
|| parent
.error
.as_ref()
.filter(|e| e.starts_with("Decrypting failed:"))
.is_some()
{
// If the parent msg is not fully downloaded or undecipherable, it may have been
// assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender).
return Ok(None);
}
Ok(None)
if parent_chat.id == DC_CHAT_ID_TRASH {
return Ok(None);
}
// If this was a private message just to self, it was probably a private reply.
// It should not go into the group then, but into the private chat.
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
return Ok(None);
}
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
// newly created ad-hoc group.
if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
chat_contacts.push(ContactId::SELF);
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
return Ok(None);
}
}
info!(
context,
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
);
Ok(Some((parent_chat.id, parent_chat.blocked)))
}
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
@@ -1663,7 +1663,7 @@ async fn apply_group_changes(
}
let mut send_event_chat_modified = false;
let mut removed_id = None;
let (mut removed_id, mut added_id) = (None, None);
let mut better_msg = None;
// True if a Delta Chat client has explicitly added our current primary address.
@@ -1674,8 +1674,9 @@ async fn apply_group_changes(
false
};
let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|| chat::is_contact_in_chat(context, chat_id, from_id).await?;
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
let is_from_in_chat =
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
// Reject group membership changes from non-members and old changes.
let allow_member_list_changes = is_from_in_chat
@@ -1685,12 +1686,10 @@ async fn apply_group_changes(
// Whether to rebuild the member list from scratch.
let recreate_member_list = {
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
!mime_parser.has_chat_version()
// Always recreate membership list if SELF has been added. The older versions of DC
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
// delivered message (so it's a race), so we have this heuristic here.
|| self_added
// Always recreate membership list if SELF has been added. The older versions of DC
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
// delivered message (so it's a race), so we have this heuristic here.
self_added
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
// If we don't know the referenced message, we missed some messages.
// Maybe they added/removed members, so we need to recreate our member list.
@@ -1716,14 +1715,8 @@ async fn apply_group_changes(
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if let Some(contact_id) = removed_id {
if allow_member_list_changes {
// Remove a single member from the chat.
if !recreate_member_list {
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
send_event_chat_modified = true;
}
} else {
if removed_id.is_some() {
if !allow_member_list_changes {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
@@ -1736,13 +1729,11 @@ async fn apply_group_changes(
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
// Add a single member to the chat.
if !recreate_member_list {
if let Some(contact_id) =
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
{
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
send_event_chat_modified = true;
added_id = Some(contact_id);
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
@@ -1809,46 +1800,76 @@ async fn apply_group_changes(
}
}
// Recreate the member list.
if recreate_member_list {
// Only delete old contacts if the sender is not a classical MUA user:
// Classical MUA users usually don't intend to remove users from an email
// thread, so if they removed a recipient then it was probably by accident.
if mime_parser.has_chat_version() {
if allow_member_list_changes {
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
new_members.insert(from_id);
}
if !recreate_member_list {
let diff: HashSet<ContactId> =
chat_contacts.difference(&new_members).copied().collect();
// Only delete old contacts if the sender is not a classical MUA user:
// Classical MUA users usually don't intend to remove users from an email
// thread, so if they removed a recipient then it was probably by accident.
if mime_parser.has_chat_version() {
// This is what provides group membership consistency: we remove group members
// locally if we see a discrepancy with the "To" list in the received message as it
// is better for privacy than adding absent members locally. But it shouldn't be a
// big problem if somebody missed a member addition, because they will likely
// recreate the member list from the next received message. The problem occurs only
// if that "somebody" managed to reply earlier. Really, it's a problem for big
// groups with high message rate, but let it be for now.
if !diff.is_empty() {
warn!(context, "Implicit removal of {diff:?} from chat {chat_id}.");
}
new_members = chat_contacts.difference(&diff).copied().collect();
} else {
new_members.extend(diff);
}
}
if let Some(removed_id) = removed_id {
new_members.remove(&removed_id);
}
if let Some(added_id) = added_id {
new_members.insert(added_id);
}
if recreate_member_list {
info!(
context,
"Recreating chat {chat_id} member list with {new_members:?}.",
);
}
if new_members != chat_contacts {
let new_members_ref = &new_members;
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
.transaction(move |transaction| {
transaction
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
for contact_id in new_members_ref {
transaction.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
(chat_id, contact_id),
)?;
}
Ok(())
})
.await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
let mut members_to_add = HashSet::new();
members_to_add.extend(to_ids);
members_to_add.insert(ContactId::SELF);
if !from_id.is_special() {
members_to_add.insert(from_id);
}
if let Some(removed_id) = removed_id {
members_to_add.remove(&removed_id);
}
info!(
context,
"Recreating chat {chat_id} with members {members_to_add:?}."
);
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?;
send_event_chat_modified = true;
}
if let Some(avatar_action) = &mime_parser.group_avatar {
if !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
if !chat_contacts.contains(&ContactId::SELF) {
warn!(
context,
"Received group avatar update for group chat {chat_id} we are not a member of."
);
} else if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
} else if !chat_contacts.contains(&from_id) {
warn!(
context,
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
@@ -2009,39 +2030,40 @@ async fn apply_mailinglist_changes(
mime_parser: &MimeMessage,
chat_id: ChatId,
) -> Result<()> {
if let Some(list_post) = &mime_parser.list_post {
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Mailinglist {
let Some(list_post) = &mime_parser.list_post else {
return Ok(());
};
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Mailinglist {
return Ok(());
}
let listid = &chat.grpid;
let list_post = match ContactAddress::new(list_post) {
Ok(list_post) => list_post,
Err(err) => {
warn!(context, "Invalid List-Post: {:#}.", err);
return Ok(());
}
let listid = &chat.grpid;
};
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
contact.update_param(context).await?;
}
let list_post = match ContactAddress::new(list_post) {
Ok(list_post) => list_post,
Err(err) => {
warn!(context, "Invalid List-Post: {:#}.", err);
return Ok(());
}
};
let (contact_id, _) =
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
contact.update_param(context).await?;
}
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
if list_post.as_ref() != old_list_post {
// Apparently the mailing list is using a different List-Post header in each message.
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
chat.param.remove(Param::ListPost);
chat.update_param(context).await?;
}
} else {
chat.param.set(Param::ListPost, list_post);
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
if list_post.as_ref() != old_list_post {
// Apparently the mailing list is using a different List-Post header in each message.
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
chat.param.remove(Param::ListPost);
chat.update_param(context).await?;
}
} else {
chat.param.set(Param::ListPost, list_post);
chat.update_param(context).await?;
}
Ok(())

View File

@@ -3369,20 +3369,15 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
alice.recv_msg(&bob.pop_sent_msg().await).await;
// bob didn't receive the addition of fiona, but alice doesn't overwrite her own
// contact list with the one from bob which only has three members instead of four.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
// bob removes a member.
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
// Bobs chat only has two members after the removal of blue, because he still
// didn't receive the addition of fiona. But that doesn't overwrite alice'
// memberlist.
// Bob didn't receive the addition of Fiona, so Alice must remove Fiona from the members list
// back to make their group members view consistent.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
alice.recv_msg(&bob.pop_sent_msg().await).await;
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
Ok(())
}
@@ -3514,6 +3509,29 @@ async fn test_mua_cant_remove() -> Result<()> {
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
4
);
// But if the parent message is missing, the message must goto a new ad-hoc group.
let bob_removes = receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>, <claire@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:32:40 +0000\r\n\
Message-ID: <bobs_answer_to_two_recipients_1@example.net>\r\n\
In-Reply-To: <Mr.missing@example.org>\r\n\
\r\n\
Hi back!\r\n",
false,
)
.await?
.unwrap();
assert_ne!(bob_removes.chat_id, alice_chat.id);
let group_chat = Chat::load_from_db(&alice, bob_removes.chat_id).await?;
assert_eq!(group_chat.typ, Chattype::Group);
assert_eq!(
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
3,
);
Ok(())
}

View File

@@ -1,4 +1,5 @@
use core::fmt;
use std::cmp::min;
use std::{iter::once, ops::Deref, sync::Arc};
use anyhow::{anyhow, Result};
@@ -457,7 +458,8 @@ impl Context {
} else {
"green"
};
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {percent}%\">{percent}%</div></div>");
let div_width_percent = min(100, percent);
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>");
ret += "</li>";
}

View File

@@ -67,6 +67,15 @@ fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
(lines, false)
}
/// Remove footers if any.
/// This also makes all newlines "\n", but why not.
pub(crate) fn remove_footers(msg: &str) -> String {
let lines = split_lines(msg);
let lines = remove_message_footer(&lines).0;
let lines = remove_nonstandard_footer(lines).0;
lines.join("\n")
}
pub(crate) fn split_lines(buf: &str) -> Vec<&str> {
buf.split('\n').collect()
}

View File

@@ -108,9 +108,15 @@ impl TestContextManager {
/// - Let one TestContext send a message
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
pub async fn send_recv_accept(
&self,
from: &TestContext,
to: &TestContext,
msg: &str,
) -> Message {
let received_msg = self.send_recv(from, to, msg).await;
received_msg.chat_id.accept(to).await.unwrap();
received_msg
}
/// - Let one TestContext send a message

View File

@@ -26,7 +26,6 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::AsyncReadExt;
use crate::blob::BlobObject;
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::ContactId;
@@ -846,35 +845,6 @@ impl Message {
}
}
/// Replaces WebXDC blob of existing message.
///
/// This API is supposed to be called from within a WebXDC to replace itself
/// e.g. with an updated or persistently reconfigured version.
pub async fn replace_webxdc(context: &Context, msg_id: MsgId, data: &[u8]) -> Result<()> {
let mut msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.get_viewtype() == Viewtype::Webxdc,
"Message {msg_id} is not a WebXDC instance"
);
let blob = BlobObject::create(
context,
&msg.get_filename()
.context("Cannot get filename of exising WebXDC instance")?,
data,
)
.await
.context("Failed to create WebXDC replacement blob")?;
let mut param = msg.param.clone();
param.set(Param::File, blob.as_name());
msg.param = param;
msg.update_param(context).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::json;
@@ -1042,7 +1012,7 @@ mod tests {
let instance = send_webxdc_instance(&t, chat_id).await?;
t.send_webxdc_status_update(
instance.id,
r#"{"info": "foo", "summary":"bar", "payload": 42}"#,
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
"descr",
)
.await?;
@@ -1050,7 +1020,7 @@ mod tests {
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
);
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
let info = Message::load_from_db(&t, instance.id)
@@ -1058,6 +1028,7 @@ mod tests {
.get_webxdc_info(&t)
.await?;
assert_eq!(info.summary, "bar".to_string());
assert_eq!(info.document, "doc".to_string());
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
@@ -1074,6 +1045,7 @@ mod tests {
.get_webxdc_info(&t)
.await?;
assert_eq!(info.summary, "".to_string());
assert_eq!(info.document, "".to_string());
Ok(())
}
@@ -2653,62 +2625,4 @@ sth_for_the = "future""#
Ok(())
}
/// Tests replacing WebXDC with a newer version.
///
/// Updates should be preserved after upgrading.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_replace_webxdc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Alice sends WebXDC instance.
let alice_chat = alice.create_chat(&bob).await;
let mut alice_instance = create_webxdc_instance(
&alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
alice_instance.set_text("user added text".to_string());
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
let alice_instance = alice.get_last_msg().await;
assert_eq!(alice_instance.get_text(), "user added text");
// Bob receives that instance.
let alice_sent_instance = alice.pop_sent_msg().await;
let bob_received_instance = bob.recv_msg(&alice_sent_instance).await;
assert_eq!(bob_received_instance.get_text(), "user added text");
// Alice sends WebXDC update.
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update")
.await?;
alice.flush_status_updates().await?;
let alice_sent_update = alice.pop_sent_msg().await;
bob.recv_msg(&alice_sent_update).await;
assert_eq!(
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
);
// Bob replaces WebXDC.
replace_webxdc(
&bob,
bob_received_instance.id,
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
)
.await
.context("Failed to replace WebXDC")?;
// Updates are not modified.
assert_eq!(
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
);
Ok(())
}
}