mirror of
https://github.com/chatmail/core.git
synced 2026-10-06 13:10:17 +03:00
Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f4b4bf201 | ||
|
|
9a7d1faf75 | ||
|
|
e59c4ee858 | ||
|
|
a520f0268f | ||
|
|
5e3b1fa540 | ||
|
|
95f29f7b63 | ||
|
|
20513475ef | ||
|
|
3b806320ec | ||
|
|
c857d6e1bd | ||
|
|
94cd9a713f | ||
|
|
7676473ebd | ||
|
|
8c778b3f5c | ||
|
|
a66f8bd9fc | ||
|
|
95b2a15930 | ||
|
|
0179ec2da9 | ||
|
|
8f2313bb2a | ||
|
|
488a3d1118 | ||
|
|
9094df7bc7 | ||
|
|
16aad3fa67 | ||
|
|
3b47c3f21d | ||
|
|
987ce58926 | ||
|
|
20c88743df | ||
|
|
03395b95cb | ||
|
|
53f04a134a | ||
|
|
885f26ea8c | ||
|
|
3ab181fdf8 | ||
|
|
e12044e6af | ||
|
|
954067eb6d | ||
|
|
aecbebd566 | ||
|
|
3111bcde5e | ||
|
|
e6cffd537e | ||
|
|
70000d9ebb | ||
|
|
d95843b0bf | ||
|
|
13e766bc37 | ||
|
|
c34edc582e | ||
|
|
8eee389c09 | ||
|
|
8ed6d4d709 | ||
|
|
60bacbec47 | ||
|
|
af013559de | ||
|
|
b784415c57 | ||
|
|
85739ba6ad | ||
|
|
a02a593f47 | ||
|
|
67f28f501a | ||
|
|
9b9703a48e | ||
|
|
c55a3d3873 | ||
|
|
f27d304f3b | ||
|
|
6d51d19f01 | ||
|
|
170968dfc2 | ||
|
|
f930576fd1 | ||
|
|
d797de7a8d | ||
|
|
acc7bb00c5 | ||
|
|
8fb8a877be | ||
|
|
b96028cd87 | ||
|
|
682e241edb | ||
|
|
3a63628f1f | ||
|
|
3705616cd9 | ||
|
|
b8fcb660ad | ||
|
|
5673294623 | ||
|
|
7062bb0502 | ||
|
|
659cffe0cc | ||
|
|
a1663a98e0 | ||
|
|
3de1dbc9e4 | ||
|
|
6d37e8601e | ||
|
|
d762753103 | ||
|
|
a020d5ccce | ||
|
|
1e28ea9bb0 | ||
|
|
17f2d33731 | ||
|
|
976797d4cf | ||
|
|
31e3169433 | ||
|
|
d2b15cb629 | ||
|
|
9cd000c4f2 | ||
|
|
243c035b03 |
@@ -81,19 +81,15 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.71.0
|
||||
- os: windows-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.71.0
|
||||
- os: macos-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.71.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.
|
||||
# Minimum Supported Rust Version = 1.67.0
|
||||
- os: ubuntu-latest
|
||||
rust: 1.65.0
|
||||
rust: 1.67.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -106,6 +102,8 @@ jobs:
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Test cargo vendor
|
||||
|
||||
@@ -76,6 +76,29 @@ 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.
|
||||
|
||||
Generated
+214
-264
File diff suppressed because it is too large
Load Diff
+4
-8
@@ -3,7 +3,7 @@ name = "deltachat"
|
||||
version = "1.120.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
rust-version = "1.67"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -48,6 +48,7 @@ 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"
|
||||
@@ -68,13 +69,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.29"
|
||||
quick-xml = "0.30"
|
||||
rand = "0.8"
|
||||
regex = "1.8"
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.4"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
@@ -118,11 +119,6 @@ members = [
|
||||
"format-flowed",
|
||||
]
|
||||
|
||||
[[example]]
|
||||
name = "simple"
|
||||
path = "examples/simple.rs"
|
||||
|
||||
|
||||
[[bench]]
|
||||
name = "create_account"
|
||||
harness = false
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
# Delta Chat Rust
|
||||
<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>
|
||||
|
||||
> Deltachat-core written in 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>
|
||||
|
||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
||||
<p align="center">
|
||||
The core library for Delta Chat, written in Rust
|
||||
</p>
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
for expected_id in 2..n {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
|
||||
@@ -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_keyring_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_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*
|
||||
######################################################
|
||||
|
||||
+49
-52
@@ -485,6 +485,13 @@ 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`.
|
||||
@@ -1103,7 +1110,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('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
|
||||
* success = window.webxdc.sendUpdate('{payload: {"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.
|
||||
@@ -1470,24 +1477,6 @@ 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.
|
||||
*
|
||||
@@ -2926,12 +2915,15 @@ 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* os_name, const char* dir);
|
||||
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3712,7 +3704,6 @@ 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.
|
||||
@@ -3721,6 +3712,26 @@ 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().
|
||||
@@ -4315,7 +4326,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(), dc_set_chat_protection()
|
||||
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||
* or dc_add_contact_to_chat().
|
||||
*
|
||||
* These messages are typically shown in the center of the chat view,
|
||||
@@ -5009,7 +5020,12 @@ 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
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -6749,15 +6765,6 @@ 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.
|
||||
@@ -7202,26 +7209,6 @@ 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.
|
||||
@@ -7232,6 +7219,16 @@ 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
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
+22
-46
@@ -29,7 +29,7 @@ use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::net::read_url_blob;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
@@ -795,21 +795,13 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let 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)
|
||||
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
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1429,32 +1421,6 @@ 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,
|
||||
@@ -3084,6 +3050,16 @@ 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() {
|
||||
@@ -4692,17 +4668,17 @@ pub type dc_accounts_t = AccountsWrapper;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_new(
|
||||
_os_name: *const libc::c_char,
|
||||
dbfile: *const libc::c_char,
|
||||
dir: *const libc::c_char,
|
||||
writable: libc::c_int,
|
||||
) -> *mut dc_accounts_t {
|
||||
setup_panic!();
|
||||
|
||||
if dbfile.is_null() {
|
||||
if dir.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_new()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accs = block_on(Accounts::new(as_path(dbfile).into()));
|
||||
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||
|
||||
match accs {
|
||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||
|
||||
@@ -22,10 +22,10 @@ log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.28" }
|
||||
serde_json = "1.0.99"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.29.1" }
|
||||
sanitize-filename = "0.4"
|
||||
sanitize-filename = "0.5"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
|
||||
|
||||
@@ -4,30 +4,30 @@ use std::{collections::HashMap, str::FromStr};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||
pub use deltachat::accounts::Accounts;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
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::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::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,11 +142,7 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(
|
||||
all_positional,
|
||||
ts_outdir = "typescript/generated",
|
||||
openrpc_outdir = "openrpc"
|
||||
)]
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -157,12 +153,12 @@ impl CommandApi {
|
||||
// Misc top level functions
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Check if an email address is valid.
|
||||
/// Checks if an email address is valid.
|
||||
async fn check_email_validity(&self, email: String) -> bool {
|
||||
may_be_valid_addr(&email)
|
||||
}
|
||||
|
||||
/// Get general system info.
|
||||
/// Returns general system info.
|
||||
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
||||
get_info()
|
||||
}
|
||||
@@ -223,11 +219,13 @@ 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(())
|
||||
@@ -237,14 +235,16 @@ impl CommandApi {
|
||||
// Methods that work on individual accounts
|
||||
// ---------------------------------------------
|
||||
|
||||
async fn start_io(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(id).await?;
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop_io(&self, id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(id).await?;
|
||||
/// Stops background tasks for a single account.
|
||||
async fn stop_io(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -311,11 +311,13 @@ 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,
|
||||
@@ -347,6 +349,7 @@ 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
|
||||
@@ -1867,7 +1870,7 @@ impl CommandApi {
|
||||
.context("path conversion to string failed")
|
||||
}
|
||||
|
||||
/// save a sticker to a collection/folder in the account's sticker folder
|
||||
/// Saves a sticker to a collection/folder in the account's sticker folder.
|
||||
async fn misc_save_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum Account {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
Configured {
|
||||
|
||||
@@ -167,10 +167,11 @@ impl BasicChat {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
Until(i64),
|
||||
Until { duration: i64 },
|
||||
}
|
||||
|
||||
impl MuteDuration {
|
||||
@@ -178,13 +179,13 @@ impl MuteDuration {
|
||||
match self {
|
||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||
MuteDuration::Until(n) => {
|
||||
if n <= 0 {
|
||||
MuteDuration::Until { duration } => {
|
||||
if duration <= 0 {
|
||||
bail!("failed to read mute duration")
|
||||
}
|
||||
|
||||
Ok(SystemTime::now()
|
||||
.checked_add(Duration::from_secs(n as u64))
|
||||
.checked_add(Duration::from_secs(duration as u64))
|
||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use super::message::MessageViewtype;
|
||||
pub struct ChatListEntry(pub u32, pub u32);
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatListItem {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl From<CoreEvent> for Event {
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum MessageLoadResult {
|
||||
Message(MessageObject),
|
||||
LoadingError { error: String },
|
||||
|
||||
@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
|
||||
@@ -13,7 +13,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
@@ -54,7 +55,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
@@ -19,7 +19,8 @@ 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 accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
|
||||
@@ -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.type === "Configured") {
|
||||
if (account.kind === "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.type !== "Configured") {
|
||||
if (info.kind !== "Configured") {
|
||||
return write($main, "Account is not configured");
|
||||
}
|
||||
write($main, `<h2>${info.addr!}</h2>`);
|
||||
@@ -81,8 +81,7 @@ async function run() {
|
||||
messageIds
|
||||
);
|
||||
for (const [_messageId, message] of Object.entries(messages)) {
|
||||
if (message.variant === "message")
|
||||
write($main, `<p>${message.text}</p>`);
|
||||
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||
}
|
||||
}
|
||||
@@ -93,9 +92,9 @@ async function run() {
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
||||
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(
|
||||
Object.assign({}, event, { type: undefined })
|
||||
Object.assign({}, event, { kind: undefined })
|
||||
)}
|
||||
</p>`
|
||||
);
|
||||
|
||||
@@ -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["type"]]: (
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { type: Property }>
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["type"]]: (
|
||||
event: Extract<EventType, { type: Property }>
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type DcEvent = EventType;
|
||||
export type DcEventType<T extends EventType["type"]> = Extract<
|
||||
export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||
EventType,
|
||||
{ type: T }
|
||||
{ kind: 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.type, event.contextId, event.event);
|
||||
this.emit(event.event.kind, event.contextId, event.event);
|
||||
this.emit("ALL", event.contextId, event.event);
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.type,
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
);
|
||||
|
||||
@@ -29,8 +29,8 @@ describe("online tests", function () {
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||
|
||||
dc.on("ALL", (contextId, { type }) => {
|
||||
if (type !== "Info") console.log(contextId, type);
|
||||
dc.on("ALL", (contextId, { kind }) => {
|
||||
if (kind !== "Info") console.log(contextId, kind);
|
||||
});
|
||||
|
||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||
@@ -177,12 +177,12 @@ describe("online tests", function () {
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent<T extends DcEvent["type"]>(
|
||||
async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
dc: DeltaChat,
|
||||
eventType: T,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
): Promise<Extract<DcEvent, { type: T }>> {
|
||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error("Timeout reached before event came in")),
|
||||
|
||||
@@ -18,6 +18,7 @@ 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;
|
||||
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
} else {
|
||||
"[FRESH]"
|
||||
},
|
||||
if msg.is_info() { "[INFO]" } else { "" },
|
||||
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.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
@@ -395,8 +406,6 @@ 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\
|
||||
@@ -1056,20 +1065,6 @@ 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()?);
|
||||
|
||||
@@ -14,9 +14,9 @@ hooks = events.HookCollection()
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
async def log_event(event):
|
||||
if event.type == EventType.INFO:
|
||||
if event.kind == EventType.INFO:
|
||||
logging.info(event.msg)
|
||||
elif event.type == EventType.WARNING:
|
||||
elif event.kind == EventType.WARNING:
|
||||
logging.warning(event.msg)
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ build-backend = "setuptools.build_meta"
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"aiodns"
|
||||
"aiohttp"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -54,14 +54,14 @@ class Chat:
|
||||
"""
|
||||
if duration is not None:
|
||||
assert duration > 0, "Invalid duration"
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
dur: dict = {"kind": "Until", "duration": duration}
|
||||
else:
|
||||
dur = "Forever"
|
||||
dur = {"kind": "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, "NotMuted")
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
|
||||
|
||||
async def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
|
||||
@@ -106,10 +106,10 @@ class Client:
|
||||
await self._process_messages() # Process old messages.
|
||||
while True:
|
||||
event = await self.account.wait_for_event()
|
||||
event["type"] = EventType(event.type)
|
||||
event["kind"] = EventType(event.kind)
|
||||
event["account"] = self.account
|
||||
await self._on_event(event)
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
await self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
|
||||
@@ -83,7 +83,7 @@ class RawEvent(EventFilter):
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
if self.types and event.type not in self.types:
|
||||
if self.types and event.kind not in self.types:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class ACFactory:
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
print(event)
|
||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
@@ -98,7 +98,7 @@ class ACFactory:
|
||||
group=group,
|
||||
)
|
||||
|
||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
return await to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -43,7 +45,7 @@ async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
||||
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||
assert event.progress != 0 # Progress 0 indicates error.
|
||||
if event.progress == 1000: # Success
|
||||
break
|
||||
@@ -74,7 +76,7 @@ async def test_account(acfactory) -> None:
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
@@ -144,7 +146,7 @@ async def test_chat(acfactory) -> None:
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
@@ -230,7 +232,7 @@ async def test_message(acfactory) -> None:
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
@@ -270,7 +272,7 @@ async def test_is_bot(acfactory) -> None:
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
@@ -355,3 +357,13 @@ 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
|
||||
|
||||
@@ -13,7 +13,7 @@ async def test_webxdc(acfactory) -> None:
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
|
||||
@@ -21,7 +21,7 @@ serde_json = "1.0.99"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.29.1", features = ["io-std"] }
|
||||
tokio-util = "0.7.8"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -32,3 +32,6 @@ 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.
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -39,6 +40,12 @@ 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));
|
||||
}
|
||||
@@ -56,7 +63,8 @@ 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 accounts = Accounts::new(PathBuf::from(&path)).await?;
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let accounts = Arc::new(RwLock::new(accounts));
|
||||
|
||||
@@ -16,7 +16,6 @@ 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" },
|
||||
@@ -30,12 +29,11 @@ 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" },
|
||||
@@ -43,8 +41,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" },
|
||||
@@ -60,8 +58,8 @@ skip = [
|
||||
{ name = "windows_i686_msvc", version = "<0.48" },
|
||||
{ name = "windows-sys", version = "<0.48" },
|
||||
{ name = "windows-targets", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||
{ name = "winreg", version = "0.10.1" },
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
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();
|
||||
}
|
||||
+2
-6
@@ -158,6 +158,8 @@ 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,
|
||||
@@ -243,12 +245,6 @@ 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,
|
||||
|
||||
@@ -158,6 +158,8 @@ 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,
|
||||
@@ -243,12 +245,6 @@ 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,
|
||||
|
||||
@@ -699,23 +699,6 @@ 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(
|
||||
|
||||
@@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter {
|
||||
accountDir: string
|
||||
jsonRpcStarted = false
|
||||
|
||||
constructor(cwd: string, os = 'deltachat-node') {
|
||||
constructor(cwd: string, writable = true) {
|
||||
super()
|
||||
debug('DeltaChat constructor')
|
||||
|
||||
this.accountDir = cwd
|
||||
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
|
||||
this.dcn_accounts = binding.dcn_accounts_new(
|
||||
this.accountDir,
|
||||
writable ? 1 : 0
|
||||
)
|
||||
}
|
||||
|
||||
getAllAccountIds() {
|
||||
|
||||
+3
-16
@@ -1399,18 +1399,6 @@ 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();
|
||||
@@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
|
||||
|
||||
NAPI_METHOD(dcn_accounts_new) {
|
||||
NAPI_ARGV(2);
|
||||
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
|
||||
NAPI_ARGV_UTF8_MALLOC(dir, 1);
|
||||
NAPI_ARGV_UTF8_MALLOC(dir, 0);
|
||||
NAPI_ARGV_INT32(writable, 1);
|
||||
TRACE("calling..");
|
||||
|
||||
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
|
||||
@@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) {
|
||||
}
|
||||
|
||||
|
||||
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
|
||||
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
|
||||
|
||||
napi_value result;
|
||||
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
|
||||
@@ -3491,7 +3479,6 @@ 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);
|
||||
|
||||
@@ -136,6 +136,7 @@ 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")
|
||||
@@ -182,8 +183,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# Skip system message about added member
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
# System message about the added member.
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_system_message()
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.is_encrypted()
|
||||
@@ -524,7 +526,8 @@ 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.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
msg_out = chat2.send_text("hello")
|
||||
|
||||
lp.sec("ac1: receiving message")
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat import account_hookimpl, Message, Chat
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
@@ -1467,13 +1467,18 @@ 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)))
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
msgs.append(msgs[-1].send_reaction(react_str))
|
||||
|
||||
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])
|
||||
|
||||
ac2.start_io()
|
||||
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
@@ -1505,7 +1510,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(2)
|
||||
time.sleep(1.1)
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
|
||||
|
||||
@@ -1664,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp):
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
def test_qr_join_chat(acfactory, lp):
|
||||
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
|
||||
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
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()
|
||||
@@ -1678,6 +1687,17 @@ def test_qr_join_chat(acfactory, lp):
|
||||
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_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
|
||||
@@ -221,8 +221,9 @@ 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(ffi.NULL, str(accounts_fname).encode("ascii")),
|
||||
lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
|
||||
+2
-1
@@ -59,7 +59,8 @@ commands =
|
||||
[testenv:doc]
|
||||
changedir=doc
|
||||
deps =
|
||||
sphinx
|
||||
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
|
||||
sphinx<=7.1.2
|
||||
breathe
|
||||
commands =
|
||||
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html
|
||||
|
||||
+135
-33
@@ -7,6 +7,9 @@ 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;
|
||||
@@ -33,16 +36,16 @@ pub struct Accounts {
|
||||
|
||||
impl Accounts {
|
||||
/// Loads or creates an accounts folder at the given `dir`.
|
||||
pub async fn new(dir: PathBuf) -> Result<Self> {
|
||||
if !dir.exists() {
|
||||
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
|
||||
if writable && !dir.exists() {
|
||||
Accounts::create(&dir).await?;
|
||||
}
|
||||
|
||||
Accounts::open(dir).await
|
||||
Accounts::open(dir, writable).await
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &Path) -> Result<()> {
|
||||
async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
@@ -54,13 +57,13 @@ impl Accounts {
|
||||
|
||||
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
|
||||
/// no account exists and no config exists.
|
||||
pub async fn open(dir: PathBuf) -> Result<Self> {
|
||||
async fn open(dir: PathBuf, writable: bool) -> 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)
|
||||
let config = Config::from_file(config_file, writable)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
@@ -298,14 +301,20 @@ 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, Clone, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
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.
|
||||
@@ -319,17 +328,74 @@ 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 configuration file in the given account manager directory.
|
||||
pub async fn new(dir: &Path) -> Result<Self> {
|
||||
/// 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")?;
|
||||
let inner = InnerConfig {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
};
|
||||
let file = dir.join(CONFIG_NAME);
|
||||
let mut cfg = Self { file, inner };
|
||||
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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -339,6 +405,11 @@ 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
|
||||
@@ -357,24 +428,28 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
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")?;
|
||||
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")?;
|
||||
let s = std::str::from_utf8(&bytes)?;
|
||||
let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
|
||||
config.inner = 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 inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
|
||||
for account in &mut config.inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut config = Self { file, inner };
|
||||
if modified {
|
||||
if modified && writable {
|
||||
config.sync().await?;
|
||||
}
|
||||
|
||||
@@ -518,26 +593,44 @@ mod tests {
|
||||
let p: PathBuf = dir.path().join("accounts1");
|
||||
|
||||
{
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
accounts.add_account().await.unwrap();
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
}
|
||||
{
|
||||
let accounts = Accounts::open(p).await.unwrap();
|
||||
for writable in [true, false] {
|
||||
let accounts = Accounts::new(p.clone(), writable).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 mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -564,7 +657,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -585,7 +679,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
@@ -622,7 +717,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
for expected_id in 1..10 {
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
@@ -642,7 +738,8 @@ mod tests {
|
||||
let dummy_accounts = 10;
|
||||
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 1);
|
||||
@@ -677,7 +774,8 @@ mod tests {
|
||||
assert!(id2 > id1 + dummy_accounts);
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable).await?;
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
@@ -722,7 +820,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(p.clone(), writable).await?;
|
||||
|
||||
// Make sure there are no accounts.
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
@@ -748,7 +847,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone())
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
@@ -768,7 +868,8 @@ mod tests {
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let accounts = Accounts::new(p.clone())
|
||||
let writable = false;
|
||||
let accounts = Accounts::new(p.clone(), writable)
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
@@ -792,7 +893,8 @@ mod tests {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await?;
|
||||
accounts.add_account().await?;
|
||||
accounts.add_account().await?;
|
||||
|
||||
|
||||
+141
-157
@@ -25,6 +25,7 @@ use crate::debug_logging::maybe_set_logging_xdc;
|
||||
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;
|
||||
@@ -33,6 +34,7 @@ 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,
|
||||
@@ -40,7 +42,6 @@ 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)]
|
||||
@@ -86,6 +87,14 @@ 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.
|
||||
@@ -102,6 +111,10 @@ 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,
|
||||
|
||||
@@ -118,6 +131,10 @@ 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")
|
||||
}
|
||||
@@ -270,6 +287,7 @@ 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(?, ?, ?, ?, ?, ?, ?);",
|
||||
@@ -278,13 +296,20 @@ impl ChatId {
|
||||
&grpname,
|
||||
grpid,
|
||||
create_blocked,
|
||||
create_smeared_timestamp(context),
|
||||
smeared_time,
|
||||
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={}.",
|
||||
@@ -332,7 +357,7 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Undefined | Chattype::Broadcast => {
|
||||
Chattype::Broadcast => {
|
||||
bail!("Can't block chat of type {:?}", chat.typ)
|
||||
}
|
||||
Chattype::Single => {
|
||||
@@ -373,7 +398,16 @@ impl ChatId {
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
match chat.typ {
|
||||
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
|
||||
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::Single | Chattype::Group | Chattype::Broadcast => {
|
||||
// User has "created a chat" with all these contacts.
|
||||
//
|
||||
@@ -400,20 +434,19 @@ impl ChatId {
|
||||
|
||||
/// Sets protection without sending a message.
|
||||
///
|
||||
/// Used when a message arrives indicating that someone else has
|
||||
/// changed the protection value for a chat.
|
||||
/// Returns whether the protection status was actually modified.
|
||||
pub(crate) async fn inner_set_protection(
|
||||
self,
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
) -> Result<()> {
|
||||
ensure!(!self.is_special(), "Invalid chat-id.");
|
||||
) -> Result<bool> {
|
||||
ensure!(!self.is_special(), "Invalid chat-id {self}.");
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
|
||||
if protect == chat.protected {
|
||||
info!(context, "Protection status unchanged for {}.", self);
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match protect {
|
||||
@@ -428,9 +461,8 @@ impl ChatId {
|
||||
}
|
||||
}
|
||||
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
|
||||
Chattype::Undefined => bail!("Undefined group type"),
|
||||
},
|
||||
ProtectionStatus::Unprotected => {}
|
||||
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
|
||||
};
|
||||
|
||||
context
|
||||
@@ -443,68 +475,58 @@ impl ChatId {
|
||||
// make sure, the receivers will get all keys
|
||||
self.reset_gossiped_timestamp(context).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Send protected status message to the chat.
|
||||
/// Adds an info message to the chat, telling the user that the protection status changed.
|
||||
///
|
||||
/// 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.
|
||||
/// Params:
|
||||
///
|
||||
/// 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.
|
||||
/// * `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.
|
||||
pub(crate) async fn add_protection_msg(
|
||||
self,
|
||||
context: &Context,
|
||||
protect: ProtectionStatus,
|
||||
promote: bool,
|
||||
from_id: ContactId,
|
||||
contact_id: Option<ContactId>,
|
||||
timestamp_sort: i64,
|
||||
) -> Result<()> {
|
||||
let text = context.stock_protection_msg(protect, from_id).await;
|
||||
let text = context.stock_protection_msg(protect, contact_id).await;
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
|
||||
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?;
|
||||
}
|
||||
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets protection and sends or adds a message.
|
||||
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);
|
||||
///
|
||||
/// `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)
|
||||
}
|
||||
}
|
||||
|
||||
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Archives or unarchives a chat.
|
||||
@@ -741,14 +763,6 @@ 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;
|
||||
@@ -1141,7 +1155,7 @@ pub struct Chat {
|
||||
pub grpid: String,
|
||||
|
||||
/// Whether the chat is blocked, unblocked or a contact request.
|
||||
pub(crate) blocked: Blocked,
|
||||
pub blocked: Blocked,
|
||||
|
||||
/// Additional chat parameters stored in the database.
|
||||
pub param: Params,
|
||||
@@ -1153,7 +1167,7 @@ pub struct Chat {
|
||||
pub mute_duration: MuteDuration,
|
||||
|
||||
/// If the chat is protected (verified).
|
||||
protected: ProtectionStatus,
|
||||
pub(crate) protected: ProtectionStatus,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
@@ -1247,6 +1261,8 @@ 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.param.get(Param::ListPost).is_none_or_empty() {
|
||||
Some(ReadOnlyMailingList)
|
||||
} else if !self.is_self_in_chat(context).await? {
|
||||
@@ -1271,7 +1287,6 @@ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1410,6 +1425,27 @@ 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
|
||||
@@ -1440,15 +1476,6 @@ 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 {
|
||||
@@ -1964,19 +1991,30 @@ 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)
|
||||
VALUES(?, ?, ?, ?, ?)",
|
||||
(type, name, param, blocked, created_timestamp, protected)
|
||||
VALUES(?, ?, ?, ?, ?, ?)",
|
||||
(
|
||||
Chattype::Single,
|
||||
chat_name,
|
||||
params.to_string(),
|
||||
create_blocked as u8,
|
||||
create_smeared_timestamp(context),
|
||||
smeared_time,
|
||||
if protected {
|
||||
ProtectionStatus::Protected
|
||||
} else {
|
||||
ProtectionStatus::Unprotected
|
||||
},
|
||||
),
|
||||
)?;
|
||||
let chat_id = ChatId::new(
|
||||
@@ -1997,6 +2035,17 @@ 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?,
|
||||
@@ -2108,7 +2157,13 @@ async fn prepare_msg_common(
|
||||
|
||||
// Check if the chat can be sent to.
|
||||
if let Some(reason) = chat.why_cant_send(context).await? {
|
||||
bail!("cannot send to {}: {}", chat_id, reason);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
// check current MessageState for drafts (to keep msg_id) ...
|
||||
@@ -2210,7 +2265,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 againts RTLO attacks
|
||||
// protect all system messages against RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
msg.text = strip_rtlo_characters(&msg.text);
|
||||
}
|
||||
@@ -2334,14 +2389,13 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
|
||||
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
|
||||
}
|
||||
|
||||
if 0 != rendered_msg.last_added_location_id {
|
||||
if let Some(last_added_location_id) = 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, rendered_msg.last_added_location_id)
|
||||
.await
|
||||
location::set_msg_location_id(context, msg.id, last_added_location_id).await
|
||||
{
|
||||
error!(context, "Failed to set msg_location_id: {err:#}.");
|
||||
}
|
||||
@@ -2858,18 +2912,14 @@ 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,
|
||||
create_smeared_timestamp(context),
|
||||
),
|
||||
(Chattype::Group, chat_name, grpid, timestamp),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -2881,9 +2931,9 @@ pub async fn create_group_chat(
|
||||
context.emit_msgs_changed_without_ids();
|
||||
|
||||
if protect == ProtectionStatus::Protected {
|
||||
// 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?;
|
||||
chat_id
|
||||
.set_protection(context, protect, timestamp, None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(chat_id)
|
||||
@@ -5139,72 +5189,6 @@ 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;
|
||||
|
||||
+1
-1
@@ -324,7 +324,7 @@ impl Chatlist {
|
||||
.context("loading contact failed")?;
|
||||
(Some(lastmsg), Some(lastcontact))
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
|
||||
Chattype::Single => (Some(lastmsg), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -311,6 +311,16 @@ 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 {
|
||||
|
||||
@@ -118,7 +118,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
/// Chat type.
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
@@ -134,10 +133,6 @@ 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,
|
||||
|
||||
@@ -216,8 +211,6 @@ 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());
|
||||
|
||||
+4
-20
@@ -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::{DcKey, SignedPublicKey};
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
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 = SignedPublicKey::load_self(context)
|
||||
let fingerprint_self = load_self_public_key(context)
|
||||
.await?
|
||||
.fingerprint()
|
||||
.to_string();
|
||||
@@ -1211,31 +1211,15 @@ 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 {
|
||||
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() {
|
||||
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
|
||||
if peerstate.is_using_verified_key() {
|
||||
return Ok(VerifiedStatus::BidirectVerified);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -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::{DcKey, SignedPublicKey};
|
||||
use crate::key::{load_self_public_key, DcKey as _};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
@@ -580,7 +580,7 @@ impl Context {
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match SignedPublicKey::load_self(self).await {
|
||||
let fingerprint_str = match load_self_public_key(self).await {
|
||||
Ok(key) => key.fingerprint().hex(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
@@ -755,6 +755,12 @@ 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()));
|
||||
|
||||
+8
-10
@@ -1,14 +1,12 @@
|
||||
//! Forward log messages to logging webxdc
|
||||
use crate::{
|
||||
chat::ChatId,
|
||||
config::Config,
|
||||
context::Context,
|
||||
message::{Message, MsgId, Viewtype},
|
||||
param::Param,
|
||||
tools::time,
|
||||
webxdc::StatusUpdateItem,
|
||||
EventType,
|
||||
};
|
||||
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 async_channel::{self as channel, Receiver, Sender};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
+9
-10
@@ -13,7 +13,6 @@ 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;
|
||||
|
||||
@@ -26,8 +25,8 @@ use crate::pgp;
|
||||
pub fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: &Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail)
|
||||
.or_else(|| get_mixed_up_mime(mail))
|
||||
@@ -211,8 +210,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: &Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
@@ -247,7 +246,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: &Keyring<SignedPublicKey>,
|
||||
public_keyring_for_validate: &[SignedPublicKey],
|
||||
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return None;
|
||||
@@ -267,13 +266,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate = Vec::new();
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
public_keyring_for_validate.push(key.clone());
|
||||
}
|
||||
}
|
||||
public_keyring_for_validate
|
||||
|
||||
+12
-19
@@ -6,8 +6,7 @@ use num_traits::FromPrimitive;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::pgp;
|
||||
|
||||
@@ -24,7 +23,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 = SignedPublicKey::load_self(context).await?;
|
||||
let public_key = load_self_public_key(context).await?;
|
||||
|
||||
Ok(EncryptHelper {
|
||||
prefer_encrypt,
|
||||
@@ -104,7 +103,7 @@ impl EncryptHelper {
|
||||
mail_to_encrypt: lettre_email::PartBuilder,
|
||||
peerstates: Vec<(Option<Peerstate>, &str)>,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
|
||||
let mut keyring: Vec<SignedPublicKey> = Vec::new();
|
||||
|
||||
for (peerstate, addr) in peerstates
|
||||
.into_iter()
|
||||
@@ -113,10 +112,10 @@ impl EncryptHelper {
|
||||
let key = peerstate
|
||||
.take_key(min_verified)
|
||||
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
|
||||
keyring.add(key);
|
||||
keyring.push(key);
|
||||
}
|
||||
keyring.add(self.public_key.clone());
|
||||
let sign_key = SignedSecretKey::load_self(context).await?;
|
||||
keyring.push(self.public_key.clone());
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||
|
||||
@@ -132,7 +131,7 @@ impl EncryptHelper {
|
||||
context: &Context,
|
||||
mail: lettre_email::PartBuilder,
|
||||
) -> Result<(lettre_email::MimeMessage, String)> {
|
||||
let sign_key = SignedSecretKey::load_self(context).await?;
|
||||
let sign_key = load_self_secret_key(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))
|
||||
@@ -145,20 +144,17 @@ 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<String> {
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
SignedPublicKey::load_self(context).await?;
|
||||
Ok(self_addr)
|
||||
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
|
||||
load_self_public_key(context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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};
|
||||
@@ -169,10 +165,7 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prexisting() {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(
|
||||
ensure_secret_key_exists(&t).await.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
assert!(ensure_secret_key_exists(&t).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
+5
-5
@@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
|
||||
|
||||
ephemeral_timestamp
|
||||
.into_iter()
|
||||
.chain(delete_device_after_timestamp.into_iter())
|
||||
.chain(delete_device_after_timestamp)
|
||||
.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.delete_from_db(&t).await?;
|
||||
msg.id.trash(&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. Thend send a saved message which will
|
||||
// Set DeleteDeviceAfter to 1800s. Then 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 from the database when its timer expires.
|
||||
msg.id.delete_from_db(&alice).await?;
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&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
|
||||
|
||||
+2
-2
@@ -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::{Message, MsgId};
|
||||
use crate::message::{self, 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.
|
||||
|
||||
+4
-2
@@ -19,7 +19,9 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::key::{
|
||||
self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey,
|
||||
};
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -186,7 +188,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 = SignedSecretKey::load_self(context).await?;
|
||||
let private_key = load_self_secret_key(context).await?;
|
||||
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
|
||||
false => None,
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
|
||||
+79
-77
@@ -3,11 +3,9 @@
|
||||
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};
|
||||
@@ -18,8 +16,7 @@ use tokio::runtime::Handle;
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
// Re-export key types
|
||||
pub use crate::pgp::KeyPair;
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::tools::{time, EmailAddress};
|
||||
|
||||
/// Convenience trait for working with keys.
|
||||
@@ -27,7 +24,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 trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
pub(crate) 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))?)
|
||||
@@ -50,11 +47,6 @@ pub 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
|
||||
@@ -85,38 +77,55 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
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 to_asc(&self, header: Option<(&str, &str)>) -> String {
|
||||
// Not using .to_armored_string() to make clear *why* it is
|
||||
// safe to ignore this error.
|
||||
@@ -135,36 +144,6 @@ 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.
|
||||
@@ -185,7 +164,7 @@ impl DcKey for SignedSecretKey {
|
||||
/// Deltachat extension trait for secret keys.
|
||||
///
|
||||
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
|
||||
pub trait DcSecretKey {
|
||||
pub(crate) trait DcSecretKey {
|
||||
/// Create a public key from a private one.
|
||||
fn split_public_key(&self) -> Result<SignedPublicKey>;
|
||||
}
|
||||
@@ -328,6 +307,29 @@ 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>);
|
||||
@@ -522,9 +524,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new_alice().await;
|
||||
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
|
||||
let pubkey = load_self_public_key(&t).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
|
||||
let seckey = load_self_secret_key(&t).await.unwrap();
|
||||
assert_eq!(alice.secret, seckey);
|
||||
}
|
||||
|
||||
@@ -534,7 +536,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedPublicKey::load_self(&t).await;
|
||||
let key = load_self_public_key(&t).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
@@ -544,7 +546,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
let key = SignedSecretKey::load_self(&t).await;
|
||||
let key = load_self_secret_key(&t).await;
|
||||
assert!(key.is_ok());
|
||||
}
|
||||
|
||||
@@ -561,7 +563,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
thread::spawn(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(SignedPublicKey::load_self(&ctx))
|
||||
.block_on(load_self_public_key(&ctx))
|
||||
})
|
||||
};
|
||||
let thr1 = {
|
||||
@@ -569,7 +571,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
thread::spawn(move || {
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(SignedPublicKey::load_self(&ctx))
|
||||
.block_on(load_self_public_key(&ctx))
|
||||
})
|
||||
};
|
||||
let res0 = thr0.join().unwrap();
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
//! 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]);
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -8,7 +8,6 @@
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
clippy::all,
|
||||
clippy::indexing_slicing,
|
||||
clippy::wildcard_imports,
|
||||
clippy::needless_borrow,
|
||||
clippy::cast_lossless,
|
||||
@@ -17,6 +16,7 @@
|
||||
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,7 +70,6 @@ mod scheduler;
|
||||
#[macro_use]
|
||||
mod job;
|
||||
pub mod key;
|
||||
mod keyring;
|
||||
pub mod location;
|
||||
mod login_param;
|
||||
pub mod message;
|
||||
|
||||
+2
-2
@@ -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).into_iter())
|
||||
.chain(u64::try_from(locations_send_until - now))
|
||||
.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).into_iter())
|
||||
.chain(u64::try_from(locations_last_sent + 61 - now))
|
||||
.min();
|
||||
}
|
||||
} else {
|
||||
|
||||
+2
-1
@@ -5,9 +5,10 @@ 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)]
|
||||
|
||||
+9
-17
@@ -113,24 +113,16 @@ WHERE id=?;
|
||||
}
|
||||
|
||||
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
|
||||
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.
|
||||
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.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,))
|
||||
.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(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -763,7 +755,7 @@ impl Message {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
Some(Contact::get_by_id(context, self.from_id).await?)
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => None,
|
||||
Chattype::Single => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
+16
-7
@@ -66,7 +66,7 @@ pub struct MimeFactory<'a> {
|
||||
in_reply_to: String,
|
||||
references: String,
|
||||
req_mdn: bool,
|
||||
last_added_location_id: u32,
|
||||
last_added_location_id: Option<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: u32,
|
||||
pub last_added_location_id: Option<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: 0,
|
||||
last_added_location_id: None,
|
||||
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: 0,
|
||||
last_added_location_id: None,
|
||||
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 = last_added_location_id;
|
||||
self.last_added_location_id = Some(last_added_location_id);
|
||||
}
|
||||
Ok(part)
|
||||
}
|
||||
@@ -896,7 +896,16 @@ impl<'a> MimeFactory<'a> {
|
||||
let mut placeholdertext = None;
|
||||
let mut meta_part = None;
|
||||
|
||||
if chat.is_protected() {
|
||||
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 {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||
@@ -1954,7 +1963,7 @@ mod tests {
|
||||
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
|
||||
|
||||
if delete_original_msg {
|
||||
incoming_msg.id.delete_from_db(&t).await.unwrap();
|
||||
incoming_msg.id.trash(&t).await.unwrap();
|
||||
}
|
||||
|
||||
if message_arrives_inbetween {
|
||||
|
||||
+6
-6
@@ -28,8 +28,7 @@ use crate::decrypt::{
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::key::{load_self_secret_key, DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::{self, set_msg_failed, update_msg_state, MessageState, MsgId, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -277,9 +276,10 @@ impl MimeMessage {
|
||||
headers.remove("chat-verified");
|
||||
|
||||
let from = from.context("No from in message")?;
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
|
||||
let private_keyring = vec![load_self_secret_key(context)
|
||||
.await
|
||||
.context("failed to get own keyring")?;
|
||||
.context("Failed to get own key")?];
|
||||
|
||||
let mut decryption_info =
|
||||
prepare_decryption(context, &mail, &from.addr, message_time).await?;
|
||||
|
||||
@@ -1900,7 +1900,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 separatedly.
|
||||
// be handled separately.
|
||||
// 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::Undefined => {}
|
||||
Chattype::Single => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -392,6 +392,31 @@ 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:
|
||||
|
||||
+24
-52
@@ -20,7 +20,6 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
@@ -230,7 +229,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: Keyring<SignedPublicKey>,
|
||||
public_keys_for_encryption: Vec<SignedPublicKey>,
|
||||
private_key_for_signing: Option<SignedSecretKey>,
|
||||
) -> Result<String> {
|
||||
let lit_msg = Message::new_literal_bytes("", plain);
|
||||
@@ -238,7 +237,6 @@ 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();
|
||||
@@ -289,15 +287,15 @@ pub fn pk_calc_signature(
|
||||
#[allow(clippy::implicit_hasher)]
|
||||
pub fn pk_decrypt(
|
||||
ctext: Vec<u8>,
|
||||
private_keys_for_decryption: &Keyring<SignedSecretKey>,
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
private_keys_for_decryption: &[SignedSecretKey],
|
||||
public_keys_for_validation: &[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.keys().iter().collect();
|
||||
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
|
||||
|
||||
let (decryptor, _) = msg.decrypt(|| "".into(), &skeys[..])?;
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
@@ -312,20 +310,13 @@ pub fn pk_decrypt(
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
ret_signature_fingerprints.extend(fingerprints);
|
||||
}
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
} else {
|
||||
@@ -337,12 +328,11 @@ pub fn pk_decrypt(
|
||||
pub fn pk_validate(
|
||||
content: &[u8],
|
||||
signature: &[u8],
|
||||
public_keys_for_validation: &Keyring<SignedPublicKey>,
|
||||
public_keys_for_validation: &[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
|
||||
@@ -351,7 +341,7 @@ pub fn pk_validate(
|
||||
.get(..content.len().saturating_sub(2))
|
||||
.context("index is out of range")?;
|
||||
|
||||
for pkey in pkeys {
|
||||
for pkey in public_keys_for_validation {
|
||||
if standalone_signature.verify(pkey, content).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret.insert(fp);
|
||||
@@ -486,9 +476,7 @@ mod tests {
|
||||
async fn ctext_signed() -> &'static String {
|
||||
CTEXT_SIGNED
|
||||
.get_or_init(|| async {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_public.clone());
|
||||
keyring.add(KEYS.bob_public.clone());
|
||||
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
|
||||
|
||||
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
|
||||
.await
|
||||
@@ -501,9 +489,7 @@ mod tests {
|
||||
async fn ctext_unsigned() -> &'static String {
|
||||
CTEXT_UNSIGNED
|
||||
.get_or_init(|| async {
|
||||
let mut keyring = Keyring::new();
|
||||
keyring.add(KEYS.alice_public.clone());
|
||||
keyring.add(KEYS.bob_public.clone());
|
||||
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
|
||||
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
|
||||
})
|
||||
.await
|
||||
@@ -528,10 +514,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_singed() {
|
||||
// Check decrypting as Alice
|
||||
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 decrypt_keyring = vec![KEYS.alice_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.alice_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
@@ -542,10 +526,8 @@ mod tests {
|
||||
assert_eq!(valid_signatures.len(), 1);
|
||||
|
||||
// Check decrypting as Bob
|
||||
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 decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.alice_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
@@ -558,15 +540,9 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_no_sig_check() {
|
||||
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();
|
||||
let keyring = vec![KEYS.alice_secret.clone()];
|
||||
let (plain, valid_signatures) =
|
||||
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
assert_eq!(valid_signatures.len(), 0);
|
||||
}
|
||||
@@ -574,10 +550,8 @@ 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 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 decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let sig_check_keyring = vec![KEYS.bob_public.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
ctext_signed().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
@@ -590,13 +564,11 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_unsigned() {
|
||||
let mut decrypt_keyring = Keyring::new();
|
||||
decrypt_keyring.add(KEYS.bob_secret.clone());
|
||||
let sig_check_keyring = Keyring::new();
|
||||
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
|
||||
let (plain, valid_signatures) = pk_decrypt(
|
||||
ctext_unsigned().await.as_bytes().to_vec(),
|
||||
&decrypt_keyring,
|
||||
&sig_check_keyring,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(plain, CLEARTEXT);
|
||||
|
||||
@@ -17,11 +17,12 @@ 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, EventType};
|
||||
use crate::token;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
|
||||
@@ -4,17 +4,15 @@ use anyhow::Result;
|
||||
use base64::Engine as _;
|
||||
use qrcodegen::{QrCode, QrCodeEcc};
|
||||
|
||||
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},
|
||||
};
|
||||
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};
|
||||
|
||||
/// Returns SVG of the QR code to join the group or verify contact.
|
||||
///
|
||||
|
||||
+159
-102
@@ -4,7 +4,7 @@ use std::cmp::min;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{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, VerifiedStatus,
|
||||
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
|
||||
};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
@@ -516,6 +516,10 @@ 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;
|
||||
}
|
||||
@@ -594,12 +598,17 @@ 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(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 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 chat.is_protected() {
|
||||
let s = stock_str::unknown_sender_for_chat(context).await;
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
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);
|
||||
}
|
||||
} 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.
|
||||
@@ -615,7 +624,7 @@ async fn add_parts(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
chat_id,
|
||||
group_chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
)
|
||||
@@ -718,6 +727,51 @@ 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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,7 +959,8 @@ async fn add_parts(
|
||||
};
|
||||
|
||||
let in_fresh = state == MessageState::InFresh;
|
||||
let sort_timestamp = calc_sort_timestamp(context, sent_timestamp, chat_id, in_fresh).await?;
|
||||
let sort_timestamp =
|
||||
calc_sort_timestamp(context, sent_timestamp, chat_id, false, incoming).await?;
|
||||
|
||||
// Apply ephemeral timer changes to the chat.
|
||||
//
|
||||
@@ -984,42 +1039,14 @@ 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() || new_status.is_some() {
|
||||
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await
|
||||
if chat.is_protected() {
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1254,7 +1281,7 @@ RETURNING id
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
// "Replace" placeholder with a message that has no parts.
|
||||
replace_msg_id.delete_from_db(context).await?;
|
||||
replace_msg_id.trash(context).await?;
|
||||
}
|
||||
|
||||
chat_id.unarchive_if_not_muted(context, state).await?;
|
||||
@@ -1361,25 +1388,43 @@ async fn calc_sort_timestamp(
|
||||
context: &Context,
|
||||
message_timestamp: i64,
|
||||
chat_id: ChatId,
|
||||
is_fresh_msg: bool,
|
||||
always_sort_to_bottom: bool,
|
||||
incoming: bool,
|
||||
) -> Result<i64> {
|
||||
let mut sort_timestamp = message_timestamp;
|
||||
|
||||
// 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
|
||||
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
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?",
|
||||
(chat_id, MessageState::InFresh),
|
||||
)
|
||||
.await?;
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1527,7 +1572,9 @@ async fn create_or_lookup_group(
|
||||
}
|
||||
|
||||
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
|
||||
if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await {
|
||||
if let VerifiedEncryption::NotVerified(err) =
|
||||
has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await?
|
||||
{
|
||||
warn!(context, "Verification problem: {err:#}.");
|
||||
let s = format!("{err}. See 'Info' for more details");
|
||||
mime_parser.repl_msg_by_error(&s);
|
||||
@@ -1687,6 +1734,22 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
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?;
|
||||
|
||||
@@ -1775,20 +1838,6 @@ 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 {
|
||||
if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
@@ -2125,49 +2174,55 @@ async fn create_adhoc_group(
|
||||
Ok(Some(new_chat_id))
|
||||
}
|
||||
|
||||
async fn check_verified_properties(
|
||||
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(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
chat_type: Chattype,
|
||||
) -> Result<VerifiedEncryption> {
|
||||
use VerifiedEncryption::*;
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
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 peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
|
||||
let Some(peerstate) = &mimeparser.decryption_info.peerstate else {
|
||||
return Ok(NotVerified(
|
||||
"No peerstate, the contact isn't verified".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"
|
||||
);
|
||||
if !peerstate.has_verified_key(&mimeparser.signatures) {
|
||||
return Ok(NotVerified(
|
||||
"The message was sent with non-verified encryption".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2179,7 +2234,7 @@ async fn check_verified_properties(
|
||||
.collect::<Vec<ContactId>>();
|
||||
|
||||
if to_ids.is_empty() {
|
||||
return Ok(());
|
||||
return Ok(Verified);
|
||||
}
|
||||
|
||||
let rows = context
|
||||
@@ -2203,10 +2258,12 @@ async fn check_verified_properties(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
|
||||
for (to_addr, mut is_verified) in rows {
|
||||
info!(
|
||||
context,
|
||||
"check_verified_properties: {:?} self={:?}.",
|
||||
"has_verified_encryption: {:?} self={:?}.",
|
||||
to_addr,
|
||||
context.is_self_addr(&to_addr).await
|
||||
);
|
||||
@@ -2240,13 +2297,13 @@ async fn check_verified_properties(
|
||||
}
|
||||
}
|
||||
if !is_verified {
|
||||
bail!(
|
||||
return Ok(NotVerified(format!(
|
||||
"{} is not a member of this protected chat",
|
||||
to_addr.to_string()
|
||||
);
|
||||
to_addr
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
|
||||
@@ -2941,7 +2941,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.unwrap().id);
|
||||
assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id);
|
||||
|
||||
let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await;
|
||||
assert_eq!(received.from_id, ContactId::SELF);
|
||||
|
||||
+1
-1
@@ -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.into_iter()) {
|
||||
for b in once(self.inbox).chain(self.oboxes) {
|
||||
tokio::time::timeout(timeout_duration, b.handle)
|
||||
.await
|
||||
.log_err(context)
|
||||
|
||||
+64
-43
@@ -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};
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
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::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::key::{load_self_public_key, DcKey, Fingerprint};
|
||||
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 SignedPublicKey::load_self(context).await {
|
||||
match load_self_public_key(context).await {
|
||||
Ok(key) => Some(key.fingerprint()),
|
||||
Err(_) => {
|
||||
warn!(context, "get_self_fingerprint(): failed to load key");
|
||||
@@ -701,6 +701,22 @@ 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(())
|
||||
}
|
||||
@@ -778,11 +794,12 @@ 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;
|
||||
|
||||
@@ -791,6 +808,14 @@ 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
|
||||
@@ -874,10 +899,7 @@ mod tests {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx)
|
||||
.await
|
||||
.unwrap()
|
||||
.fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await.unwrap().fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
@@ -921,7 +943,7 @@ mod tests {
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id())
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -929,11 +951,17 @@ mod tests {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.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"));
|
||||
.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);
|
||||
}
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
@@ -969,7 +997,7 @@ mod tests {
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
{
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id())
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -977,11 +1005,16 @@ mod tests {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.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"));
|
||||
.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);
|
||||
}
|
||||
|
||||
// Check Bob sent the final message
|
||||
@@ -1008,7 +1041,7 @@ mod tests {
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Ensure Bob knows Alice_FP
|
||||
let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?;
|
||||
let alice_pubkey = load_self_public_key(&alice.ctx).await?;
|
||||
let peerstate = Peerstate {
|
||||
addr: "alice@example.org".into(),
|
||||
last_seen: 10,
|
||||
@@ -1062,7 +1095,7 @@ mod tests {
|
||||
"vc-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
@@ -1233,7 +1266,7 @@ mod tests {
|
||||
"vg-request-with-auth"
|
||||
);
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some());
|
||||
let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint();
|
||||
let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint();
|
||||
assert_eq!(
|
||||
*msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(),
|
||||
bob_fp.hex()
|
||||
@@ -1269,26 +1302,17 @@ 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
|
||||
.expect("Alice has no 1:1 chat with bob");
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
"Alice's 1:1 chat with Bob is not hidden"
|
||||
);
|
||||
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();
|
||||
// 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;
|
||||
assert!(msg.is_info());
|
||||
assert!(msg.get_text().contains("bob@example.net verified"));
|
||||
}
|
||||
@@ -1313,10 +1337,7 @@ mod tests {
|
||||
contact_alice.is_verified(&bob.ctx).await?,
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
let chat = bob
|
||||
.get_chat(&alice)
|
||||
.await
|
||||
.expect("Bob has no 1:1 chat with Alice");
|
||||
let chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
Blocked::Yes,
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
@@ -222,6 +223,22 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::contact::{Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
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 = SignedPublicKey::load_self(context).await?.fingerprint();
|
||||
let bob_fp = load_self_public_key(context).await?.fingerprint();
|
||||
msg.param.set(Param::Arg3, bob_fp.hex());
|
||||
|
||||
// Sends the grpid in the Secure-Join-Group header.
|
||||
|
||||
+2
-1
@@ -12,6 +12,7 @@ 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;
|
||||
@@ -22,9 +23,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);
|
||||
|
||||
+55
-59
@@ -393,18 +393,6 @@ 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,
|
||||
|
||||
@@ -419,6 +407,12 @@ 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 {
|
||||
@@ -515,13 +509,21 @@ trait StockStringMods: AsRef<str> + Sized {
|
||||
}
|
||||
|
||||
impl ContactId {
|
||||
/// Get contact name for stock string.
|
||||
async fn get_stock_name(self, context: &Context) -> String {
|
||||
/// 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.
|
||||
async fn get_stock_name(self, context: &Context) -> String {
|
||||
Contact::get_by_id(context, self)
|
||||
.await
|
||||
.map(|contact| contact.get_display_name().to_string())
|
||||
.unwrap_or_else(|_| self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl StockStringMods for String {}
|
||||
@@ -583,7 +585,7 @@ pub(crate) async fn msg_grp_name(
|
||||
.await
|
||||
.replace1(from_group)
|
||||
.replace2(to_group)
|
||||
.replace3(&by_contact.get_stock_name(context).await)
|
||||
.replace3(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +595,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,7 +642,7 @@ pub(crate) async fn msg_add_member_local(
|
||||
translated(context, StockMessage::MsgAddMemberBy)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +689,7 @@ pub(crate) async fn msg_del_member_local(
|
||||
translated(context, StockMessage::MsgDelMemberBy)
|
||||
.await
|
||||
.replace1(whom)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -703,7 +705,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +758,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,13 +784,9 @@ 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 {
|
||||
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}")
|
||||
}
|
||||
translated(context, StockMessage::SecureJoinReplies)
|
||||
.await
|
||||
.replace1(&contact_id.get_stock_name(context).await)
|
||||
}
|
||||
|
||||
/// Stock string: `Scan to chat with %1$s`.
|
||||
@@ -881,7 +879,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI
|
||||
} else {
|
||||
translated(context, StockMessage::MsgLocationEnabledBy)
|
||||
.await
|
||||
.replace1(&contact.get_stock_name(context).await)
|
||||
.replace1(&contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -950,7 +948,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled(
|
||||
} else {
|
||||
translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
|
||||
.await
|
||||
.replace1(&by_contact.get_stock_name(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -968,7 +966,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled(
|
||||
translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
|
||||
.await
|
||||
.replace1(timer)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -979,7 +977,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,7 +988,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1001,7 +999,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,7 +1010,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(context).await)
|
||||
.replace1(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1053,26 +1051,16 @@ pub(crate) async fn error_no_network(context: &Context) -> String {
|
||||
translated(context, StockMessage::ErrorNoNetwork).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: `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 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: `%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: `Reply`.
|
||||
@@ -1104,7 +1092,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes(
|
||||
translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
|
||||
.await
|
||||
.replace1(minutes)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,7 +1110,7 @@ pub(crate) async fn msg_ephemeral_timer_hours(
|
||||
translated(context, StockMessage::MsgEphemeralTimerHoursBy)
|
||||
.await
|
||||
.replace1(hours)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,7 +1128,7 @@ pub(crate) async fn msg_ephemeral_timer_days(
|
||||
translated(context, StockMessage::MsgEphemeralTimerDaysBy)
|
||||
.await
|
||||
.replace1(days)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1158,7 +1146,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks(
|
||||
translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
|
||||
.await
|
||||
.replace1(weeks)
|
||||
.replace2(&by_contact.get_stock_name(context).await)
|
||||
.replace2(&by_contact.get_stock_name_n_addr(context).await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1332,11 +1320,19 @@ impl Context {
|
||||
pub(crate) async fn stock_protection_msg(
|
||||
&self,
|
||||
protect: ProtectionStatus,
|
||||
from_id: ContactId,
|
||||
contact_id: Option<ContactId>,
|
||||
) -> String {
|
||||
match protect {
|
||||
ProtectionStatus::Unprotected => protection_enabled(self, from_id).await,
|
||||
ProtectionStatus::Protected => protection_disabled(self, from_id).await,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ impl Summary {
|
||||
.map(SummaryPrefix::Username)
|
||||
}
|
||||
}
|
||||
Chattype::Single | Chattype::Undefined => None,
|
||||
Chattype::Single => None,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+85
-21
@@ -27,15 +27,19 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Blocked, 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, KeyPair, KeyPairUse};
|
||||
use crate::key::{self, DcKey, KeyPairUse};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin};
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
@@ -108,9 +112,19 @@ impl TestContextManager {
|
||||
/// - Let one TestContext send a message
|
||||
/// - Let the other TestContext receive it and accept the chat
|
||||
/// - Assert that the message arrived
|
||||
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
|
||||
pub async fn send_recv_accept(
|
||||
&self,
|
||||
from: &TestContext,
|
||||
to: &TestContext,
|
||||
msg: &str,
|
||||
) -> Message {
|
||||
let received_msg = self.send_recv(from, to, msg).await;
|
||||
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
|
||||
@@ -152,6 +166,27 @@ 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)]
|
||||
@@ -556,19 +591,21 @@ impl TestContext {
|
||||
Contact::get_by_id(&self.ctx, contact_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`Chat`] with another account, if it exists.
|
||||
/// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist.
|
||||
///
|
||||
/// This first creates a contact using the configured details on the other account, then
|
||||
/// creates a 1:1 chat with this contact.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Option<Chat> {
|
||||
/// gets the 1:1 chat with this contact.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_contact(other).await;
|
||||
match ChatId::lookup_by_contact(&self.ctx, contact.id)
|
||||
let chat_id = ChatId::lookup_by_contact(&self.ctx, contact.id)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()),
|
||||
None => None,
|
||||
}
|
||||
.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()
|
||||
}
|
||||
|
||||
/// Creates or returns an existing 1:1 [`Chat`] with another account.
|
||||
@@ -627,7 +664,6 @@ 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);
|
||||
|
||||
@@ -636,7 +672,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();
|
||||
let expected = String::from_utf8(expected).unwrap().replace("\r\n", "\n");
|
||||
if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected {
|
||||
fs::write(&filename, &actual)
|
||||
.await
|
||||
@@ -654,8 +690,6 @@ 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();
|
||||
|
||||
@@ -880,7 +914,7 @@ pub fn alice_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
@@ -898,7 +932,7 @@ pub fn bob_keypair() -> KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
@@ -908,7 +942,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() -> key::KeyPair {
|
||||
pub fn fiona_keypair() -> KeyPair {
|
||||
let addr = EmailAddress::new("fiona@example.net").unwrap();
|
||||
let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc"))
|
||||
.unwrap()
|
||||
@@ -916,7 +950,7 @@ pub fn fiona_keypair() -> key::KeyPair {
|
||||
let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc"))
|
||||
.unwrap()
|
||||
.0;
|
||||
key::KeyPair {
|
||||
KeyPair {
|
||||
addr,
|
||||
public,
|
||||
secret,
|
||||
@@ -1008,6 +1042,26 @@ 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.
|
||||
@@ -1114,7 +1168,17 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
|
||||
} else {
|
||||
"[FRESH]"
|
||||
},
|
||||
if msg.is_info() { "[INFO]" } else { "" },
|
||||
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.get_viewtype() == Viewtype::VideochatInvitation {
|
||||
format!(
|
||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
mod aeap;
|
||||
mod verified_chats;
|
||||
|
||||
+4
-17
@@ -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_accept(&bob, &fiona, "Hi back").await;
|
||||
tcm.send_recv(&bob, &fiona, "Hi back").await;
|
||||
}
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "Hi").await;
|
||||
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
|
||||
tcm.send_recv(&bob, &alice, "Hi back").await;
|
||||
|
||||
if verified {
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
@@ -327,19 +327,6 @@ 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,
|
||||
@@ -368,7 +355,7 @@ async fn test_aeap_replay_attack() -> Result<()> {
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(&alice, &bob, "Hi").await;
|
||||
tcm.send_recv_accept(&bob, &alice, "Hi back").await;
|
||||
tcm.send_recv(&bob, &alice, "Hi back").await;
|
||||
|
||||
let group =
|
||||
chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?;
|
||||
|
||||
@@ -0,0 +1,764 @@
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::chat::{Chat, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
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;
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -702,7 +702,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 occurances of the RTLO Unicode character.
|
||||
/// This method strips all occurrences 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), "")
|
||||
|
||||
+2
-2
@@ -12,11 +12,12 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::chat::Chat;
|
||||
use crate::chat::{self, 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;
|
||||
@@ -25,7 +26,6 @@ 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,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
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]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,7 @@
|
||||
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]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,7 @@
|
||||
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]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,5 @@
|
||||
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]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,7 @@
|
||||
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 √
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,12 @@
|
||||
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]
|
||||
--------------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user