Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
8b06d31190 api: add dc_replace_webxdc() 2023-09-22 10:44:30 +00:00
100 changed files with 1859 additions and 2660 deletions

View File

@@ -81,15 +81,19 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.71.0
rust: 1.68.2
- os: windows-latest
rust: 1.71.0
rust: 1.68.2
- os: macos-latest
rust: 1.71.0
rust: 1.68.2
# Minimum Supported Rust Version = 1.67.0
# Minimum Supported Rust Version = 1.65.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.67.0
rust: 1.65.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -102,8 +106,6 @@ jobs:
uses: swatinem/rust-cache@v2
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace
- name: Test cargo vendor

View File

@@ -1,22 +1,5 @@
# 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
@@ -2833,4 +2816,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0

View File

@@ -76,29 +76,6 @@ If you have multiple changes in one PR, create multiple conventional commits, an
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.

632
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.123.0"
version = "1.122.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.67"
rust-version = "1.65"
[profile.dev]
debug = 0
@@ -48,12 +48,11 @@ email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
futures = "0.3"
futures-lite = "1.13.0"
hex = "0.4.0"
humansize = "2"
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -69,13 +68,13 @@ parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.30"
quick-xml = "0.29"
rand = "0.8"
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
regex = "1.8"
reqwest = { version = "0.11.18", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
sanitize-filename = "0.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
@@ -92,7 +91,7 @@ tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.8"
toml = "0.7"
trust-dns-resolver = "0.23"
trust-dns-resolver = "0.22"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
@@ -119,6 +118,11 @@ members = [
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]]
name = "create_account"
harness = false

View File

@@ -1,16 +1,8 @@
<p align="center">
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
</p>
# Delta Chat Rust
<p align="center">
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
</a>
</p>
> Deltachat-core written in Rust
<p align="center">
The core library for Delta Chat, written in Rust
</p>
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
## Installing Rust and Cargo

View File

@@ -8,8 +8,7 @@ async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();

View File

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

View File

@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
# exclude all test directories use the pattern */test/*
######################################################
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
EXCLUDE_SYMBOLS += _dc_* jsmn*
######################################################

View File

@@ -500,13 +500,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -1125,7 +1118,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
*
* In JS land, that would be mapped to something as:
* ```
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
* ```
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
* See dc_get_webxdc_status_updates() for the receiving counterpart.
@@ -1182,6 +1175,24 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
/**
* Replaces webxdc app with a new version.
*
* On the JavaScript side this API could be used like this:
* ```
* window.webxdc.replaceWebxdc(blob);
* ```
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the WebXDC message to be replaced.
* @param blob New blob to replace WebXDC with.
* @param n Blob size.
*/
void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n);
/**
* Save a draft for a chat in the database.
*
@@ -1506,6 +1517,24 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Enable or disable protection against active attacks.
* To enable protection, it is needed that all members are verified;
* if this condition is met, end-to-end-encryption is always enabled
* and only the verified keys are used.
*
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, e.g. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -2945,15 +2974,12 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new() will try to create it.
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
/**
@@ -3734,6 +3760,7 @@ int dc_chat_can_send (const dc_chat_t* chat);
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
* The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3742,26 +3769,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -4356,7 +4363,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -5050,12 +5057,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
*
* The UI may use this in addition to a checkmark showing the verification status.
* In case of verification chains,
* the last contact in the chain is shown.
* This is because of privacy reasons, but also as it would not help the user
* to see a unknown name here - where one can mostly always ask the shown name
* as it is directly known.
* The UI may use this in addition to a checkmark showing the verification status
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5771,11 +5773,12 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param input JSON-RPC request.
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If there is no response, NULL is returned.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
@@ -6795,6 +6798,15 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
///
/// Used in summaries.
@@ -7239,6 +7251,26 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7249,16 +7281,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/**
* @}
*/

View File

@@ -29,14 +29,14 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use num_traits::{FromPrimitive, ToPrimitive};
@@ -813,13 +813,21 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
let addr = to_string_lossy(addr);
let public_data = to_string_lossy(public_data);
let secret_data = to_string_lossy(secret_data);
block_on(preconfigure_keypair(ctx, &addr, &public_data, &secret_data))
.context("Failed to save keypair")
.log_err(ctx)
.is_ok() as libc::c_int
block_on(async move {
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.context("Failed to save keypair")
.log_err(ctx)
.unwrap_or(0)
}
#[no_mangle]
@@ -1089,6 +1097,32 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_replace_webxdc(
context: *mut dc_context_t,
msg_id: u32,
blob: *const u8,
n: libc::size_t,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_replace_webxdc()");
return;
}
let msg_id = MsgId::new(msg_id);
let blob_slice = std::slice::from_raw_parts(blob, n);
let ctx = &*context;
block_on(async move {
replace_webxdc(ctx, msg_id, blob_slice)
.await
.context("Failed to replace WebXDC")
.log_err(ctx)
.ok();
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1463,6 +1497,32 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -1505,14 +1565,10 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
}
let ctx = &*context;
block_on(async move {
ChatId::new(chat_id)
.delete(ctx)
.await
.context("Failed chat delete")
.log_err(ctx)
.ok();
})
block_on(ChatId::new(chat_id).delete(ctx))
.context("Failed chat delete")
.log_err(ctx)
.ok();
}
#[no_mangle]
@@ -3092,16 +3148,6 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
ffi_chat.chat.is_protected() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -4710,17 +4756,17 @@ pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
dir: *const libc::c_char,
writable: libc::c_int,
_os_name: *const libc::c_char,
dbfile: *const libc::c_char,
) -> *mut dc_accounts_t {
setup_panic!();
if dir.is_null() {
if dbfile.is_null() {
eprintln!("ignoring careless call to dc_accounts_new()");
return ptr::null_mut();
}
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
let accs = block_on(Accounts::new(as_path(dbfile).into()));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -4986,7 +5032,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use super::*;
@@ -5062,24 +5108,25 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
method: *const libc::c_char,
params: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
match res {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.123.0"
version = "1.122.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -15,26 +15,26 @@ required-features = ["webserver"]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.13"
schemars = "0.8.11"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.8.0"
tempfile = "3.6.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.105"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.32.0" }
sanitize-filename = "0.5"
serde_json = "1.0.99"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.29.1" }
sanitize-filename = "0.4"
walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.20", optional = true, features = ["ws"] }
axum = { version = "0.6.18", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -4,30 +4,30 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
use deltachat::qr::Qr;
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
},
chatlist::Chatlist,
config::Config,
constants::DC_MSG_ID_DAYMARKER,
contact::{may_be_valid_addr, Contact, ContactId, Origin},
context::get_info,
ephemeral::Timer,
imex, location,
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
reaction::{get_msg_reactions, send_reaction},
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -142,7 +142,11 @@ impl CommandApi {
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
#[rpc(
all_positional,
ts_outdir = "typescript/generated",
openrpc_outdir = "openrpc"
)]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -153,12 +157,12 @@ impl CommandApi {
// Misc top level functions
// ---------------------------------------------
/// Checks if an email address is valid.
/// Check if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Returns general system info.
/// Get general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
@@ -219,13 +223,11 @@ impl CommandApi {
Ok(accounts)
}
/// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
Ok(())
}
/// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
Ok(())
@@ -235,16 +237,14 @@ impl CommandApi {
// Methods that work on individual accounts
// ---------------------------------------------
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
async fn start_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
ctx.start_io().await;
Ok(())
}
/// Stops background tasks for a single account.
async fn stop_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
async fn stop_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
ctx.stop_io().await;
Ok(())
}
@@ -311,13 +311,11 @@ impl CommandApi {
ctx.get_info().await
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
/// Updates a batch of configuration values.
async fn batch_set_config(
&self,
account_id: u32,
@@ -349,7 +347,6 @@ impl CommandApi {
Ok(qr_object)
}
/// Returns configuration value for the given key.
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
@@ -1885,7 +1882,7 @@ impl CommandApi {
.context("path conversion to string failed")
}
/// Saves a sticker to a collection/folder in the account's sticker folder.
/// save a sticker to a collection/folder in the account's sticker folder
async fn misc_save_sticker(
&self,
account_id: u32,

View File

@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
#[serde(tag = "type")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {

View File

@@ -167,11 +167,10 @@ impl BasicChat {
}
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
pub enum MuteDuration {
NotMuted,
Forever,
Until { duration: i64 },
Until(i64),
}
impl MuteDuration {
@@ -179,13 +178,13 @@ impl MuteDuration {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until { duration } => {
if duration <= 0 {
MuteDuration::Until(n) => {
if n <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
.checked_add(Duration::from_secs(duration as u64))
.checked_add(Duration::from_secs(n as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}

View File

@@ -15,7 +15,7 @@ use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {

View File

@@ -22,7 +22,7 @@ impl From<CoreEvent> for Event {
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
#[serde(tag = "type")]
pub enum EventType {
/// The library-user may write an informational string to the log.
///

View File

@@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },

View File

@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "kind")]
#[serde(tag = "type")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,

View File

@@ -13,8 +13,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
@@ -55,8 +54,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let accounts = Accounts::new(tmp_dir).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();

View File

@@ -19,8 +19,7 @@ async fn main() -> Result<(), std::io::Error> {
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()

View File

@@ -35,7 +35,7 @@ async function run() {
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
for (const account of accounts) {
if (account.kind === "Configured") {
if (account.type === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
@@ -57,7 +57,7 @@ async function run() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.kind !== "Configured") {
if (info.type !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
@@ -81,7 +81,8 @@ async function run() {
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
}
}
@@ -92,9 +93,9 @@ async function run() {
$side,
`
<p class="message">
[<strong>${event.kind}</strong> on account ${accountId}]<br>
[<strong>${event.type}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { kind: undefined })
Object.assign({}, event, { type: undefined })
)}
</p>`
);

View File

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

View File

@@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["kind"]]: (
[Property in EventType["type"]]: (
accountId: number,
event: Extract<EventType, { kind: Property }>
event: Extract<EventType, { type: Property }>
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
[Property in EventType["type"]]: (
event: Extract<EventType, { type: Property }>
) => void;
};
export type DcEvent = EventType;
export type DcEventType<T extends EventType["kind"]> = Extract<
export type DcEventType<T extends EventType["type"]> = Extract<
EventType,
{ kind: T }
{ type: T }
>;
export class BaseDeltaChat<
@@ -46,12 +46,12 @@ export class BaseDeltaChat<
while (true) {
const event = await this.rpc.getNextEvent();
//@ts-ignore
this.emit(event.event.kind, event.contextId, event.event);
this.emit(event.event.type, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.kind,
event.event.type,
//@ts-ignore
event.event as any
);

View File

@@ -29,8 +29,8 @@ describe("online tests", function () {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc.on("ALL", (contextId, { kind }) => {
if (kind !== "Info") console.log(contextId, kind);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -177,12 +177,12 @@ describe("online tests", function () {
});
});
async function waitForEvent<T extends DcEvent["kind"]>(
async function waitForEvent<T extends DcEvent["type"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { kind: T }>> {
): Promise<Extract<DcEvent, { type: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.123.0"
version = "1.122.0"
license = "MPL-2.0"
edition = "2021"
@@ -9,7 +9,7 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.20"
log = "0.4.19"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rustyline = "12"

View File

@@ -18,7 +18,6 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -212,17 +211,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -407,6 +396,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
@@ -1081,6 +1072,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);

View File

@@ -14,9 +14,9 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent)
async def log_event(event):
if event.kind == EventType.INFO:
if event.type == EventType.INFO:
logging.info(event.msg)
elif event.kind == EventType.WARNING:
elif event.type == EventType.WARNING:
logging.warning(event.msg)

View File

@@ -54,14 +54,14 @@ class Chat:
"""
if duration is not None:
assert duration > 0, "Invalid duration"
dur: dict = {"kind": "Until", "duration": duration}
dur: Union[str, dict] = {"Until": duration}
else:
dur = {"kind": "Forever"}
dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
async def unmute(self) -> None:
"""Unmute this chat."""
await self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
async def pin(self) -> None:
"""Pin this chat."""

View File

@@ -106,10 +106,10 @@ class Client:
await self._process_messages() # Process old messages.
while True:
event = await self.account.wait_for_event()
event["kind"] = EventType(event.kind)
event["type"] = EventType(event.type)
event["account"] = self.account
await self._on_event(event)
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
await self._process_messages()
stop = func(event)

View File

@@ -83,7 +83,7 @@ class RawEvent(EventFilter):
return False
async def filter(self, event: "AttrDict") -> bool:
if self.types and event.kind not in self.types:
if self.types and event.type not in self.types:
return False
return await self._call_func(event)

View File

@@ -57,7 +57,7 @@ class ACFactory:
while True:
event = await account.wait_for_event()
print(event)
if event.kind == EventType.IMAP_INBOX_IDLE:
if event.type == EventType.IMAP_INBOX_IDLE:
break
return account
@@ -98,7 +98,7 @@ class ACFactory:
group=group,
)
return await to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
@pytest_asyncio.fixture

View File

@@ -1,6 +1,4 @@
import asyncio
import json
import subprocess
from unittest.mock import MagicMock
import pytest
@@ -45,7 +43,7 @@ async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = await account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
@@ -76,7 +74,7 @@ async def test_account(acfactory) -> None:
while True:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
@@ -146,7 +144,7 @@ async def test_chat(acfactory) -> None:
while True:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
@@ -232,7 +230,7 @@ async def test_message(acfactory) -> None:
while True:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
@@ -272,7 +270,7 @@ async def test_is_bot(acfactory) -> None:
while True:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
@@ -357,13 +355,3 @@ async def test_import_export(acfactory, tmp_path) -> None:
files = list(tmp_path.glob("*.tar"))
alice2 = await acfactory.get_unconfigured_account()
await alice2.import_backup(files[0])
assert await alice2.manager.get_system_info()
def test_openrpc_command_line() -> None:
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc

View File

@@ -13,7 +13,7 @@ async def test_webxdc(acfactory) -> None:
while True:
event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
if event.type == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.123.0"
version = "1.122.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -17,11 +17,11 @@ anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
log = "0.4"
serde_json = "1.0.105"
serde_json = "1.0.99"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.32.0", features = ["io-std"] }
tokio = { version = "1.29.1", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features]
default = ["vendored"]

View File

@@ -32,6 +32,3 @@ languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.

View File

@@ -10,7 +10,6 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
use tokio::signal::unix as signal_unix;
@@ -40,12 +39,6 @@ async fn main_impl() -> Result<()> {
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
println!("{}", CommandApi::openrpc_specification()?);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
}
@@ -63,8 +56,7 @@ async fn main_impl() -> Result<()> {
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
let accounts = Accounts::new(PathBuf::from(&path)).await?;
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));

View File

@@ -11,6 +11,7 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "ahash", version = "0.7.6" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
@@ -24,10 +25,12 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "idna", version = "<0.3" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "linux-raw-sys", version = "0.3.8" },
{ name = "num-derive", version = "0.3.3" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -35,8 +38,8 @@ skip = [
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
@@ -52,10 +55,11 @@ skip = [
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "winreg", version = "0.10.1" },
]

100
examples/simple.rs Normal file
View File

@@ -0,0 +1,100 @@
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress { progress, .. } => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
log::info!("{}", msg);
}
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) => {
log::error!("{}", msg);
}
event => {
log::info!("{:?}", event);
}
}
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
#[tokio::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
}
});
log::info!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap();
ctx.configure().await.unwrap();
log::info!("------ RUN ------");
ctx.start_io().await;
log::info!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
.await
.unwrap();
}
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);
}
log::info!("stopping");
ctx.stop_io().await;
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
}

320
fuzz/Cargo.lock generated
View File

@@ -177,13 +177,13 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.1"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
dependencies = [
"async-channel",
"base64 0.21.0",
"bytes",
"byte-pool",
"chrono",
"futures",
"imap-proto",
@@ -337,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.0"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
[[package]]
name = "blake3"
@@ -529,6 +529,16 @@ version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byte-pool"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
dependencies = [
"crossbeam-queue",
"stable_deref_trait",
]
[[package]]
name = "bytemuck"
version = "1.12.3"
@@ -731,6 +741,16 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
@@ -906,7 +926,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.122.0"
version = "1.117.0"
dependencies = [
"anyhow",
"async-channel",
@@ -923,7 +943,6 @@ dependencies = [
"encoded-words",
"escaper",
"fast-socks5",
"fd-lock",
"format-flowed",
"futures",
"futures-lite",
@@ -936,7 +955,7 @@ dependencies = [
"libc",
"mailparse 0.14.0",
"mime",
"num-derive 0.4.0",
"num-derive",
"num-traits",
"num_cpus",
"once_cell",
@@ -1410,14 +1429,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.6.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.15",
"syn 1.0.107",
]
[[package]]
@@ -1445,17 +1464,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "errno"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
@@ -1524,17 +1532,6 @@ dependencies = [
"instant",
]
[[package]]
name = "fd-lock"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix 0.38.14",
"windows-sys 0.48.0",
]
[[package]]
name = "ff"
version = "0.12.1"
@@ -1619,9 +1616,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
@@ -1846,15 +1843,18 @@ dependencies = [
[[package]]
name = "heck"
version = "0.4.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.3.3"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
[[package]]
name = "hex"
@@ -2012,9 +2012,20 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -2022,9 +2033,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.7"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
dependencies = [
"bytemuck",
"byteorder",
@@ -2092,7 +2103,7 @@ dependencies = [
"socket2",
"widestring",
"winapi",
"winreg 0.10.1",
"winreg",
]
[[package]]
@@ -2219,9 +2230,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.148"
version = "0.2.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
[[package]]
name = "libm"
@@ -2268,12 +2279,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "linux-raw-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
[[package]]
name = "lock_api"
version = "0.4.9"
@@ -2336,9 +2341,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md-5"
version = "0.10.5"
@@ -2356,9 +2367,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
[[package]]
name = "memchr"
version = "2.6.3"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "mime"
@@ -2544,17 +2555,6 @@ dependencies = [
"syn 1.0.107",
]
[[package]]
name = "num-derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -2599,9 +2599,9 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.16.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi",
"libc",
@@ -2854,7 +2854,7 @@ dependencies = [
"md-5",
"nom",
"num-bigint-dig",
"num-derive 0.3.3",
"num-derive",
"num-traits",
"p256 0.13.1",
"p384 0.13.0",
@@ -3087,9 +3087,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.30.0"
version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
dependencies = [
"memchr",
]
@@ -3114,9 +3114,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.9.5"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989"
checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9"
dependencies = [
"bytes",
"rand 0.8.5",
@@ -3268,14 +3268,13 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.5"
version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
"regex-syntax 0.7.2",
]
[[package]]
@@ -3287,17 +3286,6 @@ dependencies = [
"regex-syntax 0.6.28",
]
[[package]]
name = "regex-automata"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
@@ -3306,15 +3294,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "regex-syntax"
version = "0.7.5"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "reqwest"
version = "0.11.20"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3344,7 +3332,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
"winreg",
]
[[package]]
@@ -3450,7 +3438,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.4.0",
"bitflags 2.0.2",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -3501,26 +3489,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags 1.3.2",
"errno 0.2.8",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys 0.1.4",
"linux-raw-sys",
"windows-sys 0.42.0",
]
[[package]]
name = "rustix"
version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno 0.3.3",
"libc",
"linux-raw-sys 0.4.7",
"windows-sys 0.48.0",
]
[[package]]
name = "rustls"
version = "0.20.8"
@@ -3582,9 +3557,9 @@ dependencies = [
[[package]]
name = "sanitize-filename"
version = "0.5.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
dependencies = [
"lazy_static",
"regex",
@@ -3944,6 +3919,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stop-token"
version = "0.7.0"
@@ -3964,21 +3945,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.25.0"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]]
name = "strum_macros"
version = "0.25.2"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.15",
"syn 1.0.107",
]
[[package]]
@@ -4057,7 +4038,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix 0.36.7",
"rustix",
"windows-sys 0.42.0",
]
@@ -4384,9 +4365,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.23.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
dependencies = [
"async-trait",
"cfg-if",
@@ -4395,9 +4376,9 @@ dependencies = [
"futures-channel",
"futures-io",
"futures-util",
"idna",
"idna 0.2.3",
"ipnet",
"once_cell",
"lazy_static",
"rand 0.8.5",
"smallvec",
"thiserror",
@@ -4409,17 +4390,16 @@ dependencies = [
[[package]]
name = "trust-dns-resolver"
version = "0.23.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror",
@@ -4451,9 +4431,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
@@ -4500,12 +4480,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.4.1"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"idna 0.3.0",
"percent-encoding",
]
@@ -4751,51 +4731,21 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -4814,12 +4764,6 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -4838,12 +4782,6 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -4862,12 +4800,6 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -4886,24 +4818,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -4922,12 +4842,6 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winreg"
version = "0.10.1"
@@ -4937,16 +4851,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "x25519-dalek"
version = "2.0.0-pre.1"

View File

@@ -159,8 +159,6 @@ module.exports = {
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
DC_STR_CHAT_PROTECTION_DISABLED: 171,
DC_STR_CHAT_PROTECTION_ENABLED: 170,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
@@ -246,6 +244,12 @@ module.exports = {
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,

View File

@@ -159,8 +159,6 @@ export enum C {
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CHAT_PROTECTION_DISABLED = 171,
DC_STR_CHAT_PROTECTION_ENABLED = 170,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
@@ -246,6 +244,12 @@ export enum C {
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,

View File

@@ -699,6 +699,23 @@ export class Context extends EventEmitter {
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(

View File

@@ -21,15 +21,12 @@ export class AccountManager extends EventEmitter {
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, writable = true) {
constructor(cwd: string, os = 'deltachat-node') {
super()
debug('DeltaChat constructor')
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(
this.accountDir,
writable ? 1 : 0
)
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
}
getAllAccountIds() {

View File

@@ -1399,6 +1399,18 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_set_chat_protection) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(chat_id, 1);
NAPI_ARGV_INT32(protect, 1);
int result = dc_set_chat_protection(dcn_context->dc_context,
chat_id,
protect);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -2903,8 +2915,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
NAPI_METHOD(dcn_accounts_new) {
NAPI_ARGV(2);
NAPI_ARGV_UTF8_MALLOC(dir, 0);
NAPI_ARGV_INT32(writable, 1);
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
NAPI_ARGV_UTF8_MALLOC(dir, 1);
TRACE("calling..");
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
@@ -2913,7 +2925,7 @@ NAPI_METHOD(dcn_accounts_new) {
}
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
napi_value result;
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
@@ -3479,6 +3491,7 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);

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.123.0"
"version": "1.122.0"
}

View File

@@ -136,7 +136,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
assert "added" in msg.text.lower()
lp.sec("ac1: send message")
@@ -183,9 +182,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
# System message about the added member.
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.is_system_message()
# Skip system message about added member
ac3._evtracker.wait_next_incoming_message()
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.is_encrypted()
@@ -526,8 +524,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
lp.sec("ac2: sending message")
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
# "Member Me (<addr>) added by <addr>." message.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
ac2._evtracker.wait_next_incoming_message()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")

View File

@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message, Chat
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
@@ -1467,18 +1467,13 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
lp.sec("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
ac1._evtracker.wait_msg_delivered(msgs[-1])
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
@@ -1510,7 +1505,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
time.sleep(2)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
@@ -1669,12 +1664,8 @@ def test_qr_setup_contact(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
def test_qr_join_chat(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
@@ -1687,17 +1678,6 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
assert not ch.is_protected()
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.

View File

@@ -1,4 +1,3 @@
import json
from queue import Queue
import deltachat as dc
@@ -222,32 +221,15 @@ def test_logged_ac_process_ffi_failure(acfactory):
def test_jsonrpc_blocking_call(tmp_path):
accounts_fname = tmp_path / "accounts"
writable = True
accounts = ffi.gc(
lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
).encode("utf-8"),
),
),
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
assert res == "true"
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
).encode("utf-8"),
),
),
)
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -59,8 +59,7 @@ commands =
[testenv:doc]
changedir=doc
deps =
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
sphinx<=7.1.2
sphinx
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
2023-09-22
2023-09-12

View File

@@ -6,4 +6,3 @@ FROM $BASEIMAGE
RUN pipx install tox
COPY install-rust.sh /scripts/
RUN /scripts/install-rust.sh
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi

View File

@@ -7,9 +7,6 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
use uuid::Uuid;
use crate::context::Context;
@@ -36,16 +33,16 @@ pub struct Accounts {
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
if writable && !dir.exists() {
pub async fn new(dir: PathBuf) -> Result<Self> {
if !dir.exists() {
Accounts::create(&dir).await?;
}
Accounts::open(dir, writable).await
Accounts::open(dir).await
}
/// Creates a new default structure.
async fn create(dir: &Path) -> Result<()> {
pub async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
@@ -57,13 +54,13 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
pub async fn open(dir: PathBuf) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
let config = Config::from_file(config_file, writable)
let config = Config::from_file(config_file)
.await
.context("failed to load accounts config")?;
let events = Events::new();
@@ -301,20 +298,14 @@ impl Accounts {
/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";
/// Lockfile name.
const LOCKFILE_NAME: &str = "accounts.lock";
/// Database file name.
const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug)]
#[derive(Debug, Clone, PartialEq)]
struct Config {
file: PathBuf,
inner: InnerConfig,
// We lock the lockfile in the Config constructors to protect also from having multiple Config
// objects for the same config file.
lock_task: Option<JoinHandle<anyhow::Result<()>>>,
}
/// Account manager configuration file contents.
@@ -328,74 +319,17 @@ struct InnerConfig {
pub accounts: Vec<AccountConfig>,
}
impl Drop for Config {
fn drop(&mut self) {
if let Some(lock_task) = self.lock_task.take() {
lock_task.abort();
}
}
}
impl Config {
/// Creates a new Config for `file`, but doesn't open/sync it.
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
let dir = file.parent().context("Cannot get config file directory")?;
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
if !lock {
let cfg = Self {
file,
inner,
lock_task: None,
};
return Ok(cfg);
}
let lockfile = dir.join(LOCKFILE_NAME);
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
let (locked_tx, locked_rx) = oneshot::channel();
let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let mut timeout = Duration::from_millis(100);
let _guard = loop {
match lock.try_write() {
Ok(guard) => break Ok(guard),
Err(err) => {
if timeout.as_millis() > 1600 {
break Err(err);
}
// We need to wait for the previous lock_task to be aborted thus unlocking
// the lockfile. We don't open configs for writing often outside of the
// tests, so this adds delays to the tests, but otherwise ok.
sleep(timeout).await;
if err.kind() == std::io::ErrorKind::WouldBlock {
timeout *= 2;
}
}
}
}?;
locked_tx
.send(())
.ok()
.context("Cannot notify about lockfile locking")?;
let (_tx, rx) = oneshot::channel();
rx.await?;
Ok(())
});
let cfg = Self {
file,
inner,
lock_task: Some(lock_task),
};
locked_rx.await?;
Ok(cfg)
}
let file = dir.join(CONFIG_NAME);
let mut cfg = Self { file, inner };
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
let lock = true;
let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
cfg.sync().await?;
Ok(cfg)
@@ -405,11 +339,6 @@ impl Config {
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
ensure!(!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished());
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
.await
@@ -428,28 +357,24 @@ impl Config {
}
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
let dir = file
.parent()
.context("Cannot get config file directory")?
.to_path_buf();
let mut config = Self::new_nosync(file, writable).await?;
let bytes = fs::read(&config.file)
.await
.context("Failed to read file")?;
pub async fn from_file(file: PathBuf) -> Result<Self> {
let dir = file.parent().context("can't get config file directory")?;
let bytes = fs::read(&file).await.context("failed to read file")?;
let s = std::str::from_utf8(&bytes)?;
config.inner = toml::from_str(s).context("Failed to parse config")?;
let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
// Previous versions of the core stored absolute paths in account config.
// Convert them to relative paths.
let mut modified = false;
for account in &mut config.inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
for account in &mut inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
if modified && writable {
let mut config = Self { file, inner };
if modified {
config.sync().await?;
}
@@ -593,44 +518,26 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1");
{
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
accounts.add_account().await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
for writable in [true, false] {
let accounts = Accounts::new(p.clone(), writable).await.unwrap();
{
let accounts = Accounts::open(p).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_open_conflict() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
let writable = true;
assert!(Accounts::new(p.clone(), writable).await.is_err());
let writable = false;
let accounts = Accounts::new(p, writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -657,8 +564,7 @@ mod tests {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
let mut accounts = Accounts::new(p.clone()).await?;
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -679,8 +585,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -717,8 +622,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
let mut accounts = Accounts::new(p.clone()).await.unwrap();
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
@@ -738,8 +642,7 @@ mod tests {
let dummy_accounts = 10;
let (id0, id1, id2) = {
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
let mut accounts = Accounts::new(p.clone()).await?;
accounts.add_account().await?;
let ids = accounts.get_all();
assert_eq!(ids.len(), 1);
@@ -774,8 +677,7 @@ mod tests {
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
let writable = false;
let accounts = Accounts::new(p.clone(), writable).await?;
let accounts = Accounts::new(p.clone()).await?;
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
@@ -820,8 +722,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let accounts = Accounts::new(p.clone(), writable).await?;
let accounts = Accounts::new(p.clone()).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.len(), 0);
@@ -847,8 +748,7 @@ mod tests {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable)
let mut accounts = Accounts::new(p.clone())
.await
.context("failed to create accounts manager")?;
@@ -868,8 +768,7 @@ mod tests {
assert!(passphrase_set_success);
drop(accounts);
let writable = false;
let accounts = Accounts::new(p.clone(), writable)
let accounts = Accounts::new(p.clone())
.await
.context("failed to create second accounts manager")?;
let account = accounts
@@ -893,8 +792,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
let mut accounts = Accounts::new(p.clone()).await?;
accounts.add_account().await?;
accounts.add_account().await?;

View File

@@ -27,7 +27,6 @@ use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -36,7 +35,6 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::receive_imf::ReceivedMsg;
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
@@ -44,6 +42,7 @@ use crate::tools::{
strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -89,14 +88,6 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -113,10 +104,6 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -133,10 +120,6 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -289,7 +272,6 @@ impl ChatId {
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let smeared_time = create_smeared_timestamp(context);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -298,20 +280,13 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
smeared_time,
create_smeared_timestamp(context),
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
.await?;
}
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
@@ -359,7 +334,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Broadcast => {
Chattype::Undefined | Chattype::Broadcast => {
bail!("Can't block chat of type {:?}", chat.typ)
}
Chattype::Single => {
@@ -400,16 +375,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Single
if chat.blocked == Blocked::Not
&& chat.protected == ProtectionStatus::ProtectionBroken =>
{
// The protection was broken, then the user clicked 'Accept'/'OK',
// so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
@@ -436,19 +402,20 @@ impl ChatId {
/// Sets protection without sending a message.
///
/// Returns whether the protection status was actually modified.
/// Used when a message arrives indicating that someone else has
/// changed the protection value for a chat.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<bool> {
ensure!(!self.is_special(), "Invalid chat-id {self}.");
) -> Result<()> {
ensure!(!self.is_special(), "Invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(false);
return Ok(());
}
match protect {
@@ -463,8 +430,9 @@ impl ChatId {
}
}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
Chattype::Undefined => bail!("Undefined group type"),
},
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
ProtectionStatus::Unprotected => {}
};
context
@@ -477,58 +445,68 @@ impl ChatId {
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
Ok(true)
Ok(())
}
/// Adds an info message to the chat, telling the user that the protection status changed.
/// Send protected status message to the chat.
///
/// Params:
/// This sends the message with the protected status change to the chat,
/// notifying the user on this device as well as the other users in the chat.
///
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
/// * `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
/// If `promote` is false this means, the message must not be sent out
/// and only a local info message should be added to the chat.
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
contact_id: Option<ContactId>,
timestamp_sort: i64,
promote: bool,
from_id: ContactId,
) -> Result<()> {
let text = context.stock_protection_msg(protect, contact_id).await;
let text = context.stock_protection_msg(protect, from_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
if promote {
let mut msg = Message {
viewtype: Viewtype::Text,
text,
..Default::default()
};
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
create_smeared_timestamp(context),
None,
None,
None,
)
.await?;
}
Ok(())
}
/// Sets protection and sends or adds a message.
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
timestamp_sort: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
match self.inner_set_protection(context, protect).await {
Ok(protection_status_modified) => {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
}
Ok(())
}
Err(e) => {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
Err(e)
}
pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
ensure!(!self.is_special(), "set protection: invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
return Err(e);
}
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
.await
}
/// Archives or unarchives a chat.
@@ -765,6 +743,14 @@ impl ChatId {
}
}
let chat = Chat::load_from_db(context, self).await?;
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
bail!(
"Can't set a draft because chat is not writeable: {}",
cant_send_reason
);
}
// set back draft information to allow identifying the draft later on -
// no matter if message object is reused or reloaded from db
msg.state = MessageState::OutDraft;
@@ -1273,7 +1259,7 @@ pub struct Chat {
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
pub blocked: Blocked,
pub(crate) blocked: Blocked,
/// Additional chat parameters stored in the database.
pub param: Params,
@@ -1285,7 +1271,7 @@ pub struct Chat {
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
pub(crate) protected: ProtectionStatus,
protected: ProtectionStatus,
}
impl Chat {
@@ -1380,8 +1366,6 @@ impl Chat {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_protection_broken() {
Some(ProtectionBroken)
} else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
@@ -1406,6 +1390,7 @@ impl Chat {
match self.typ {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
Chattype::Undefined => Ok(false),
}
}
@@ -1544,27 +1529,6 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// The UI should let the user confirm that this is OK with a message like
/// `Bob sent a message from another device. Tap to learn more`
/// and then call `chat_id.accept()`.
pub fn is_protection_broken(&self) -> bool {
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
@@ -1595,6 +1559,15 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if let Some(reason) = self.why_cant_send(context).await? {
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
@@ -2115,30 +2088,19 @@ impl ChatIdBlocked {
_ => (),
}
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
});
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
.sql
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp, protected)
VALUES(?, ?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
smeared_time,
if protected {
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
},
create_smeared_timestamp(context),
),
)?;
let chat_id = ChatId::new(
@@ -2159,17 +2121,6 @@ impl ChatIdBlocked {
})
.await?;
if protected {
chat_id
.add_protection_msg(
context,
ProtectionStatus::Protected,
Some(contact_id),
smeared_time,
)
.await?;
}
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
@@ -2283,13 +2234,7 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification
} else {
bail!("cannot send to {chat_id}: {reason}");
}
bail!("cannot send to {}: {}", chat_id, reason);
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2391,7 +2336,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages against RTLO attacks
// protect all system messages againts RTLO attacks
if msg.is_system_message() {
msg.text = strip_rtlo_characters(&msg.text);
}
@@ -2513,13 +2458,14 @@ pub(crate) async fn create_send_msg_job(
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, last_added_location_id).await
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!(context, "Failed to set msg_location_id: {err:#}.");
}
@@ -3029,14 +2975,18 @@ pub async fn create_group_chat(
let grpid = create_id();
let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(Chattype::Group, chat_name, grpid, timestamp),
(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
),
)
.await?;
@@ -3048,9 +2998,9 @@ pub async fn create_group_chat(
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
chat_id
.set_protection(context, protect, timestamp, None)
.await?;
// this part is to stay compatible to verified groups,
// in some future, we will drop the "protect"-flag from create_group_chat()
chat_id.inner_set_protection(context, protect).await?;
}
Ok(chat_id)
@@ -5304,6 +5254,72 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;

View File

@@ -397,7 +397,7 @@ impl Chatlist {
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single => (Some(lastmsg), None),
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
}
}
} else {

View File

@@ -312,16 +312,6 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
}
impl Context {

View File

@@ -125,6 +125,7 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -140,6 +141,10 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// Undefined chat type.
#[default]
Undefined = 0,
/// 1:1 chat.
Single = 100,
@@ -218,6 +223,8 @@ mod tests {
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
assert_eq!(Chattype::Undefined, Chattype::default());
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());

View File

@@ -25,7 +25,7 @@ use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{load_self_public_key, DcKey};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
@@ -1009,7 +1009,7 @@ impl Contact {
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{stock_message}.\n{finger_prints}:");
let fingerprint_self = load_self_public_key(context)
let fingerprint_self = SignedPublicKey::load_self(context)
.await?
.fingerprint()
.to_string();
@@ -1211,15 +1211,31 @@ impl Contact {
/// and if the key has not changed since this verification.
///
/// The UI may draw a checkbox or something like that beside verified contacts.
///
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
self.is_verified_ex(context, None).await
}
/// Same as `Contact::is_verified` but allows speeding up things
/// by adding the peerstate belonging to the contact.
/// If you do not have the peerstate available, it is loaded automatically.
pub async fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate>,
) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
return Ok(VerifiedStatus::BidirectVerified);
}
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.is_using_verified_key() {
if let Some(peerstate) = peerstate {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
}

View File

@@ -19,7 +19,7 @@ use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{load_self_public_key, DcKey as _};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
@@ -586,7 +586,7 @@ impl Context {
.sql
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match load_self_public_key(self).await {
let fingerprint_str = match SignedPublicKey::load_self(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -761,12 +761,6 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));

View File

@@ -1,12 +1,14 @@
//! Forward log messages to logging webxdc
use crate::chat::ChatId;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{Message, MsgId, Viewtype};
use crate::param::Param;
use crate::tools::time;
use crate::webxdc::StatusUpdateItem;
use crate::{
chat::ChatId,
config::Config,
context::Context,
message::{Message, MsgId, Viewtype},
param::Param,
tools::time,
webxdc::StatusUpdateItem,
EventType,
};
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;

View File

@@ -13,6 +13,7 @@ use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -25,8 +26,8 @@ use crate::pgp;
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match {
let mime = get_autocrypt_mime(mail);
@@ -226,8 +227,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
@@ -262,7 +263,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
pub(crate) fn validate_detached_signature<'a, 'b>(
mail: &'a ParsedMail<'b>,
public_keyring_for_validate: &[SignedPublicKey],
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
if mail.ctype.mimetype != "multipart/signed" {
return None;
@@ -282,13 +283,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
let mut public_keyring_for_validate = Vec::new();
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
if let Some(peerstate) = peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.push(key.clone());
public_keyring_for_validate.add(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.push(key.clone());
public_keyring_for_validate.add(key.clone());
}
}
public_keyring_for_validate

View File

@@ -6,7 +6,8 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -23,7 +24,7 @@ impl EncryptHelper {
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = context.get_primary_self_addr().await?;
let public_key = load_self_public_key(context).await?;
let public_key = SignedPublicKey::load_self(context).await?;
Ok(EncryptHelper {
prefer_encrypt,
@@ -103,7 +104,7 @@ impl EncryptHelper {
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
let mut keyring: Vec<SignedPublicKey> = Vec::new();
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
for (peerstate, addr) in peerstates
.into_iter()
@@ -112,10 +113,10 @@ impl EncryptHelper {
let key = peerstate
.take_key(min_verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.push(key);
keyring.add(key);
}
keyring.push(self.public_key.clone());
let sign_key = load_self_secret_key(context).await?;
keyring.add(self.public_key.clone());
let sign_key = SignedSecretKey::load_self(context).await?;
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -131,7 +132,7 @@ impl EncryptHelper {
context: &Context,
mail: lettre_email::PartBuilder,
) -> Result<(lettre_email::MimeMessage, String)> {
let sign_key = load_self_secret_key(context).await?;
let sign_key = SignedSecretKey::load_self(context).await?;
let mime_message = mail.build();
let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
Ok((mime_message, signature))
@@ -144,17 +145,20 @@ impl EncryptHelper {
/// sent but in a few locations there are no such guarantees,
/// e.g. when exporting keys, and calling this function ensures a
/// private key will be present.
///
/// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
load_self_public_key(context).await?;
Ok(())
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_primary_self_addr().await?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::key::DcKey;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::test_utils::{bob_keypair, TestContext};
@@ -165,7 +169,10 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert!(ensure_secret_key_exists(&t).await.is_ok());
assert_eq!(
ensure_secret_key_exists(&t).await.unwrap(),
"alice@example.org"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp)
.chain(delete_device_after_timestamp.into_iter())
.min()
}
@@ -986,7 +986,7 @@ mod tests {
t.send_text(self_chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(self_chat.id).await;
msg.id.trash(&t).await?;
msg.id.delete_from_db(&t).await?;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
self_chat
@@ -1003,7 +1003,7 @@ mod tests {
.await
.unwrap();
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
// Set DeleteDeviceAfter to 1800s. Thend send a saved message which will
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
.await?;
@@ -1260,8 +1260,8 @@ mod tests {
);
let msg = alice.get_last_msg().await;
// Message is deleted when its timer expires.
msg.id.trash(&alice).await?;
// Message is deleted from the database when its timer expires.
msg.id.delete_from_db(&alice).await?;
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
// <second@example.com>, is received. The message <second@example.come> is not in the

View File

@@ -17,12 +17,12 @@ use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{self, Message, MsgId};
use crate::message::{Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
impl Message {
/// Check if the message can be retrieved as HTML.

View File

@@ -19,9 +19,7 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{
self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey,
};
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
@@ -188,7 +186,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
} else {
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = load_self_secret_key(context).await?;
let private_key = SignedSecretKey::load_self(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
@@ -783,11 +781,6 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");
conn.execute(
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
[],
)
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
@@ -918,73 +911,55 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_backup() -> Result<()> {
for set_verified_oneonone_chats in [true, false] {
let backup_dir = tempfile::tempdir().unwrap();
let backup_dir = tempfile::tempdir().unwrap();
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
if set_verified_oneonone_chats {
context1
.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await?;
}
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
.await
.is_err());
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
.await
.is_err());
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
assert_eq!(
context2
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
false
);
assert_eq!(
context1
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
set_verified_oneonone_chats
);
}
Ok(())
}

View File

@@ -3,9 +3,11 @@
use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use std::pin::Pin;
use anyhow::{ensure, Context as _, Result};
use base64::Engine as _;
use futures::Future;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
@@ -16,7 +18,8 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::pgp::KeyPair;
// Re-export key types
pub use crate::pgp::KeyPair;
use crate::tools::{time, EmailAddress};
/// Convenience trait for working with keys.
@@ -24,7 +27,7 @@ use crate::tools::{time, EmailAddress};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
@@ -47,6 +50,11 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
Self::from_armor_single(Cursor::new(bytes)).context("rPGP error")
}
/// Load the users' default key from the database.
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>>;
/// Serialise the key as bytes.
fn to_bytes(&self) -> Vec<u8> {
// Not using Serialize::to_bytes() to make clear *why* it is
@@ -77,55 +85,38 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
}
}
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
match context
.sql
.query_row_optional(
r#"SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => SignedPublicKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
}
}
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
match context
.sql
.query_row_optional(
r#"SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => SignedSecretKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
}
}
impl DcKey for SignedPublicKey {
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
let addr = context.get_primary_self_addr().await?;
match context
.sql
.query_row_optional(
r#"
SELECT public_key
FROM keypairs
WHERE addr=?
AND is_default=1;
"#,
(addr,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => Self::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
}
})
}
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to ignore this error.
@@ -144,6 +135,36 @@ impl DcKey for SignedPublicKey {
}
impl DcKey for SignedSecretKey {
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
match context
.sql
.query_row_optional(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => Self::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
}
})
}
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to do these unwraps.
@@ -164,7 +185,7 @@ impl DcKey for SignedSecretKey {
/// Deltachat extension trait for secret keys.
///
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
pub(crate) trait DcSecretKey {
pub trait DcSecretKey {
/// Create a public key from a private one.
fn split_public_key(&self) -> Result<SignedPublicKey>;
}
@@ -307,29 +328,6 @@ pub async fn store_self_keypair(
Ok(())
}
/// Saves a keypair as the default keys.
///
/// This API is used for testing purposes
/// to avoid generating the key in tests.
/// Use import/export APIs instead.
pub async fn preconfigure_keypair(
context: &Context,
addr: &str,
public_data: &str,
secret_data: &str,
) -> Result<()> {
let addr = EmailAddress::new(addr)?;
let public = SignedPublicKey::from_asc(public_data)?.0;
let secret = SignedSecretKey::from_asc(secret_data)?.0;
let keypair = KeyPair {
addr,
public,
secret,
};
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
Ok(())
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Fingerprint(Vec<u8>);
@@ -524,9 +522,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = TestContext::new_alice().await;
let pubkey = load_self_public_key(&t).await.unwrap();
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = load_self_secret_key(&t).await.unwrap();
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
assert_eq!(alice.secret, seckey);
}
@@ -536,7 +534,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = load_self_public_key(&t).await;
let key = SignedPublicKey::load_self(&t).await;
assert!(key.is_ok());
}
@@ -546,7 +544,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = load_self_secret_key(&t).await;
let key = SignedSecretKey::load_self(&t).await;
assert!(key.is_ok());
}
@@ -563,7 +561,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(load_self_public_key(&ctx))
.block_on(SignedPublicKey::load_self(&ctx))
})
};
let thr1 = {
@@ -571,7 +569,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(load_self_public_key(&ctx))
.block_on(SignedPublicKey::load_self(&ctx))
})
};
let res0 = thr0.join().unwrap();

87
src/keyring.rs Normal file
View File

@@ -0,0 +1,87 @@
//! Keyring to perform rpgp operations with.
use anyhow::Result;
use crate::context::Context;
use crate::key::DcKey;
/// An in-memory keyring.
///
/// Instances are usually constructed just for the rpgp operation and
/// short-lived.
#[derive(Clone, Debug, Default)]
pub struct Keyring<T>
where
T: DcKey,
{
keys: Vec<T>,
}
impl<T> Keyring<T>
where
T: DcKey,
{
/// New empty keyring.
pub fn new() -> Keyring<T> {
Keyring { keys: Vec::new() }
}
/// Create a new keyring with the the user's secret key loaded.
pub async fn new_self(context: &Context) -> Result<Keyring<T>> {
let mut keyring: Keyring<T> = Keyring::new();
keyring.load_self(context).await?;
Ok(keyring)
}
/// Load the user's key into the keyring.
pub async fn load_self(&mut self, context: &Context) -> Result<()> {
self.add(T::load_self(context).await?);
Ok(())
}
/// Add a key to the keyring.
pub fn add(&mut self, key: T) {
self.keys.push(key);
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
/// A vector with reference to all the keys in the keyring.
pub fn keys(&self) -> &[T] {
&self.keys
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key::{SignedPublicKey, SignedSecretKey};
use crate::test_utils::{alice_keypair, TestContext};
#[test]
fn test_keyring_add_keys() {
let alice = alice_keypair();
let mut pub_ring: Keyring<SignedPublicKey> = Keyring::new();
pub_ring.add(alice.public.clone());
assert_eq!(pub_ring.keys(), [alice.public]);
let mut sec_ring: Keyring<SignedSecretKey> = Keyring::new();
sec_ring.add(alice.secret.clone());
assert_eq!(sec_ring.keys(), [alice.secret]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_keyring_load_self() {
// new_self() implies load_self()
let t = TestContext::new_alice().await;
let alice = alice_keypair();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(pub_ring.keys(), [alice.public]);
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(sec_ring.keys(), [alice.secret]);
}
}

View File

@@ -8,6 +8,7 @@
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
@@ -16,7 +17,6 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
@@ -70,6 +70,7 @@ mod scheduler;
#[macro_use]
mod job;
pub mod key;
mod keyring;
pub mod location;
mod login_param;
pub mod message;

View File

@@ -735,7 +735,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_send_until - now))
.chain(u64::try_from(locations_send_until - now).into_iter())
.min();
if has_locations {
@@ -759,7 +759,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
);
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_last_sent + 61 - now))
.chain(u64::try_from(locations_last_sent + 61 - now).into_iter())
.min();
}
} else {

View File

@@ -5,10 +5,9 @@ use std::fmt;
use anyhow::{ensure, Result};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::provider::Socket;
use crate::provider::{get_provider_by_id, Provider};
use crate::socks::Socks5Config;
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[repr(u32)]

View File

@@ -114,16 +114,24 @@ WHERE id=?;
}
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
pub async fn delete_from_db(self, context: &Context) -> Result<()> {
// We don't use transactions yet, so remove MDNs first to make
// sure they are not left while the message is deleted.
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?;
Ok(())
})
.execute("DELETE FROM smtp WHERE msg_id=?", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs_status_updates WHERE msg_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE id=?;", (self,))
.await?;
Ok(())
}
@@ -764,7 +772,7 @@ impl Message {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single => None,
Chattype::Single | Chattype::Undefined => None,
}
} else {
None

View File

@@ -66,7 +66,7 @@ pub struct MimeFactory<'a> {
in_reply_to: String,
references: String,
req_mdn: bool,
last_added_location_id: Option<u32>,
last_added_location_id: u32,
/// If the created mime-structure contains sync-items,
/// the IDs of these items are listed here.
@@ -85,7 +85,7 @@ pub struct RenderedEmail {
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub last_added_location_id: Option<u32>,
pub last_added_location_id: u32,
/// A comma-separated string of sync-IDs that are used by the rendered email
/// and must be deleted once the message is actually queued for sending
@@ -223,7 +223,7 @@ impl<'a> MimeFactory<'a> {
in_reply_to,
references,
req_mdn,
last_added_location_id: None,
last_added_location_id: 0,
sync_ids_to_delete: None,
attach_selfavatar,
};
@@ -264,7 +264,7 @@ impl<'a> MimeFactory<'a> {
in_reply_to: String::default(),
references: String::default(),
req_mdn: false,
last_added_location_id: None,
last_added_location_id: 0,
sync_ids_to_delete: None,
attach_selfavatar: false,
};
@@ -876,7 +876,7 @@ impl<'a> MimeFactory<'a> {
.body(kml_content);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = Some(last_added_location_id);
self.last_added_location_id = last_added_location_id;
}
Ok(part)
}
@@ -896,16 +896,7 @@ impl<'a> MimeFactory<'a> {
let mut placeholdertext = None;
let mut meta_part = None;
let send_verified_headers = match chat.typ {
// In single chats, the protection status isn't necessarily the same for both sides,
// so we don't send the Chat-Verified header:
Chattype::Single => false,
Chattype::Group => true,
// Mailinglists and broadcast lists can actually never be verified:
Chattype::Mailinglist => false,
Chattype::Broadcast => false,
};
if chat.is_protected() && send_verified_headers {
if chat.is_protected() {
headers
.protected
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -1963,7 +1954,7 @@ mod tests {
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
if delete_original_msg {
incoming_msg.id.trash(&t).await.unwrap();
incoming_msg.id.delete_from_db(&t).await.unwrap();
}
if message_arrives_inbetween {

View File

@@ -27,7 +27,8 @@ use crate::decrypt::{
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{load_self_secret_key, DcKey, Fingerprint, SignedPublicKey};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::message::{
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
};
@@ -277,10 +278,9 @@ impl MimeMessage {
headers.remove("chat-verified");
let from = from.context("No from in message")?;
let private_keyring = vec![load_self_secret_key(context)
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
.await
.context("Failed to get own key")?];
.context("failed to get own keyring")?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, message_time).await?;
@@ -1899,7 +1899,7 @@ fn get_mime_type(
} else {
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separately.
// be handled separatedly.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
@@ -2198,7 +2198,7 @@ async fn ndn_maybe_add_info_msg(
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single => {}
Chattype::Single | Chattype::Undefined => {}
}
Ok(())
}

View File

@@ -392,31 +392,6 @@ impl Peerstate {
}
}
/// Returns a reference to the contact's public key fingerprint.
///
/// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key_fingerprint
.as_ref()
.or(self.gossip_key_fingerprint.as_ref()),
}
}
/// Returns true if the key used for opportunistic encryption in the 1:1 chat
/// is the same as the verified key.
///
/// Note that verified groups always use the verified key no matter if the
/// opportunistic key matches or not.
pub(crate) fn is_using_verified_key(&self) -> bool {
let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified);
verified.is_some()
&& verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified)
}
/// Set this peerstate to verified
/// Make sure to call `self.save_to_db` to save these changes
/// Params:

View File

@@ -20,6 +20,7 @@ use tokio::runtime::Handle;
use crate::constants::KeyGenType;
use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring;
use crate::tools::EmailAddress;
#[allow(missing_docs)]
@@ -236,7 +237,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSu
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Vec<SignedPublicKey>,
public_keys_for_encryption: Keyring<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
@@ -244,6 +245,7 @@ pub async fn pk_encrypt(
Handle::current()
.spawn_blocking(move || {
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
.iter()
.filter_map(select_pk_for_encryption)
.collect();
@@ -296,15 +298,15 @@ pub fn pk_calc_signature(
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
private_keys_for_decryption: &Keyring<SignedSecretKey>,
public_keys_for_validation: &Keyring<SignedPublicKey>,
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect();
let (decryptor, _) = msg.decrypt(|| "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
@@ -319,13 +321,20 @@ pub fn pk_decrypt(
None => bail!("The decrypted message is empty"),
};
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
ret_signature_fingerprints.insert(fp);
if !public_keys_for_validation.is_empty() {
let pkeys = public_keys_for_validation.keys();
let mut fingerprints: Vec<Fingerprint> = Vec::new();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in pkeys {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
}
}
ret_signature_fingerprints.extend(fingerprints);
}
Ok((content, ret_signature_fingerprints))
} else {
@@ -337,11 +346,12 @@ pub fn pk_decrypt(
pub fn pk_validate(
content: &[u8],
signature: &[u8],
public_keys_for_validation: &[SignedPublicKey],
public_keys_for_validation: &Keyring<SignedPublicKey>,
) -> Result<HashSet<Fingerprint>> {
let mut ret: HashSet<Fingerprint> = Default::default();
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
let pkeys = public_keys_for_validation.keys();
// Remove trailing CRLF before the delimiter.
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
@@ -350,7 +360,7 @@ pub fn pk_validate(
.get(..content.len().saturating_sub(2))
.context("index is out of range")?;
for pkey in public_keys_for_validation {
for pkey in pkeys {
if standalone_signature.verify(pkey, content).is_ok() {
let fp = DcKey::fingerprint(pkey);
ret.insert(fp);
@@ -485,7 +495,9 @@ mod tests {
async fn ctext_signed() -> &'static String {
CTEXT_SIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
@@ -498,7 +510,9 @@ mod tests {
async fn ctext_unsigned() -> &'static String {
CTEXT_UNSIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
@@ -523,8 +537,10 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_singed() {
// Check decrypting as Alice
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let mut decrypt_keyring: Keyring<SignedSecretKey> = Keyring::new();
decrypt_keyring.add(KEYS.alice_secret.clone());
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -535,8 +551,10 @@ mod tests {
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -549,9 +567,15 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let keyring = vec![KEYS.alice_secret.clone()];
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&keyring,
&empty_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -559,8 +583,10 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.bob_public.clone());
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -573,11 +599,13 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&[],
&sig_check_keyring,
)
.unwrap();
assert_eq!(plain, CLEARTEXT);

View File

@@ -171,7 +171,7 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
let resolver = AsyncResolver::tokio(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
);
)?;
Ok(resolver)
}

View File

@@ -17,12 +17,11 @@ use crate::contact::{
addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::socks::Socks5Config;
use crate::token;
use crate::{token, EventType};
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";

View File

@@ -4,15 +4,17 @@ use anyhow::Result;
use base64::Engine as _;
use qrcodegen::{QrCode, QrCodeEcc};
use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId};
use crate::color::color_int_to_hex_string;
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::qr::{self, Qr};
use crate::securejoin;
use crate::stock_str::{self, backup_transfer_qr};
use crate::{
blob::BlobObject,
chat::{Chat, ChatId},
color::color_int_to_hex_string,
config::Config,
contact::{Contact, ContactId},
context::Context,
qr::{self, Qr},
securejoin,
stock_str::{self, backup_transfer_qr},
};
/// Returns SVG of the QR code to join the group or verify contact.
///

View File

@@ -4,7 +4,7 @@ use std::cmp::min;
use std::collections::HashSet;
use std::convert::TryFrom;
use anyhow::{Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use mailparse::{parse_mail, SingleInfo};
use num_traits::FromPrimitive;
use once_cell::sync::Lazy;
@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, VerifiedStatus,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -516,10 +516,6 @@ async fn add_parts(
securejoin_seen = true;
}
}
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate =
Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
securejoin_seen = false;
}
@@ -609,17 +605,12 @@ async fn add_parts(
// In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat
// but the From-address is not a member of this chat.
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if let Some(chat_id) = chat_id {
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_protected() {
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
}
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.repl_msg_by_error(&s);
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -635,7 +626,7 @@ async fn add_parts(
context,
mime_parser,
sent_timestamp,
group_chat_id,
chat_id,
from_id,
to_ids,
)
@@ -738,51 +729,6 @@ async fn add_parts(
);
}
}
// Check if the message was sent with verified encryption and set the protection of
// the 1:1 chat accordingly.
let chat = match is_partial_download.is_none()
&& mime_parser.get_header(HeaderDef::SecureJoin).is_none()
&& !is_mdn
{
true => Some(Chat::load_from_db(context, chat_id).await?)
.filter(|chat| chat.typ == Chattype::Single),
false => None,
};
if let Some(chat) = chat {
let mut new_protection = match has_verified_encryption(
context,
mime_parser,
from_id,
to_ids,
Chattype::Single,
)
.await?
{
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
if chat.protected != ProtectionStatus::Unprotected
&& new_protection == ProtectionStatus::Unprotected
// `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`.
// That's why the config is checked here, and not above.
&& context.get_config_bool(Config::VerifiedOneOnOneChats).await?
{
new_protection = ProtectionStatus::ProtectionBroken;
}
if chat.protected != new_protection {
// The message itself will be sorted under the device message since the device
// message is `MessageState::InNoticed`, which means that all following
// messages are sorted under it.
let sort_timestamp =
calc_sort_timestamp(context, sent_timestamp, chat_id, true, incoming)
.await?;
chat_id
.set_protection(context, new_protection, sort_timestamp, Some(from_id))
.await?;
}
}
}
}
@@ -970,8 +916,7 @@ async fn add_parts(
};
let in_fresh = state == MessageState::InFresh;
let sort_timestamp =
calc_sort_timestamp(context, sent_timestamp, chat_id, false, incoming).await?;
let sort_timestamp = calc_sort_timestamp(context, sent_timestamp, chat_id, in_fresh).await?;
// Apply ephemeral timer changes to the chat.
//
@@ -1050,14 +995,42 @@ async fn add_parts(
// if a chat is protected and the message is fully downloaded, check additional properties
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
let new_status = match mime_parser.is_system_message {
SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected),
SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected),
_ => None,
};
if chat.is_protected() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
if chat.is_protected() || new_status.is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
} else {
// change chat protection only when verification check passes
if let Some(new_status) = new_status {
if chat_id
.update_timestamp(
context,
Param::ProtectionSettingsTimestamp,
sent_timestamp,
)
.await?
{
if let Err(e) = chat_id.inner_set_protection(context, new_status).await {
chat::add_info_msg(
context,
chat_id,
&format!("Cannot set protection: {e}"),
sort_timestamp,
)
.await?;
// do not return an error as this would result in retrying the message
}
}
better_msg = Some(context.stock_protection_msg(new_status, from_id).await);
}
}
}
}
@@ -1294,7 +1267,7 @@ RETURNING id
if let Some(replace_msg_id) = replace_msg_id {
// "Replace" placeholder with a message that has no parts.
replace_msg_id.trash(context).await?;
replace_msg_id.delete_from_db(context).await?;
}
chat_id.unarchive_if_not_muted(context, state).await?;
@@ -1401,43 +1374,25 @@ async fn calc_sort_timestamp(
context: &Context,
message_timestamp: i64,
chat_id: ChatId,
always_sort_to_bottom: bool,
incoming: bool,
is_fresh_msg: bool,
) -> Result<i64> {
let mut sort_timestamp = message_timestamp;
let last_msg_time: Option<i64> = if always_sort_to_bottom {
// get newest message for this chat
context
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
(chat_id,),
)
.await?
} else if incoming {
// get newest non fresh message for this chat.
// If a user hasn't been online for some time, the Inbox is fetched first and then the
// Sentbox. In order for Inbox and Sent messages to be allowed to mingle, outgoing messages
// are purely sorted by their sent timestamp. NB: The Inbox must be fetched first otherwise
// Inbox messages would be always below old Sentbox messages. We could take in the query
// below only incoming messages, but then new incoming messages would mingle with just sent
// outgoing ones and apear somewhere in the middle of the chat.
context
// get newest non fresh message for this chat
// update sort_timestamp if less than that
if is_fresh_msg {
let last_msg_time: Option<i64> = context
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
(chat_id, MessageState::InFresh),
)
.await?
} else {
None
};
.await?;
if let Some(last_msg_time) = last_msg_time {
if last_msg_time > sort_timestamp {
sort_timestamp = last_msg_time;
if let Some(last_msg_time) = last_msg_time {
if last_msg_time > sort_timestamp {
sort_timestamp = last_msg_time;
}
}
}
@@ -1592,9 +1547,7 @@ async fn create_or_lookup_group(
}
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await?
{
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
@@ -1754,22 +1707,6 @@ async fn apply_group_changes(
allow_member_list_changes
};
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
{
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
}
}
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
@@ -1858,6 +1795,20 @@ async fn apply_group_changes(
}
}
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.repl_msg_by_error(&s);
}
if !chat.is_protected() {
chat_id
.inner_set_protection(context, ProtectionStatus::Protected)
.await?;
}
}
// Recreate the member list.
if recreate_member_list {
// Only delete old contacts if the sender is not a classical MUA user:
@@ -2186,55 +2137,49 @@ async fn create_adhoc_group(
Ok(Some(new_chat_id))
}
enum VerifiedEncryption {
Verified,
NotVerified(String), // The string contains the reason why it's not verified
}
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted, signed with a verified key,
/// and if it's a group, all the recipients are verified.
async fn has_verified_encryption(
async fn check_verified_properties(
context: &Context,
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
chat_type: Chattype,
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
) -> Result<()> {
let contact = Contact::get_by_id(context, from_id).await?;
if from_id == ContactId::SELF && chat_type == Chattype::Single {
// For outgoing emails in the 1:1 chat, we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
return Ok(Verified);
ensure!(mimeparser.was_encrypted(), "This message is not encrypted");
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
// we do not fail here currently, this would exclude (a) non-deltas
// and (b) deltas with different protection views across multiple devices.
// for group creation or protection enabled/disabled, however, Chat-Verified is respected.
warn!(
context,
"{} did not mark message as protected.",
contact.get_addr()
);
}
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
};
// ensure, the contact is verified
// and the message is signed with a verified key of the sender.
// this check is skipped for SELF as there is no proper SELF-peerstate
// and results in group-splits otherwise.
if from_id != ContactId::SELF {
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
return Ok(NotVerified(
"No peerstate, the contact isn't verified".to_string(),
));
};
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
if !peerstate.has_verified_key(&mimeparser.signatures) {
return Ok(NotVerified(
"The message was sent with non-verified encryption".to_string(),
));
if peerstate.is_none()
|| contact.is_verified_ex(context, peerstate.as_ref()).await?
!= VerifiedStatus::BidirectVerified
{
bail!(
"Sender of this message is not verified: {}",
contact.get_addr()
);
}
if let Some(peerstate) = peerstate {
ensure!(
peerstate.has_verified_key(&mimeparser.signatures),
"The message was sent with non-verified encryption"
);
}
}
@@ -2246,7 +2191,7 @@ async fn has_verified_encryption(
.collect::<Vec<ContactId>>();
if to_ids.is_empty() {
return Ok(Verified);
return Ok(());
}
let rows = context
@@ -2270,12 +2215,10 @@ async fn has_verified_encryption(
)
.await?;
let contact = Contact::get_by_id(context, from_id).await?;
for (to_addr, mut is_verified) in rows {
info!(
context,
"has_verified_encryption: {:?} self={:?}.",
"check_verified_properties: {:?} self={:?}.",
to_addr,
context.is_self_addr(&to_addr).await
);
@@ -2309,13 +2252,13 @@ async fn has_verified_encryption(
}
}
if !is_verified {
return Ok(NotVerified(format!(
bail!(
"{} is not a member of this protected chat",
to_addr
)));
to_addr.to_string()
);
}
}
Ok(Verified)
Ok(())
}
/// Returns the last message referenced from `References` header if it is in the database.

View File

@@ -2944,7 +2944,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
let received = alice2.get_last_msg().await;
// That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949:
assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id);
assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id);
let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await;
assert_eq!(received.from_id, ContactId::SELF);

View File

@@ -864,7 +864,7 @@ impl Scheduler {
// Actually shutdown tasks.
let timeout_duration = std::time::Duration::from_secs(30);
for b in once(self.inbox).chain(self.oboxes) {
for b in once(self.inbox).chain(self.oboxes.into_iter()) {
tokio::time::timeout(timeout_duration, b.handle)
.await
.log_err(context)

View File

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

View File

@@ -6,15 +6,15 @@ use anyhow::{bail, Context as _, Error, Result};
use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
use crate::aheader::EncryptPreference;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::constants::Blocked;
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
use crate::context::Context;
use crate::e2ee::ensure_secret_key_exists;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{load_self_public_key, DcKey, Fingerprint};
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -130,7 +130,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
}
async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
match load_self_public_key(context).await {
match SignedPublicKey::load_self(context).await {
Ok(key) => Some(key.fingerprint()),
Err(_) => {
warn!(context, "get_self_fingerprint(): failed to load key");
@@ -701,22 +701,6 @@ async fn secure_connection_established(
let contact = Contact::get_by_id(context, contact_id).await?;
let msg = stock_str::contact_verified(context, &contact).await;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
if context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
{
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ == Chattype::Single {
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact_id),
)
.await?;
}
}
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}
@@ -794,12 +778,11 @@ mod tests {
use crate::chat;
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str::chat_protection_enabled;
use crate::test_utils::get_chat_msg;
use crate::test_utils::{TestContext, TestContextManager};
use crate::tools::EmailAddress;
@@ -808,14 +791,6 @@ mod tests {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
alice
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
bob.set_config(Config::VerifiedOneOnOneChats, Some("1"))
.await
.unwrap();
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
.await
@@ -899,7 +874,10 @@ mod tests {
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await.unwrap().fingerprint();
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
.await
.unwrap()
.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -943,7 +921,7 @@ mod tests {
// Check Alice got the verified message in her 1:1 chat.
{
let chat = alice.create_chat(&bob).await;
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -951,17 +929,11 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.collect();
assert_eq!(msg_ids.len(), 2);
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("bob@example.net verified"));
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&alice).await;
assert_eq!(msg1.get_text(), expected_text);
.max()
.expect("No messages in Alice's 1:1 chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
}
// Check Alice sent the right message to Bob.
@@ -997,7 +969,7 @@ mod tests {
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id())
.await
.unwrap()
.into_iter()
@@ -1005,16 +977,11 @@ mod tests {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.collect();
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
assert!(msg0.is_info());
assert!(msg0.get_text().contains("alice@example.org verified"));
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
assert!(msg1.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg1.get_text(), expected_text);
.max()
.expect("No messages in Bob's 1:1 chat");
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("alice@example.org verified"));
}
// Check Bob sent the final message
@@ -1041,7 +1008,7 @@ mod tests {
let bob = tcm.bob().await;
// Ensure Bob knows Alice_FP
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
let peerstate = Peerstate {
addr: "alice@example.org".into(),
last_seen: 10,
@@ -1095,7 +1062,7 @@ mod tests {
"vc-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1266,7 +1233,7 @@ mod tests {
"vg-request-with-auth"
);
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
assert_eq!(
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
bob_fp.hex()
@@ -1302,17 +1269,26 @@ mod tests {
// Now Alice's chat with Bob should still be hidden, the verified message should
// appear in the group chat.
let chat = alice.get_chat(&bob).await;
let chat = alice
.get_chat(&bob)
.await
.expect("Alice has no 1:1 chat with bob");
assert_eq!(
chat.blocked,
Blocked::Yes,
"Alice's 1:1 chat with Bob is not hidden"
);
// There should be 3 messages in the chat:
// - The ChatProtectionEnabled message
// - bob@example.net verified
// - You added member bob@example.net
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid)
.await
.unwrap()
.into_iter()
.filter_map(|item| match item {
chat::ChatItem::Message { msg_id } => Some(msg_id),
_ => None,
})
.min()
.expect("No messages in Alice's group chat");
let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap();
assert!(msg.is_info());
assert!(msg.get_text().contains("bob@example.net verified"));
}
@@ -1337,7 +1313,10 @@ mod tests {
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
let chat = bob.get_chat(&alice).await;
let chat = bob
.get_chat(&alice)
.await
.expect("Bob has no 1:1 chat with Alice");
assert_eq!(
chat.blocked,
Blocked::Yes,

View File

@@ -9,7 +9,6 @@ use super::bobstate::{BobHandshakeStage, BobState};
use super::qrinvite::QrInvite;
use super::HandshakeMessage;
use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::contact::Contact;
use crate::context::Context;
@@ -223,22 +222,6 @@ impl BobState {
let msg = stock_str::contact_verified(context, &contact).await;
let chat_id = self.joining_chat_id(context).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
if context
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
&& chat_id == self.alice_chat()
{
chat_id
.set_protection(
context,
ProtectionStatus::Protected,
time(),
Some(contact.id),
)
.await?;
}
context.emit_event(EventType::ChatModified(chat_id));
Ok(())
}

View File

@@ -17,7 +17,7 @@ use crate::contact::{Contact, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{load_self_public_key, DcKey};
use crate::key::{DcKey, SignedPublicKey};
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -448,7 +448,7 @@ async fn send_handshake_message(
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
let bob_fp = load_self_public_key(context).await?.fingerprint();
let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint();
msg.param.set(Param::Arg3, bob_fp.hex());
// Sends the grpid in the Secure-Join-Group header.

View File

@@ -12,7 +12,6 @@ use tokio::task;
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::Message;
@@ -23,9 +22,9 @@ use crate::net::session::SessionBufStream;
use crate::net::tls::wrap_tls;
use crate::oauth2::get_oauth2_access_token;
use crate::provider::Socket;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
/// SMTP write and read timeout.
const SMTP_TIMEOUT: Duration = Duration::from_secs(30);

View File

@@ -393,6 +393,18 @@ pub enum StockMessage {
#[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
MsgEphemeralTimerWeeksBy = 157,
#[strum(props(fallback = "You enabled chat protection."))]
YouEnabledProtection = 158,
#[strum(props(fallback = "Chat protection enabled by %1$s."))]
ProtectionEnabledBy = 159,
#[strum(props(fallback = "You disabled chat protection."))]
YouDisabledProtection = 160,
#[strum(props(fallback = "Chat protection disabled by %1$s."))]
ProtectionDisabledBy = 161,
#[strum(props(fallback = "Scan to set up second device for %1$s"))]
BackupTransferQr = 162,
@@ -407,12 +419,6 @@ pub enum StockMessage {
#[strum(props(fallback = "I left the group."))]
MsgILeftGroup = 166,
#[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
ChatProtectionEnabled = 170,
#[strum(props(fallback = "%1$s sent a message from another device."))]
ChatProtectionDisabled = 171,
}
impl StockMessage {
@@ -509,19 +515,11 @@ trait StockStringMods: AsRef<str> + Sized {
}
impl ContactId {
/// Get contact name and address for stock string, e.g. `Bob (bob@example.net)`
async fn get_stock_name_n_addr(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
/// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set.
/// Get contact name for stock string.
async fn get_stock_name(self, context: &Context) -> String {
Contact::get_by_id(context, self)
.await
.map(|contact| contact.get_display_name().to_string())
.map(|contact| contact.get_name_n_addr())
.unwrap_or_else(|_| self.to_string())
}
}
@@ -585,7 +583,7 @@ pub(crate) async fn msg_grp_name(
.await
.replace1(from_group)
.replace2(to_group)
.replace3(&by_contact.get_stock_name_n_addr(context).await)
.replace3(&by_contact.get_stock_name(context).await)
}
}
@@ -595,7 +593,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgChangedBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -642,7 +640,7 @@ pub(crate) async fn msg_add_member_local(
translated(context, StockMessage::MsgAddMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -689,7 +687,7 @@ pub(crate) async fn msg_del_member_local(
translated(context, StockMessage::MsgDelMemberBy)
.await
.replace1(whom)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -705,7 +703,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
} else {
translated(context, StockMessage::MsgGroupLeftBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -758,7 +756,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId
} else {
translated(context, StockMessage::MsgGrpImgDeletedBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -784,9 +782,13 @@ pub(crate) async fn secure_join_started(
/// Stock string: `%1$s replied, waiting for being added to the group…`.
pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(&contact_id.get_stock_name(context).await)
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
translated(context, StockMessage::SecureJoinReplies)
.await
.replace1(contact.get_display_name())
} else {
format!("secure_join_replies: unknown contact {contact_id}")
}
}
/// Stock string: `Scan to chat with %1$s`.
@@ -879,7 +881,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
} else {
translated(context, StockMessage::MsgLocationEnabledBy)
.await
.replace1(&contact.get_stock_name_n_addr(context).await)
.replace1(&contact.get_stock_name(context).await)
}
}
@@ -948,7 +950,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
} else {
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -966,7 +968,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
.await
.replace1(timer)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -977,7 +979,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co
} else {
translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -988,7 +990,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerHourBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -999,7 +1001,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta
} else {
translated(context, StockMessage::MsgEphemeralTimerDayBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -1010,7 +1012,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont
} else {
translated(context, StockMessage::MsgEphemeralTimerWeekBy)
.await
.replace1(&by_contact.get_stock_name_n_addr(context).await)
.replace1(&by_contact.get_stock_name(context).await)
}
}
@@ -1051,16 +1053,26 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
translated(context, StockMessage::ErrorNoNetwork).await
}
/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.`
pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
translated(context, StockMessage::ChatProtectionEnabled).await
/// Stock string: `Chat protection enabled.`.
pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouEnabledProtection).await
} else {
translated(context, StockMessage::ProtectionEnabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `%1$s sent a message from another device.`
pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
translated(context, StockMessage::ChatProtectionDisabled)
.await
.replace1(&contact_id.get_stock_name(context).await)
/// Stock string: `Chat protection disabled.`.
pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::YouDisabledProtection).await
} else {
translated(context, StockMessage::ProtectionDisabledBy)
.await
.replace1(&by_contact.get_stock_name(context).await)
}
}
/// Stock string: `Reply`.
@@ -1092,7 +1104,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
.await
.replace1(minutes)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -1110,7 +1122,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
.await
.replace1(hours)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -1128,7 +1140,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
.await
.replace1(days)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -1146,7 +1158,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
.await
.replace1(weeks)
.replace2(&by_contact.get_stock_name_n_addr(context).await)
.replace2(&by_contact.get_stock_name(context).await)
}
}
@@ -1320,19 +1332,11 @@ impl Context {
pub(crate) async fn stock_protection_msg(
&self,
protect: ProtectionStatus,
contact_id: Option<ContactId>,
from_id: ContactId,
) -> String {
match protect {
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
if let Some(contact_id) = contact_id {
chat_protection_disabled(self, contact_id).await
} else {
// In a group chat, it's not possible to downgrade verification.
// In a 1:1 chat, the `contact_id` always has to be provided.
"[Error] No contact_id given".to_string()
}
}
ProtectionStatus::Protected => chat_protection_enabled(self).await,
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
ProtectionStatus::Protected => protection_disabled(self, from_id).await,
}
}

View File

@@ -82,7 +82,7 @@ impl Summary {
.map(SummaryPrefix::Username)
}
}
Chattype::Single => None,
Chattype::Single | Chattype::Undefined => None,
}
};

View File

@@ -27,19 +27,15 @@ use crate::chat::{
};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::{Blocked, Chattype};
use crate::constants::Chattype;
use crate::constants::{DC_GCL_NO_SPECIALS, DC_MSG_ID_DAYMARKER};
use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::e2ee::EncryptHelper;
use crate::events::{Event, EventType, Events};
use crate::key::{self, DcKey, KeyPairUse};
use crate::key::{self, DcKey, KeyPair, KeyPairUse};
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::peerstate::Peerstate;
use crate::pgp::KeyPair;
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::securejoin::{get_securejoin_qr, join_securejoin};
use crate::stock_str::StockStrings;
use crate::tools::EmailAddress;
@@ -112,19 +108,9 @@ impl TestContextManager {
/// - Let one TestContext send a message
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(
&self,
from: &TestContext,
to: &TestContext,
msg: &str,
) -> Message {
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
let received_msg = self.send_recv(from, to, msg).await;
assert_eq!(
received_msg.chat_blocked, Blocked::Request,
"`send_recv_accept()` is meant to be used for chat requests. Use `send_recv()` if the chat is already accepted."
);
received_msg.chat_id.accept(to).await.unwrap();
received_msg
}
/// - Let one TestContext send a message
@@ -166,27 +152,6 @@ impl TestContextManager {
new_addr
);
}
pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) {
self.section(&format!(
"{} scans {}'s QR code",
scanner.name(),
scanned.name()
));
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
join_securejoin(&scanner.ctx, &qr).await.unwrap();
loop {
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
scanned.recv_msg(&sent).await;
} else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await {
scanner.recv_msg(&sent).await;
} else {
break;
}
}
}
}
#[derive(Debug, Clone, Default)]
@@ -591,21 +556,19 @@ impl TestContext {
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
}
/// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist.
/// Returns 1:1 [`Chat`] with another account, if it exists.
///
/// This first creates a contact using the configured details on the other account, then
/// gets the 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Chat {
/// creates a 1:1 chat with this contact.
pub async fn get_chat(&self, other: &TestContext) -> Option<Chat> {
let contact = self.add_or_lookup_contact(other).await;
let chat_id = ChatId::lookup_by_contact(&self.ctx, contact.id)
match ChatId::lookup_by_contact(&self.ctx, contact.id)
.await
.unwrap()
.expect(
"There is no chat with this contact. \
Hint: Use create_chat() instead of get_chat() if this is expected.",
);
Chat::load_from_db(&self.ctx, chat_id).await.unwrap()
{
Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()),
None => None,
}
}
/// Creates or returns an existing 1:1 [`Chat`] with another account.
@@ -664,6 +627,7 @@ impl TestContext {
res
}
#[allow(unused)]
pub async fn golden_test_chat(&self, chat_id: ChatId, filename: &str) {
let filename = Path::new("test-data/golden/").join(filename);
@@ -672,7 +636,7 @@ impl TestContext {
// We're using `unwrap_or_default()` here so that if the file doesn't exist,
// it can be created using `write` below.
let expected = fs::read(&filename).await.unwrap_or_default();
let expected = String::from_utf8(expected).unwrap().replace("\r\n", "\n");
let expected = String::from_utf8(expected).unwrap();
if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected {
fs::write(&filename, &actual)
.await
@@ -690,6 +654,8 @@ impl TestContext {
/// You can use this to debug your test by printing the entire chat conversation.
// This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could
// merge them to a public function in the `deltachat` crate.
#[allow(dead_code)]
#[allow(clippy::indexing_slicing)]
async fn display_chat(&self, chat_id: ChatId) -> String {
let mut res = String::new();
@@ -914,7 +880,7 @@ pub fn alice_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
.unwrap()
.0;
KeyPair {
key::KeyPair {
addr,
public,
secret,
@@ -932,7 +898,7 @@ pub fn bob_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
.unwrap()
.0;
KeyPair {
key::KeyPair {
addr,
public,
secret,
@@ -942,7 +908,7 @@ pub fn bob_keypair() -> KeyPair {
/// Load a pre-generated keypair for fiona@example.net from disk.
///
/// Like [alice_keypair] but a different key and identity.
pub fn fiona_keypair() -> KeyPair {
pub fn fiona_keypair() -> key::KeyPair {
let addr = EmailAddress::new("fiona@example.net").unwrap();
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc"))
.unwrap()
@@ -950,7 +916,7 @@ pub fn fiona_keypair() -> KeyPair {
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
.unwrap()
.0;
KeyPair {
key::KeyPair {
addr,
public,
secret,
@@ -1042,26 +1008,6 @@ fn print_logevent(logevent: &LogEvent) {
}
}
/// Saves the other account's public key as verified.
pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let mut peerstate = Peerstate::from_header(
&EncryptHelper::new(other).await.unwrap().get_aheader(),
// We have to give 0 as the time, not the current time:
// The time is going to be saved in peerstate.last_seen.
// The code in `peerstate.rs` then compares `if message_time > self.last_seen`,
// and many similar checks in peerstate.rs, and doesn't allow changes otherwise.
// Giving the current time would mean that message_time == peerstate.last_seen,
// so changes would not be allowed.
// This might lead to flaky tests.
0,
);
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
/// Pretty-print an event to stdout
///
/// Done during tests this is captured by `cargo test` and associated with the test itself.
@@ -1168,17 +1114,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
} else {
"[FRESH]"
},
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",

View File

@@ -1,2 +1 @@
mod aeap;
mod verified_chats;

View File

@@ -8,10 +8,10 @@ use crate::contact;
use crate::contact::Contact;
use crate::contact::ContactId;
use crate::message::Message;
use crate::peerstate;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::mark_as_verified;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
@@ -134,11 +134,11 @@ async fn check_aeap_transition(
let fiona = tcm.fiona().await;
tcm.send_recv_accept(&fiona, &bob, "Hi").await;
tcm.send_recv(&bob, &fiona, "Hi back").await;
tcm.send_recv_accept(&bob, &fiona, "Hi back").await;
}
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv(&bob, &alice, "Hi back").await;
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
if verified {
mark_as_verified(&alice, &bob).await;
@@ -327,6 +327,19 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob:
}
}
async fn mark_as_verified(this: &TestContext, other: &TestContext) {
let other_addr = other.get_primary_self_addr().await.unwrap();
let mut peerstate = peerstate::Peerstate::from_addr(this, &other_addr)
.await
.unwrap()
.unwrap();
peerstate.verified_key = peerstate.public_key.clone();
peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone();
peerstate.save_to_db(&this.sql).await.unwrap();
}
async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option<Message> {
let msgs = chat::get_chat_msgs_ex(
&t.ctx,
@@ -355,7 +368,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
let bob = tcm.bob().await;
tcm.send_recv_accept(&alice, &bob, "Hi").await;
tcm.send_recv(&bob, &alice, "Hi back").await;
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
let group =
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?;

View File

@@ -1,770 +0,0 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::{Chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_GCL_FOR_FORWARDING;
use crate::contact::VerifiedStatus;
use crate::contact::{Contact, Origin};
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::receive_imf::receive_imf;
use crate::stock_str;
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
use crate::{e2ee, message};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_classical() {
check_verified_oneonone_chat(true).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_broken_by_device_change() {
check_verified_oneonone_chat(false).await;
}
async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
tcm.execute_securejoin(&alice, &bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
if broken_by_classical_email {
tcm.section("Bob uses a classical MUA to send a message to Alice");
receive_imf(
&alice,
b"Subject: Re: Message from alice\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
} else {
tcm.section("Bob sets up another Delta Chat device");
let bob2 = TestContext::new().await;
enable_verified_oneonone_chats(&[&bob2]).await;
bob2.set_name("bob2");
bob2.configure_addr("bob@example.net").await;
tcm.send_recv(&bob2, &alice, "Using another device now")
.await;
}
// Bob's contact is still verified, but the chat isn't marked as protected anymore
assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await;
tcm.section("Bob sends another message from DC");
tcm.send_recv(&bob, &alice, "Using DC again").await;
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(
contact.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// Bob's chat is marked as verified again
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await;
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
assert_verified(&bob, &alice, ProtectionStatus::Protected).await;
assert_verified(&bob, &fiona, ProtectionStatus::Protected).await;
assert_verified(&fiona, &bob, ProtectionStatus::Protected).await;
let group_id = bob
.create_group_with_members(
ProtectionStatus::Protected,
"Group with everyone",
&[&alice, &fiona],
)
.await;
assert_eq!(
get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
{
let sent = bob.send_text(group_id, "Heyho").await;
alice.recv_msg(&sent).await;
let msg = fiona.recv_msg(&sent).await;
assert_eq!(
get_chat_msg(&fiona, msg.chat_id, 0, 2)
.await
.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
}
// Alice and Fiona should now be verified because of gossip
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert_eq!(
alice_fiona_contact.is_verified(&alice).await.unwrap(),
VerifiedStatus::BidirectVerified
);
// As soon as Alice creates a chat with Fiona, it should directly be protected
{
let chat = alice.create_chat(&fiona).await;
assert!(chat.is_protected());
let msg = alice.get_last_msg().await;
let expected_text = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg.text, expected_text);
}
// Fiona should also see the chat as protected
{
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
let alice_fiona_id = rcvd.chat_id;
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
assert!(chat.is_protected());
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
assert_eq!(msg0.text, expected_text);
}
tcm.section("Fiona reinstalls DC");
drop(fiona);
let fiona_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&fiona_new]).await;
fiona_new.configure_addr("fiona@example.net").await;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
tcm.send_recv(&fiona_new, &alice, "I have a new device")
.await;
// The chat should be and stay unprotected
{
let chat = alice.get_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
// After recreating the chat, it should still be unprotected
chat.id.delete(&alice).await?;
let chat = alice.create_chat(&fiona_new).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_unverified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
chat.id.delete(&alice).await.unwrap();
// Now Bob is a known contact, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
tcm.send_recv(&bob, &alice, "hi").await;
chat.id.delete(&alice).await.unwrap();
// Now we have a public key, new chats should still be created unprotected
let chat = alice.create_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice_chat.is_protected());
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2@example.org>\n\
\n\
hello\n",
false,
)
.await?;
let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden)
.await?
.unwrap();
let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await;
let enabled = stock_str::chat_protection_enabled(&alice).await;
assert_eq!(msg0.text, enabled);
assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled);
let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await;
let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await;
assert_eq!(msg1.text, disabled);
assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled);
let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await;
assert_eq!(msg2.text, "hello".to_string());
assert!(!msg2.is_system_message());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_oneonone_chat_enable_disable() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
for alice_accepts_breakage in [true, false] {
// Bob uses Thunderbird to send a message
receive_imf(
&alice,
format!(
"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\
\n\
Message from Thunderbird\n"
)
.as_bytes(),
false,
)
.await?;
let chat = alice.get_chat(&bob).await;
assert!(!chat.is_protected());
assert!(chat.is_protection_broken());
if alice_accepts_breakage {
tcm.section("Alice clicks 'Accept' on the input-bar-dialog");
chat.id.accept(&alice).await?;
let chat = alice.get_chat(&bob).await;
assert!(!chat.is_protected());
assert!(!chat.is_protection_broken());
}
// Bob sends a message from DC again
tcm.send_recv(&bob, &alice, "Hello from DC").await;
let chat = alice.get_chat(&bob).await;
assert!(chat.is_protected());
assert!(!chat.is_protection_broken());
}
alice
.golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable")
.await;
Ok(())
}
/// Messages with old timestamps are difficult for verified chats:
/// - They must not be sorted over a protection-changed info message.
/// That's what `test_old_message_2` tests
/// - If they change the protection, then they must not be sorted over existing other messages,
/// because then the protection-changed info message would also be above these existing messages.
/// That's what `test_old_message_3` tests.
///
/// `test_old_message_1` tests the case where both the old and the new message
/// change verification
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_1() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
let chat = alice.create_chat(&bob).await; // This creates a protection-changed info message
assert!(chat.is_protected());
// This creates protection-changed info message #2;
// even though the date is old, info message and email must be sorted below the original info message.
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Message from Thunderbird\n",
true,
)
.await?;
alice.golden_test_chat(chat.id, "test_old_message_1").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_2() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
// This creates protection-changed info message #1:
let chat = alice.create_chat(&bob).await;
assert!(chat.is_protected());
let protection_msg = alice.get_last_msg().await;
assert_eq!(
protection_msg.param.get_cmd(),
SystemMessage::ChatProtectionEnabled
);
// This creates protection-changed info message #2.
let first_email = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sun, 08 Dec 2019 19:00:27 +0000\n\
\n\
Somewhat old message\n",
false,
)
.await?
.unwrap();
// Both messages will get the same timestamp as the protection-changed
// message, so this one will be sorted under the previous one
// even though it has an older timestamp.
let second_email = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <2319-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Even older message, that must NOT be shown before the info message\n",
true,
)
.await?
.unwrap();
assert_eq!(first_email.sort_timestamp, second_email.sort_timestamp);
assert_eq!(first_email.sort_timestamp, protection_msg.timestamp_sort);
alice.golden_test_chat(chat.id, "test_old_message_2").await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_3() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from my verified device!")
.await;
// This unverified message must not be sorted over the message sent in the previous line:
receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Old, unverified message\n",
true,
)
.await?;
alice
.golden_test_chat(alice.get_chat(&bob).await.id, "test_old_message_3")
.await;
Ok(())
}
/// Alice is offline for some time.
/// When she comes online, first her inbox is synced and then her sentbox.
/// This test tests that the messages are still in the right order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_4() -> Result<()> {
let alice = TestContext::new_alice().await;
let msg_incoming = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sun, 08 Dec 2019 19:00:27 +0000\n\
\n\
Thanks, Alice!\n",
true,
)
.await?
.unwrap();
let msg_sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: Bob <bob@example.net>\n\
Message-ID: <1234-2-4@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Happy birthday, Bob!\n",
true,
)
.await?
.unwrap();
// The "Happy birthday" message should be shown first, and then the "Thanks" message
assert!(msg_sent.sort_timestamp < msg_incoming.sort_timestamp);
Ok(())
}
/// Alice is offline for some time.
/// When they come online, first their sentbox is synced and then their inbox.
/// This test tests that the messages are still in the right order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_old_message_5() -> Result<()> {
let alice = TestContext::new_alice().await;
let msg_sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: Bob <bob@example.net>\n\
Message-ID: <1234-2-4@example.org>\n\
Date: Sat, 07 Dec 2019 19:00:27 +0000\n\
\n\
Happy birthday, Bob!\n",
true,
)
.await?
.unwrap();
let msg_incoming = receive_imf(
&alice,
b"From: Bob <bob@example.net>\n\
To: alice@example.org\n\
Message-ID: <1234-2-3@example.org>\n\
Date: Sun, 07 Dec 2019 19:00:26 +0000\n\
\n\
Happy birthday to me, Alice!\n",
false,
)
.await?
.unwrap();
assert!(msg_sent.sort_timestamp == msg_incoming.sort_timestamp);
alice
.golden_test_chat(msg_sent.chat_id, "test_old_message_5")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// Alice & Bob verify each other
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
let rendered_msg = mimefactory.render(&bob).await?;
let body = rendered_msg.message;
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let sent = receive_imf(
&alice,
b"From: alice@example.org\n\
To: bob@example.net\n\
\n\
One classical MUA message",
false,
)
.await?
.unwrap();
tcm.send_recv(&alice, &bob, "Sending with DC again").await;
alice
.golden_test_chat(sent.chat_id, "test_outgoing_mua_msg")
.await;
Ok(())
}
/// If Bob answers unencrypted from another address with a classical MUA,
/// the message is under some circumstances still assigned to the original
/// chat (see lookup_chat_by_reply()); this is meant to make aliases
/// work nicely.
/// However, if the original chat is verified, the unencrypted message
/// must NOT be assigned to it (it would be replaced by an error
/// message in the verified chat, so, this would just be a usability issue,
/// not a security issue).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reply() -> Result<()> {
for verified in [false, true] {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
if verified {
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
}
tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await;
let encrypted_msg = tcm.send_recv(&alice, &bob, "Heyho back").await;
let unencrypted_msg = receive_imf(
&alice,
format!(
"From: bob@someotherdomain.org\n\
To: some-alias-forwarding-to-alice@example.org\n\
In-Reply-To: {}\n\
\n\
Weird reply",
encrypted_msg.rfc724_mid
)
.as_bytes(),
false,
)
.await?
.unwrap();
let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?;
assert_eq!(unencrypted_msg.text, "Weird reply");
if verified {
assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
} else {
assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id);
}
}
Ok(())
}
/// Regression test for the following bug:
///
/// - Scan your chat partner's QR Code
/// - They change devices
/// - They send you a message
/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again
///
/// -> The re-verification fails.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_break_protection_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// Cave: Bob can't write a message to Alice here.
// If he did, alice would increase his peerstate's last_seen timestamp.
// Then, after Bob reinstalls DC, alice's `if message_time > last_seen*`
// checks would return false (there are many checks of this form in peerstate.rs).
// Therefore, during the securejoin, Alice wouldn't accept the new key
// and reject the securejoin.
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert!(chats.len() == 1);
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
tcm.send_recv(&bob_new, &alice, "I have a new device").await;
let contact = alice.add_or_lookup_contact(&bob_new).await;
assert_eq!(
contact.is_verified(&alice).await.unwrap(),
// Bob sent a message with a new key, so he most likely doesn't have
// the old key anymore. This means that Alice's device should show
// him as unverified:
VerifiedStatus::Unverified
);
let chat = alice.get_chat(&bob_new).await;
assert_eq!(chat.is_protected(), false);
assert_eq!(chat.is_protection_broken(), true);
let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?;
assert!(chats.len() == 1);
{
let alice_bob_chat = alice.get_chat(&bob_new).await;
assert!(!alice_bob_chat.can_send(&alice).await?);
// Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message:
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Draftttt".to_string());
alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?;
assert_eq!(
alice_bob_chat.id.get_draft(&alice).await?.unwrap().text,
"Draftttt"
);
}
tcm.execute_securejoin(&alice, &bob_new).await;
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
Ok(())
}
/// Regression test:
/// - Verify a contact
/// - The contact stops using DC and sends a message from a classical MUA instead
/// - Delete the 1:1 chat
/// - Create a 1:1 chat
/// - Check that the created chat is not marked as protected
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice]).await;
mark_as_verified(&alice, &bob).await;
receive_imf(
&alice,
b"Subject: Message from bob\r\n\
From: <bob@example.net>\r\n\
To: <alice@example.org>\r\n\
Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\
Message-ID: <abcd@example.net>\r\n\
\r\n\
Heyho!\r\n",
false,
)
.await
.unwrap()
.unwrap();
alice.create_chat(&bob).await;
assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await;
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(
contact.is_verified(this).await.unwrap(),
VerifiedStatus::BidirectVerified
);
let chat = this.get_chat(other).await;
let (expect_protected, expect_broken) = match protected {
ProtectionStatus::Unprotected => (false, false),
ProtectionStatus::Protected => (true, false),
ProtectionStatus::ProtectionBroken => (false, true),
};
assert_eq!(chat.is_protected(), expect_protected);
assert_eq!(chat.is_protection_broken(), expect_broken);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
for t in test_contexts {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap()
}
}

View File

@@ -700,7 +700,7 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
}
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
/// This method strips all occurrences of the RTLO Unicode character.
/// This method strips all occurances of the RTLO Unicode character.
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
pub(crate) fn strip_rtlo_characters(input_str: &str) -> String {
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")

View File

@@ -26,12 +26,12 @@ use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::AsyncReadExt;
use crate::chat::{self, Chat};
use crate::blob::BlobObject;
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
use crate::download::DownloadState;
use crate::events::EventType;
use crate::message::{Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::wrapped_base64_encode;
use crate::mimeparser::SystemMessage;
@@ -40,6 +40,7 @@ use crate::param::Params;
use crate::scheduler::InterruptInfo;
use crate::tools::strip_rtlo_characters;
use crate::tools::{create_smeared_timestamp, get_abs_path};
use crate::{chat, EventType};
/// The current API version.
/// If `min_api` in manifest.toml is set to a larger value,
@@ -845,6 +846,35 @@ impl Message {
}
}
/// Replaces WebXDC blob of existing message.
///
/// This API is supposed to be called from within a WebXDC to replace itself
/// e.g. with an updated or persistently reconfigured version.
pub async fn replace_webxdc(context: &Context, msg_id: MsgId, data: &[u8]) -> Result<()> {
let mut msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.get_viewtype() == Viewtype::Webxdc,
"Message {msg_id} is not a WebXDC instance"
);
let blob = BlobObject::create(
context,
&msg.get_filename()
.context("Cannot get filename of exising WebXDC instance")?,
data,
)
.await
.context("Failed to create WebXDC replacement blob")?;
let mut param = msg.param.clone();
param.set(Param::File, blob.as_name());
msg.param = param;
msg.update_param(context).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::json;
@@ -2623,4 +2653,62 @@ sth_for_the = "future""#
Ok(())
}
/// Tests replacing WebXDC with a newer version.
///
/// Updates should be preserved after upgrading.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_replace_webxdc() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
// Alice sends WebXDC instance.
let alice_chat = alice.create_chat(&bob).await;
let mut alice_instance = create_webxdc_instance(
&alice,
"minimal.xdc",
include_bytes!("../test-data/webxdc/minimal.xdc"),
)
.await?;
alice_instance.set_text("user added text".to_string());
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
let alice_instance = alice.get_last_msg().await;
assert_eq!(alice_instance.get_text(), "user added text");
// Bob receives that instance.
let alice_sent_instance = alice.pop_sent_msg().await;
let bob_received_instance = bob.recv_msg(&alice_sent_instance).await;
assert_eq!(bob_received_instance.get_text(), "user added text");
// Alice sends WebXDC update.
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update")
.await?;
alice.flush_status_updates().await?;
let alice_sent_update = alice.pop_sent_msg().await;
bob.recv_msg(&alice_sent_update).await;
assert_eq!(
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
);
// Bob replaces WebXDC.
replace_webxdc(
&bob,
bob_received_instance.id,
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
)
.await
.context("Failed to replace WebXDC")?;
// Updates are not modified.
assert_eq!(
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
);
Ok(())
}
}

View File

@@ -1,6 +0,0 @@
Single#Chat#10: Bob [bob@example.net]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#12: (Contact#Contact#10): Message from Thunderbird [SEEN]
--------------------------------------------------------------------------------

View File

@@ -1,7 +0,0 @@
Single#Chat#10: Bob [bob@example.net]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#12: (Contact#Contact#10): Somewhat old message [FRESH]
Msg#13: (Contact#Contact#10): Even older message, that must NOT be shown before the info message [SEEN]
--------------------------------------------------------------------------------

View File

@@ -1,7 +0,0 @@
Single#Chat#10: Bob [bob@example.net]
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from my verified device! [FRESH]
Msg#12: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#13: (Contact#Contact#10): Old, unverified message [SEEN]
--------------------------------------------------------------------------------

View File

@@ -1,5 +0,0 @@
Single#Chat#10: Bob [bob@example.net]
--------------------------------------------------------------------------------
Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √
Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH]
--------------------------------------------------------------------------------

View File

@@ -1,7 +0,0 @@
Single#Chat#10: bob@example.net [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11🔒: (Contact#Contact#10): Heyho from DC [FRESH]
Msg#12: Me (Contact#Contact#Self): One classical MUA message √
Msg#13🔒: Me (Contact#Contact#Self): Sending with DC again √
--------------------------------------------------------------------------------

View File

@@ -1,12 +0,0 @@
Single#Chat#10: Bob [bob@example.net] 🛡️
--------------------------------------------------------------------------------
Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#12: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#13: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#14🔒: (Contact#Contact#10): Hello from DC [FRESH]
Msg#15: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO 🛡️❌]
Msg#16: (Contact#Contact#10): Message from Thunderbird [FRESH]
Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO 🛡️]
Msg#18🔒: (Contact#Contact#10): Hello from DC [FRESH]
--------------------------------------------------------------------------------