Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
22aa1b7b12 feat: add send_webxdc_replacement() 2023-09-06 20:34:14 +00:00
52 changed files with 813 additions and 1067 deletions

View File

@@ -182,15 +182,15 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.12
python: 3.11
- os: macos-latest
python: 3.12
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
@@ -231,10 +231,6 @@ 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
@@ -242,9 +238,9 @@ jobs:
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.8
#

View File

@@ -1,105 +1,5 @@
# Changelog
## [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
- jsonrpc: Return only chat IDs for similar chats.
### Fixes
- Reopen all connections on database passpharse change.
- Do not block new group chats if 1:1 chat is blocked.
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
### Build system
- Update to OpenSSL 3.0.
- Bump webpki from 0.22.0 to 0.22.1.
- python: Add link to Mastodon into projects.urls.
### Features / Changes
- Add RSA-4096 key generation support.
### Refactor
- pgp: Add constants for encryption algorithm and hash.
## [1.121.0] - 2023-09-06
### API-Changes
- Add `dc_context_change_passphrase()`.
- Add `Message.set_file_from_bytes()` API.
- Add experimental API to get similar chats.
### Build system
- Build node packages on Ubuntu 18.04 instead of Debian 10.
This reduces the requirement for glibc version from 2.28 to 2.27.
### Fixes
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
- Do not allow dots at the end of email addresses.
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
## [1.120.0] - 2023-08-28
### API-Changes
@@ -2864,7 +2764,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[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

44
Cargo.lock generated
View File

@@ -312,9 +312,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.20"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
dependencies = [
"async-trait",
"axum-core",
@@ -1103,7 +1103,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.124.0"
version = "1.120.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1179,7 +1179,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.124.0"
version = "1.120.0"
dependencies = [
"anyhow",
"async-channel",
@@ -1203,7 +1203,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.124.0"
version = "1.120.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1218,7 +1218,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.124.0"
version = "1.120.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1243,7 +1243,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.124.0"
version = "1.120.0"
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#37778c89d5eb5a94b7983f3f37ff67769bde3cf9"
source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
dependencies = [
"base64 0.11.0",
"chrono",
@@ -3078,11 +3078,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.57"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags 2.3.3",
"bitflags 1.3.2",
"cfg-if",
"foreign-types",
"libc",
@@ -3110,18 +3110,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.1.5+3.1.3"
version = "111.26.0+1.1.1u"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.93"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
@@ -4947,9 +4947,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.20.1"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
dependencies = [
"futures-util",
"log",
@@ -5159,13 +5159,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "tungstenite"
version = "0.20.1"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
dependencies = [
"base64 0.13.1",
"byteorder",
"bytes",
"data-encoding",
"http",
"httparse",
"log",
@@ -5448,9 +5448,9 @@ dependencies = [
[[package]]
name = "webpki"
version = "0.22.2"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [
"ring",
"untrusted",

View File

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

View File

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

View File

@@ -443,9 +443,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -824,7 +822,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 Ignored, actual public key is extracted from secret_data.
* @param public_data ASCII armored public key.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -1481,7 +1479,6 @@ 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.
@@ -2285,7 +2282,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
*
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
* If `param1` is a filename, import the private key from the file and make it the default.
*
* While dc_imex() returns immediately, the started job may take a while,
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
@@ -5048,7 +5044,6 @@ 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);
@@ -5061,7 +5056,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5757,11 +5752,12 @@ 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 input JSON-RPC request.
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If there is no response, NULL is returned.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
@@ -6297,7 +6293,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**

View File

@@ -29,7 +29,7 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::{DcKey, DcSecretKey};
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
@@ -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,
@@ -1431,7 +1431,6 @@ 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,
@@ -2568,12 +2567,7 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
}
#[no_mangle]
@@ -4537,14 +4531,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4571,14 +4558,11 @@ 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_by_addr(
match block_on(provider::get_provider_info(
ctx,
addr.as_str(),
socks5_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -5026,7 +5010,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, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use super::*;
@@ -5102,24 +5086,25 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
method: *const libc::c_char,
params: *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 input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
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));
match res {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

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

View File

@@ -39,6 +39,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::events::Event;
use types::http::HttpResponse;
@@ -567,18 +568,22 @@ impl CommandApi {
}
/// Returns chats similar to the given one.
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
async fn get_similar_chatlist_entries(
&self,
account_id: u32,
chat_id: u32,
) -> Result<Vec<ChatListEntry>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
let list = chat_id.get_similar_chatlist(&ctx).await?;
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
for i in 0..list.len() {
l.push(ChatListEntry(
list.get_chat_id(i)?.to_u32(),
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
));
}
Ok(l)
}
async fn get_chatlist_items_by_entries(
@@ -1427,10 +1432,6 @@ 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

@@ -8,12 +8,15 @@ use deltachat::{
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {

View File

@@ -318,7 +318,6 @@ pub enum DownloadState {
Done,
Available,
Failure,
Undecipherable,
InProgress,
}
@@ -328,7 +327,6 @@ impl From<download::DownloadState> for DownloadState {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}

View File

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

View File

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

View File

@@ -187,7 +187,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]",
DownloadState::Undecipherable => " [⬇ Decryption failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -894,7 +893,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,6 +1,5 @@
import asyncio
import json
import logging
import os
from typing import Any, Dict, Optional
@@ -29,16 +28,10 @@ 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
@@ -64,20 +57,16 @@ class Rpc:
await self.close()
async def reader_loop(self) -> None:
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")
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)
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
@@ -86,17 +75,13 @@ class Rpc:
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
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")
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"])
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.124.0"
version = "1.120.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -3,6 +3,11 @@ unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Exponential CPU time usage for TLS certificate processing in webpki.
# It is only used for backup transfer, so does not affect IMAP and SMTP connections.
# Waiting for `iroh` to update dependencies.
"RUSTSEC-2023-0052",
]
[bans]

View File

@@ -90,7 +90,6 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,

View File

@@ -90,7 +90,6 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,

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.124.0"
"version": "1.120.0"
}

View File

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

View File

@@ -34,7 +34,6 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"

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, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, public: 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),
ffi.NULL,
as_dc_charpointer(public),
as_dc_charpointer(secret),
)
if res == 0:

View File

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

View File

@@ -478,9 +478,10 @@ 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_sec:
account._preconfigure_keypair(addr, fname_sec)
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")

View File

@@ -1,7 +1,6 @@
import sys
import pytest
import deltachat as dc
class TestGroupStressTests:
@@ -150,8 +149,9 @@ 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).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
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).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")

View File

@@ -1679,36 +1679,6 @@ def test_qr_join_chat(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
ac1_new_chat.send_text("Hello!")
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
# Receive "Hello!" message.
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group

View File

@@ -67,9 +67,10 @@ 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_secret
ac._preconfigure_keypair("alice@example.org", alice_secret)
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -1,4 +1,3 @@
import json
from queue import Queue
import deltachat as dc
@@ -227,26 +226,10 @@ def test_jsonrpc_blocking_call(tmp_path):
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
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"),
),
),
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
assert res == "true"
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}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -1 +1 @@
2023-10-04
2023-08-28

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,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -23,7 +23,6 @@ use crate::constants::{
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
@@ -918,10 +917,9 @@ impl ChatId {
.sql
.query_map(
"SELECT chat_id, count(*) AS n
FROM chats_contacts
WHERE contact_id > ? AND chat_id > ?
FROM chats_contacts where contact_id > 9
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)?;
@@ -1040,14 +1038,11 @@ impl ChatId {
T: Send + 'static,
{
let sql = &context.sql;
// Do not reply to not fully downloaded messages. Such a message could be a group chat
// message that we assigned to 1:1 chat.
let query = format!(
"SELECT {fields} \
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?, ?, ?) AND NOT hidden \
ORDER BY timestamp DESC, id DESC \
LIMIT 1;",
DownloadState::Done as u32,
LIMIT 1;"
);
let row = sql
.query_row_optional(
@@ -1056,11 +1051,8 @@ impl ChatId {
self,
MessageState::OutPreparing,
MessageState::OutDraft,
// We don't filter `OutPending` and `OutFailed` messages because the new message
// for which `parent_query()` is done may assume that it will be received in a
// context affected by those messages, e.g. they could add new members to a
// group and the new message will contain them in "To:". Anyway recipients must
// be prepared to orphaned references.
MessageState::OutPending,
MessageState::OutFailed,
),
f,
)
@@ -1072,17 +1064,34 @@ impl ChatId {
self,
context: &Context,
) -> Result<Option<(String, String, String)>> {
self.parent_query(
context,
"rfc724_mid, mime_in_reply_to, mime_references",
|row: &rusqlite::Row| {
let rfc724_mid: String = row.get(0)?;
let mime_in_reply_to: String = row.get(1)?;
let mime_references: String = row.get(2)?;
Ok((rfc724_mid, mime_in_reply_to, mime_references))
},
)
.await
if let Some((rfc724_mid, mime_in_reply_to, mime_references, error)) = self
.parent_query(
context,
"rfc724_mid, mime_in_reply_to, mime_references, error",
|row: &rusqlite::Row| {
let rfc724_mid: String = row.get(0)?;
let mime_in_reply_to: String = row.get(1)?;
let mime_references: String = row.get(2)?;
let error: String = row.get(3)?;
Ok((rfc724_mid, mime_in_reply_to, mime_references, error))
},
)
.await?
{
if !error.is_empty() {
// Do not reply to error messages.
//
// An error message could be a group chat message that we failed to decrypt and
// assigned to 1:1 chat. A reply to it will show up as a reply to group message
// on the other side. To avoid such situations, it is better not to reply to
// error messages at all.
Ok(None)
} else {
Ok(Some((rfc724_mid, mime_in_reply_to, mime_references)))
}
} else {
Ok(None)
}
}
/// Returns multi-line text summary of encryption preferences of all chat contacts.
@@ -2898,9 +2907,6 @@ 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,
@@ -3532,8 +3538,6 @@ 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

@@ -62,15 +62,8 @@ pub enum MediaQuality {
pub enum KeyGenType {
#[default]
Default = 0,
/// 2048-bit RSA.
Rsa2048 = 1,
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
Ed25519 = 2,
/// 4096-bit RSA.
Rsa4096 = 3,
}
/// Video chat URL type.
@@ -238,7 +231,6 @@ mod tests {
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
}
#[test]

View File

@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -1252,22 +1252,11 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
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)
}
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)
}
}

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 at least 1 message every 10 seconds + a burst of 6.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
@@ -814,22 +814,7 @@ 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 => {
// 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?
}
None => MsgId::new_unset(),
};
let list = self
@@ -1474,35 +1459,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_context_change_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
context
.set_config(Config::Addr, Some("alice@example.org"))
.await?;
context
.change_passphrase("bar".to_string())
.await
.context("Failed to change passphrase")?;
assert_eq!(
context.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;

View File

@@ -59,9 +59,6 @@ pub enum DownloadState {
/// Failed to fully download the message.
Failure = 20,
/// Undecipherable message.
Undecipherable = 30,
/// Full download of the message is in progress.
InProgress = 1000,
}
@@ -83,9 +80,7 @@ impl MsgId {
pub async fn download_full(self, context: &Context) -> Result<()> {
let msg = Message::load_from_db(context, self).await?;
match msg.download_state() {
DownloadState::Done | DownloadState::Undecipherable => {
return Err(anyhow!("Nothing to download."))
}
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)

View File

@@ -77,6 +77,9 @@ pub enum HeaderDef {
SecureJoinAuth,
Sender,
/// [`Supersedes`](https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46) header.
Supersedes,
/// Ephemeral message timer.
EphemeralTimer,
Received,

View File

@@ -586,74 +586,63 @@ async fn export_backup_inner(
Ok(())
}
/// Imports secret key from a file.
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
let buf = read_file(context, &path).await?;
let armored = std::string::String::from_utf8_lossy(&buf);
set_self_key(context, &armored, set_default, false).await?;
Ok(())
}
/// Imports secret keys from the provided file or directory.
///
/// If provided path is a file, ASCII-armored secret key is read from the file
/// and set as the default key.
///
/// If provided path is a directory, all files with .asc extension
/// containing secret keys are imported and the last successfully
/// imported which does not contain "legacy" in its filename
/// is set as the default.
async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
let attr = tokio::fs::metadata(path).await?;
if attr.is_file() {
info!(
context,
"Importing secret key from {} as the default key.",
path.display()
);
let set_default = true;
import_secret_key(context, path, set_default).await?;
return Ok(());
}
/*******************************************************************************
* Classic key import
******************************************************************************/
async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
Maybe we should make the "default" key handlong also a little bit smarter
(currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */
let mut set_default: bool;
let mut imported_cnt = 0;
let mut dir_handle = tokio::fs::read_dir(&path).await?;
let dir_name = dir.to_string_lossy();
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
while let Ok(Some(entry)) = dir_handle.next_entry().await {
let entry_fn = entry.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = path.join(&entry_fn);
if let Some(suffix) = get_filesuffix_lc(&name_f) {
if suffix != "asc" {
let path_plus_name = dir.join(&entry_fn);
match get_filesuffix_lc(&name_f) {
Some(suffix) => {
if suffix != "asc" {
continue;
}
set_default = if name_f.contains("legacy") {
info!(context, "found legacy key '{}'", path_plus_name.display());
false
} else {
true
}
}
None => {
continue;
}
} else {
continue;
};
let set_default = !name_f.contains("legacy");
}
info!(
context,
"Considering key file: {}.",
"considering key file: {}",
path_plus_name.display()
);
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
warn!(
context,
"Failed to import secret key from {}: {:#}.",
path_plus_name.display(),
err
);
continue;
match read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
info!(context, "set_self_key: {}", err);
continue;
}
}
Err(_) => continue,
}
imported_cnt += 1;
}
ensure!(
imported_cnt > 0,
"No private keys found in {}.",
path.display()
"No private keys found in \"{}\".",
dir_name
);
Ok(())
}
@@ -684,8 +673,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
.await?;
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default == 0);
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
error!(context, "Failed to export public key: {:#}.", err);
@@ -876,35 +864,14 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_key() {
let export_dir = tempfile::tempdir().unwrap();
let context = TestContext::new_alice().await;
if let Err(err) = imex(
&context.ctx,
ImexMode::ExportSelfKeys,
export_dir.path(),
None,
)
.await
{
let blobdir = context.ctx.get_blobdir();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
panic!("got error on export: {err:#}");
}
let context2 = TestContext::new_alice().await;
if let Err(err) = imex(
&context2.ctx,
ImexMode::ImportSelfKeys,
export_dir.path(),
None,
)
.await
{
panic!("got error on import: {err:#}");
}
let keyfile = export_dir.path().join("private-key-default.asc");
let context3 = TestContext::new_alice().await;
if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await {
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
panic!("got error on import: {err:#}");
}
}

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) -> Result<bool> {
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
if latitude == 0.0 && longitude == 0.0 {
return Ok(true);
return true;
}
let mut continue_streaming = false;
let chats = context
if let Ok(chats) = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
@@ -346,29 +346,33 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
.map_err(Into::into)
},
)
.await?;
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;
.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)));
};
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
};
Ok(continue_streaming)
continue_streaming
}
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
@@ -460,7 +464,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
}
/// Returns `location.kml` contents.
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
let mut last_added_location_id = 0;
let self_addr = context.get_primary_self_addr().await?;
@@ -530,11 +534,9 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(Strin
ret += "</Document>\n</kml>";
}
if location_count > 0 {
Ok(Some((ret, last_added_location_id)))
} else {
Ok(None)
}
ensure!(location_count > 0, "No locations processed");
Ok((ret, last_added_location_id))
}
fn get_kml_timestamp(utc: i64) -> String {
@@ -926,38 +928,4 @@ 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

@@ -435,6 +435,18 @@ pub struct Message {
/// `In-Reply-To` header value.
pub(crate) in_reply_to: Option<String>,
/// `Supersedes` header value.
///
/// It is only used for sending and not stored in the database.
///
/// The header contains `Message-ID` of the original message
/// superseded by this one.
///
/// The header is specified
/// in <https://www.rfc-editor.org/rfc/rfc4021.html#section-2.1.46>.
pub(crate) supersedes: Option<String>,
pub(crate) is_dc_message: MessengerMessage,
pub(crate) mime_modified: bool,
pub(crate) chat_blocked: Blocked,
@@ -530,6 +542,7 @@ impl Message {
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
.filter(|error| !error.is_empty()),
supersedes: None, // `Supersedes` header is only used for sending and not stored in the database.
is_dc_message: row.get("msgrmsg")?,
mime_modified: row.get("mime_modified")?,
text,

View File

@@ -549,6 +549,11 @@ impl<'a> MimeFactory<'a> {
Loaded::Mdn { .. } => create_outgoing_rfc724_mid(None, &self.from_addr),
};
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
if let Some(supersedes) = &self.msg.supersedes {
headers
.protected
.push(Header::new("Supersedes".into(), supersedes.to_string()));
}
// Amazon's SMTP servers change the `Message-ID`, just as Outlook's SMTP servers do.
// Outlook's servers add an `X-Microsoft-Original-Message-ID` header with the original `Message-ID`,
@@ -678,12 +683,6 @@ 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);
@@ -720,7 +719,10 @@ impl<'a> MimeFactory<'a> {
if !existing_ct.ends_with(';') {
existing_ct += ";";
}
let message = message.header(get_content_type_directives_header());
let message = message.replace_header(Header::new(
"Content-Type".to_string(),
format!("{existing_ct} protected-headers=\"v1\";"),
));
// Set the appropriate Content-Type for the outer message
let outer_message = PartBuilder::new().header((
@@ -789,12 +791,11 @@ 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",
"multipart/signed; protocol=\"application/pgp-signature\"",
"Content-Type".to_string(),
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
))
.child(payload)
.child(
@@ -864,13 +865,9 @@ impl<'a> MimeFactory<'a> {
}
/// Returns MIME part with a `location.kml` attachment.
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);
};
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?;
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
@@ -886,7 +883,7 @@ impl<'a> MimeFactory<'a> {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
}
Ok(Some(part))
Ok(part)
}
#[allow(clippy::cognitive_complexity)]
@@ -1176,10 +1173,7 @@ impl<'a> MimeFactory<'a> {
}
let flowed_text = format_flowed(final_text);
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
let footer = if is_reaction { "" } else { &self.selfstatus };
let footer = &self.selfstatus;
let message_text = format!(
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
@@ -1202,7 +1196,7 @@ impl<'a> MimeFactory<'a> {
))
.body(message_text);
if is_reaction {
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
@@ -1241,8 +1235,11 @@ impl<'a> MimeFactory<'a> {
}
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
match self.get_location_kml_part(context).await {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not send location: {}", err);
}
}
}
@@ -1256,7 +1253,7 @@ impl<'a> MimeFactory<'a> {
} else if command == SystemMessage::WebxdcStatusUpdate {
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
parts.push(context.build_status_update_part(json));
} else if self.msg.viewtype == Viewtype::Webxdc {
} else if self.msg.viewtype == Viewtype::Webxdc && self.msg.supersedes.is_none() {
if let Some(json) = context
.render_webxdc_status_update_object(self.msg.id, None)
.await?
@@ -1371,16 +1368,15 @@ impl<'a> MimeFactory<'a> {
}
}
/// Returns base64-encoded buffer `buf` split into 76-bytes long
/// Returns base64-encoded buffer `buf` split into 78-bytes long
/// chunks separated by CRLF.
///
/// [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.
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
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(76).collect::<String>())
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
.take_while(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\r\n")
@@ -1539,7 +1535,6 @@ 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;
@@ -1548,11 +1543,10 @@ 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, TestContextManager};
use crate::test_utils::{get_chat_msg, TestContext};
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1622,8 +1616,8 @@ mod tests {
fn test_wrapped_base64_encode() {
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let output =
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
QUFBQUFBQUFBQQ==";
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
FBQUFBQUFBQQ==";
assert_eq!(wrapped_base64_encode(input), output);
}
@@ -2201,11 +2195,7 @@ mod tests {
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("multipart/mixed").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);
@@ -2317,37 +2307,4 @@ 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

@@ -438,8 +438,6 @@ impl MimeMessage {
typ: Viewtype::Text,
msg_raw: Some(txt.clone()),
msg: txt,
// Don't change the error prefix for now,
// receive_imf.rs:lookup_chat_by_reply() checks it.
error: Some(format!("Decrypting failed: {err:#}")),
..Default::default()
};
@@ -804,6 +802,18 @@ 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,
@@ -840,10 +850,7 @@ 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()
@@ -1433,36 +1440,33 @@ impl MimeMessage {
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
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);
};
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()
});
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()
});
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)
);
Ok(Some(Report {
original_message_id,
additional_message_ids,
}))
Ok(None)
}
fn process_delivery_status(

View File

@@ -4,20 +4,12 @@ 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 {
@@ -87,10 +79,7 @@ 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)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)

View File

@@ -30,12 +30,6 @@ pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
#[allow(missing_docs)]
pub const HEADER_SETUPCODE: &str = "passphrase-begin";
/// Preferred symmetric encryption algorithm.
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
/// Preferred cryptographic hash.
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
/// A wrapper for rPGP public key types
#[derive(Debug)]
enum SignedPublicKeyOrSubkey<'a> {
@@ -142,7 +136,6 @@ pub struct KeyPair {
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
let (secret_key_type, public_key_type) = match keygen_type {
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
};
@@ -256,13 +249,11 @@ pub async fn pk_encrypt(
// TODO: measure time
let encrypted_msg = if let Some(ref skey) = private_key_for_signing {
lit_msg
.sign(skey, || "".into(), HASH_ALGORITHM)
.sign(skey, || "".into(), Default::default())
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
.and_then(|msg| {
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
})
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
} else {
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
};
let msg = encrypted_msg?;
@@ -281,7 +272,7 @@ pub fn pk_calc_signature(
let msg = Message::new_literal_bytes("", plain).sign(
private_key_for_signing,
|| "".into(),
HASH_ALGORITHM,
Default::default(),
)?;
let signature = msg.into_signature().to_armored_string(None)?;
Ok(signature)
@@ -378,7 +369,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
let mut rng = thread_rng();
let s2k = StringToKey::new_default(&mut rng);
let msg =
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
lit_msg.encrypt_with_password(&mut rng, s2k, Default::default(), || passphrase)?;
let encoded_msg = msg.to_armored_string(None)?;

View File

@@ -8,7 +8,6 @@ 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)]
@@ -176,30 +175,21 @@ 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);
}
@@ -324,25 +314,15 @@ 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());
}
#[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());
// get_provider_info() accepts email addresses for backwards compatibility
assert!(
get_provider_info_by_addr(&t, "example@google.com", false)
.await?
get_provider_info(&t, "example@google.com", false)
.await
.unwrap()
.id
== "gmail"
);
Ok(())
}
#[test]

View File

@@ -464,16 +464,6 @@ 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

@@ -62,6 +62,10 @@ pub struct ReceivedMsg {
/// Whether IMAP messages should be immediately deleted.
pub needs_delete_job: bool,
/// Message-ID saved into the database.
/// For messages with `Supersedes` header this is the Message-ID of the original message.
pub rfc724_mid: String,
}
/// Emulates reception of a message from the network.
@@ -113,26 +117,28 @@ pub(crate) async fn receive_imf_inner(
{
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
if rfc724_mid.starts_with(GENERATED_PREFIX) {
// We don't have an rfc724_mid, there's no point in adding a trash entry
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);
// We don't have an rfc724_mid, there's no point in adding a trash entry
}
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,
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
rfc724_mid: rfc724_mid.to_string(),
}));
}
Ok(mime_parser) => mime_parser,
@@ -337,12 +343,12 @@ pub(crate) async fn receive_imf_inner(
.sql
.execute(
"UPDATE imap SET target=? WHERE rfc724_mid=?",
(target, rfc724_mid),
(target, &received_msg.rfc724_mid),
)
.await?;
} else if !mime_parser.mdn_reports.is_empty() && mime_parser.has_chat_version() {
// This is a Delta Chat MDN. Mark as read.
markseen_on_imap_table(context, rfc724_mid).await?;
markseen_on_imap_table(context, &received_msg.rfc724_mid).await?;
}
}
@@ -545,30 +551,19 @@ async fn add_parts(
// signals whether the current user is a bot
let is_bot = context.get_config_bool(Config::Bot).await?;
let create_blocked_default = if is_bot {
Blocked::Not
} else {
Blocked::Request
};
let create_blocked = if let Some(ChatIdBlocked { id: _, blocked }) = test_normal_chat {
match blocked {
Blocked::Request => create_blocked_default,
Blocked::Not => Blocked::Not,
Blocked::Yes => {
if Contact::is_blocked_load(context, from_id).await? {
// User has blocked the contact.
// Block the group contact created as well.
Blocked::Yes
} else {
// 1:1 chat is blocked, but the contact is not.
// This happens when 1:1 chat is hidden
// during scanning of a group invitation code.
Blocked::Request
}
let create_blocked = match test_normal_chat {
Some(ChatIdBlocked {
id: _,
blocked: Blocked::Request,
}) if is_bot => Blocked::Not,
Some(ChatIdBlocked { id: _, blocked }) => blocked,
None => {
if is_bot {
Blocked::Not
} else {
Blocked::Request
}
}
} else {
create_blocked_default
};
if chat_id.is_none() {
@@ -1060,6 +1055,43 @@ async fn add_parts(
.cloned()
.unwrap_or_default();
let rfc724_mid = if let Some(supersedes) = mime_parser.get_header(HeaderDef::Supersedes) {
supersedes.to_string()
} else {
rfc724_mid.to_string()
};
let supersedes_msg_id = match mime_parser.get_header(HeaderDef::Supersedes) {
Some(supersedes) => {
if let Some(msg_id) = rfc724_mid_exists(context, supersedes).await? {
if let Some(orig_from_id) = context
.sql
.query_row_optional("SELECT from_id FROM msgs WHERE id=?", (msg_id,), |row| {
let from_id: ContactId = row.get(0)?;
Ok(from_id)
})
.await?
{
if from_id == orig_from_id {
Some(msg_id)
} else {
None
}
} else {
None
}
} else {
None
}
}
None => None,
};
if supersedes_msg_id.is_some() {
replace_msg_id = supersedes_msg_id;
info!(context, "Superseding {supersedes_msg_id:?}");
}
// fine, so far. now, split the message into simple parts usable as "short messages"
// and add them to the database (mails sent by other messenger clients should result
// into only one message; mails sent by other clients may result in several messages
@@ -1128,8 +1160,7 @@ async fn add_parts(
(&part.msg, part.typ)
};
let part_is_empty =
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
let part_is_empty = part.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,8 +1185,7 @@ 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() && typ == Viewtype::Text);
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
let row_id = context
.sql
@@ -1228,8 +1258,6 @@ RETURNING id
ephemeral_timestamp,
if is_partial_download.is_some() {
DownloadState::Available
} else if mime_parser.decrypting_failed {
DownloadState::Undecipherable
} else {
DownloadState::Done
},
@@ -1315,6 +1343,7 @@ RETURNING id
sort_timestamp,
msg_ids: created_db_entries,
needs_delete_job,
rfc724_mid,
})
}
@@ -1409,53 +1438,49 @@ async fn lookup_chat_by_reply(
) -> Result<Option<(ChatId, Blocked)>> {
// Try to assign message to the same chat as the parent message.
let Some(parent) = parent else {
return Ok(None);
};
if let Some(parent) = parent {
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
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);
}
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)) {
if parent.error.is_some() {
// If the parent msg is undecipherable, then it may have been assigned to the wrong chat
// (undecipherable group msgs often get assigned to the 1:1 chat with the sender).
// We don't have any way of finding out whether a msg is undecipherable, so we check for
// error.is_some() instead.
return 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
);
return Ok(Some((parent_chat.id, parent_chat.blocked)));
}
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)))
Ok(None)
}
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
@@ -1661,7 +1686,7 @@ async fn apply_group_changes(
}
let mut send_event_chat_modified = false;
let (mut removed_id, mut added_id) = (None, None);
let mut removed_id = None;
let mut better_msg = None;
// True if a Delta Chat client has explicitly added our current primary address.
@@ -1672,36 +1697,40 @@ async fn apply_group_changes(
false
};
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
&& chat_id
// Whether to allow any changes to the member list at all.
let allow_member_list_changes = if chat::is_contact_in_chat(context, chat_id, ContactId::SELF)
.await?
|| self_added
|| !mime_parser.has_chat_version()
{
// Reject old group changes.
chat_id
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
.await?;
.await?
} else {
// Member list changes are not allowed if we're not in the group
// and are not explicitly added.
// This message comes from a Delta Chat that restored an old backup
// or the message is a MUA reply to an old message.
false
};
// Whether to rebuild the member list from scratch.
let recreate_member_list = {
// 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) {
let recreate_member_list = if allow_member_list_changes {
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
// Always recreate membership list if self has been added.
if !mime_parser.has_chat_version() || self_added {
true
} else {
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.
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
None => false,
}
} && {
if !allow_member_list_changes {
info!(
context,
"Ignoring a try to recreate member list of {chat_id} by {from_id}.",
);
}
allow_member_list_changes
} else {
false
};
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
@@ -1713,8 +1742,14 @@ async fn apply_group_changes(
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
if removed_id.is_some() {
if !allow_member_list_changes {
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 {
info!(
context,
"Ignoring removal of {removed_addr:?} from {chat_id}."
@@ -1727,11 +1762,13 @@ 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?
{
added_id = Some(contact_id);
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
send_event_chat_modified = true;
} else {
warn!(context, "Added {added_addr:?} has no contact id.")
}
@@ -1798,76 +1835,54 @@ async fn apply_group_changes(
}
}
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();
// Recreate the member list.
if recreate_member_list {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
warn!(
context,
"Contact {from_id} attempts to modify group chat {chat_id} member list without being a member."
);
} else {
// 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);
context
.sql
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
.await?;
}
}
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 {
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} member list with {new_members:?}.",
"Recreating chat {chat_id} with members {members_to_add:?}."
);
}
if new_members != chat_contacts {
let new_members_ref = &new_members;
context
.sql
.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(())
})
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
.await?;
chat_contacts = new_members;
send_event_chat_modified = true;
}
}
if let Some(avatar_action) = &mime_parser.group_avatar {
if !chat_contacts.contains(&ContactId::SELF) {
if !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
warn!(
context,
"Received group avatar update for group chat {chat_id} we are not a member of."
);
} else if !chat_contacts.contains(&from_id) {
} else if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
warn!(
context,
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
@@ -2028,40 +2043,39 @@ async fn apply_mailinglist_changes(
mime_parser: &MimeMessage,
chat_id: ChatId,
) -> Result<()> {
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);
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 {
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?;
}
let listid = &chat.grpid;
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);
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);
chat.update_param(context).await?;
}
} else {
chat.param.set(Param::ListPost, list_post);
chat.update_param(context).await?;
}
Ok(())

View File

@@ -8,7 +8,6 @@ use crate::chat::{
};
use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
use crate::imap::prefetch_should_download;
use crate::message::Message;
@@ -3369,14 +3368,19 @@ 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, 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);
// 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);
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
// bob removes a member.
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);
// 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.
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
Ok(())
}
@@ -3509,29 +3513,6 @@ 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(())
}
@@ -3630,70 +3611,3 @@ async fn test_mua_can_readd() -> Result<()> {
assert!(is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
alice.pop_sent_msg().await;
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
// Bob missed the message adding them, but must recreate the member list.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
)
.await?;
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
let fiona = TestContext::new_fiona().await;
add_contact_to_chat(
&alice,
alice_chat_id,
Contact::create(
&alice,
"fiona",
&fiona.get_config(Config::Addr).await?.unwrap(),
)
.await?,
)
.await?;
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
fiona_chat_id.accept(&fiona).await?;
send_text_msg(&fiona, fiona_chat_id, "hi".to_string()).await?;
bob.recv_msg(&fiona.pop_sent_msg().await).await;
// Bob missed the message adding fiona, but mustn't recreate the member list.
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
let bob_alice_contact = Contact::create(
&bob,
"alice",
&alice.get_config(Config::Addr).await?.unwrap(),
)
.await?;
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
Ok(())
}

View File

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

View File

@@ -310,17 +310,12 @@ impl Sql {
/// It is impossible to turn encrypted database into unencrypted
/// and vice versa this way, use import/export for this.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
let mut lock = self.pool.write().await;
let pool = lock.take().context("SQL connection pool is not open")?;
let conn = pool.get().await?;
conn.pragma_update(None, "rekey", passphrase.clone())
.context("failed to set PRAGMA rekey")?;
drop(pool);
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
Ok(())
self.call_write(move |conn| {
conn.pragma_update(None, "rekey", passphrase)
.context("failed to set PRAGMA rekey")?;
Ok(())
})
.await
}
/// Locks the write transactions mutex in order to make sure that there never are
@@ -1270,7 +1265,7 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sql_change_passphrase() -> Result<()> {
async fn test_change_passphrase() -> Result<()> {
use tempfile::tempdir;
// The context is used only for logging.
@@ -1294,23 +1289,6 @@ mod tests {
sql.change_passphrase("bar".to_string())
.await
.context("failed to change passphrase")?;
// Test that at least two connections are still working.
// This ensures that not only the connection which changed the password is working,
// but other connections as well.
{
let lock = sql.pool.read().await;
let pool = lock.as_ref().unwrap();
let conn1 = pool.get().await?;
let conn2 = pool.get().await?;
conn1
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.unwrap();
conn2
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.unwrap();
}
sql.close().await;
let sql = Sql::new(dbfile);

View File

@@ -108,15 +108,9 @@ 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,
) -> Message {
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
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,8 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::AsyncReadExt;
use crate::chat::Chat;
use crate::blob::BlobObject;
use crate::chat::{self, create_send_msg_job, Chat};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
@@ -34,12 +35,12 @@ use crate::download::DownloadState;
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::wrapped_base64_encode;
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::param::Params;
use crate::param::{Param, Params};
use crate::scheduler::InterruptInfo;
use crate::tools::strip_rtlo_characters;
use crate::tools::{create_smeared_timestamp, get_abs_path};
use crate::{chat, EventType};
use crate::tools::{
create_outgoing_rfc724_mid, create_smeared_timestamp, get_abs_path, strip_rtlo_characters,
};
use crate::EventType;
/// The current API version.
/// If `min_api` in manifest.toml is set to a larger value,
@@ -845,6 +846,77 @@ impl Message {
}
}
/// Sends a replacement for an own WebXDC message.
pub async fn send_webxdc_replacement(
context: &Context,
msg_id: MsgId,
filename: &str,
) -> Result<()> {
let mut msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.from_id == ContactId::SELF,
"Can update WebXDC only in own messages"
);
ensure!(
msg.get_viewtype() == Viewtype::Webxdc,
"Message {msg_id} is not a WebXDC instance"
);
let state = msg.get_state();
match state {
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {}
MessageState::Undefined
| MessageState::InFresh
| MessageState::InNoticed
| MessageState::InSeen
| MessageState::OutPreparing
| MessageState::OutPending
| MessageState::OutDraft => bail!("Unexpected message state: {state}"),
}
let chat = Chat::load_from_db(context, msg.chat_id).await?;
let mut param = msg.param.clone();
if !chat.is_protected() {
param.remove(Param::GuaranteeE2ee);
}
let blob = BlobObject::new_from_path(context, Path::new(filename))
.await
.context("Failed to create webxdc replacement blob")?;
param.set(Param::File, blob.as_name());
msg.param = param;
// Generate new Message-ID.
context
.sql
.execute(
"UPDATE msgs
SET state=?, param=?
WHERE id=?",
(MessageState::OutPending, msg.param.to_string(), msg_id),
)
.await?;
msg.supersedes = Some(msg.rfc724_mid);
msg.rfc724_mid = {
let grpid = match chat.typ {
Chattype::Group => Some(chat.grpid.as_str()),
_ => None,
};
let from = context.get_primary_self_addr().await?;
create_outgoing_rfc724_mid(grpid, &from)
};
if create_send_msg_job(context, &mut msg).await?.is_some() {
context
.scheduler
.interrupt_smtp(InterruptInfo::new(false))
.await;
}
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::json;
@@ -1012,7 +1084,7 @@ mod tests {
let instance = send_webxdc_instance(&t, chat_id).await?;
t.send_webxdc_status_update(
instance.id,
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
r#"{"info": "foo", "summary":"bar", "payload": 42}"#,
"descr",
)
.await?;
@@ -1020,7 +1092,7 @@ mod tests {
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
r#"[{"payload":42,"info":"foo","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)
@@ -1028,7 +1100,6 @@ 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?;
@@ -1045,7 +1116,6 @@ mod tests {
.get_webxdc_info(&t)
.await?;
assert_eq!(info.summary, "".to_string());
assert_eq!(info.document, "".to_string());
Ok(())
}
@@ -2625,4 +2695,174 @@ sth_for_the = "future""#
Ok(())
}
/// Tests sending webxdc and replacing it 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");
let original_rfc724_mid = alice_instance.rfc724_mid;
// 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}]"#
);
// Alice sends WebXDC instance replacement.
send_webxdc_replacement(
&alice,
alice_instance.id,
"test-data/webxdc/with-minimal-manifest.xdc",
)
.await
.context("Failed to send WebXDC replacement")?;
let alice_replacement_instance = alice.get_last_msg().await;
let alice_replacement_info = alice_replacement_instance.get_webxdc_info(&alice).await?;
assert_eq!(alice_replacement_info.name, "nice app!");
assert_eq!(alice_instance.id, alice_replacement_instance.id);
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
assert!(alice_sent_replacement_instance
.payload
.contains(&format!("Supersedes: {original_rfc724_mid}")));
assert_eq!(alice_replacement_instance.rfc724_mid, original_rfc724_mid);
// Bob receives WebXDC instance replacement.
let bob_received_replacement_instance =
bob.recv_msg(&alice_sent_replacement_instance).await;
assert_eq!(
bob_received_instance.id,
bob_received_replacement_instance.id
);
assert_eq!(
bob_received_replacement_instance.rfc724_mid,
original_rfc724_mid
);
let bob_received_replacement_info = bob_received_replacement_instance
.get_webxdc_info(&bob)
.await?;
assert_eq!(bob_received_replacement_info.name, "nice app!");
// Updates are not modified.
assert_eq!(
bob.get_webxdc_status_updates(
bob_received_replacement_instance.id,
StatusUpdateSerial(0)
)
.await?,
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
);
// Bob is not allowed to replace the instance.
assert!(send_webxdc_replacement(
&bob,
bob_received_instance.id,
"test-data/webxdc/minimal.xdc"
)
.await
.is_err());
// Alice sends a second WebXDC instance replacement.
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
.await
.context("Failed to send second WebXDC replacement")?;
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
let bob_received_second_replacement_instance =
bob.recv_msg(&alice_second_sent_replacement_instance).await;
assert_eq!(
bob_received_instance.id,
bob_received_second_replacement_instance.id
);
assert_eq!(
bob_received_second_replacement_instance.rfc724_mid,
original_rfc724_mid
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_replace_webxdc_missing_original() -> 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?;
alice.pop_sent_msg().await;
let alice_instance = alice.get_last_msg().await;
assert_eq!(alice_instance.get_text(), "user added text");
let original_rfc724_mid = alice_instance.rfc724_mid;
// Bob missed the original instance message.
// Alice sends WebXDC instance replacement.
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
.await
.context("Failed to send WebXDC replacement")?;
let alice_sent_replacement_instance = alice.pop_sent_msg().await;
assert!(alice_sent_replacement_instance
.payload
.contains(&format!("Supersedes: {original_rfc724_mid}")));
// Bob receives WebXDC instance replacement.
let bob_received_replacement_instance =
bob.recv_msg(&alice_sent_replacement_instance).await;
assert_eq!(
bob_received_replacement_instance.rfc724_mid,
original_rfc724_mid
);
// Alice sends a second WebXDC instance replacement.
send_webxdc_replacement(&alice, alice_instance.id, "test-data/webxdc/minimal.xdc")
.await
.context("Failed to send second WebXDC replacement")?;
let alice_second_sent_replacement_instance = alice.pop_sent_msg().await;
let bob_received_second_replacement_instance =
bob.recv_msg(&alice_second_sent_replacement_instance).await;
assert_eq!(
bob_received_replacement_instance.id,
bob_received_second_replacement_instance.id
);
assert_eq!(
bob_received_second_replacement_instance.rfc724_mid,
original_rfc724_mid
);
Ok(())
}
}