Compare commits

...

40 Commits

Author SHA1 Message Date
link2xt
aa78e82fed chore(release): prepare for 1.123.0 2023-09-22 22:13:47 +00:00
link2xt
d4e670d5e9 chore(deps): update OpenSSL from 3.1.2 to 3.1.3 2023-09-22 21:57:36 +00:00
link2xt
4553c6521f api!: make dc_jsonrpc_blocking_call accept JSON-RPC request 2023-09-22 21:33:52 +00:00
link2xt
e72d527d88 api: make it possible to import secret key from a file
Previously it was required that a directory path is provided to the import API.
Now it is possible to point directly to the .asc file containing a secret key.

This allows UI to present a file selection dialog to the user
and let select the file directly.

Selecting a directory is still supported for backwards compatibility.
2023-09-20 16:18:45 +00:00
link2xt
f5c36043f6 fix(imex): use "default" in the filename of the default key
Previously the logic was inverted, default key was exported with a number
and all other keys were exported into "default".
2023-09-20 16:18:45 +00:00
iequidoo
b227ff87dc fix: lookup_chat_by_reply(): Skip undecipherable parent messages created by older versions (#4676)
It's just a dirty hack checking the error prefix, but as undecipherable messages are remembered in
the db now, the hack may be removed soon.
2023-09-18 14:55:56 +00:00
iequidoo
676f311f97 fix: lookup_chat_by_reply(): Skip not fully downloaded and undecipherable messages (#4676)
Such a message may be assigned to a wrong chat (e.g. undecipherable group msgs often get assigned to
the 1:1 chat with the sender). Add `DownloadState::Undecipherable` so that messages referencing
undecipherable ones don't go to that wrong chat too. Also do not reply to not fully downloaded
messages. Before `Message.error` was checked for that purpose, but a message can be error for many
reasons.
2023-09-18 14:55:56 +00:00
link2xt
f02299c06c chore(release): prepare for 1.122.0 2023-09-12 17:33:22 +00:00
link2xt
ed781af52c chore(cargo): update to OpenSSL 3.0
OpenSSL 1.1.1 has reached End of Life:
https://www.openssl.org/blog/blog/2023/09/11/eol-111/
2023-09-12 17:11:58 +00:00
link2xt
67043177a9 fix: reopen all connections on database passpharse change
Previously only one connection, the one used to change the key,
was working after passphrase change.

With this fix the whole pool of connections
is recreated on passphrase change, so there is no need
to reopen the database manually.
2023-09-12 16:34:26 +00:00
link2xt
49cc5fb673 feat: add RSA-4096 key generation support 2023-09-12 12:33:34 +00:00
link2xt
68c95dee17 refactor(pgp): add constants for encryption algorithm and hash
These constants are current defaults in `pgp` crate,
this change would prevent accidental change due to rPGP upgrade
and make it easier to change in a single place.
2023-09-12 11:12:59 +02:00
iequidoo
9bd7ab7280 fix: apply_group_changes(): Forbid membership changes from possible non-members (#3782)
It can be not good for membership consistency if we missed a message adding a member, but improves
security because nobody can add themselves to a group from now on.
2023-09-12 00:30:02 -03:00
link2xt
7a359f6318 build(python): add link to mastodon into projects.urls
Such links are displayed on PyPI with mastodon icon.
2023-09-11 04:47:55 +00:00
link2xt
38b31aa88d fix: do not block new group chats if 1:1 chat is blocked
1:1 chat may be blocked while the contact is not
if 1:1 chat was created as a result of scanning
a verified group join QR code with the contact
as the inviter. In this case 1:1 chat is blocked to hide it
while the contact is unblocked.
2023-09-10 21:44:56 +00:00
iequidoo
e12e026bd8 fix: Switch to original Hocuri's group membership consistency algo (#3782)(#4624)
- If we don't know the parent (=In-Reply-To) message, then completely recreate the group member list
  (i.e. use the member list of the incoming message) (because we assume that we missed some messages
  & have a wrong group state).
- If the message has a "Chat-Group-Member-Removed: member@example.com" header, then remove this
  member.
- If the message has a "Chat-Group-Member-Added: member@example.com" header, then add this member.

That means:
- Remove checks for the presense of `ContactId::SELF` in the group. Thus all recipients of a message
  take the same decision about group membership changes, no matter if they are in the group
  currently. This fixes a situation when a recipient thinks it's not a member because it missed a
  message about its addition before.
  NOTE: But 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 need this heuristic currently.
- Recreate the group member list if we don't know the parent (=In-Reply-To) message, even if the
  sender isn't in the group as per our view, because we missed some messages and our view may be
  stale.
2023-09-09 16:22:13 -03:00
iequidoo
212fbc125c fix: ChatId::parent_query(): Don't filter out OutPending and OutFailed messages
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.
2023-09-09 16:22:13 -03:00
link2xt
2939de013b api(jsonrpc): return only chat IDs for similar chats
This is already the way `get_chatlist_entries` works.

`get_similar_chatlist_entries` is renamed into
`get_similar_chat_ids` because return values are not entries anymore.
2023-09-08 18:47:31 +00:00
link2xt
0562e23ee0 chore(cargo): bump webpki from 0.22.0 to 0.22.1 2023-09-08 07:00:18 +00:00
link2xt
a8551510cd chore(release): prepare for 1.121.0 2023-09-06 20:53:00 +00:00
link2xt
087f6edd0c refactor: make set_msg_failed accept &mut Message instead of MsgId 2023-09-06 20:33:37 +00:00
link2xt
d6b7ee04a0 docs: document range argument of render_webxdc_status_update_object() 2023-09-06 20:33:37 +00:00
link2xt
d5c5ff8b3f refactor: accept &mut Message in create_send_msg_job() 2023-09-06 20:33:37 +00:00
link2xt
dc4396a699 fix: update passed by reference Message in prepare_msg_raw() 2023-09-06 20:33:37 +00:00
link2xt
a74b00c3f9 refactor: move try_calc_and_set_dimensions() call to prepare_msg_blob() 2023-09-06 20:33:37 +00:00
link2xt
2fdb9f8b7e fix: do not ignore errors in try_calc_and_set_dimensions 2023-09-06 20:33:37 +00:00
link2xt
80fac3f1b8 refactor: use let-else in resend_msgs() 2023-09-06 20:33:37 +00:00
link2xt
17a6c88cc7 api: add Message.set_file_from_bytes() API 2023-09-06 20:33:37 +00:00
link2xt
1ba69dbb9b fix: reset MIME type if passed to set_file value is None 2023-09-06 20:33:37 +00:00
link2xt
ab1c7ebbe2 refactor: remove unnecessary local variable save_param 2023-09-06 20:33:37 +00:00
link2xt
ee715da078 fix: do not ignore chat loading errors in forward_msgs() 2023-09-06 20:33:37 +00:00
link2xt
27e177dc05 refactor: resultify set_msg_failed() 2023-09-06 20:33:37 +00:00
link2xt
7aac4bfc83 docs: document WebXDC-related SQL tables 2023-09-06 20:33:37 +00:00
link2xt
7b24f9b7a4 docs: comment that msgs_status_updates.update_item_read column is unused 2023-09-06 20:33:37 +00:00
link2xt
b36b902eeb build: build node packages on Ubuntu 18.04
Debian 10 has glibc 2.28, so packages built with it
do not work on Ubuntu 18.04.
2023-09-06 17:23:04 +00:00
link2xt
30024abb6c feat: add API to get similar chats 2023-09-05 16:47:19 +00:00
link2xt
1d9702e9e7 refactor: add ChatId::get_timestamp() 2023-09-05 16:47:19 +00:00
link2xt
ee2eae63d6 fix: do not allow dots at the end of email addresses 2023-09-05 13:14:37 +00:00
link2xt
cd477936b5 api: add dc_context_change_passphrase() 2023-09-05 12:41:31 +00:00
link2xt
dbe9d7e34e refactor(provider): create provider database entries at compile time
According to cargo-llvm-lines it reduces the number of lines
in the crate by 0.7%.
2023-09-05 00:03:34 +00:00
44 changed files with 1689 additions and 1048 deletions

View File

@@ -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.

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

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

View File

@@ -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
/**

View File

@@ -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(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
}
}
}

View File

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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,
}
}

View File

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

View File

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

View File

@@ -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())

View File

@@ -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"

View File

@@ -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]

View File

@@ -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,

View File

@@ -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,

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.120.0"
"version": "1.123.0"
}

View File

@@ -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"

View File

@@ -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

View File

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

View File

@@ -1 +1 @@
2023-08-28
2023-09-22

View File

@@ -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")

View File

@@ -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);

View File

@@ -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()

View File

@@ -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]

View File

@@ -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]

View File

@@ -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;

View File

@@ -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)

View File

@@ -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:#}");
}
}

View File

@@ -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

View File

@@ -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?;

View File

@@ -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()

View File

@@ -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)?;

View File

@@ -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,

File diff suppressed because it is too large Load Diff

View File

@@ -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 {

View File

@@ -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(())
}

View File

@@ -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,))

View File

@@ -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(())
}
}

View File

@@ -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,
)

View File

@@ -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(),

View File

@@ -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(())