mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
40 Commits
hoc/merge-
...
v1.123.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa78e82fed | ||
|
|
d4e670d5e9 | ||
|
|
4553c6521f | ||
|
|
e72d527d88 | ||
|
|
f5c36043f6 | ||
|
|
b227ff87dc | ||
|
|
676f311f97 | ||
|
|
f02299c06c | ||
|
|
ed781af52c | ||
|
|
67043177a9 | ||
|
|
49cc5fb673 | ||
|
|
68c95dee17 | ||
|
|
9bd7ab7280 | ||
|
|
7a359f6318 | ||
|
|
38b31aa88d | ||
|
|
e12e026bd8 | ||
|
|
212fbc125c | ||
|
|
2939de013b | ||
|
|
0562e23ee0 | ||
|
|
a8551510cd | ||
|
|
087f6edd0c | ||
|
|
d6b7ee04a0 | ||
|
|
d5c5ff8b3f | ||
|
|
dc4396a699 | ||
|
|
a74b00c3f9 | ||
|
|
2fdb9f8b7e | ||
|
|
80fac3f1b8 | ||
|
|
17a6c88cc7 | ||
|
|
1ba69dbb9b | ||
|
|
ab1c7ebbe2 | ||
|
|
ee715da078 | ||
|
|
27e177dc05 | ||
|
|
7aac4bfc83 | ||
|
|
7b24f9b7a4 | ||
|
|
b36b902eeb | ||
|
|
30024abb6c | ||
|
|
1d9702e9e7 | ||
|
|
ee2eae63d6 | ||
|
|
cd477936b5 | ||
|
|
dbe9d7e34e |
4
.github/workflows/node-package.yml
vendored
4
.github/workflows/node-package.yml
vendored
@@ -67,7 +67,9 @@ jobs:
|
||||
|
||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||
container: debian:10
|
||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||
container: ubuntu:18.04
|
||||
steps:
|
||||
# Working directory is owned by 1001:1001 by default.
|
||||
# Change it to our user.
|
||||
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,72 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -2764,3 +2831,6 @@ 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
|
||||
|
||||
28
Cargo.lock
generated
28
Cargo.lock
generated
@@ -1103,7 +1103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1179,7 +1179,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1203,7 +1203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1218,7 +1218,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1243,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -3078,11 +3078,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.55"
|
||||
version = "0.10.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.3.3",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
@@ -3110,18 +3110,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "111.26.0+1.1.1u"
|
||||
version = "300.1.5+3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37"
|
||||
checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.90"
|
||||
version = "0.9.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -5448,9 +5448,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -301,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The new passphrase.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
@@ -430,7 +443,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Ed25519 keypair
|
||||
* generate Curve25519 keypair
|
||||
* DC_KEY_GEN_RSA4096 (3)=
|
||||
* generate RSA 4096 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)
|
||||
@@ -1321,6 +1336,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of similar chats.
|
||||
*
|
||||
* @warning This is an experimental API which may change or be removed in the future.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat for which to find similar chats.
|
||||
* @return The list of similar chats.
|
||||
* On errors, NULL is returned.
|
||||
* Must be freed using dc_chatlist_unref() when no longer used.
|
||||
*/
|
||||
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
@@ -2255,6 +2284,7 @@ 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,
|
||||
@@ -5725,12 +5755,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param method JSON-RPC method name, e.g. `check_email_validity`.
|
||||
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
|
||||
* @param input JSON-RPC request.
|
||||
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
|
||||
* On error, NULL is returned.
|
||||
* If there is no response, NULL is returned.
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
@@ -6266,6 +6295,7 @@ 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
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -167,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_change_passphrase(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_change_passphrase()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.change_passphrase(passphrase))
|
||||
.context("dc_context_change_passphrase() failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -1242,6 +1260,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_similar_chatlist(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut dc_chatlist_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
match block_on(chat_id.get_similar_chatlist(ctx))
|
||||
.context("failed to get similar chatlist")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(list) => {
|
||||
let ffi_list = ChatlistWrapper { context, list };
|
||||
Box::into_raw(Box::new(ffi_list))
|
||||
}
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||
context: *mut dc_context_t,
|
||||
@@ -4968,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, RpcServer, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -5044,25 +5086,24 @@ mod jsonrpc {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
method: *const libc::c_char,
|
||||
params: *const libc::c_char,
|
||||
input: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let method = to_string_lossy(method);
|
||||
let params = to_string_lossy(params);
|
||||
let params: Option<yerpc::Params> = match serde_json::from_str(¶ms) {
|
||||
Ok(params) => Some(params),
|
||||
Err(_) => None,
|
||||
};
|
||||
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
|
||||
let res = block_on(api.handle.server().handle_request(method, params));
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
match res {
|
||||
Ok(res) => res.to_string().strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -566,6 +566,21 @@ impl CommandApi {
|
||||
Ok(l)
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
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)
|
||||
}
|
||||
|
||||
async fn get_chatlist_items_by_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -8,15 +8,12 @@ use deltachat::{
|
||||
chatlist::Chatlist,
|
||||
};
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::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 {
|
||||
|
||||
@@ -318,6 +318,7 @@ pub enum DownloadState {
|
||||
Done,
|
||||
Available,
|
||||
Failure,
|
||||
Undecipherable,
|
||||
InProgress,
|
||||
}
|
||||
|
||||
@@ -327,6 +328,7 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.120.0"
|
||||
"version": "1.123.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ 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());
|
||||
@@ -805,15 +806,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"chatinfo" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts", contacts.len());
|
||||
|
||||
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
|
||||
if !similar_chats.is_empty() {
|
||||
println!("Similar chats: ");
|
||||
for (similar_chat_id, metric) in similar_chats {
|
||||
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
|
||||
println!(
|
||||
"{} (#{}) {:.1}",
|
||||
similar_chat.name,
|
||||
similar_chat_id,
|
||||
100.0 * metric
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
"Location streaming: {}",
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.120.0"
|
||||
version = "1.123.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -3,11 +3,6 @@ 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]
|
||||
|
||||
@@ -90,6 +90,7 @@ 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,
|
||||
|
||||
@@ -90,6 +90,7 @@ 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,
|
||||
|
||||
@@ -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.120.0"
|
||||
"version": "1.123.0"
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ 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"
|
||||
|
||||
@@ -1679,6 +1679,36 @@ 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
import deltachat as dc
|
||||
@@ -226,10 +227,26 @@ def test_jsonrpc_blocking_call(tmp_path):
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
res = from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == "true"
|
||||
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
|
||||
|
||||
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
|
||||
assert res == "false"
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-08-28
|
||||
2023-09-22
|
||||
@@ -44,7 +44,7 @@ def file2url(f):
|
||||
|
||||
def process_opt(data):
|
||||
if not "opt" in data:
|
||||
return "Default::default()"
|
||||
return "ProviderOptions::new()"
|
||||
opt = "ProviderOptions {\n"
|
||||
opt_data = data.get("opt", "")
|
||||
for key in opt_data:
|
||||
@@ -54,7 +54,7 @@ def process_opt(data):
|
||||
if value in {"True", "False"}:
|
||||
value = value.lower()
|
||||
opt += " " + key + ": " + value + ",\n"
|
||||
opt += " ..Default::default()\n"
|
||||
opt += " ..ProviderOptions::new()\n"
|
||||
opt += " }"
|
||||
return opt
|
||||
|
||||
@@ -96,11 +96,11 @@ def process_data(data, file):
|
||||
raise TypeError("domain used twice: " + domain)
|
||||
domains_set.add(domain)
|
||||
|
||||
domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n"
|
||||
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
|
||||
comment += domain + ", "
|
||||
|
||||
ids = ""
|
||||
ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n"
|
||||
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
|
||||
|
||||
server = ""
|
||||
has_imap = False
|
||||
@@ -155,7 +155,7 @@ def process_data(data, file):
|
||||
provider += (
|
||||
"static "
|
||||
+ file2varname(file)
|
||||
+ ": Lazy<Provider> = Lazy::new(|| Provider {\n"
|
||||
+ ": Provider = Provider {\n"
|
||||
)
|
||||
provider += ' id: "' + file2id(file) + '",\n'
|
||||
provider += " status: Status::" + status.capitalize() + ",\n"
|
||||
@@ -166,7 +166,7 @@ def process_data(data, file):
|
||||
provider += " opt: " + opt + ",\n"
|
||||
provider += " config_defaults: " + config_defaults + ",\n"
|
||||
provider += " oauth2_authorizer: " + oauth2 + ",\n"
|
||||
provider += "});\n\n"
|
||||
provider += "};\n\n"
|
||||
else:
|
||||
raise TypeError("SMTP and IMAP must be specified together or left out both")
|
||||
|
||||
|
||||
432
src/chat.rs
432
src/chat.rs
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -22,6 +23,7 @@ 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;
|
||||
@@ -871,6 +873,133 @@ impl ChatId {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
.sql
|
||||
.query_get_value("SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", (self,))
|
||||
.await?;
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
/// Returns a list of active similar chat IDs sorted by similarity metric.
|
||||
///
|
||||
/// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
|
||||
///
|
||||
/// Chat is considered active if something was posted there within the last 42 days.
|
||||
pub async fn get_similar_chat_ids(self, context: &Context) -> Result<Vec<(ChatId, f64)>> {
|
||||
// Count number of common members in this and other chats.
|
||||
let intersection: Vec<(ChatId, f64)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT y.chat_id, SUM(x.contact_id = y.contact_id)
|
||||
FROM chats_contacts as x
|
||||
JOIN chats_contacts as y
|
||||
WHERE x.contact_id > 9
|
||||
AND y.contact_id > 9
|
||||
AND x.chat_id=?
|
||||
AND y.chat_id<>x.chat_id
|
||||
GROUP BY y.chat_id",
|
||||
(self,),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let intersection: f64 = row.get(1)?;
|
||||
Ok((chat_id, intersection))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to calculate member set intersections")?;
|
||||
|
||||
let chat_size: HashMap<ChatId, f64> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id, count(*) AS n
|
||||
FROM chats_contacts where contact_id > 9
|
||||
GROUP BY chat_id",
|
||||
(),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let size: f64 = row.get(1)?;
|
||||
Ok((chat_id, size))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<HashMap<ChatId, f64>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("failed to count chat member sizes")?;
|
||||
|
||||
let our_chat_size = chat_size.get(&self).copied().unwrap_or_default();
|
||||
let mut chats_with_metrics = Vec::new();
|
||||
for (chat_id, intersection_size) in intersection {
|
||||
if intersection_size > 0.0 {
|
||||
let other_chat_size = chat_size.get(&chat_id).copied().unwrap_or_default();
|
||||
let union_size = our_chat_size + other_chat_size - intersection_size;
|
||||
let metric = intersection_size / union_size;
|
||||
chats_with_metrics.push((chat_id, metric))
|
||||
}
|
||||
}
|
||||
chats_with_metrics.sort_unstable_by(|(chat_id1, metric1), (chat_id2, metric2)| {
|
||||
metric2
|
||||
.partial_cmp(metric1)
|
||||
.unwrap_or(chat_id2.cmp(chat_id1))
|
||||
});
|
||||
|
||||
// Select up to five similar active chats.
|
||||
let mut res = Vec::new();
|
||||
let now = time();
|
||||
for (chat_id, metric) in chats_with_metrics {
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if now > chat_timestamp + 42 * 24 * 3600 {
|
||||
// Chat was inactive for 42 days, skip.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if metric < 0.1 {
|
||||
// Chat is unrelated.
|
||||
break;
|
||||
}
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
continue;
|
||||
}
|
||||
|
||||
match chat.visibility {
|
||||
ChatVisibility::Normal | ChatVisibility::Pinned => {}
|
||||
ChatVisibility::Archived => continue,
|
||||
}
|
||||
|
||||
res.push((chat_id, metric));
|
||||
if res.len() >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Returns similar chats as a [`Chatlist`].
|
||||
///
|
||||
/// [`Chatlist`]: crate::chatlist::Chatlist
|
||||
pub async fn get_similar_chatlist(self, context: &Context) -> Result<Chatlist> {
|
||||
let chat_ids: Vec<ChatId> = self
|
||||
.get_similar_chat_ids(context)
|
||||
.await
|
||||
.context("failed to get similar chat IDs")?
|
||||
.into_iter()
|
||||
.map(|(chat_id, _metric)| chat_id)
|
||||
.collect();
|
||||
let chatlist = Chatlist::from_chat_ids(context, &chat_ids).await?;
|
||||
Ok(chatlist)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
|
||||
let res: Option<String> = context
|
||||
.sql
|
||||
@@ -910,11 +1039,14 @@ 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 \
|
||||
FROM msgs WHERE chat_id=? AND state NOT IN (?, ?) AND NOT hidden AND download_state={} \
|
||||
ORDER BY timestamp DESC, id DESC \
|
||||
LIMIT 1;"
|
||||
LIMIT 1;",
|
||||
DownloadState::Done as u32,
|
||||
);
|
||||
let row = sql
|
||||
.query_row_optional(
|
||||
@@ -923,8 +1055,11 @@ impl ChatId {
|
||||
self,
|
||||
MessageState::OutPreparing,
|
||||
MessageState::OutDraft,
|
||||
MessageState::OutPending,
|
||||
MessageState::OutFailed,
|
||||
// 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.
|
||||
),
|
||||
f,
|
||||
)
|
||||
@@ -936,34 +1071,17 @@ impl ChatId {
|
||||
self,
|
||||
context: &Context,
|
||||
) -> Result<Option<(String, String, String)>> {
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// Returns multi-line text summary of encryption preferences of all chat contacts.
|
||||
@@ -1585,6 +1703,11 @@ impl Chat {
|
||||
None
|
||||
};
|
||||
|
||||
msg.chat_id = self.id;
|
||||
msg.from_id = ContactId::SELF;
|
||||
msg.rfc724_mid = new_rfc724_mid;
|
||||
msg.timestamp_sort = timestamp;
|
||||
|
||||
// add message to the database
|
||||
if let Some(update_msg_id) = update_msg_id {
|
||||
context
|
||||
@@ -1598,11 +1721,11 @@ impl Chat {
|
||||
ephemeral_timestamp=?
|
||||
WHERE id=?;",
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
msg.timestamp_sort,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
@@ -1647,11 +1770,11 @@ impl Chat {
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
new_rfc724_mid,
|
||||
self.id,
|
||||
ContactId::SELF,
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
msg.from_id,
|
||||
to_id,
|
||||
timestamp,
|
||||
msg.timestamp_sort,
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
@@ -2086,6 +2209,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
msg.try_calc_and_set_dimensions(context).await?;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Attaching \"{}\" for message type #{}.",
|
||||
@@ -2253,7 +2378,7 @@ async fn prepare_send_msg(
|
||||
);
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?;
|
||||
}
|
||||
let row_id = create_send_msg_job(context, msg.id).await?;
|
||||
let row_id = create_send_msg_job(context, msg).await?;
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
@@ -2263,13 +2388,10 @@ async fn prepare_send_msg(
|
||||
/// group with only self and no BCC-to-self configured.
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process a new row.
|
||||
async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<i64>> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
msg.try_calc_and_set_dimensions(context)
|
||||
.await
|
||||
.context("failed to calculate media dimensions")?;
|
||||
|
||||
/* create message */
|
||||
pub(crate) async fn create_send_msg_job(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
) -> Result<Option<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
|
||||
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
|
||||
@@ -2280,7 +2402,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
}
|
||||
};
|
||||
|
||||
let mimefactory = MimeFactory::from_msg(context, &msg, attach_selfavatar).await?;
|
||||
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
|
||||
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
@@ -2302,16 +2424,17 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
// may happen eg. for groups with only SELF and bcc_self disabled
|
||||
info!(
|
||||
context,
|
||||
"Message {msg_id} has no recipient, skipping smtp-send."
|
||||
"Message {} has no recipient, skipping smtp-send.", msg.id
|
||||
);
|
||||
msg_id.set_delivered(context).await?;
|
||||
msg.id.set_delivered(context).await?;
|
||||
msg.state = MessageState::OutDelivered;
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
Err(err)
|
||||
}
|
||||
}?;
|
||||
@@ -2320,13 +2443,13 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
/* unrecoverable */
|
||||
message::set_msg_failed(
|
||||
context,
|
||||
msg_id,
|
||||
msg,
|
||||
"End-to-end-encryption unavailable unexpectedly.",
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
bail!(
|
||||
"e2e encryption unavailable {} - {:?}",
|
||||
msg_id,
|
||||
msg.id,
|
||||
needs_encryption
|
||||
);
|
||||
}
|
||||
@@ -2382,7 +2505,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
&rendered_msg.rfc724_mid,
|
||||
recipients,
|
||||
&rendered_msg.message,
|
||||
msg_id,
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
@@ -2575,14 +2698,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
if let Some(chat_timestamp) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
|
||||
if timestamp > chat_timestamp {
|
||||
marknoticed_chat(context, chat_id).await?;
|
||||
}
|
||||
@@ -3372,89 +3488,86 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
chat_id
|
||||
.unarchive_if_not_muted(context, MessageState::Undefined)
|
||||
.await?;
|
||||
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for id in ids {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
|
||||
let ids = context
|
||||
.sql
|
||||
.query_map(
|
||||
&format!(
|
||||
"SELECT id FROM msgs WHERE id IN({}) ORDER BY timestamp,id",
|
||||
sql::repeat_vars(msg_ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(msg_ids),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?;
|
||||
|
||||
for id in ids {
|
||||
let src_msg_id: MsgId = id;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
if msg.state == MessageState::OutDraft {
|
||||
bail!("cannot forward drafts.");
|
||||
}
|
||||
let original_param = msg.param.clone();
|
||||
|
||||
let original_param = msg.param.clone();
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
|
||||
// we tested a sort of broadcast
|
||||
// by not marking own forwarded messages as such,
|
||||
// however, this turned out to be to confusing and unclear.
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
|
||||
if msg.get_viewtype() != Viewtype::Sticker {
|
||||
msg.param
|
||||
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
|
||||
}
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
msg.subject = "".to_string();
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
msg.subject = "".to_string();
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
|
||||
let new_msg_id: MsgId;
|
||||
if msg.state == MessageState::OutPreparing {
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
let save_param = msg.param.clone();
|
||||
msg.param = original_param;
|
||||
msg.id = src_msg_id;
|
||||
|
||||
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
|
||||
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
|
||||
msg.param.set(Param::PrepForwards, new_fwd);
|
||||
} else {
|
||||
msg.param
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
msg.param = save_param;
|
||||
if let Some(old_fwd) = msg.param.get(Param::PrepForwards) {
|
||||
let new_fwd = format!("{} {}", old_fwd, new_msg_id.to_u32());
|
||||
msg.param.set(Param::PrepForwards, new_fwd);
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if create_send_msg_job(context, new_msg_id).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
msg.param
|
||||
.set(Param::PrepForwards, new_msg_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
msg.update_param(context).await?;
|
||||
} else {
|
||||
msg.state = MessageState::OutPending;
|
||||
new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
curr_timestamp += 1;
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
created_msgs.push(new_msg_id);
|
||||
}
|
||||
created_chats.push(chat_id);
|
||||
created_msgs.push(new_msg_id);
|
||||
}
|
||||
for (chat_id, msg_id) in created_chats.iter().zip(created_msgs.iter()) {
|
||||
context.emit_msgs_changed(*chat_id, *msg_id);
|
||||
@@ -3486,29 +3599,31 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
msgs.push(msg)
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
}
|
||||
_ => bail!("unexpected message state"),
|
||||
}
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, msg.id).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
let Some(chat_id) = chat_id else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
for mut msg in msgs {
|
||||
if msg.get_showpadlock() && !chat.is_protected() {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
msg.update_param(context).await?;
|
||||
}
|
||||
match msg.get_state() {
|
||||
MessageState::OutFailed | MessageState::OutDelivered | MessageState::OutMdnRcvd => {
|
||||
message::update_msg_state(context, msg.id, MessageState::OutPending).await?
|
||||
}
|
||||
_ => bail!("unexpected message state"),
|
||||
}
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
if create_send_msg_job(context, &mut msg).await?.is_some() {
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -3578,7 +3693,6 @@ pub async fn add_device_msg_with_importance(
|
||||
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
|
||||
msg.try_calc_and_set_dimensions(context).await.ok();
|
||||
prepare_msg_blob(context, msg).await?;
|
||||
|
||||
let timestamp_sent = create_smeared_timestamp(context);
|
||||
|
||||
@@ -296,6 +296,27 @@ impl Chatlist {
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
/// Converts list of chat IDs to a chatlist.
|
||||
pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
|
||||
let mut ids = Vec::new();
|
||||
for &chat_id in chat_ids {
|
||||
let msg_id: Option<MsgId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=?1
|
||||
AND (hidden=0 OR state=?2)
|
||||
ORDER BY timestamp DESC, id DESC LIMIT 1",
|
||||
(chat_id, MessageState::OutDraft),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
|
||||
ids.push((chat_id, msg_id));
|
||||
}
|
||||
Ok(Chatlist { ids })
|
||||
}
|
||||
|
||||
/// Find out the number of chats.
|
||||
pub fn len(&self) -> usize {
|
||||
self.ids.len()
|
||||
|
||||
@@ -62,8 +62,15 @@ 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.
|
||||
@@ -231,6 +238,7 @@ 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]
|
||||
|
||||
@@ -1732,7 +1732,7 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("dd.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d."), true);
|
||||
assert_eq!(may_be_valid_addr("u@d."), false);
|
||||
assert_eq!(may_be_valid_addr("u@d.t"), true);
|
||||
assert_eq!(may_be_valid_addr("u@d.tt"), true);
|
||||
assert_eq!(may_be_valid_addr("u@.tt"), true);
|
||||
@@ -1741,6 +1741,7 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
|
||||
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
|
||||
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
|
||||
assert_eq!(may_be_valid_addr("user@domain.tld."), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -332,6 +332,12 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes encrypted database passphrase.
|
||||
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
|
||||
self.sql.change_passphrase(passphrase).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if database is open.
|
||||
pub async fn is_open(&self) -> bool {
|
||||
self.sql.is_open().await
|
||||
@@ -1453,6 +1459,35 @@ 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;
|
||||
|
||||
@@ -59,6 +59,9 @@ 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,
|
||||
}
|
||||
@@ -80,7 +83,9 @@ 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 => return Err(anyhow!("Nothing to download.")),
|
||||
DownloadState::Done | DownloadState::Undecipherable => {
|
||||
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)
|
||||
|
||||
119
src/imex.rs
119
src/imex.rs
@@ -586,63 +586,74 @@ async fn export_backup_inner(
|
||||
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.
|
||||
/// 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(());
|
||||
}
|
||||
|
||||
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 dir_name = dir.to_string_lossy();
|
||||
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
|
||||
let mut dir_handle = tokio::fs::read_dir(&path).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 = 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 => {
|
||||
let path_plus_name = path.join(&entry_fn);
|
||||
if let Some(suffix) = get_filesuffix_lc(&name_f) {
|
||||
if suffix != "asc" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let set_default = !name_f.contains("legacy");
|
||||
info!(
|
||||
context,
|
||||
"considering key file: {}",
|
||||
"Considering key file: {}.",
|
||||
path_plus_name.display()
|
||||
);
|
||||
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
imported_cnt += 1;
|
||||
}
|
||||
ensure!(
|
||||
imported_cnt > 0,
|
||||
"No private keys found in \"{}\".",
|
||||
dir_name
|
||||
"No private keys found in {}.",
|
||||
path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -673,7 +684,8 @@ 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);
|
||||
@@ -864,14 +876,35 @@ 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;
|
||||
let blobdir = context.ctx.get_blobdir();
|
||||
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
|
||||
if let Err(err) = imex(
|
||||
&context.ctx,
|
||||
ImexMode::ExportSelfKeys,
|
||||
export_dir.path(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
panic!("got error on export: {err:#}");
|
||||
}
|
||||
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).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 {
|
||||
panic!("got error on import: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
101
src/message.rs
101
src/message.rs
@@ -7,6 +7,7 @@ use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -582,14 +583,22 @@ impl Message {
|
||||
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
|
||||
&& !self.param.exists(Param::Width)
|
||||
{
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
let buf = read_file(context, &path_and_filename).await?;
|
||||
|
||||
if let Ok(buf) = read_file(context, path_and_filename).await {
|
||||
if let Ok((width, height)) = get_filemeta(&buf) {
|
||||
match get_filemeta(&buf) {
|
||||
Ok((width, height)) => {
|
||||
self.param.set_int(Param::Width, width as i32);
|
||||
self.param.set_int(Param::Height, height as i32);
|
||||
}
|
||||
Err(err) => {
|
||||
self.param.set_int(Param::Width, 0);
|
||||
self.param.set_int(Param::Height, 0);
|
||||
warn!(
|
||||
context,
|
||||
"Failed to get width and height for {}: {err:#}.",
|
||||
path_and_filename.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.id.is_unset() {
|
||||
@@ -980,19 +989,28 @@ impl Message {
|
||||
}
|
||||
}
|
||||
self.param.set(Param::File, file);
|
||||
if let Some(filemime) = filemime {
|
||||
self.param.set(Param::MimeType, filemime);
|
||||
}
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
}
|
||||
|
||||
/// Creates a new blob and sets it as a file associated with a message.
|
||||
pub async fn set_file_from_bytes(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
suggested_name: &str,
|
||||
data: &[u8],
|
||||
filemime: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let blob = BlobObject::create(context, suggested_name, data).await?;
|
||||
self.param.set(Param::File, blob.as_name());
|
||||
self.param.set_optional(Param::MimeType, filemime);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set different sender name for a message.
|
||||
/// This overrides the name set by the `set_config()`-option `displayname`.
|
||||
pub fn set_override_sender_name(&mut self, name: Option<String>) {
|
||||
if let Some(name) = name {
|
||||
self.param.set(Param::OverrideSenderDisplayname, name);
|
||||
} else {
|
||||
self.param.remove(Param::OverrideSenderDisplayname);
|
||||
}
|
||||
self.param
|
||||
.set_optional(Param::OverrideSenderDisplayname, name);
|
||||
}
|
||||
|
||||
/// Sets the dimensions of associated image or video file.
|
||||
@@ -1649,35 +1667,36 @@ pub(crate) async fn update_msg_state(
|
||||
|
||||
// Context functions to work with messages
|
||||
|
||||
pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) {
|
||||
if let Ok(mut msg) = Message::load_from_db(context, msg_id).await {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg_id, error);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg_id, error, msg.state
|
||||
)
|
||||
}
|
||||
|
||||
match context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
(msg.state, error, msg_id),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => context.emit_event(EventType::MsgFailed {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id,
|
||||
}),
|
||||
Err(e) => {
|
||||
warn!(context, "{:?}", e);
|
||||
}
|
||||
}
|
||||
pub(crate) async fn set_msg_failed(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
error: &str,
|
||||
) -> Result<()> {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
|
||||
)
|
||||
}
|
||||
msg.error = Some(error.to_string());
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET state=?, error=? WHERE id=?;",
|
||||
(msg.state, error, msg.id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::MsgFailed {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The number of messages assigned to unblocked chats
|
||||
@@ -2284,7 +2303,7 @@ mod tests {
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
|
||||
set_msg_failed(&alice, alice_msg.id, "badly failed").await;
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
// check incoming message states on receiver side
|
||||
|
||||
@@ -29,7 +29,9 @@ use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::message::{self, set_msg_failed, update_msg_state, MessageState, MsgId, Viewtype};
|
||||
use crate::message::{
|
||||
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
@@ -436,6 +438,8 @@ 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()
|
||||
};
|
||||
@@ -2156,7 +2160,8 @@ async fn handle_ndn(
|
||||
let mut first = true;
|
||||
for msg in msgs {
|
||||
let (msg_id, chat_id, chat_type) = msg?;
|
||||
set_msg_failed(context, msg_id, &error).await;
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
set_msg_failed(context, &mut message, &error).await?;
|
||||
if first {
|
||||
// Add only one info msg for all failed messages
|
||||
ndn_maybe_add_info_msg(context, failed, chat_id, chat_type).await?;
|
||||
|
||||
10
src/param.rs
10
src/param.rs
@@ -281,6 +281,16 @@ impl Params {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the given key from an optional value.
|
||||
/// Removes the key if the value is `None`.
|
||||
pub fn set_optional(&mut self, key: Param, value: Option<impl ToString>) -> &mut Self {
|
||||
if let Some(value) = value {
|
||||
self.set(key, value)
|
||||
} else {
|
||||
self.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if there are any values in this.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
|
||||
19
src/pgp.rs
19
src/pgp.rs
@@ -30,6 +30,12 @@ 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> {
|
||||
@@ -136,6 +142,7 @@ 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),
|
||||
};
|
||||
|
||||
@@ -249,11 +256,13 @@ 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(), Default::default())
|
||||
.sign(skey, || "".into(), HASH_ALGORITHM)
|
||||
.and_then(|msg| msg.compress(CompressionAlgorithm::ZLIB))
|
||||
.and_then(|msg| msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs))
|
||||
.and_then(|msg| {
|
||||
msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
})
|
||||
} else {
|
||||
lit_msg.encrypt_to_keys(&mut rng, Default::default(), &pkeys_refs)
|
||||
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)
|
||||
};
|
||||
|
||||
let msg = encrypted_msg?;
|
||||
@@ -272,7 +281,7 @@ pub fn pk_calc_signature(
|
||||
let msg = Message::new_literal_bytes("", plain).sign(
|
||||
private_key_for_signing,
|
||||
|| "".into(),
|
||||
Default::default(),
|
||||
HASH_ALGORITHM,
|
||||
)?;
|
||||
let signature = msg.into_signature().to_armored_string(None)?;
|
||||
Ok(signature)
|
||||
@@ -369,7 +378,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, Default::default(), || passphrase)?;
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
|
||||
|
||||
let encoded_msg = msg.to_armored_string(None)?;
|
||||
|
||||
|
||||
@@ -149,8 +149,8 @@ pub struct ProviderOptions {
|
||||
pub delete_to_trash: bool,
|
||||
}
|
||||
|
||||
impl Default for ProviderOptions {
|
||||
fn default() -> Self {
|
||||
impl ProviderOptions {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
strict_tls: true,
|
||||
max_smtp_rcpt_to: None,
|
||||
|
||||
1246
src/provider/data.rs
1246
src/provider/data.rs
File diff suppressed because it is too large
Load Diff
@@ -546,19 +546,30 @@ 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 = 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
create_blocked_default
|
||||
};
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -1216,6 +1227,8 @@ RETURNING id
|
||||
ephemeral_timestamp,
|
||||
if is_partial_download.is_some() {
|
||||
DownloadState::Available
|
||||
} else if mime_parser.decrypting_failed {
|
||||
DownloadState::Undecipherable
|
||||
} else {
|
||||
DownloadState::Done
|
||||
},
|
||||
@@ -1398,11 +1411,18 @@ async fn lookup_chat_by_reply(
|
||||
if let Some(parent) = parent {
|
||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||
|
||||
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.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1654,40 +1674,37 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
// 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
|
||||
let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
|| chat::is_contact_in_chat(context, chat_id, from_id).await?;
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = is_from_in_chat
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.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
|
||||
};
|
||||
.await?;
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = if allow_member_list_changes {
|
||||
let recreate_member_list = {
|
||||
// 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) {
|
||||
!mime_parser.has_chat_version()
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
|| self_added
|
||||
|| 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}.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
false
|
||||
allow_member_list_changes
|
||||
};
|
||||
|
||||
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
|
||||
@@ -1794,43 +1811,35 @@ async fn apply_group_changes(
|
||||
|
||||
// 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() {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add))
|
||||
// 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() {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -3611,3 +3612,70 @@ 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(())
|
||||
}
|
||||
|
||||
18
src/smtp.rs
18
src/smtp.rs
@@ -492,7 +492,20 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
if let SendResult::Failure(err) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
message::set_msg_failed(context, msg_id, &err.to_string()).await;
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
@@ -539,7 +552,8 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
)
|
||||
.await?;
|
||||
if retries > 6 {
|
||||
message::set_msg_failed(context, msg_id, "Number of retries exceeded the limit.").await;
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.").await?;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
|
||||
79
src/sql.rs
79
src/sql.rs
@@ -304,6 +304,25 @@ impl Sql {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the passphrase of encrypted database.
|
||||
///
|
||||
/// The database must already be encrypted and the passphrase cannot be empty.
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
/// Locks the write transactions mutex in order to make sure that there never are
|
||||
/// multiple write transactions at once.
|
||||
///
|
||||
@@ -1246,6 +1265,66 @@ mod tests {
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sql_change_passphrase() -> Result<()> {
|
||||
use tempfile::tempdir;
|
||||
|
||||
// The context is used only for logging.
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Create a separate empty database for testing.
|
||||
let dir = tempdir()?;
|
||||
let dbfile = dir.path().join("testdb.sqlite");
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database first time")?;
|
||||
sql.close().await;
|
||||
|
||||
// Change the passphrase from "foo" to "bar".
|
||||
let sql = Sql::new(dbfile.clone());
|
||||
sql.open(&t, "foo".to_string())
|
||||
.await
|
||||
.context("failed to open the database second time")?;
|
||||
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);
|
||||
|
||||
// Test that old passphrase is not working.
|
||||
assert!(sql.open(&t, "foo".to_string()).await.is_err());
|
||||
|
||||
// Open the database with the new passphrase.
|
||||
sql.check_passphrase("bar".to_string()).await?;
|
||||
sql.open(&t, "bar".to_string())
|
||||
.await
|
||||
.context("failed to open the database third time")?;
|
||||
sql.close().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +516,8 @@ DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
msg_id INTEGER,
|
||||
update_item TEXT DEFAULT '',
|
||||
update_item_read INTEGER DEFAULT 0);
|
||||
update_item_read INTEGER DEFAULT 0 -- XXX unused
|
||||
);
|
||||
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
|
||||
84,
|
||||
)
|
||||
|
||||
@@ -535,6 +535,9 @@ impl EmailAddress {
|
||||
if domain.is_empty() {
|
||||
bail!("missing domain after '@' in {:?}", input);
|
||||
}
|
||||
if domain.ends_with('.') {
|
||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
||||
}
|
||||
Ok(EmailAddress {
|
||||
local: (*local).to_string(),
|
||||
domain: (*domain).to_string(),
|
||||
@@ -996,7 +999,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
|
||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
||||
assert!(EmailAddress::new("u@d").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_ok());
|
||||
assert!(EmailAddress::new("u@d.").is_err());
|
||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
||||
assert_eq!(
|
||||
EmailAddress::new("u@d.tt").unwrap(),
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
//! # Handle webxdc messages.
|
||||
//!
|
||||
//! Internally status updates are stored in the `msgs_status_updates` SQL table.
|
||||
//! `msgs_status_updates` contains the following columns:
|
||||
//! - `id` - status update serial number
|
||||
//! - `msg_id` - ID of the message in the `msgs` table
|
||||
//! - `update_item` - JSON representation of the status update
|
||||
//!
|
||||
//! Status updates are scheduled for sending by adding a record
|
||||
//! to `smtp_status_updates_table` SQL table.
|
||||
//! `smtp_status_updates` contains the following columns:
|
||||
//! - `msg_id` - ID of the message in the `msgs` table
|
||||
//! - `first_serial` - serial number of the first status update to send
|
||||
//! - `last_serial` - serial number of the last status update to send
|
||||
//! - `descr` - text to send along with the updates
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::path::Path;
|
||||
@@ -665,6 +679,10 @@ impl Context {
|
||||
///
|
||||
/// Example: `{"updates": [{"payload":"any update data"},
|
||||
/// {"payload":"another update data"}]}`
|
||||
///
|
||||
/// `range` is an optional range of status update serials to send.
|
||||
/// If it is `None`, all updates are sent.
|
||||
/// This is used when a message is resent using [`crate::chat::resend_msgs`].
|
||||
pub(crate) async fn render_webxdc_status_update_object(
|
||||
&self,
|
||||
instance_msg_id: MsgId,
|
||||
@@ -895,10 +913,8 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn create_webxdc_instance(t: &TestContext, name: &str, bytes: &[u8]) -> Result<Message> {
|
||||
let file = t.get_blobdir().join(name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance.set_file_from_bytes(t, name, bytes, None).await?;
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
@@ -926,10 +942,10 @@ mod tests {
|
||||
assert_eq!(instance.chat_id, chat_id);
|
||||
|
||||
// sending using bad extension is not working, even when setting Viewtype to webxdc
|
||||
let file = t.get_blobdir().join("index.html");
|
||||
tokio::fs::write(&file, b"<html>ola!</html>").await?;
|
||||
let mut instance = Message::new(Viewtype::Webxdc);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance
|
||||
.set_file_from_bytes(&t, "index.html", b"<html>ola!</html>", None)
|
||||
.await?;
|
||||
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
||||
|
||||
Ok(())
|
||||
@@ -953,14 +969,15 @@ mod tests {
|
||||
assert_eq!(test.viewtype, Viewtype::File);
|
||||
|
||||
// sending invalid .xdc as Viewtype::Webxdc should fail already on sending
|
||||
let file = t.get_blobdir().join("invalid2.xdc");
|
||||
tokio::fs::write(
|
||||
&file,
|
||||
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
||||
)
|
||||
.await?;
|
||||
let mut instance = Message::new(Viewtype::Webxdc);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
instance
|
||||
.set_file_from_bytes(
|
||||
&t,
|
||||
"invalid2.xdc",
|
||||
include_bytes!("../test-data/webxdc/invalid-no-zip-but-7z.xdc"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
assert!(send_msg(&t, chat_id, &mut instance).await.is_err());
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user