mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
4 Commits
v1.157.2
...
r10s/can-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48c9945be5 | ||
|
|
2281f36ce5 | ||
|
|
307b61a2c0 | ||
|
|
b455344a14 |
4
.github/workflows/deltachat-rpc-server.yml
vendored
4
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -248,10 +248,6 @@ jobs:
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-win32-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
cp result/*.whl dist/
|
||||
nix build .#deltachat-rpc-server-source
|
||||
cp result/*.tar.gz dist/
|
||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||
|
||||
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -55,9 +55,7 @@ jobs:
|
||||
- deltachat-rpc-server-aarch64-linux
|
||||
- deltachat-rpc-server-aarch64-linux-wheel
|
||||
- deltachat-rpc-server-arm64-v8a-android
|
||||
- deltachat-rpc-server-arm64-v8a-android-wheel
|
||||
- deltachat-rpc-server-armeabi-v7a-android
|
||||
- deltachat-rpc-server-armeabi-v7a-android-wheel
|
||||
- deltachat-rpc-server-armv6l-linux
|
||||
- deltachat-rpc-server-armv6l-linux-wheel
|
||||
- deltachat-rpc-server-armv7l-linux
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,9 +1,7 @@
|
||||
target/
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/build
|
||||
/dist
|
||||
/fuzz/fuzz_targets/corpus/
|
||||
/fuzz/fuzz_targets/crashes/
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
|
||||
1121
CHANGELOG.md
1121
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
## Bug reports
|
||||
|
||||
If you found a bug, [report it on GitHub](https://github.com/chatmail/core/issues).
|
||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||
If the bug you found is specific to
|
||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||
@@ -67,7 +67,7 @@ If you want to contribute a code, follow this guide.
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
4. [**Open a Pull Request**](https://github.com/chatmail/core/pulls).
|
||||
4. [**Open a Pull Request**](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
|
||||
Refer to the corresponding issue.
|
||||
|
||||
@@ -116,7 +116,7 @@ For other ways to contribute, refer to the [website](https://delta.chat/en/contr
|
||||
|
||||
You can find the list of good first issues
|
||||
and a link to this guide
|
||||
on the contributing page: <https://github.com/chatmail/core/contribute>
|
||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
644
Cargo.lock
generated
644
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.81"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.10.2", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -57,18 +57,18 @@ fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.5"
|
||||
hickory-resolver = "=0.25.0-alpha.4"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.10"
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.33", default-features = false }
|
||||
iroh-gossip = { version = "0.32", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.32", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.4.2", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", branch = "main", default-features = false }
|
||||
mailparse = "0.16"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
@@ -94,14 +94,14 @@ serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.14.0"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
tokio-rustls = { version = "0.26.1", default-features = false }
|
||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = { workspace = true }
|
||||
@@ -110,7 +110,7 @@ toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.6.1"
|
||||
blake3 = "1.5.5"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -135,7 +135,6 @@ members = [
|
||||
"deltachat-time",
|
||||
"format-flowed",
|
||||
"deltachat-contact-tools",
|
||||
"fuzz",
|
||||
]
|
||||
|
||||
[[bench]]
|
||||
@@ -175,7 +174,7 @@ harness = false
|
||||
anyhow = "1"
|
||||
async-channel = "2.3.1"
|
||||
base64 = "0.22"
|
||||
chrono = { version = "0.4.40", default-features = false }
|
||||
chrono = { version = "0.4.39", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
@@ -183,7 +182,6 @@ futures = "0.3.31"
|
||||
futures-lite = "2.6.0"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.20.2"
|
||||
|
||||
12
README.md
12
README.md
@@ -3,11 +3,11 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/chatmail/core/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/chatmail/core/actions/workflows/ci.yml/badge.svg">
|
||||
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||
</a>
|
||||
<a href="https://deps.rs/repo/github/chatmail/core">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/chatmail/core/status.svg">
|
||||
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
|
||||
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -104,7 +104,7 @@ For more commands type:
|
||||
## Installing libdeltachat system wide
|
||||
|
||||
```
|
||||
$ git clone https://github.com/chatmail/core.git
|
||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
$ cd deltachat-core-rust
|
||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||
$ cmake --build build
|
||||
@@ -139,7 +139,7 @@ $ cargo test -- --ignored
|
||||
|
||||
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
||||
```sh
|
||||
$ cargo install cargo-bolero@0.8.0
|
||||
$ cargo install cargo-bolero
|
||||
```
|
||||
|
||||
Run fuzzing tests with
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
1. Resolve all [blocker issues](https://github.com/chatmail/core/labels/blocker).
|
||||
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||
|
||||
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||
`[1.116.0]: https://github.com/chatmail/core/compare/v1.115.2...v1.116.0`
|
||||
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ filter_unconventional = false
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/chatmail/core/pull/${2}))"}, # replace pull request / issue numbers
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
@@ -82,11 +82,11 @@ footer = """
|
||||
{% if release.version -%}
|
||||
{% if release.previous.version -%}
|
||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||
https://github.com/chatmail/core\
|
||||
https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||
{% endif -%}
|
||||
{% else -%}
|
||||
[unreleased]: https://github.com/chatmail/core\
|
||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||
/compare/{{ release.previous.version }}..HEAD
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -220,7 +220,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
* - Strings in function arguments or return values are usually UTF-8 encoded.
|
||||
*
|
||||
* - The issue-tracker for the core library is here:
|
||||
* <https://github.com/chatmail/core/issues>
|
||||
* <https://github.com/deltachat/deltachat-core-rust/issues>
|
||||
*
|
||||
* If you need further assistance,
|
||||
* please do not hesitate to contact us
|
||||
@@ -440,6 +440,17 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||
* generate recommended key type (default),
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Curve25519 keypair
|
||||
* DC_KEY_GEN_RSA4096 (3)=
|
||||
* generate RSA 4096 keypair
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
* - `delete_device_after` = 0=do not delete messages from device automatically (default),
|
||||
* >=1=seconds, after which messages are deleted automatically from the device.
|
||||
* Messages in the "saved messages" chat (see dc_chat_is_self_talk()) are skipped.
|
||||
@@ -1028,6 +1039,20 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
|
||||
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message can be edited using dc_send_edit_request().
|
||||
*
|
||||
* Messages that cannot be edited are eg. info messages or messages not sent by self.
|
||||
* UI will usually check this function whether to display an "Edit" option or not.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to check.
|
||||
* @return 1=message can be edited, 0=message cannot be edited.
|
||||
*/
|
||||
int dc_can_send_edit_request (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send chat members a request to edit the given message's text.
|
||||
*
|
||||
@@ -1045,21 +1070,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
void dc_send_edit_request (dc_context_t* context, uint32_t msg_id, const char* new_text);
|
||||
|
||||
|
||||
/**
|
||||
* Send chat members a request to delete the given messages.
|
||||
*
|
||||
* Only outgoing messages can be deleted this way
|
||||
* and all messages must be in the same chat.
|
||||
* No tombstone or sth. like that is left.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_ids An array of uint32_t containing all message IDs to delete.
|
||||
* @param msg_cnt The number of messages IDs in the msg_ids array.
|
||||
*/
|
||||
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
|
||||
|
||||
|
||||
/**
|
||||
* Send invitation to a videochat.
|
||||
*
|
||||
@@ -1950,7 +1960,24 @@ void dc_download_full_msg (dc_context_t* context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on all devices and
|
||||
* Get the raw mime-headers of the given message.
|
||||
* Raw headers are saved for incoming messages
|
||||
* only if `dc_set_config(context, "save_mime_headers", "1")`
|
||||
* was called before.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID, must be the ID of an incoming message.
|
||||
* @return Raw headers as a multi-line string, must be released using dc_str_unref() after usage.
|
||||
* Returns NULL if there are no headers saved for the given message,
|
||||
* e.g. because of save_mime_headers is not set
|
||||
* or the message is not incoming.
|
||||
*/
|
||||
char* dc_get_mime_headers (dc_context_t* context, uint32_t msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* on the IMAP server.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2463,9 +2490,8 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251 // deprecated
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
@@ -2512,14 +2538,11 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to create an account on the given domain,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* - DC_QR_BACKUP2:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
* - DC_QR_BACKUP_TOO_NEW:
|
||||
* show a hint to the user that this backup comes from a newer Delta Chat version
|
||||
* and this device needs an update
|
||||
*
|
||||
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
@@ -6286,18 +6309,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED 2021
|
||||
|
||||
|
||||
/**
|
||||
* Chat was deleted.
|
||||
* This event is emitted in response to dc_delete_chat()
|
||||
* called on this or another device.
|
||||
* The event is a good place to remove notifications or homescreen shortcuts.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) 0
|
||||
*/
|
||||
#define DC_EVENT_CHAT_DELETED 2023
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
*
|
||||
@@ -6538,6 +6549,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_MEDIA_QUALITY_WORSE 1
|
||||
|
||||
|
||||
/*
|
||||
* Values for dc_get|set_config("key_gen_type")
|
||||
*/
|
||||
#define DC_KEY_GEN_DEFAULT 0
|
||||
#define DC_KEY_GEN_RSA2048 1
|
||||
#define DC_KEY_GEN_ED25519 2
|
||||
#define DC_KEY_GEN_RSA4096 3
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_PROVIDER_STATUS DC_PROVIDER_STATUS
|
||||
*
|
||||
|
||||
@@ -544,7 +544,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgDeleted { .. } => 2016,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ChatDeleted { .. } => 2023,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
EventType::LocationChanged(_) => 2035,
|
||||
EventType::ConfigureProgress { .. } => 2041,
|
||||
@@ -611,8 +610,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::MsgDeleted { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. }
|
||||
| EventType::ChatDeleted { chat_id } => chat_id.to_u32() as libc::c_int,
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id.to_u32() as libc::c_int
|
||||
@@ -678,7 +676,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::AccountsItemChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
@@ -770,7 +767,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
@@ -1045,6 +1041,22 @@ pub unsafe extern "C" fn dc_send_text_msg(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_can_send_edit_request(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_can_send_edit_request()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(chat::can_send_edit_request(ctx, MsgId::new(msg_id)))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_edit_request(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1062,25 +1074,6 @@ pub unsafe extern "C" fn dc_send_edit_request(
|
||||
.unwrap_or_log_default(ctx, "Failed to send text edit")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_delete_request(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_send_delete_request()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
block_on(message::delete_msgs_ex(ctx, &msg_ids, true))
|
||||
.context("failed dc_send_delete_request() call")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1658,7 +1651,6 @@ pub unsafe extern "C" fn dc_get_chat(context: *mut dc_context_t, chat_id: u32) -
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let context: Context = ctx.clone();
|
||||
|
||||
block_on(async move {
|
||||
match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
|
||||
@@ -1954,6 +1946,28 @@ pub unsafe extern "C" fn dc_get_msg_html(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_mime_headers(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_mime_headers()");
|
||||
return ptr::null_mut(); // NULL explicitly defined as "no mime headers"
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let mime = message::get_mime_headers(ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get mime headers");
|
||||
if mime.is_empty() {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
mime.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_delete_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -2983,7 +2997,7 @@ pub unsafe extern "C" fn dc_chatlist_get_context(
|
||||
/// context, but the Rust API does not, so the FFI layer needs to glue
|
||||
/// these together.
|
||||
pub struct ChatWrapper {
|
||||
context: Context,
|
||||
context: *const dc_context_t,
|
||||
chat: chat::Chat,
|
||||
}
|
||||
|
||||
@@ -3050,13 +3064,14 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
return ptr::null_mut(); // NULL explicitly defined as "no image"
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(&ffi_chat.context).await {
|
||||
match ffi_chat.chat.get_profile_image(ctx).await {
|
||||
Ok(Some(p)) => p.to_string_lossy().strdup(),
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ffi_chat.context, "failed to get profile image: {err:#}");
|
||||
error!(ctx, "failed to get profile image: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
@@ -3070,9 +3085,9 @@ pub unsafe extern "C" fn dc_chat_get_color(chat: *mut dc_chat_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
let ctx = &*ffi_chat.context;
|
||||
|
||||
block_on(ffi_chat.chat.get_color(&ffi_chat.context))
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed get_color")
|
||||
block_on(ffi_chat.chat.get_color(ctx)).unwrap_or_log_default(ctx, "Failed get_color")
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3136,9 +3151,10 @@ pub unsafe extern "C" fn dc_chat_can_send(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
block_on(ffi_chat.chat.can_send(&ffi_chat.context))
|
||||
let ctx = &*ffi_chat.context;
|
||||
block_on(ffi_chat.chat.can_send(ctx))
|
||||
.context("can_send failed")
|
||||
.log_err(&ffi_chat.context)
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
||||
@@ -104,7 +103,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Proxy { .. } => LotState::QrProxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
@@ -131,7 +129,6 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
@@ -181,9 +178,9 @@ pub enum LotState {
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
QrBackup2 = 252,
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackupTooNew = 255,
|
||||
QrBackup2 = 252,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
license = "MPL-2.0"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-jsonrpc-server"
|
||||
@@ -17,7 +17,7 @@ anyhow = { workspace = true }
|
||||
deltachat = { workspace = true }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
schemars = "0.8.22"
|
||||
schemars = "0.8.21"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
@@ -1518,18 +1518,6 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Imports contacts from a vCard.
|
||||
///
|
||||
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||
async fn import_vcard_contents(&self, account_id: u32, vcard: String) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
Ok(deltachat::contact::import_vcard(&ctx, &vcard)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns a vCard containing contacts with the given ids.
|
||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -2016,16 +2004,6 @@ impl CommandApi {
|
||||
Ok(msg_id)
|
||||
}
|
||||
|
||||
async fn send_edit_request(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
new_text: String,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::send_edit_request(&ctx, MsgId::new(msg_id), new_text).await
|
||||
}
|
||||
|
||||
/// Checks if messages can be sent to a given chat.
|
||||
async fn can_send(&self, account_id: u32, chat_id: u32) -> Result<bool> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
@@ -243,12 +243,6 @@ pub enum EventType {
|
||||
timer: u32,
|
||||
},
|
||||
|
||||
/// Chat deleted.
|
||||
ChatDeleted {
|
||||
/// Chat ID.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
@@ -505,9 +499,6 @@ impl From<CoreEventType> for EventType {
|
||||
timer: timer.to_u32(),
|
||||
}
|
||||
}
|
||||
CoreEventType::ChatDeleted { chat_id } => ChatDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
|
||||
@@ -63,7 +63,6 @@ pub enum QrObject {
|
||||
/// Iroh node address.
|
||||
node_addr: String,
|
||||
},
|
||||
BackupTooNew {},
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
@@ -101,15 +100,11 @@ pub enum QrObject {
|
||||
/// URL scanned.
|
||||
///
|
||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Url { url: String },
|
||||
/// Text scanned.
|
||||
///
|
||||
/// Ask the user if they want to copy the text to clipboard.
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
Text { text: String },
|
||||
/// Ask the user if they want to withdraw their own QR code.
|
||||
WithdrawVerifyContact {
|
||||
/// Contact ID.
|
||||
@@ -165,9 +160,7 @@ pub enum QrObject {
|
||||
/// `dclogin:` scheme parameters.
|
||||
///
|
||||
/// Ask the user if they want to login with the email address.
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
Login { address: String },
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
@@ -224,7 +217,6 @@ impl From<Qr> for QrObject {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
auth_token,
|
||||
},
|
||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chatmail/core.git"
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.157.2"
|
||||
"version": "1.155.6"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -92,7 +92,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = receive_imf(context, &data, false).await {
|
||||
@@ -126,7 +126,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
real_spec = rs.unwrap();
|
||||
}
|
||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||
if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() {
|
||||
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
} else {
|
||||
@@ -140,10 +140,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
if name.ends_with(".eml") {
|
||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||
println!("Import: {path_plus_name}");
|
||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if poke_eml_file(context, path_plus_name).await.is_ok() {
|
||||
read_cnt += 1
|
||||
}
|
||||
}
|
||||
@@ -1281,7 +1278,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
||||
if let Ok(buf) = read_file(&context, &arg1).await {
|
||||
let (width, height) = get_filemeta(&buf)?;
|
||||
println!("width={width}, height={height}");
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
@@ -40,16 +38,6 @@ class Account:
|
||||
"""Remove the account."""
|
||||
self._rpc.remove_account(self.id)
|
||||
|
||||
def clone(self) -> "Account":
|
||||
"""Clone given account."""
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
self.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
new_account = self.manager.add_account()
|
||||
new_account.import_backup(files[0])
|
||||
return new_account
|
||||
|
||||
def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
self._rpc.start_io(self.id)
|
||||
@@ -95,10 +83,6 @@ class Account:
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
def check_qr(self, qr):
|
||||
"""Parse QR code contents.
|
||||
|
||||
This function takes the raw text scanned
|
||||
and checks what can be done with it."""
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
@@ -134,28 +118,11 @@ class Account:
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def make_vcard(self, contacts: list[Contact]) -> str:
|
||||
"""Create vCard with the given contacts."""
|
||||
assert all(contact.account == self for contact in contacts)
|
||||
contact_ids = [contact.id for contact in contacts]
|
||||
return self._rpc.make_vcard(self.id, contact_ids)
|
||||
|
||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||
"""Import vCard.
|
||||
|
||||
Return created or modified contacts in the order they appear in vCard."""
|
||||
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
vcard = account.self_contact.make_vcard()
|
||||
[contact] = self.import_vcard(vcard)
|
||||
addr = account.get_config("addr")
|
||||
contact = self.create_contact(addr)
|
||||
return contact.create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
"""Return device chat."""
|
||||
return self.device_contact.create_chat()
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
@@ -216,11 +183,6 @@ class Account:
|
||||
"""This account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
"""This account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
def get_chatlist(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
@@ -339,20 +301,6 @@ class Account:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_msgs_changed_event(self):
|
||||
"""Wait for messages changed event and return it."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED:
|
||||
return event
|
||||
|
||||
def wait_for_msgs_noticed_event(self):
|
||||
"""Wait for messages noticed event and return it."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
return event
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
@@ -404,7 +352,3 @@ class Account:
|
||||
"""Import keys."""
|
||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||
|
||||
def initiate_autocrypt_key_transfer(self) -> None:
|
||||
"""Send Autocrypt Setup Message."""
|
||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
||||
|
||||
@@ -66,4 +66,4 @@ class Contact:
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
return self.account.make_vcard([self])
|
||||
return self._rpc.make_vcard(self.account.id, [self.id])
|
||||
|
||||
@@ -52,9 +52,6 @@ class Message:
|
||||
"""Mark the message as seen."""
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
|
||||
@@ -157,7 +157,11 @@ def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Acc
|
||||
|
||||
bob.wait_for_incoming_msg_event()
|
||||
|
||||
alice_second_device = alice.clone()
|
||||
alice_second_device: Account = acfactory.get_unconfigured_account()
|
||||
|
||||
alice._rpc.provide_backup.future(alice.id)
|
||||
backup_code = alice._rpc.get_backup_qr(alice.id)
|
||||
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
|
||||
alice_second_device.start_io()
|
||||
alice.clear_all_events()
|
||||
alice_second_device.clear_all_events()
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def wait_for_autocrypt_setup_message(account):
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.msg_id != 0:
|
||||
msg_id = event.msg_id
|
||||
msg = account.get_message_by_id(msg_id)
|
||||
if msg.get_snapshot().is_setupmessage:
|
||||
return msg
|
||||
|
||||
|
||||
def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Test that entering wrong code returns an error.
|
||||
with pytest.raises(JsonRpcError):
|
||||
msg.continue_autocrypt_key_transfer("7037-0673-6287-3013-4095-7956-5617-6806-6756")
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
# Send the second Autocrypt Setup Message and import it.
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
|
||||
msg.continue_autocrypt_key_transfer(setup_code)
|
||||
@@ -4,7 +4,6 @@ import time
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
@@ -27,21 +26,17 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Test that if Bob imports a key,
|
||||
# backwards verification is not lost
|
||||
# because default key is not changed.
|
||||
# Test that if Bob changes the key, backwards verification is lost.
|
||||
logging.info("Bob 2 is created")
|
||||
bob2 = acfactory.new_configured_account()
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob tries to import a key")
|
||||
# Importing a second key is not allowed.
|
||||
with pytest.raises(JsonRpcError):
|
||||
bob.import_self_keys(tmp_path)
|
||||
logging.info("Bob imports a key")
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "1"
|
||||
assert bob.get_config("key_id") == "2"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
assert not bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
@@ -60,12 +55,15 @@ def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice2 = alice.clone()
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group", protect=protect)
|
||||
@@ -655,14 +653,12 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_msgs_changed_event().msg_id).get_snapshot()
|
||||
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
|
||||
|
||||
logging.info("Alice withdraws QR code.")
|
||||
|
||||
@@ -287,9 +287,12 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
def test_reaction_seen_on_another_dev(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
alice2.start_io()
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
@@ -658,7 +661,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
assert snapshot.chat == bob_chat_alice
|
||||
|
||||
|
||||
def test_markseen_contact_request(acfactory):
|
||||
def test_markseen_contact_request(acfactory, tmp_path):
|
||||
"""
|
||||
Test that seen status is synchronized for contact request messages
|
||||
even though read receipt is not sent.
|
||||
@@ -666,7 +669,10 @@ def test_markseen_contact_request(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Bob sets up a second device.
|
||||
bob2 = bob.clone()
|
||||
bob.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
bob2 = acfactory.get_unconfigured_account()
|
||||
bob2.import_backup(files[0])
|
||||
bob2.start_io()
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -710,28 +716,3 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.start_io()
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
|
||||
logging.info("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1.wait_for_incoming_msg_event()
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone_chat.send_text("Hi back")
|
||||
ev = ac1.wait_for_msgs_noticed_event()
|
||||
|
||||
assert ev.chat_id == first_msg.get_snapshot().chat_id
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -5,13 +5,13 @@ over standard I/O.
|
||||
|
||||
## Install
|
||||
|
||||
To download binary pre-builds check the [releases page](https://github.com/chatmail/core/releases).
|
||||
To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases).
|
||||
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
|
||||
|
||||
To install from source run:
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server
|
||||
cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server
|
||||
```
|
||||
|
||||
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chatmail/core.git"
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "node scripts/update_optional_dependencies_and_version.js"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.157.2"
|
||||
"version": "1.155.6"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ def write_package_json(platform_path, rust_target, my_binary_name):
|
||||
"license": "MPL-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chatmail/core.git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ENV_VAR_NAME } from "./const.js";
|
||||
|
||||
const cargoInstallCommand =
|
||||
"cargo install --git https://github.com/chatmail/core deltachat-rpc-server";
|
||||
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
|
||||
|
||||
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
|
||||
return `deltachat-rpc-server not found:
|
||||
|
||||
20
deny.toml
20
deny.toml
@@ -10,11 +10,8 @@ ignore = [
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# Unmaintained backoff
|
||||
"RUSTSEC-2025-0012",
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
# DNSSEC validation that we don't use anyway.
|
||||
"RUSTSEC-2025-0006",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -30,16 +27,12 @@ skip = [
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
@@ -52,7 +45,6 @@ skip = [
|
||||
{ name = "tokio-tungstenite", version = "0.21.0" },
|
||||
{ name = "tungstenite", version = "0.21.0" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
{ name = "windows_aarch64_msvc" },
|
||||
@@ -69,7 +61,6 @@ skip = [
|
||||
{ name = "windows_x86_64_gnu" },
|
||||
{ name = "windows_x86_64_gnullvm" },
|
||||
{ name = "windows_x86_64_msvc" },
|
||||
{ name = "zerocopy", version = "0.7.32" },
|
||||
]
|
||||
|
||||
|
||||
@@ -84,6 +75,7 @@ allow = [
|
||||
"ISC",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Zlib",
|
||||
@@ -95,3 +87,9 @@ expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[sources.allow-org]
|
||||
# Organisations which we allow git sources from.
|
||||
github = [
|
||||
"stalwartlabs",
|
||||
]
|
||||
|
||||
47
flake.nix
47
flake.nix
@@ -18,9 +18,9 @@
|
||||
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
|
||||
androidSdk = android.sdk.${system} (sdkPkgs:
|
||||
builtins.attrValues {
|
||||
inherit (sdkPkgs) ndk-27-2-12479018 cmdline-tools-latest;
|
||||
inherit (sdkPkgs) ndk-27-0-11902837 cmdline-tools-latest;
|
||||
});
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.2.12479018";
|
||||
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/27.0.11902837";
|
||||
|
||||
rustSrc = nix-filter.lib {
|
||||
root = ./.;
|
||||
@@ -30,7 +30,6 @@
|
||||
include = [
|
||||
./benches
|
||||
./assets
|
||||
./fuzz
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
./CMakeLists.txt
|
||||
@@ -88,6 +87,9 @@
|
||||
};
|
||||
cargoLock = {
|
||||
lockFile = ./Cargo.lock;
|
||||
outputHashes = {
|
||||
"mail-builder-0.4.1" = "sha256-1hnsU76ProcX7iXT2UBjHnHbJ/ROT3077sLi3+yAV58=";
|
||||
};
|
||||
};
|
||||
mkRustPackage = packageName:
|
||||
naersk'.buildPackage {
|
||||
@@ -309,41 +311,10 @@
|
||||
LD = "${targetCc}";
|
||||
};
|
||||
|
||||
mkAndroidPackages = arch:
|
||||
let
|
||||
rpc-server = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
in
|
||||
{
|
||||
"deltachat-rpc-server-${arch}-android" = rpc-server;
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
"deltachat-rpc-server-${arch}-android-wheel" =
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "deltachat-rpc-server-${arch}-android-wheel";
|
||||
version = manifest.version;
|
||||
src = nix-filter.lib {
|
||||
root = ./.;
|
||||
include = [
|
||||
"scripts/wheel-rpc-server.py"
|
||||
"deltachat-rpc-server/README.md"
|
||||
"LICENSE"
|
||||
"Cargo.toml"
|
||||
];
|
||||
};
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.python3Packages.wheel
|
||||
];
|
||||
buildInputs = [
|
||||
rpc-server
|
||||
];
|
||||
buildPhase = ''
|
||||
mkdir tmp
|
||||
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
|
||||
python3 scripts/wheel-rpc-server.py ${arch}-android tmp/deltachat-rpc-server
|
||||
'';
|
||||
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
|
||||
};
|
||||
};
|
||||
mkAndroidPackages = arch: {
|
||||
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
|
||||
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
|
||||
};
|
||||
|
||||
mkRustPackages = arch:
|
||||
let
|
||||
|
||||
6836
fuzz/Cargo.lock
generated
Normal file
6836
fuzz/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,28 @@ name = "deltachat-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.8"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
mailparse = "0.13"
|
||||
deltachat = { path = ".." }
|
||||
format-flowed = { path = "../format-flowed" }
|
||||
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_dateparse"
|
||||
path = "fuzz_targets/fuzz_dateparse.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_simplify"
|
||||
path = "fuzz_targets/fuzz_simplify.rs"
|
||||
harness = false
|
||||
|
||||
[[test]]
|
||||
name = "fuzz_mailparse"
|
||||
path = "fuzz_targets/fuzz_mailparse.rs"
|
||||
|
||||
@@ -9,7 +9,7 @@ fn round_trip(input: &str) -> String {
|
||||
|
||||
fn main() {
|
||||
check!().for_each(|data: &[u8]| {
|
||||
if let Ok(input) = std::str::from_utf8(data) {
|
||||
if let Ok(input) = std::str::from_utf8(data.into()) {
|
||||
let input = input.trim().to_string();
|
||||
|
||||
// Only consider inputs that are the result of unformatting format=flowed text.
|
||||
|
||||
13
fuzz/fuzz_targets/fuzz_simplify.rs
Normal file
13
fuzz/fuzz_targets/fuzz_simplify.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use bolero::check;
|
||||
|
||||
use deltachat::fuzzing::simplify;
|
||||
|
||||
fn main() {
|
||||
check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) {
|
||||
Ok(input) => {
|
||||
simplify(input.clone(), true);
|
||||
simplify(input, false);
|
||||
}
|
||||
Err(_err) => {}
|
||||
});
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
CFFI Python Bindings
|
||||
============================
|
||||
|
||||
This package provides `Python bindings`_ to the `chatmail core library`_
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
.. _`chatmail core library`: https://github.com/chatmail/core
|
||||
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`Python bindings`: https://py.delta.chat/
|
||||
|
||||
@@ -43,7 +43,7 @@ Bootstrap Rust and Cargo by using rustup::
|
||||
|
||||
Then clone the deltachat-core-rust repo::
|
||||
|
||||
git clone https://github.com/chatmail/core
|
||||
git clone https://github.com/deltachat/deltachat-core-rust
|
||||
cd deltachat-core-rust
|
||||
|
||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||
|
||||
@@ -2,7 +2,7 @@ Delta Chat Python bindings, new and old
|
||||
=======
|
||||
|
||||
`Delta Chat <https://delta.chat/>`_ provides two kinds of Python bindings
|
||||
to the `Rust Core <https://github.com/chatmail/core>`_:
|
||||
to the `Rust Core <https://github.com/deltachat/deltachat-core-rust>`_:
|
||||
JSON-RPC bindings and CFFI bindings.
|
||||
When starting a new project it is recommended to use JSON-RPC bindings,
|
||||
which are used in the Delta Chat Desktop app through generated Typescript-bindings.
|
||||
@@ -41,4 +41,4 @@ as the CFFI bindings are increasingly in maintenance-only mode.
|
||||
.. _virtualenv: http://pypi.org/project/virtualenv/
|
||||
.. _merlinux: http://merlinux.eu
|
||||
.. _pypi: http://pypi.org/
|
||||
.. _`issue-tracker`: https://github.com/chatmail/core
|
||||
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core-rust
|
||||
|
||||
@@ -3,9 +3,9 @@ Development
|
||||
===========
|
||||
|
||||
To develop JSON-RPC bindings,
|
||||
clone the `chatmail core <https://github.com/chatmail/core/>`_ repository::
|
||||
clone the `deltachat-core-rust <https://github.com/deltachat/deltachat-core-rust/>`_ repository::
|
||||
|
||||
git clone https://github.com/chatmail/core.git
|
||||
git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
@@ -17,8 +17,8 @@ Install ``deltachat-rpc-server``
|
||||
To get ``deltachat-rpc-server`` binary you have three options:
|
||||
|
||||
1. Install ``deltachat-rpc-server`` from PyPI using ``pip install deltachat-rpc-server``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/chatmail/core/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/chatmail/core/releases and install it into ``PATH``.
|
||||
2. Build and install ``deltachat-rpc-server`` from source with ``cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server``.
|
||||
3. Download prebuilt release from https://github.com/deltachat/deltachat-core-rust/releases and install it into ``PATH``.
|
||||
|
||||
Check that ``deltachat-rpc-server`` is installed and can run::
|
||||
|
||||
@@ -33,4 +33,4 @@ Install ``deltachat-rpc-client``
|
||||
To get ``deltachat-rpc-client`` Python library you can:
|
||||
|
||||
1. Install ``deltachat-rpc-client`` from PyPI using ``pip install deltachat-rpc-client``.
|
||||
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/chatmail/core.git@main#subdirectory=deltachat-rpc-client``.
|
||||
2. Install ``deltachat-rpc-client`` from source with ``pip install git+https://github.com/deltachat/deltachat-core-rust.git@main#subdirectory=deltachat-rpc-client``.
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.157.2"
|
||||
version = "1.155.6"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
@@ -29,8 +29,8 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/chatmail/core/"
|
||||
"Bug Tracker" = "https://github.com/chatmail/core/issues"
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
"Mastodon" = "https://chaos.social/@delta"
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ class Message:
|
||||
"""extract key and use it as primary key for this account."""
|
||||
res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
|
||||
if res == 0:
|
||||
raise ValueError("Importing the key from Autocrypt Setup Message failed")
|
||||
raise ValueError("could not decrypt")
|
||||
|
||||
@props.with_doc
|
||||
def time_sent(self):
|
||||
@@ -285,6 +285,23 @@ class Message:
|
||||
"""Force the message to be sent in plain text."""
|
||||
lib.dc_msg_force_plaintext(self._dc_msg)
|
||||
|
||||
def get_mime_headers(self):
|
||||
"""return mime-header object for an incoming message.
|
||||
|
||||
This only returns a non-None object if ``save_mime_headers``
|
||||
config option was set and the message is incoming.
|
||||
|
||||
:returns: email-mime message object (with headers only, no body).
|
||||
"""
|
||||
import email
|
||||
|
||||
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
|
||||
if mime_headers:
|
||||
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
|
||||
if isinstance(s, bytes):
|
||||
return email.message_from_bytes(s)
|
||||
return email.message_from_string(s)
|
||||
|
||||
@property
|
||||
def error(self) -> Optional[str]:
|
||||
"""Error message."""
|
||||
|
||||
@@ -423,6 +423,8 @@ class ACFactory:
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
|
||||
@@ -510,7 +510,6 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
for ac in [ac1, ac1_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
@@ -561,7 +560,6 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
missing, cannot encrypt".
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
@@ -617,7 +615,6 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
- Now the seconds device has all members verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
|
||||
@@ -31,6 +31,37 @@ def test_basic_imap_api(acfactory, tmp_path):
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@pytest.mark.ignored()
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
|
||||
acfactory.bring_accounts_online()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send unencrypted message to ac2")
|
||||
chat.send_text("message1")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message1"
|
||||
assert not msg_in.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send encrypted message to ac1")
|
||||
msg_in.chat.send_text("message2")
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
msg2_in = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg2_in.text == "message2"
|
||||
assert msg2_in.is_encrypted()
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
msg2_in.chat.send_text("message3")
|
||||
lp.sec("ac2: waiting for message from ac1")
|
||||
msg3_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg3_in.text == "message3"
|
||||
assert msg3_in.is_encrypted()
|
||||
|
||||
|
||||
def test_configure_canceled(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac1.stop_ongoing()
|
||||
@@ -54,6 +85,27 @@ def test_configure_unref(tmp_path):
|
||||
lib.dc_context_unref(dc_context)
|
||||
|
||||
|
||||
def test_export_import_self_keys(acfactory, tmp_path, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
export_files = ac1.export_self_keys(str(dir))
|
||||
assert len(export_files) == 2
|
||||
for x in export_files:
|
||||
assert x.startswith(str(dir))
|
||||
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
ac1._evtracker.consume_events()
|
||||
|
||||
lp.sec("exported keys (private and public)")
|
||||
for name in dir.iterdir():
|
||||
lp.indent(str(dir / name))
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(str(dir))
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
|
||||
assert key_id2 == key_id
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
@@ -919,8 +971,67 @@ def test_gossip_optimization(acfactory, lp):
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
|
||||
def test_gossip_encryption_preference(acfactory, lp):
|
||||
"""Test that encryption preference of group members is gossiped to new members.
|
||||
This is a Delta Chat extension to Autocrypt 1.1.0, which Autocrypt-Gossip headers
|
||||
SHOULD NOT contain encryption preference.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
lp.sec("ac1 learns that ac2 prefers encryption")
|
||||
ac1.create_chat(ac2)
|
||||
msg = ac2.create_chat(ac1).send_text("first message")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "first message"
|
||||
assert not msg.is_encrypted()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
assert msg.chat.get_encryption_info() == res
|
||||
lp.sec("ac2 learns that ac3 prefers encryption")
|
||||
ac2.create_chat(ac3)
|
||||
msg = ac3.create_chat(ac2).send_text("I prefer encryption")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "I prefer encryption"
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
lp.sec("ac3 does not know that ac1 prefers encryption")
|
||||
ac1.create_chat(ac3)
|
||||
chat = ac3.create_chat(ac1)
|
||||
res = "No encryption:\n{}".format(ac1.get_config("addr"))
|
||||
assert chat.get_encryption_info() == res
|
||||
msg = chat.send_text("not encrypted")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "not encrypted"
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
encryption_info = group_chat.get_encryption_info()
|
||||
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
|
||||
assert encryption_info == res
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac3 learns that ac1 prefers encryption")
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
encryption_info = msg.chat.get_encryption_info().splitlines()
|
||||
assert encryption_info[0] == "End-to-end encryption preferred:"
|
||||
assert ac1.get_config("addr") in encryption_info[1:]
|
||||
assert ac2.get_config("addr") in encryption_info[1:]
|
||||
msg = chat.send_text("encrypted")
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2.set_config("save_mime_headers", "1")
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1109,6 +1220,93 @@ def test_dont_show_emails(acfactory, lp):
|
||||
assert len(msg.chat.get_messages()) == 3
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac1_clone.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "0")
|
||||
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1.create_chat(ac2)
|
||||
ac1_clone.create_chat(ac2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
|
||||
lp.sec("Send a first message from ac2 to ac1 and check that it's 'fresh'")
|
||||
first_msg_id = ac2.create_chat(ac1).send_text("Hi")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone.create_chat(ac2).send_text("Hi back")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
|
||||
assert ev.data1 == first_msg_id.chat.id
|
||||
assert ac1.create_chat(ac2).count_fresh_messages() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
|
||||
def test_prefer_encrypt(acfactory, lp):
|
||||
"""Test quorum rule for encryption preference in 1:1 and group chat."""
|
||||
ac1 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac2 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
ac3 = acfactory.new_online_configuring_account(fix_is_chatmail=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
ac2.set_config("e2ee_enabled", "1")
|
||||
ac3.set_config("e2ee_enabled", "0")
|
||||
|
||||
# Make sure we do not send a copy to ourselves. This is to
|
||||
# test that we count own preference even when we are not in
|
||||
# the recipient list.
|
||||
ac1.set_config("bcc_self", "0")
|
||||
ac2.set_config("bcc_self", "0")
|
||||
ac3.set_config("bcc_self", "0")
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac1: sending message to ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
msg1 = chat1.send_text("message1")
|
||||
assert not msg1.is_encrypted()
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac2: sending message to ac1")
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
msg2 = chat2.send_text("message2")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg2.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending message to group chat with ac2 and ac3")
|
||||
group = ac1.create_group_chat("hello")
|
||||
group.add_contact(ac2)
|
||||
group.add_contact(ac3)
|
||||
msg3 = group.send_text("message3")
|
||||
assert not msg3.is_encrypted()
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac3: start preferring encryption and inform ac1")
|
||||
ac3.set_config("e2ee_enabled", "1")
|
||||
chat3 = ac3.create_chat(ac1)
|
||||
msg4 = chat3.send_text("message4")
|
||||
# Own preference is `Mutual` and we have the peer's key.
|
||||
assert msg4.is_encrypted()
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: sending another message to group chat with ac2 and ac3")
|
||||
msg5 = group.send_text("message5")
|
||||
# Majority prefers encryption now
|
||||
assert msg5.is_encrypted()
|
||||
|
||||
|
||||
def test_bot(acfactory, lp):
|
||||
"""Test that bot messages can be identified as such"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1137,6 +1335,59 @@ def test_bot(acfactory, lp):
|
||||
assert msg_in.is_bot()
|
||||
|
||||
|
||||
def test_quote_encrypted(acfactory, lp):
|
||||
"""Test that replies to encrypted messages with quotes are encrypted."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg1 = chat.send_text("message1")
|
||||
assert not msg1.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "message1"
|
||||
assert not msg2.is_encrypted()
|
||||
|
||||
lp.sec("create new chat with contact and send back (encrypted) message")
|
||||
msg2.create_chat().send_text("message-back")
|
||||
|
||||
lp.sec("wait for ac1 to receive message")
|
||||
msg3 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg3.text == "message-back"
|
||||
assert msg3.is_encrypted()
|
||||
|
||||
lp.sec("ac1: e2ee_enabled=0 and see if reply is encrypted")
|
||||
print("ac1: e2ee_enabled={}".format(ac1.get_config("e2ee_enabled")))
|
||||
print("ac2: e2ee_enabled={}".format(ac2.get_config("e2ee_enabled")))
|
||||
ac1.set_config("e2ee_enabled", "0")
|
||||
|
||||
for quoted_msg in msg1, msg3:
|
||||
# Save the draft with a quote.
|
||||
msg_draft = Message.new_empty(ac1, "text")
|
||||
msg_draft.set_text("message reply")
|
||||
msg_draft.quote = quoted_msg
|
||||
chat.set_draft(msg_draft)
|
||||
|
||||
# Get the draft and send it.
|
||||
msg_draft = chat.get_draft()
|
||||
chat.send_msg(msg_draft)
|
||||
|
||||
chat.set_draft(None)
|
||||
assert chat.get_draft() is None
|
||||
|
||||
# Quote should be replaced with "..." if quoted message is encrypted.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.text == "message reply"
|
||||
assert not msg_in.is_encrypted()
|
||||
if quoted_msg.is_encrypted():
|
||||
assert msg_in.quoted_text == "..."
|
||||
else:
|
||||
assert msg_in.quoted_text == quoted_msg.text
|
||||
|
||||
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
"""Test that replies with an attachment and a quote are received correctly."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1170,6 +1421,26 @@ def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
assert open(received_reply.filename).read() == "data to send"
|
||||
|
||||
|
||||
def test_saved_mime_on_received_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("configure ac2 to save mime headers, create ac1/ac2 chat")
|
||||
ac2.set_config("save_mime_headers", "1")
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("sending text message from ac1 to ac2")
|
||||
msg_out = chat.send_text("message1")
|
||||
ac1._evtracker.wait_msg_delivered(msg_out)
|
||||
assert msg_out.get_mime_headers() is None
|
||||
|
||||
lp.sec("wait for ac2 to receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
in_id = ev.data2
|
||||
mime = ac2.get_message_by_id(in_id).get_mime_headers()
|
||||
assert mime.get_all("From")
|
||||
assert mime.get_all("Received")
|
||||
|
||||
|
||||
def test_send_mark_seen_clean_incoming_events(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -1326,6 +1597,53 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_ac_setup_message(acfactory, lp):
|
||||
# note that the receiving account needs to be configured and running
|
||||
# before the setup message is send. DC does not read old messages
|
||||
# as of Jul2019
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("trigger ac setup message and return setupcode")
|
||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
||||
setup_code = ac1.initiate_key_transfer()
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.is_setup_message()
|
||||
assert msg.get_setupcodebegin() == setup_code[:2]
|
||||
lp.sec("try a bad setup code")
|
||||
with pytest.raises(ValueError):
|
||||
msg.continue_key_transfer(str(reversed(setup_code)))
|
||||
lp.sec("try a good setup code")
|
||||
print("*************** Incoming ASM File at: ", msg.filename)
|
||||
print("*************** Setup Code: ", setup_code)
|
||||
msg.continue_key_transfer(setup_code)
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
|
||||
def test_ac_setup_message_twice(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("trigger ac setup message but ignore")
|
||||
assert ac1.get_info()["fingerprint"] != ac2.get_info()["fingerprint"]
|
||||
ac1.initiate_key_transfer()
|
||||
ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
|
||||
lp.sec("trigger second ac setup message, wait for receive ")
|
||||
setup_code2 = ac1.initiate_key_transfer()
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
assert msg.is_setup_message()
|
||||
assert msg.get_setupcodebegin() == setup_code2[:2]
|
||||
|
||||
lp.sec("process second setup message")
|
||||
msg.continue_key_transfer(setup_code2)
|
||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
|
||||
@@ -105,6 +105,10 @@ class TestOfflineAccountBasic:
|
||||
ac1.update_config({"mvbox_move": False})
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-03-15
|
||||
2025-02-17
|
||||
@@ -4,14 +4,14 @@ resources:
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
|
||||
- name: deltachat-core-rust-release
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
branch: main
|
||||
uri: https://github.com/chatmail/core.git
|
||||
uri: https://github.com/deltachat/deltachat-core-rust.git
|
||||
tag_filter: "v*"
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -154,8 +154,6 @@ arch2tags = {
|
||||
"armv6l-linux": "linux_armv6l",
|
||||
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
|
||||
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
|
||||
"arm64-v8a-android": "android_21_arm64_v8a",
|
||||
"armeabi-v7a-android": "android_21_armeabi_v7a",
|
||||
"win64": "win_amd64",
|
||||
"win32": "win32",
|
||||
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
|
||||
|
||||
80
src/blob.rs
80
src/blob.rs
@@ -1,6 +1,7 @@
|
||||
//! # Blob directory management.
|
||||
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
@@ -150,11 +151,11 @@ impl<'a> BlobObject<'a> {
|
||||
let rel_path = path
|
||||
.strip_prefix(context.get_blobdir())
|
||||
.with_context(|| format!("wrong blobdir: {}", path.display()))?;
|
||||
let name = rel_path.to_str().context("wrong name")?;
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
if !BlobObject::is_acceptible_blob_name(rel_path) {
|
||||
return Err(format_err!("bad blob name: {}", rel_path.display()));
|
||||
}
|
||||
BlobObject::from_name(context, name)
|
||||
let name = rel_path.to_str().context("wrong name")?;
|
||||
BlobObject::from_name(context, name.to_string())
|
||||
}
|
||||
|
||||
/// Returns a [BlobObject] for an existing blob.
|
||||
@@ -163,13 +164,13 @@ impl<'a> BlobObject<'a> {
|
||||
/// prefixed, as returned by [BlobObject::as_name]. This is how
|
||||
/// you want to create a [BlobObject] for a filename read from the
|
||||
/// database.
|
||||
pub fn from_name(context: &'a Context, name: &str) -> Result<BlobObject<'a>> {
|
||||
let name = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap(),
|
||||
pub fn from_name(context: &'a Context, name: String) -> Result<BlobObject<'a>> {
|
||||
let name: String = match name.starts_with("$BLOBDIR/") {
|
||||
true => name.splitn(2, '/').last().unwrap().to_string(),
|
||||
false => name,
|
||||
};
|
||||
if !BlobObject::is_acceptible_blob_name(name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", name));
|
||||
if !BlobObject::is_acceptible_blob_name(&name) {
|
||||
return Err(format_err!("not an acceptable blob name: {}", &name));
|
||||
}
|
||||
Ok(BlobObject {
|
||||
blobdir: context.get_blobdir(),
|
||||
@@ -215,17 +216,19 @@ impl<'a> BlobObject<'a> {
|
||||
///
|
||||
/// This is slightly less strict than stanitise_name, presumably
|
||||
/// someone already created a file with such a name so we just
|
||||
/// ensure it's not actually a path in disguise.
|
||||
///
|
||||
/// Acceptible blob name always have to be valid utf-8.
|
||||
fn is_acceptible_blob_name(name: &str) -> bool {
|
||||
if name.find('/').is_some() {
|
||||
/// ensure it's not actually a path in disguise is actually utf-8.
|
||||
fn is_acceptible_blob_name(name: impl AsRef<OsStr>) -> bool {
|
||||
let uname = match name.as_ref().to_str() {
|
||||
Some(name) => name,
|
||||
None => return false,
|
||||
};
|
||||
if uname.find('/').is_some() {
|
||||
return false;
|
||||
}
|
||||
if name.find('\\').is_some() {
|
||||
if uname.find('\\').is_some() {
|
||||
return false;
|
||||
}
|
||||
if name.find('\0').is_some() {
|
||||
if uname.find('\0').is_some() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
@@ -252,28 +255,25 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
|
||||
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
|
||||
let (img_wh, max_bytes) =
|
||||
let img_wh =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
MediaQuality::Balanced => (
|
||||
constants::BALANCED_AVATAR_SIZE,
|
||||
constants::BALANCED_AVATAR_BYTES,
|
||||
),
|
||||
MediaQuality::Worse => {
|
||||
(constants::WORSE_AVATAR_SIZE, constants::WORSE_AVATAR_BYTES)
|
||||
}
|
||||
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
|
||||
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
|
||||
};
|
||||
|
||||
let maybe_sticker = &mut false;
|
||||
let is_avatar = true;
|
||||
let strict_limits = true;
|
||||
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
|
||||
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
|
||||
self.recode_to_size(
|
||||
context,
|
||||
None, // The name of an avatar doesn't matter
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
is_avatar,
|
||||
20_000,
|
||||
strict_limits,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -302,17 +302,21 @@ impl<'a> BlobObject<'a> {
|
||||
),
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
let new_name =
|
||||
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
|
||||
let strict_limits = false;
|
||||
let new_name = self.recode_to_size(
|
||||
context,
|
||||
name,
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
strict_limits,
|
||||
)?;
|
||||
|
||||
Ok(new_name)
|
||||
}
|
||||
|
||||
/// Recodes the image so that it fits into limits on width/height and byte size.
|
||||
///
|
||||
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
|
||||
/// with the result without rechecking.
|
||||
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
|
||||
/// proceed with the result.
|
||||
///
|
||||
/// This modifies the blob object in-place.
|
||||
///
|
||||
@@ -327,10 +331,10 @@ impl<'a> BlobObject<'a> {
|
||||
maybe_sticker: &mut bool,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
strict_limits: bool,
|
||||
) -> Result<String> {
|
||||
// Add white background only to avatars to spare the CPU.
|
||||
let mut add_white_bg = is_avatar;
|
||||
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let mut name = name.unwrap_or_else(|| self.name.clone());
|
||||
@@ -401,7 +405,7 @@ impl<'a> BlobObject<'a> {
|
||||
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
|
||||
// images.
|
||||
let do_scale = exceeds_max_bytes
|
||||
|| is_avatar
|
||||
|| strict_limits
|
||||
&& (exceeds_wh
|
||||
|| exif.is_some() && {
|
||||
if mem::take(&mut add_white_bg) {
|
||||
@@ -438,7 +442,7 @@ impl<'a> BlobObject<'a> {
|
||||
ofmt.clone(),
|
||||
max_bytes,
|
||||
&mut encoded,
|
||||
)? && is_avatar
|
||||
)? && strict_limits
|
||||
{
|
||||
if img_wh < 20 {
|
||||
return Err(format_err!(
|
||||
@@ -488,7 +492,7 @@ impl<'a> BlobObject<'a> {
|
||||
match res {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !is_avatar && no_exif {
|
||||
if !strict_limits && no_exif {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
|
||||
@@ -100,7 +100,7 @@ async fn test_create_long_names() {
|
||||
let t = TestContext::new().await;
|
||||
let s = format!("file.{}", "a".repeat(100));
|
||||
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
||||
let blobname = blob.as_name().split('/').next_back().unwrap();
|
||||
let blobname = blob.as_name().split('/').last().unwrap();
|
||||
assert!(blobname.len() < 70);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn test_create_from_name_long() {
|
||||
fn test_is_blob_name() {
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
||||
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
||||
assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128)));
|
||||
assert!(BlobObject::is_acceptible_blob_name("f".repeat(128)));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
||||
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
||||
@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
|
||||
avatar_blob.ends_with("d98cd30ed8f2129bf3968420208849d.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
@@ -226,7 +226,7 @@ async fn test_selfavatar_in_blobdir() {
|
||||
.unwrap();
|
||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
assert!(
|
||||
avatar_cfg.ends_with("ec054c444a5755adf2b0aaea40209f2.png"),
|
||||
avatar_cfg.ends_with("fa7418e646301203538041f60d03190.png"),
|
||||
"Avatar file name {avatar_cfg} should end with its hash"
|
||||
);
|
||||
|
||||
|
||||
117
src/chat.rs
117
src/chat.rs
@@ -434,7 +434,7 @@ impl ChatId {
|
||||
.ok();
|
||||
}
|
||||
if delete {
|
||||
self.delete_ex(context, Nosync).await?;
|
||||
self.delete(context).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -773,10 +773,6 @@ impl ChatId {
|
||||
|
||||
/// Deletes a chat.
|
||||
pub async fn delete(self, context: &Context) -> Result<()> {
|
||||
self.delete_ex(context, Sync).await
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"bad chat_id, can not be a special chat: {}",
|
||||
@@ -784,23 +780,10 @@ impl ChatId {
|
||||
);
|
||||
|
||||
let chat = Chat::load_from_db(context, self).await?;
|
||||
let delete_msgs_target = context.get_delete_msgs_target().await?;
|
||||
let sync_id = match sync {
|
||||
Nosync => None,
|
||||
Sync => chat.get_sync_id(context).await?,
|
||||
};
|
||||
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
transaction.execute(
|
||||
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT rfc724_mid FROM msgs WHERE chat_id=?)",
|
||||
(delete_msgs_target, self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
)?;
|
||||
transaction.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
|
||||
(self,),
|
||||
@@ -812,20 +795,7 @@ impl ChatId {
|
||||
})
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ChatDeleted { chat_id: self });
|
||||
context.emit_msgs_changed_without_ids();
|
||||
|
||||
if let Some(id) = sync_id {
|
||||
self::sync(context, id, SyncAction::Delete)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
context
|
||||
@@ -833,6 +803,12 @@ impl ChatId {
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
let mut msg = Message::new_text(stock_str::self_deleted_msg_body(context).await);
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1072,14 +1048,6 @@ impl ChatId {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub(crate) async fn created_timestamp(self, context: &Context) -> Result<i64> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
@@ -1313,7 +1281,8 @@ impl ChatId {
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let mut ret_available = String::new();
|
||||
let mut ret_mutual = String::new();
|
||||
let mut ret_nopreference = String::new();
|
||||
let mut ret_reset = String::new();
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
@@ -1329,9 +1298,8 @@ impl ChatId {
|
||||
.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
.map(|peerstate| peerstate.prefer_encrypt)
|
||||
{
|
||||
Some(EncryptPreference::Mutual) | Some(EncryptPreference::NoPreference) => {
|
||||
ret_available += &format!("{addr}\n")
|
||||
}
|
||||
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{addr}\n"),
|
||||
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{addr}\n"),
|
||||
};
|
||||
}
|
||||
@@ -1343,14 +1311,23 @@ impl ChatId {
|
||||
ret.push('\n');
|
||||
ret += &ret_reset;
|
||||
}
|
||||
if !ret_available.is_empty() {
|
||||
if !ret_nopreference.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_available;
|
||||
ret += &ret_nopreference;
|
||||
}
|
||||
if !ret_mutual.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_preferred(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_mutual;
|
||||
}
|
||||
|
||||
Ok(ret.trim().to_string())
|
||||
@@ -3054,11 +3031,6 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.state = MessageState::OutDelivered;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
|
||||
msg.chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
@@ -3159,24 +3131,39 @@ pub async fn send_text_msg(
|
||||
send_msg(context, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
/// Returns true if the message can be edited.
|
||||
pub async fn can_send_edit_request(context: &Context, msg_id: MsgId) -> Result<bool> {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
if !can_send_edit_request_for_msg(&msg) {
|
||||
return Ok(false);
|
||||
}
|
||||
let chat = Chat::load_from_db(context, msg.get_chat_id()).await?;
|
||||
let can_send = chat.can_send(context).await?;
|
||||
Ok(can_send)
|
||||
}
|
||||
|
||||
fn can_send_edit_request_for_msg(msg: &Message) -> bool {
|
||||
if msg.from_id != ContactId::SELF
|
||||
|| msg.is_info()
|
||||
|| msg.viewtype == Viewtype::VideochatInvitation
|
||||
|| msg.has_html()
|
||||
|| msg.text.is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Sends chat members a request to edit the given message's text.
|
||||
pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
|
||||
ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
|
||||
|
||||
let mut original_msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
original_msg.from_id == ContactId::SELF,
|
||||
"Can edit only own messages"
|
||||
can_send_edit_request_for_msg(&original_msg),
|
||||
"Message cannot be edited"
|
||||
);
|
||||
ensure!(!original_msg.is_info(), "Cannot edit info messages");
|
||||
ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
|
||||
ensure!(
|
||||
original_msg.viewtype != Viewtype::VideochatInvitation,
|
||||
"Cannot edit videochat invitations"
|
||||
);
|
||||
ensure!(
|
||||
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
|
||||
"Cannot add text"
|
||||
);
|
||||
ensure!(!new_text.trim().is_empty(), "Edited text cannot be empty");
|
||||
|
||||
if original_msg.text == new_text {
|
||||
info!(context, "Text unchanged.");
|
||||
return Ok(());
|
||||
@@ -4394,7 +4381,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context.send_sync_msg().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4890,7 +4877,6 @@ pub(crate) enum SyncAction {
|
||||
Rename(String),
|
||||
/// Set chat contacts by their addresses.
|
||||
SetContacts(Vec<String>),
|
||||
Delete,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -4950,7 +4936,6 @@ impl Context {
|
||||
}
|
||||
SyncAction::Rename(to) => rename_ex(self, Nosync, chat_id, to).await,
|
||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||
SyncAction::Delete => chat_id.delete_ex(self, Nosync).await,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -299,6 +299,10 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Disable encryption so we can inspect raw message contents.
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
|
||||
// Create contact for Bob on the Alice side with name "robert".
|
||||
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
|
||||
|
||||
@@ -369,6 +373,9 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
@@ -688,45 +695,16 @@ async fn test_leave_group() -> Result<()> {
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
// Clear events so that we can later check
|
||||
// that the 'Group left' message didn't trigger IncomingMsg:
|
||||
alice.evtracker.clear_events();
|
||||
|
||||
// Shift the time so that we can later check the 'Group left' message's timestamp:
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
|
||||
// Bob leaves the group.
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
|
||||
let leave_msg = bob.pop_sent_msg().await;
|
||||
let rcvd_leave_msg = alice.recv_msg(&leave_msg).await;
|
||||
alice.recv_msg(&leave_msg).await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
assert_eq!(rcvd_leave_msg.state, MessageState::InSeen);
|
||||
|
||||
alice.emit_event(EventType::Test);
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|ev| match ev {
|
||||
EventType::Test => true,
|
||||
EventType::IncomingMsg { .. } => panic!("'Group left' message should be silent"),
|
||||
EventType::MsgsNoticed(..) => {
|
||||
panic!("'Group left' message shouldn't clear notifications")
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// The 'Group left' message timestamp should be the same as the previous message in the chat
|
||||
// so that the chat is not popped up in the chatlist:
|
||||
assert_eq!(
|
||||
sent_msg.load_from_db().await.timestamp_sort,
|
||||
rcvd_leave_msg.timestamp_sort
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1876,6 +1854,10 @@ async fn test_sticker(
|
||||
msg.set_file_and_deduplicate(&alice, &file, Some(filename), None)?;
|
||||
|
||||
let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let mime = sent_msg.payload();
|
||||
if res_viewtype == Viewtype::Sticker {
|
||||
assert_eq!(mime.match_indices("Chat-Content: sticker").count(), 1);
|
||||
}
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, bob_chat.id);
|
||||
@@ -2670,10 +2652,11 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
End-to-end encryption preferred:\n\
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
@@ -3018,48 +3001,6 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_delete_chat() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1b_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
a0b_chat_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1b_chat_id).await;
|
||||
alice1
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
|
||||
.await;
|
||||
|
||||
let bob_grp_chat_id = bob
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
|
||||
.await;
|
||||
let sent_msg = bob.send_text(bob_grp_chat_id, "hi").await;
|
||||
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
|
||||
a0_grp_chat_id.accept(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
a0_grp_chat_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1_grp_chat_id).await;
|
||||
alice0
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ChatDeleted { .. }))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
@@ -3242,10 +3183,6 @@ async fn test_sync_broadcast() -> Result<()> {
|
||||
assert!(get_past_chat_contacts(alice1, a1_broadcast_id)
|
||||
.await?
|
||||
.is_empty());
|
||||
|
||||
a0_broadcast_id.delete(alice0).await?;
|
||||
sync(alice0, alice1).await;
|
||||
alice1.assert_no_chat(a1_broadcast_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3709,6 +3646,58 @@ async fn test_one_to_one_chat_no_group_member_timestamps() {
|
||||
assert!(!payload.contains("Chat-Group-Member-Timestamps:"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn text_can_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[&bob])
|
||||
.await;
|
||||
|
||||
// Alice can edit her message
|
||||
let sent1 = alice.send_text(chat_id, "foo").await;
|
||||
assert!(can_send_edit_request(&alice, sent1.sender_msg_id).await?);
|
||||
|
||||
// Bob cannot edit Alice's message
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(!can_send_edit_request(&bob, msg.id).await?);
|
||||
|
||||
// HTML messages cannot be edited
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
let sent2 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(!can_send_edit_request(&alice, sent2.sender_msg_id).await?);
|
||||
|
||||
// Info messages cannot be edited
|
||||
set_chat_name(&alice, chat_id, "bar").await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert!(!can_send_edit_request(&alice, msg.id).await?);
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(&alice, chat_id).await?;
|
||||
assert!(!can_send_edit_request(&alice, msg_id).await?);
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.make_vcard(&alice, &[ContactId::SELF]).await?;
|
||||
let sent3 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(msg.text.is_empty());
|
||||
assert!(!can_send_edit_request(&alice, sent3.sender_msg_id).await?);
|
||||
|
||||
// Messages in left groups cannot be edited any longer (as one cannot send to that group)
|
||||
remove_contact_from_chat(&alice, chat_id, ContactId::SELF).await?;
|
||||
assert!(!can_send_edit_request(&alice, sent1.sender_msg_id).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_edit_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3775,122 +3764,19 @@ async fn test_receive_edit_request_after_removal() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_cannot_send_edit_request() -> Result<()> {
|
||||
async fn test_cannot_edit_html() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "My Group", &[bob])
|
||||
.await;
|
||||
let chat = alice.create_chat(bob).await;
|
||||
|
||||
// Alice can edit her message
|
||||
let sent1 = alice.send_text(chat_id, "foo").await;
|
||||
send_edit_request(alice, sent1.sender_msg_id, "bar".to_string()).await?;
|
||||
|
||||
// Bob cannot edit Alice's message
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(send_edit_request(bob, msg.id, "bar".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// HTML messages cannot be edited
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
let sent2 = alice.send_msg(chat_id, &mut msg).await;
|
||||
send_msg(alice, chat.id, &mut msg).await.unwrap();
|
||||
assert!(msg.has_html());
|
||||
assert!(
|
||||
send_edit_request(alice, sent2.sender_msg_id, "foo".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Info messages cannot be edited
|
||||
set_chat_name(alice, chat_id, "bar").await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.from_id, ContactId::SELF);
|
||||
assert!(send_edit_request(alice, msg.id, "bar".to_string())
|
||||
assert!(send_edit_request(alice, msg.id, "foo".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// Videochat invitations cannot be edited
|
||||
alice
|
||||
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
|
||||
.await?;
|
||||
let msg_id = send_videochat_invitation(alice, chat_id).await?;
|
||||
assert!(send_edit_request(alice, msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
// If not text was given initally, there is nothing to edit
|
||||
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.make_vcard(alice, &[ContactId::SELF]).await?;
|
||||
let sent3 = alice.send_msg(chat_id, &mut msg).await;
|
||||
assert!(msg.text.is_empty());
|
||||
assert!(
|
||||
send_edit_request(alice, sent3.sender_msg_id, "bar".to_string())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_delete_request() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let bob_chat = bob.create_chat(alice).await;
|
||||
|
||||
// Bobs sends a message to Alice, so Alice learns Bob's key
|
||||
let sent0 = bob.send_text(bob_chat.id, "¡ola!").await;
|
||||
alice.recv_msg(&sent0).await;
|
||||
|
||||
// Alice sends a message, then sends a deletion request
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 2);
|
||||
|
||||
message::delete_msgs_ex(alice, &[alice_msg.id], true).await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
// Bob receives both messages and has nothing the end
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "wtf");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
|
||||
// Alice has another device, and there is also nothing at the end
|
||||
let alice2 = &tcm.alice().await;
|
||||
alice2.recv_msg(&sent0).await;
|
||||
let alice2_msg = alice2.recv_msg(&sent1).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 2);
|
||||
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
assert_eq!(alice2_msg.chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_delete_request_no_encryption() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends a message, then tries to send a deletion request which fails.
|
||||
let sent1 = alice.send_text(alice_chat.id, "wtf").await;
|
||||
assert!(message::delete_msgs_ex(alice, &[sent1.sender_msg_id], true)
|
||||
.await
|
||||
.is_err());
|
||||
sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -407,17 +407,16 @@ impl Chatlist {
|
||||
let lastcontact = if let Some(lastmsg) = &lastmsg {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
Some(lastcontact)
|
||||
} else {
|
||||
None
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
|
||||
.await
|
||||
.context("loading contact failed")?;
|
||||
Some(lastcontact)
|
||||
}
|
||||
Chattype::Single => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
@@ -480,7 +479,6 @@ pub async fn get_last_message_for_chat(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
@@ -488,7 +486,6 @@ mod tests {
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
@@ -790,31 +787,6 @@ mod tests {
|
||||
assert!(summary_res.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_summary_for_saved_messages() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
|
||||
send_text_msg(&alice, chat_alice.id, "hi".into()).await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
save_msgs(&alice, &[sent1.sender_msg_id]).await?;
|
||||
let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&alice, 0, None).await?;
|
||||
assert_eq!(summary.prefix.unwrap().to_string(), "Me");
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
save_msgs(&bob, &[msg.id]).await?;
|
||||
let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
let summary = chatlist.get_summary(&bob, 0, None).await?;
|
||||
assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org");
|
||||
assert_eq!(summary.text, "hi");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
@@ -193,6 +193,10 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
FetchedExistingMsgs,
|
||||
|
||||
/// Type of the OpenPGP key to generate.
|
||||
#[strum(props(default = "0"))]
|
||||
KeyGenType,
|
||||
|
||||
/// Timer in seconds after which the message is deleted from the
|
||||
/// server.
|
||||
///
|
||||
@@ -216,6 +220,9 @@ pub enum Config {
|
||||
/// `ProviderOptions::delete_to_trash`.
|
||||
DeleteToTrash,
|
||||
|
||||
/// Save raw MIME messages with headers in the database if true.
|
||||
SaveMimeHeaders,
|
||||
|
||||
/// The primary email address. Also see `SecondaryAddrs`.
|
||||
ConfiguredAddr,
|
||||
|
||||
@@ -709,6 +716,7 @@ impl Context {
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::FetchExistingMsgs
|
||||
| Config::DeleteToTrash
|
||||
| Config::SaveMimeHeaders
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
|
||||
@@ -86,7 +86,7 @@ async fn test_set_config_bool() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// We need some config that defaults to true
|
||||
let c = Config::MdnsEnabled;
|
||||
let c = Config::E2eeEnabled;
|
||||
assert_eq!(t.get_config_bool(c).await?, true);
|
||||
t.set_config_bool(c, false).await?;
|
||||
assert_eq!(t.get_config_bool(c).await?, false);
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, provider};
|
||||
use crate::{chat, e2ee, provider};
|
||||
use crate::{stock_str, EventType};
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
|
||||
@@ -442,6 +442,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
ctx.set_config(Config::ShowEmails, None).await?;
|
||||
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = !is_chatmail;
|
||||
@@ -472,6 +473,9 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
|
||||
progress!(ctx, 920);
|
||||
|
||||
e2ee::ensure_secret_key_exists(ctx).await?;
|
||||
info!(ctx, "key generation completed");
|
||||
|
||||
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
@@ -58,6 +58,25 @@ pub enum MediaQuality {
|
||||
Worse = 1,
|
||||
}
|
||||
|
||||
/// Type of the key to generate.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum KeyGenType {
|
||||
#[default]
|
||||
Default = 0,
|
||||
|
||||
/// 2048-bit RSA.
|
||||
Rsa2048 = 1,
|
||||
|
||||
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
|
||||
Ed25519 = 2,
|
||||
|
||||
/// 4096-bit RSA.
|
||||
Rsa4096 = 3,
|
||||
}
|
||||
|
||||
/// Video chat URL type.
|
||||
#[derive(
|
||||
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
@@ -185,11 +204,9 @@ pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
|
||||
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
|
||||
pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
|
||||
// max. width/height and bytes of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 512;
|
||||
pub(crate) const BALANCED_AVATAR_BYTES: usize = 60_000;
|
||||
// max. width/height of an avatar
|
||||
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
|
||||
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
|
||||
pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outlook servers don't allowing headers larger than 32k.
|
||||
|
||||
// max. width/height of images scaled down because of being too huge
|
||||
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
|
||||
@@ -236,6 +253,16 @@ mod tests {
|
||||
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keygentype_values() {
|
||||
// values may be written to disk and must not change
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::default());
|
||||
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
|
||||
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
|
||||
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_showemails_values() {
|
||||
// values may be written to disk and must not change
|
||||
|
||||
@@ -1435,7 +1435,7 @@ impl Contact {
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if self.id == ContactId::SELF {
|
||||
if let Some(p) = context.get_config(Config::Selfavatar).await? {
|
||||
return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
|
||||
return Ok(Some(PathBuf::from(p)));
|
||||
}
|
||||
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
if !image_rel.is_empty() {
|
||||
|
||||
@@ -1207,16 +1207,16 @@ async fn test_reset_encryption() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let msg = tcm.send_recv(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
@@ -1235,7 +1235,6 @@ async fn test_reset_verified_encryption() -> Result<()> {
|
||||
|
||||
let alice_bob_chat_id = msg.chat_id;
|
||||
let alice_bob_contact_id = msg.from_id;
|
||||
|
||||
alice_bob_contact_id.reset_encryption(alice).await?;
|
||||
|
||||
// Check that the contact is still verified after resetting encryption.
|
||||
@@ -1251,8 +1250,7 @@ async fn test_reset_verified_encryption() -> Result<()> {
|
||||
"bob@example.net sent a message from another device."
|
||||
);
|
||||
|
||||
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
let msg = tcm.send_recv(alice, bob, "Unencrypted").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -919,6 +919,12 @@ impl Context {
|
||||
"show_emails",
|
||||
self.get_config_int(Config::ShowEmails).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"save_mime_headers",
|
||||
self.get_config_bool(Config::SaveMimeHeaders)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"download_limit",
|
||||
self.get_config_int(Config::DownloadLimit)
|
||||
@@ -938,6 +944,10 @@ impl Context {
|
||||
res.insert("configured_trash_folder", configured_trash_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
res.insert("e2ee_enabled", e2ee_enabled.to_string());
|
||||
res.insert(
|
||||
"key_gen_type",
|
||||
self.get_config_int(Config::KeyGenType).await?.to_string(),
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
|
||||
76
src/e2ee.rs
76
src/e2ee.rs
@@ -54,13 +54,22 @@ impl EncryptHelper {
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<bool> {
|
||||
let is_chatmail = context.is_chatmail().await?;
|
||||
let mut prefer_encrypt_count = 1;
|
||||
let mut prefer_encrypt_count = if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for (peerstate, addr) in peerstates {
|
||||
match peerstate {
|
||||
Some(peerstate) => {
|
||||
let prefer_encrypt = peerstate.prefer_encrypt;
|
||||
info!(context, "Peerstate for {addr:?} is {prefer_encrypt}.");
|
||||
if match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Reset => is_chatmail,
|
||||
EncryptPreference::NoPreference | EncryptPreference::Mutual => true,
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {
|
||||
(peerstate.prefer_encrypt != EncryptPreference::Reset || is_chatmail)
|
||||
&& self.prefer_encrypt == EncryptPreference::Mutual
|
||||
}
|
||||
EncryptPreference::Mutual => true,
|
||||
} {
|
||||
prefer_encrypt_count += 1;
|
||||
}
|
||||
@@ -167,7 +176,6 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
@@ -220,8 +228,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat_alice = alice.create_email_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_email_chat(&alice).await.id;
|
||||
let chat_alice = alice.create_chat(&bob).await.id;
|
||||
let chat_bob = bob.create_chat(&alice).await.id;
|
||||
|
||||
// Alice sends unencrypted message to Bob
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -321,6 +329,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert!(t.get_config_bool(Config::E2eeEnabled).await?);
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
@@ -343,6 +352,61 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_should_encrypt_e2ee_disabled() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::E2eeEnabled, false).await?;
|
||||
let encrypt_helper = EncryptHelper::new(t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await?);
|
||||
|
||||
let mut ps = new_peerstates(EncryptPreference::Mutual);
|
||||
// Own preference is `NoPreference` and there's no majority with `Mutual`.
|
||||
assert!(!encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
// Now the majority wants to encrypt. Let's encrypt, anyway there are other cases when we
|
||||
// can't send unencrypted, e.g. protected groups.
|
||||
ps.push(ps[0].clone());
|
||||
assert!(encrypt_helper.should_encrypt(t, false, &ps).await?);
|
||||
|
||||
// Test with missing peerstate.
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(encrypt_helper.should_encrypt(t, true, &ps).await.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_prefers_to_encrypt() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
let bob_chat_id = tcm
|
||||
.send_recv_accept(alice, bob, "Hello from DC")
|
||||
.await
|
||||
.chat_id;
|
||||
receive_imf(
|
||||
bob,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Message-ID: <2222@example.org>\n\
|
||||
Date: Sun, 22 Mar 3000 22:37:58 +0000\n\
|
||||
\n\
|
||||
Hello from another MUA\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(bob, bob_chat_id, "hi".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(bob, sent_msg.sender_msg_id).await?;
|
||||
assert!(msg.get_showpadlock());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
@@ -219,12 +219,6 @@ pub enum EventType {
|
||||
timer: EphemeralTimer,
|
||||
},
|
||||
|
||||
/// Chat was deleted.
|
||||
ChatDeleted {
|
||||
/// Chat ID.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked, deleted or changed their "recently seen" status.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
|
||||
12
src/fuzzing.rs
Normal file
12
src/fuzzing.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! # Fuzzing module.
|
||||
//!
|
||||
//! This module exposes private APIs for fuzzing.
|
||||
|
||||
/// Fuzzing target for simplify().
|
||||
///
|
||||
/// Calls simplify() and panics if simplify() panics.
|
||||
/// Does not return any value to avoid exposing internal crate types.
|
||||
#[cfg(fuzzing)]
|
||||
pub fn simplify(input: String, is_chat_message: bool) {
|
||||
crate::simplify::simplify(input, is_chat_message);
|
||||
}
|
||||
@@ -57,7 +57,6 @@ pub enum HeaderDef {
|
||||
ChatGroupId,
|
||||
ChatGroupName,
|
||||
ChatGroupNameChanged,
|
||||
ChatGroupNameTimestamp,
|
||||
ChatVerified,
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
@@ -81,9 +80,6 @@ pub enum HeaderDef {
|
||||
ChatDispositionNotificationTo,
|
||||
ChatWebrtcRoom,
|
||||
|
||||
/// This message deletes the messages listed in the value by rfc724_mid.
|
||||
ChatDelete,
|
||||
|
||||
/// This message obsoletes the text of the message defined here by rfc724_mid.
|
||||
ChatEdit,
|
||||
|
||||
|
||||
65
src/imex.rs
65
src/imex.rs
@@ -23,7 +23,6 @@ use crate::events::EventType;
|
||||
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp;
|
||||
use crate::qr::DCBACKUP_VERSION;
|
||||
use crate::sql;
|
||||
use crate::tools::{
|
||||
create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard,
|
||||
@@ -139,7 +138,7 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Result<()> {
|
||||
// try hard to only modify key-state
|
||||
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
|
||||
let public_key = private_key.split_public_key()?;
|
||||
@@ -171,7 +170,16 @@ async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
public: public_key,
|
||||
secret: private_key,
|
||||
};
|
||||
key::store_self_keypair(context, &keypair).await?;
|
||||
key::store_self_keypair(
|
||||
context,
|
||||
&keypair,
|
||||
if set_default {
|
||||
key::KeyPairUse::Default
|
||||
} else {
|
||||
key::KeyPairUse::ReadOnly
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(context, "stored self key: {:?}", keypair.secret.key_id());
|
||||
Ok(())
|
||||
@@ -201,7 +209,7 @@ async fn imex_inner(
|
||||
.await
|
||||
.context("Cannot create private key or private key not available")?;
|
||||
|
||||
create_folder(context, path).await?;
|
||||
create_folder(context, &path).await?;
|
||||
}
|
||||
|
||||
match what {
|
||||
@@ -393,9 +401,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
|
||||
.await
|
||||
.context("cannot import unpacked database");
|
||||
}
|
||||
if res.is_ok() {
|
||||
res = check_backup_version(context).await;
|
||||
}
|
||||
if res.is_ok() {
|
||||
res = adjust_bcc_self(context).await;
|
||||
}
|
||||
@@ -594,10 +599,10 @@ where
|
||||
}
|
||||
|
||||
/// Imports secret key from a file.
|
||||
async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
|
||||
let buf = read_file(context, path).await?;
|
||||
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
|
||||
let buf = read_file(context, &path).await?;
|
||||
let armored = std::string::String::from_utf8_lossy(&buf);
|
||||
set_self_key(context, &armored).await?;
|
||||
set_self_key(context, &armored, set_default).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -619,7 +624,8 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
|
||||
"Importing secret key from {} as the default key.",
|
||||
path.display()
|
||||
);
|
||||
import_secret_key(context, path).await?;
|
||||
let set_default = true;
|
||||
import_secret_key(context, path, set_default).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -637,13 +643,14 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let set_default = !name_f.contains("legacy");
|
||||
info!(
|
||||
context,
|
||||
"Considering key file: {}.",
|
||||
path_plus_name.display()
|
||||
);
|
||||
|
||||
if let Err(err) = import_secret_key(context, &path_plus_name).await {
|
||||
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to import secret key from {}: {:#}.",
|
||||
@@ -780,10 +787,6 @@ async fn export_database(
|
||||
.sql
|
||||
.set_raw_config_int("backup_time", timestamp)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config_int("backup_version", DCBACKUP_VERSION)
|
||||
.await?;
|
||||
sql::housekeeping(context).await.log_err(context).ok();
|
||||
context
|
||||
.sql
|
||||
@@ -821,15 +824,6 @@ async fn adjust_bcc_self(context: &Context) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_backup_version(context: &Context) -> Result<()> {
|
||||
let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
|
||||
ensure!(
|
||||
version <= DCBACKUP_VERSION,
|
||||
"Backup too new, please update Delta Chat"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
@@ -877,7 +871,7 @@ mod tests {
|
||||
|
||||
assert_eq!(bytes, key.to_asc(None).into_bytes());
|
||||
|
||||
let alice = &TestContext::new().await;
|
||||
let alice = &TestContext::new_alice().await;
|
||||
if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
|
||||
panic!("got error on import: {err:#}");
|
||||
}
|
||||
@@ -899,7 +893,7 @@ mod tests {
|
||||
panic!("got error on export: {err:#}");
|
||||
}
|
||||
|
||||
let context2 = TestContext::new().await;
|
||||
let context2 = TestContext::new_alice().await;
|
||||
if let Err(err) = imex(
|
||||
&context2.ctx,
|
||||
ImexMode::ImportSelfKeys,
|
||||
@@ -926,18 +920,15 @@ mod tests {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let old_key = key::load_self_secret_key(alice).await?;
|
||||
|
||||
assert!(
|
||||
imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None)
|
||||
.await
|
||||
.is_err()
|
||||
imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
|
||||
let new_key = key::load_self_secret_key(alice).await?;
|
||||
assert_ne!(new_key, old_key);
|
||||
assert_eq!(
|
||||
key::load_self_secret_keyring(alice).await?,
|
||||
vec![new_key, old_key]
|
||||
);
|
||||
|
||||
// Importing a second key is not allowed anymore,
|
||||
// even as a non-default key.
|
||||
assert_eq!(key::load_self_secret_key(alice).await?, old_key);
|
||||
|
||||
assert_eq!(key::load_self_secret_keyring(alice).await?, vec![old_key]);
|
||||
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn continue_key_transfer(
|
||||
let file = open_file_std(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, file).await?;
|
||||
set_self_key(context, &armored_key).await?;
|
||||
set_self_key(context, &armored_key, true).await?;
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
Ok(())
|
||||
@@ -315,19 +315,18 @@ mod tests {
|
||||
alice2.recv_msg(&sent).await;
|
||||
let msg = alice2.get_last_msg().await;
|
||||
assert!(msg.is_setupmessage());
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
0
|
||||
);
|
||||
|
||||
// Send a message that cannot be decrypted because the keys are
|
||||
// not synchronized yet.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
let trashed_message = alice.recv_msg_opt(&sent).await;
|
||||
assert!(trashed_message.is_none());
|
||||
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
|
||||
|
||||
// Transfer the key.
|
||||
alice2.set_config(Config::BccSelf, Some("0")).await?;
|
||||
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
|
||||
assert_eq!(alice2.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(
|
||||
crate::key::load_self_secret_keyring(&alice2).await?.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// Alice sends a message to self from the new device.
|
||||
let sent = alice2.send_text(msg.chat_id, "Test").await;
|
||||
|
||||
76
src/key.rs
76
src/key.rs
@@ -7,6 +7,7 @@ use std::io::Cursor;
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use num_traits::FromPrimitive;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::ser::Serialize;
|
||||
@@ -14,6 +15,8 @@ use pgp::types::{PublicKeyTrait, SecretKeyTrait};
|
||||
use rand::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::pgp::KeyPair;
|
||||
@@ -279,12 +282,14 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
|
||||
Some(key_pair) => Ok(key_pair),
|
||||
None => {
|
||||
let start = tools::Time::now();
|
||||
info!(context, "Generating keypair.");
|
||||
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
|
||||
.unwrap_or_default();
|
||||
info!(context, "Generating keypair with type {}", keytype);
|
||||
let keypair = Handle::current()
|
||||
.spawn_blocking(move || crate::pgp::create_keypair(addr))
|
||||
.spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
|
||||
.await??;
|
||||
|
||||
store_self_keypair(context, &keypair).await?;
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||
info!(
|
||||
context,
|
||||
"Keypair generated in {:.3}s.",
|
||||
@@ -321,6 +326,18 @@ pub(crate) async fn load_keypair(context: &Context) -> Result<Option<KeyPair>> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Use of a key pair for encryption or decryption.
|
||||
///
|
||||
/// This is used by `store_self_keypair` to know what kind of key is
|
||||
/// being saved.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum KeyPairUse {
|
||||
/// The default key used to encrypt new messages.
|
||||
Default,
|
||||
/// Only used to decrypt existing message.
|
||||
ReadOnly,
|
||||
}
|
||||
|
||||
/// Store the keypair as an owned keypair for addr in the database.
|
||||
///
|
||||
/// This will save the keypair as keys for the given address. The
|
||||
@@ -333,7 +350,11 @@ pub(crate) async fn load_keypair(context: &Context) -> Result<Option<KeyPair>> {
|
||||
/// same key again overwrites it.
|
||||
///
|
||||
/// [Config::ConfiguredAddr]: crate::config::Config::ConfiguredAddr
|
||||
pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> Result<()> {
|
||||
pub(crate) async fn store_self_keypair(
|
||||
context: &Context,
|
||||
keypair: &KeyPair,
|
||||
default: KeyPairUse,
|
||||
) -> Result<()> {
|
||||
let mut config_cache_lock = context.sql.config_cache.write().await;
|
||||
let new_key_id = context
|
||||
.sql
|
||||
@@ -341,28 +362,29 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
|
||||
let public_key = DcKey::to_bytes(&keypair.public);
|
||||
let secret_key = DcKey::to_bytes(&keypair.secret);
|
||||
|
||||
// private_key and public_key columns
|
||||
// are UNIQUE since migration 107,
|
||||
// so this fails if we already have this key.
|
||||
let is_default = match default {
|
||||
KeyPairUse::Default => true,
|
||||
KeyPairUse::ReadOnly => false,
|
||||
};
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO keypairs (public_key, private_key)
|
||||
"INSERT OR REPLACE INTO keypairs (public_key, private_key)
|
||||
VALUES (?,?)",
|
||||
(&public_key, &secret_key),
|
||||
)
|
||||
.context("Failed to insert keypair")?;
|
||||
|
||||
let new_key_id = transaction.last_insert_rowid();
|
||||
|
||||
// This will fail if we already have `key_id`.
|
||||
//
|
||||
// Setting default key is only possible if we don't
|
||||
// have a key already.
|
||||
transaction.execute(
|
||||
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(Some(new_key_id))
|
||||
if is_default {
|
||||
let new_key_id = transaction.last_insert_rowid();
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('key_id', ?)",
|
||||
(new_key_id,),
|
||||
)?;
|
||||
Ok(Some(new_key_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -383,7 +405,7 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul
|
||||
let secret = SignedSecretKey::from_asc(secret_data)?.0;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = KeyPair { public, secret };
|
||||
store_self_keypair(context, &keypair).await?;
|
||||
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -461,7 +483,6 @@ mod tests {
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
|
||||
@@ -679,7 +700,6 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(pubkey.primary_key, KEYPAIR.public.primary_key);
|
||||
}
|
||||
|
||||
/// Tests that setting a default key second time is not allowed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_self_key_twice() {
|
||||
// Saving the same key twice should result in only one row in
|
||||
@@ -694,13 +714,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
.unwrap()
|
||||
};
|
||||
assert_eq!(nrows().await, 0);
|
||||
store_self_keypair(&ctx, &KEYPAIR).await.unwrap();
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(nrows().await, 1);
|
||||
|
||||
// Saving a second key fails.
|
||||
let res = store_self_keypair(&ctx, &KEYPAIR).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
store_self_keypair(&ctx, &KEYPAIR, KeyPairUse::Default)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(nrows().await, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,3 +116,6 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
mod test_utils;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(fuzzing)]
|
||||
pub mod fuzzing;
|
||||
|
||||
171
src/message.rs
171
src/message.rs
@@ -1,7 +1,6 @@
|
||||
//! # Messages and their identifiers.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
|
||||
@@ -12,7 +11,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::{fs, io};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
|
||||
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
@@ -32,7 +31,6 @@ use crate::pgp::split_armored_data;
|
||||
use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
use crate::sync::SyncData;
|
||||
use crate::tools::{
|
||||
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
|
||||
sanitize_filename, time, timestamp_to_str, truncate,
|
||||
@@ -975,7 +973,7 @@ impl Message {
|
||||
}
|
||||
|
||||
if let Some(filename) = self.get_file(context) {
|
||||
if let Ok(ref buf) = read_file(context, &filename).await {
|
||||
if let Ok(ref buf) = read_file(context, filename).await {
|
||||
if let Ok((typ, headers, _)) = split_armored_data(buf) {
|
||||
if typ == pgp::armor::BlockType::Message {
|
||||
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
|
||||
@@ -1362,7 +1360,7 @@ impl Message {
|
||||
/// * Lack of valid signature on an e2ee message, usually for received messages.
|
||||
/// * Failure to decrypt an e2ee message, usually for received messages.
|
||||
/// * When a message could not be delivered to one or more recipients the non-delivery
|
||||
/// notification text can be stored in the error status.
|
||||
/// notification text can be stored in the error status.
|
||||
pub fn error(&self) -> Option<String> {
|
||||
self.error.clone()
|
||||
}
|
||||
@@ -1596,12 +1594,14 @@ pub(crate) fn guess_msgtype_from_path_suffix(path: &Path) -> Option<(Viewtype, &
|
||||
}
|
||||
|
||||
/// Get the raw mime-headers of the given message.
|
||||
/// Raw headers are saved for large messages
|
||||
/// that need a "Show full message..."
|
||||
/// to see HTML part.
|
||||
/// Raw headers are saved for incoming messages
|
||||
/// only if `set_config(context, "save_mime_headers", "1")`
|
||||
/// was called before.
|
||||
///
|
||||
/// Returns an empty vector if there are no headers saved for the given message.
|
||||
pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
|
||||
/// Returns an empty vector if there are no headers saved for the given message,
|
||||
/// e.g. because of save_mime_headers is not set
|
||||
/// or the message is not incoming.
|
||||
pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result<Vec<u8>> {
|
||||
let (headers, compressed) = context
|
||||
.sql
|
||||
.query_row(
|
||||
@@ -1651,90 +1651,34 @@ pub(crate) async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Delete a single message from the database, including references in other tables.
|
||||
/// This may be called in batches; the final events are emitted in delete_msgs_locally_done() then.
|
||||
pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Result<()> {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg.id
|
||||
.trash(context, on_server)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg.id))?;
|
||||
|
||||
context.emit_event(EventType::MsgDeleted {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id: msg.id,
|
||||
});
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: msg.id });
|
||||
}
|
||||
|
||||
let logging_xdc_id = context
|
||||
.debug_logging
|
||||
.read()
|
||||
.expect("RwLock is poisoned")
|
||||
.as_ref()
|
||||
.map(|dl| dl.msg_id);
|
||||
if let Some(id) = logging_xdc_id {
|
||||
if id == msg.id {
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Do final events and jobs after batch deletion using calls to delete_msg_locally().
|
||||
/// To avoid additional database queries, collecting data is up to the caller.
|
||||
pub(crate) async fn delete_msgs_locally_done(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
modified_chat_ids: HashSet<ChatId>,
|
||||
) -> Result<()> {
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// Run housekeeping to delete unused blobs.
|
||||
context
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete messages on all devices and on IMAP.
|
||||
/// Deletes requested messages
|
||||
/// by moving them to the trash chat
|
||||
/// and scheduling for deletion on IMAP.
|
||||
pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
delete_msgs_ex(context, msg_ids, false).await
|
||||
}
|
||||
|
||||
/// Delete messages on all devices, on IMAP and optionally for all chat members.
|
||||
/// Deleted messages are moved to the trash chat and scheduling for deletion on IMAP.
|
||||
/// When deleting messages for others, all messages must be self-sent and in the same chat.
|
||||
pub async fn delete_msgs_ex(
|
||||
context: &Context,
|
||||
msg_ids: &[MsgId],
|
||||
delete_for_all: bool,
|
||||
) -> Result<()> {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut deleted_rfc724_mid = Vec::new();
|
||||
let mut modified_chat_ids = BTreeSet::new();
|
||||
let mut res = Ok(());
|
||||
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
ensure!(
|
||||
!delete_for_all || msg.from_id == ContactId::SELF,
|
||||
"Can delete only own messages for others"
|
||||
);
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg_id
|
||||
.trash(context, on_server)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))?;
|
||||
|
||||
context.emit_event(EventType::MsgDeleted {
|
||||
chat_id: msg.chat_id,
|
||||
msg_id,
|
||||
});
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
|
||||
let target = context.get_delete_msgs_target().await?;
|
||||
let update_db = |trans: &mut rusqlite::Transaction| {
|
||||
@@ -1750,43 +1694,38 @@ pub async fn delete_msgs_ex(
|
||||
res = Err(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
let logging_xdc_id = context
|
||||
.debug_logging
|
||||
.read()
|
||||
.expect("RwLock is poisoned")
|
||||
.as_ref()
|
||||
.map(|dl| dl.msg_id);
|
||||
|
||||
if let Some(id) = logging_xdc_id {
|
||||
if id == msg_id {
|
||||
set_debug_logging_xdc(context, None).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
res?;
|
||||
|
||||
if delete_for_all {
|
||||
ensure!(
|
||||
modified_chat_ids.len() == 1,
|
||||
"Can delete only from same chat."
|
||||
);
|
||||
if let Some(chat_id) = modified_chat_ids.iter().next() {
|
||||
let mut msg = Message::new_text("🚮".to_owned());
|
||||
// We don't want to send deletion requests in chats w/o encryption:
|
||||
// - These are usually chats with non-DC clients who won't respect deletion requests
|
||||
// anyway and display a weird trash bin message instead.
|
||||
// - Deletion of world-visible unencrypted messages seems not very useful.
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
msg.param
|
||||
.set(Param::DeleteRequestFor, deleted_rfc724_mid.join(" "));
|
||||
msg.hidden = true;
|
||||
send_msg(context, *chat_id, &mut msg).await?;
|
||||
}
|
||||
} else {
|
||||
for modified_chat_id in modified_chat_ids {
|
||||
context.emit_msgs_changed_without_msg_id(modified_chat_id);
|
||||
chatlist_events::emit_chatlist_item_changed(context, modified_chat_id);
|
||||
}
|
||||
|
||||
if !msg_ids.is_empty() {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
// Run housekeeping to delete unused blobs.
|
||||
context
|
||||
.add_sync_item(SyncData::DeleteMessages {
|
||||
msgs: deleted_rfc724_mid,
|
||||
})
|
||||
.set_config_internal(Config::LastHousekeeping, None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for &msg_id in msg_ids {
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
delete_msg_locally(context, &msg).await?;
|
||||
}
|
||||
delete_msgs_locally_done(context, msg_ids, modified_chat_ids).await?;
|
||||
|
||||
// Interrupt Inbox loop to start message deletion, run housekeeping and call send_sync_msg().
|
||||
// Interrupt Inbox loop to start message deletion and run housekeeping.
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::reaction::send_reaction;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils as test;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
@@ -106,7 +106,7 @@ async fn test_create_webrtc_instance_noroom() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_width_height() {
|
||||
let t = TestContext::new().await;
|
||||
let t = test::TestContext::new().await;
|
||||
|
||||
// test that get_width() and get_height() are returning some dimensions for images;
|
||||
// (as the device-chat contains a welcome-images, we check that)
|
||||
@@ -136,7 +136,7 @@ async fn test_get_width_height() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quote() {
|
||||
let d = TestContext::new().await;
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
ctx.set_config(Config::ConfiguredAddr, Some("self@example.com"))
|
||||
@@ -756,37 +756,6 @@ async fn test_delete_msgs_offline() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_msgs_sync() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = alice.create_chat(bob).await.id;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
bob.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice sends a messsage and receives it on the other device
|
||||
let sent1 = alice.send_text(alice_chat_id, "foo").await;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
let msg = alice2.recv_msg(&sent1).await;
|
||||
let alice2_chat_id = msg.chat_id;
|
||||
assert_eq!(alice2.get_last_msg_in(alice2_chat_id).await.id, msg.id);
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 1);
|
||||
|
||||
// Alice deletes the message; this should happen on both devices as well
|
||||
delete_msgs(alice, &[sent1.sender_msg_id]).await?;
|
||||
assert_eq!(alice_chat_id.get_msg_cnt(alice).await?, 0);
|
||||
|
||||
test_utils::sync(alice, alice2).await;
|
||||
assert_eq!(alice2_chat_id.get_msg_cnt(alice2).await?, 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sanitize_filename_message() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{is_hidden, SystemMessage};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::create_iroh_header;
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -734,12 +734,6 @@ impl MimeFactory {
|
||||
)
|
||||
.into(),
|
||||
));
|
||||
} else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) {
|
||||
headers.push((
|
||||
"Chat-Delete",
|
||||
mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -835,13 +829,11 @@ impl MimeFactory {
|
||||
// placed here.
|
||||
let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
|
||||
|
||||
// Headers that MUST NOT (only) go into IMF header section:
|
||||
// - Large headers which may hit the header section size limit on the server, such as
|
||||
// Chat-User-Avatar with a base64-encoded image inside.
|
||||
// - Headers duplicated here that servers mess up with in the IMF header section, like
|
||||
// Message-ID.
|
||||
// - Nonstandard headers that should be DKIM-protected because e.g. OpenDKIM only signs
|
||||
// known headers.
|
||||
// Headers that MUST NOT go into IMF header section.
|
||||
//
|
||||
// These are large headers which may hit the header section size limit on the server, such as
|
||||
// Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here
|
||||
// that servers mess up with in the IMF header section, like Message-ID.
|
||||
//
|
||||
// The header should be hidden from MTA
|
||||
// by moving it either into protected part
|
||||
@@ -869,7 +861,7 @@ impl MimeFactory {
|
||||
if header_name == "message-id" {
|
||||
unprotected_headers.push(header.clone());
|
||||
hidden_headers.push(header.clone());
|
||||
} else if is_hidden(&header_name) {
|
||||
} else if header_name == "chat-user-avatar" || header_name == "chat-edit" {
|
||||
hidden_headers.push(header.clone());
|
||||
} else if header_name == "autocrypt"
|
||||
&& !context.get_config_bool(Config::ProtectAutocrypt).await?
|
||||
@@ -1189,12 +1181,6 @@ impl MimeFactory {
|
||||
"Chat-Group-Name",
|
||||
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
|
||||
));
|
||||
if let Some(ts) = chat.param.get_i64(Param::GroupNameTimestamp) {
|
||||
headers.push((
|
||||
"Chat-Group-Name-Timestamp",
|
||||
mail_builder::headers::text::Text::new(ts.to_string()).into(),
|
||||
));
|
||||
}
|
||||
|
||||
match command {
|
||||
SystemMessage::MemberRemovedFromGroup => {
|
||||
@@ -1713,7 +1699,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'s
|
||||
|
||||
async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
|
||||
let blob = match path.starts_with("$BLOBDIR/") {
|
||||
true => BlobObject::from_name(context, path)?,
|
||||
true => BlobObject::from_name(context, path.to_string())?,
|
||||
false => BlobObject::from_path(context, path.as_ref())?,
|
||||
};
|
||||
let body = fs::read(blob.to_abs_path()).await?;
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::str;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{
|
||||
self, add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
|
||||
add_contact_to_chat, create_group_chat, remove_contact_from_chat, send_text_msg, ChatId,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
@@ -622,43 +622,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_avatar_unencrypted() -> anyhow::Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
let group_id = chat::create_group_chat(t, chat::ProtectionStatus::Unprotected, "Group")
|
||||
.await
|
||||
.unwrap();
|
||||
let bob = Contact::create(t, "", "bob@example.org").await?;
|
||||
chat::add_contact_to_chat(t, group_id, bob).await?;
|
||||
|
||||
let file = t.dir.path().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
chat::set_chat_profile_image(t, group_id, file.to_str().unwrap()).await?;
|
||||
|
||||
// Send message to bob: that should get multipart/mixed because of the avatar moved to inner header.
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let sent_msg = t.send_msg(group_id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
|
||||
|
||||
let outer = payload.next().unwrap();
|
||||
let inner = payload.next().unwrap();
|
||||
let body = payload.next().unwrap();
|
||||
|
||||
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
|
||||
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(outer.match_indices("Chat-Group-Avatar:").count(), 0);
|
||||
|
||||
assert_eq!(inner.match_indices("text/plain").count(), 1);
|
||||
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(inner.match_indices("Chat-Group-Avatar:").count(), 1);
|
||||
|
||||
assert_eq!(body.match_indices("this is the text!").count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_selfavatar_unencrypted_signed() {
|
||||
// create chat with bob, set selfavatar
|
||||
|
||||
@@ -253,7 +253,6 @@ impl MimeMessage {
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail.headers,
|
||||
);
|
||||
headers.retain(|k, _| !is_hidden(k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -288,7 +287,13 @@ impl MimeMessage {
|
||||
if let Some(part) = part.subparts.first() {
|
||||
for field in &part.headers {
|
||||
let key = field.get_key().to_lowercase();
|
||||
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
|
||||
|
||||
// For now only avatar headers can be hidden.
|
||||
if !headers.contains_key(&key)
|
||||
&& (key == "chat-user-avatar"
|
||||
|| key == "chat-group-avatar"
|
||||
|| key == "chat-edit")
|
||||
{
|
||||
headers.insert(key.to_string(), field.get_value());
|
||||
}
|
||||
}
|
||||
@@ -419,6 +424,19 @@ impl MimeMessage {
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
|
||||
if !signatures.is_empty() {
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
|
||||
// but only if the mail was correctly signed. Probably it's ok to not require
|
||||
// encryption here, but let's follow the standard.
|
||||
let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_keys = update_gossip_peerstates(
|
||||
context,
|
||||
timestamp_sent,
|
||||
&from.addr,
|
||||
&recipients,
|
||||
gossip_headers,
|
||||
)
|
||||
.await?;
|
||||
// Remove unsigned opportunistically protected headers from messages considered
|
||||
// Autocrypt-encrypted / displayed with padlock.
|
||||
// For "Subject" see <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
|
||||
@@ -427,15 +445,12 @@ impl MimeMessage {
|
||||
HeaderDef::ChatGroupId,
|
||||
HeaderDef::ChatGroupName,
|
||||
HeaderDef::ChatGroupNameChanged,
|
||||
HeaderDef::ChatGroupNameTimestamp,
|
||||
HeaderDef::ChatGroupAvatar,
|
||||
HeaderDef::ChatGroupMemberRemoved,
|
||||
HeaderDef::ChatGroupMemberAdded,
|
||||
HeaderDef::ChatGroupMemberTimestamps,
|
||||
HeaderDef::ChatGroupPastMembers,
|
||||
HeaderDef::ChatDelete,
|
||||
HeaderDef::ChatEdit,
|
||||
HeaderDef::ChatUserAvatar,
|
||||
] {
|
||||
headers.remove(h.get_headername());
|
||||
}
|
||||
@@ -459,22 +474,6 @@ impl MimeMessage {
|
||||
&mail.headers,
|
||||
);
|
||||
|
||||
if !signatures.is_empty() {
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
|
||||
// but only if the mail was correctly signed. Probably it's ok to not require
|
||||
// encryption here, but let's follow the standard.
|
||||
let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
|
||||
gossiped_keys = update_gossip_peerstates(
|
||||
context,
|
||||
timestamp_sent,
|
||||
&from.addr,
|
||||
&recipients,
|
||||
gossip_headers,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(inner_from) = inner_from {
|
||||
if !addr_cmp(&inner_from.addr, &from.addr) {
|
||||
// There is a From: header in the encrypted
|
||||
@@ -1985,14 +1984,6 @@ fn is_known(key: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns if the header is hidden and must be ignored in the IMF section.
|
||||
pub(crate) fn is_hidden(key: &str) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
"chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
|
||||
)
|
||||
}
|
||||
|
||||
/// Parsed MIME part.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Part {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use mailparse::ParsedMail;
|
||||
use std::mem;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
chat,
|
||||
chatlist::Chatlist,
|
||||
constants::{self, Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
message::{MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
test_utils::{TestContext, TestContextManager},
|
||||
@@ -313,19 +312,6 @@ fn test_mailparse_content_type() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Test to reproduce
|
||||
/// <https://github.com/staktrace/mailparse/issues/130>.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mailparse_0_16_0_panic() {
|
||||
let context = TestContext::new_alice().await;
|
||||
let raw = include_bytes!("../../test-data/message/mailparse-0.16.0-panic.eml");
|
||||
|
||||
// There should be an error, but no panic.
|
||||
assert!(MimeMessage::from_bytes(&context.ctx, &raw[..], None)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_first_addr() {
|
||||
let context = TestContext::new().await;
|
||||
@@ -1812,39 +1798,26 @@ async fn test_take_last_header() {
|
||||
);
|
||||
}
|
||||
|
||||
async fn test_protect_autocrypt(enabled: bool) -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protect_autocrypt() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.set_config_bool(Config::ProtectAutocrypt, enabled)
|
||||
.set_config_bool(Config::ProtectAutocrypt, true)
|
||||
.await?;
|
||||
let sent = alice.send_text(chat.id, "Hello!").await;
|
||||
assert_eq!(sent.payload().contains("Autocrypt: "), !enabled);
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
bob.set_config_bool(Config::ProtectAutocrypt, true).await?;
|
||||
|
||||
let msg = tcm.send_recv_accept(alice, bob, "Hello!").await;
|
||||
assert_eq!(msg.get_showpadlock(), false);
|
||||
|
||||
let msg = tcm.send_recv(bob, alice, "Hi!").await;
|
||||
assert_eq!(msg.get_showpadlock(), true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that if `protect_autocrypt` is enabled,
|
||||
/// `Autocrypt` header does not appear in the outer headers
|
||||
/// of encrypted messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protect_autocrypt_enabled() -> Result<()> {
|
||||
test_protect_autocrypt(true).await
|
||||
}
|
||||
|
||||
/// Tests that if `protect_autocrypt` is disabled,
|
||||
/// `Autocrypt` header appears in the outer headers
|
||||
/// of encrypted messages.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protect_autocrypt_false() -> Result<()> {
|
||||
test_protect_autocrypt(false).await
|
||||
}
|
||||
|
||||
/// Tests that CRLF before MIME boundary
|
||||
/// is not treated as the part body.
|
||||
///
|
||||
@@ -1917,68 +1890,3 @@ This is the epilogue. It is also to be ignored.";
|
||||
"This is explicitly typed plain US-ASCII text.\r\nIt DOES end with a linebreak.\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_hidden_message_id() {
|
||||
let t = &TestContext::new().await;
|
||||
let raw = br#"Message-ID: bar@example.org
|
||||
Date: Sun, 08 Dec 2019 23:12:55 +0000
|
||||
To: <alice@example.org>
|
||||
From: <tunis4@example.org>
|
||||
Content-Type: multipart/mixed; boundary="luTiGu6GBoVLCvTkzVtmZmwsmhkNMw"
|
||||
|
||||
|
||||
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw
|
||||
Message-ID: foo@example.org
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Message with a correct Message-ID hidden header
|
||||
|
||||
--luTiGu6GBoVLCvTkzVtmZmwsmhkNMw--
|
||||
"#;
|
||||
|
||||
let message = MimeMessage::from_bytes(t, &raw[..], None).await.unwrap();
|
||||
assert_eq!(message.get_rfc724_mid().unwrap(), "foo@example.org");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_chat_edit_imf_header() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_email_chat(bob).await;
|
||||
|
||||
// Alice sends a message, then sends an invalid edit request.
|
||||
let sent1 = alice.send_text(alice_chat.id, "foo").await;
|
||||
let alice_msg = sent1.load_from_db().await;
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
|
||||
chat::send_edit_request(alice, alice_msg.id, "bar".to_string()).await?;
|
||||
let mut sent2 = alice.pop_sent_msg().await;
|
||||
let mut s0 = String::new();
|
||||
let mut s1 = String::new();
|
||||
for l in sent2.payload.lines() {
|
||||
if l.starts_with("Chat-Edit:") {
|
||||
s1 += l;
|
||||
s1 += "\n";
|
||||
continue;
|
||||
}
|
||||
s0 += l;
|
||||
s0 += "\n";
|
||||
if l.starts_with("Message-ID:") && s1.is_empty() {
|
||||
s1 = mem::take(&mut s0);
|
||||
}
|
||||
}
|
||||
sent2.payload = s1 + &s0;
|
||||
|
||||
// Bob receives both messages, the edit request with "Chat-Edit" in IMF headers is
|
||||
// received as text message.
|
||||
let bob_msg = bob.recv_msg(&sent1).await;
|
||||
assert_eq!(bob_msg.text, "foo");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 1);
|
||||
let bob_msg = bob.recv_msg(&sent2).await;
|
||||
assert_eq!(bob_msg.text, constants::EDITED_PREFIX.to_string() + "bar");
|
||||
assert_eq!(bob_msg.chat_id.get_msg_cnt(bob).await?, 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response
|
||||
};
|
||||
let is_stale = now > stale_timestamp;
|
||||
|
||||
let blob_object = BlobObject::from_name(context, &blob_name)?;
|
||||
let blob_object = BlobObject::from_name(context, blob_name)?;
|
||||
let blob_abs_path = blob_object.to_abs_path();
|
||||
let blob = match fs::read(blob_abs_path)
|
||||
.await
|
||||
@@ -253,7 +253,7 @@ async fn fetch_url(context: &Context, original_url: &str) -> Result<Response> {
|
||||
.headers()
|
||||
.get_all("location")
|
||||
.iter()
|
||||
.next_back()
|
||||
.last()
|
||||
.ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
|
||||
.to_str()?;
|
||||
info!(context, "Following redirect to {}", header);
|
||||
|
||||
@@ -207,9 +207,6 @@ pub enum Param {
|
||||
/// For messages: Whether [crate::message::Viewtype::Sticker] should be forced.
|
||||
ForceSticker = b'X',
|
||||
|
||||
/// For messages: Message is a deletion request. The value is a list of rfc724_mid of the messages to delete.
|
||||
DeleteRequestFor = b'M',
|
||||
|
||||
/// For messages: Message is a text edit message. the value of this parameter is the rfc724_mid of the original message.
|
||||
TextEditFor = b'I',
|
||||
|
||||
@@ -370,7 +367,7 @@ impl Params {
|
||||
return Ok(None);
|
||||
};
|
||||
ensure!(val.starts_with("$BLOBDIR/"));
|
||||
let blob = BlobObject::from_name(context, val)?;
|
||||
let blob = BlobObject::from_name(context, val.to_string())?;
|
||||
Ok(Some(blob))
|
||||
}
|
||||
|
||||
|
||||
@@ -676,7 +676,14 @@ impl Peerstate {
|
||||
let lastmsg = Message::load_from_db(context, *msg_id).await?;
|
||||
lastmsg.timestamp_sort
|
||||
} else {
|
||||
chat_id.created_timestamp(context).await?
|
||||
context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT created_timestamp FROM chats WHERE id=?;",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
if let PeerstateChange::Aeap(new_addr) = &change {
|
||||
|
||||
25
src/pgp.rs
25
src/pgp.rs
@@ -18,6 +18,7 @@ use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToK
|
||||
use rand::{thread_rng, CryptoRng, Rng};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::constants::KeyGenType;
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -180,9 +181,15 @@ impl KeyPair {
|
||||
///
|
||||
/// Both secret and public key consist of signing primary key and encryption subkey
|
||||
/// as [described in the Autocrypt standard](https://autocrypt.org/level1.html#openpgp-based-key-data).
|
||||
pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
|
||||
let signing_key_type = PgpKeyType::EdDSALegacy;
|
||||
let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
|
||||
pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Result<KeyPair> {
|
||||
let (signing_key_type, encryption_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (
|
||||
PgpKeyType::EdDSALegacy,
|
||||
PgpKeyType::ECDH(ECCCurve::Curve25519),
|
||||
),
|
||||
};
|
||||
|
||||
let user_id = format!("<{addr}>");
|
||||
let key_params = SecretKeyParamsBuilder::default()
|
||||
@@ -471,8 +478,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_create_keypair() {
|
||||
let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
|
||||
let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
|
||||
let keypair0 = create_keypair(
|
||||
EmailAddress::new("foo@bar.de").unwrap(),
|
||||
KeyGenType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
let keypair1 = create_keypair(
|
||||
EmailAddress::new("two@zwo.de").unwrap(),
|
||||
KeyGenType::Default,
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(keypair0.public, keypair1.public);
|
||||
}
|
||||
|
||||
|
||||
31
src/qr.rs
31
src/qr.rs
@@ -40,11 +40,7 @@ const HTTPS_SCHEME: &str = "https://";
|
||||
const SHADOWSOCKS_SCHEME: &str = "ss://";
|
||||
|
||||
/// Backup transfer based on iroh-net.
|
||||
pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
|
||||
|
||||
/// Version written to Backups and Backup-QR-Codes.
|
||||
/// Imports will fail when they have a larger version.
|
||||
pub(crate) const DCBACKUP_VERSION: i32 = 2;
|
||||
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
|
||||
|
||||
/// Scanned QR code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -122,9 +118,6 @@ pub enum Qr {
|
||||
auth_token: String,
|
||||
},
|
||||
|
||||
/// The QR code is a backup, but it is too new. The user has to update its Delta Chat.
|
||||
BackupTooNew {},
|
||||
|
||||
/// Ask the user if they want to use the given service for video chats.
|
||||
WebrtcInstance {
|
||||
/// Server domain name.
|
||||
@@ -303,7 +296,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
decode_tg_socks_proxy(context, qr)?
|
||||
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
|
||||
decode_shadowsocks_proxy(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME_PREFIX) {
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
|
||||
let qr_fixed = fix_add_second_device_qr(qr);
|
||||
decode_backup2(&qr_fixed)?
|
||||
} else if qr.starts_with(MAILTO_SCHEME) {
|
||||
@@ -374,9 +367,7 @@ pub fn format_backup(qr: &Qr) -> Result<String> {
|
||||
ref auth_token,
|
||||
} => {
|
||||
let node_addr = serde_json::to_string(node_addr)?;
|
||||
Ok(format!(
|
||||
"{DCBACKUP_SCHEME_PREFIX}{DCBACKUP_VERSION}:{auth_token}&{node_addr}"
|
||||
))
|
||||
Ok(format!("{DCBACKUP2_SCHEME}{auth_token}&{node_addr}"))
|
||||
}
|
||||
_ => Err(anyhow!("Not a backup QR code")),
|
||||
}
|
||||
@@ -652,19 +643,11 @@ fn decode_shadowsocks_proxy(qr: &str) -> Result<Qr> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes a `DCBACKUP` QR code.
|
||||
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
|
||||
fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
let version_and_payload = qr
|
||||
.strip_prefix(DCBACKUP_SCHEME_PREFIX)
|
||||
.ok_or_else(|| anyhow!("Invalid DCBACKUP scheme"))?;
|
||||
let (version, payload) = version_and_payload
|
||||
.split_once(':')
|
||||
.context("DCBACKUP scheme separator missing")?;
|
||||
let version: i32 = version.parse().context("Not a valid number")?;
|
||||
if version > DCBACKUP_VERSION {
|
||||
return Ok(Qr::BackupTooNew {});
|
||||
}
|
||||
|
||||
let payload = qr
|
||||
.strip_prefix(DCBACKUP2_SCHEME)
|
||||
.ok_or_else(|| anyhow!("Invalid DCBACKUP2 scheme"))?;
|
||||
let (auth_token, node_addr) = payload
|
||||
.split_once('&')
|
||||
.context("Backup QR code has no separator")?;
|
||||
|
||||
@@ -917,11 +917,5 @@ async fn test_decode_backup() -> Result<()> {
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP2:AIvFjRFBt_aMiisSZ8P33JqY&{"node_id":"buzkyd4x76w66qtanjk5fm6ikeuo4quletajowsl3a3p7l6j23pa","info":{"relay_url":null,"direct_addresses":["192.168.1.5:12345"]}}"#).await?;
|
||||
assert!(matches!(qr, Qr::Backup2 { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP9:from-the-future"#).await?;
|
||||
assert!(matches!(qr, Qr::BackupTooNew { .. }));
|
||||
|
||||
let qr = check_qr(&ctx, r#"DCBACKUP99:far-from-the-future"#).await?;
|
||||
assert!(matches!(qr, Qr::BackupTooNew { .. }));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -730,7 +730,7 @@ async fn add_parts(
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
let mut better_msg = None;
|
||||
let mut group_changes = GroupChangesInfo::default();
|
||||
let mut group_changes_msgs = (Vec::new(), None);
|
||||
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
|
||||
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
|
||||
}
|
||||
@@ -922,7 +922,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
group_changes = apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
group_chat_id,
|
||||
@@ -1040,11 +1040,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
state = if seen
|
||||
|| fetching_existing_messages
|
||||
|| is_mdn
|
||||
|| chat_id_blocked == Blocked::Yes
|
||||
|| group_changes.silent
|
||||
state = if seen || fetching_existing_messages || is_mdn || chat_id_blocked == Blocked::Yes
|
||||
// No check for `hidden` because only reactions are such and they should be `InFresh`.
|
||||
{
|
||||
MessageState::InSeen
|
||||
@@ -1193,7 +1189,7 @@ async fn add_parts(
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
group_changes = apply_group_changes(
|
||||
group_changes_msgs = apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
chat_id,
|
||||
@@ -1396,14 +1392,22 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
let sort_timestamp = tweak_sort_timestamp(
|
||||
context,
|
||||
mime_parser,
|
||||
group_changes.silent,
|
||||
chat_id,
|
||||
sort_timestamp,
|
||||
)
|
||||
.await?;
|
||||
// Ensure replies to messages are sorted after the parent message.
|
||||
//
|
||||
// This is useful in a case where sender clocks are not
|
||||
// synchronized and parent message has a Date: header with a
|
||||
// timestamp higher than reply timestamp.
|
||||
//
|
||||
// This does not help if parent message arrives later than the
|
||||
// reply.
|
||||
let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
|
||||
let sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
|
||||
std::cmp::max(sort_timestamp, parent_timestamp)
|
||||
});
|
||||
|
||||
// if the mime-headers should be saved, find out its size
|
||||
// (the mime-header ends with an empty line)
|
||||
let save_mime_headers = context.get_config_bool(Config::SaveMimeHeaders).await?;
|
||||
|
||||
let mime_in_reply_to = mime_parser
|
||||
.get_header(HeaderDef::InReplyTo)
|
||||
@@ -1430,7 +1434,7 @@ async fn add_parts(
|
||||
// `true` finally.
|
||||
let mut save_mime_modified = false;
|
||||
|
||||
let mime_headers = if mime_parser.is_mime_modified {
|
||||
let mime_headers = if save_mime_headers || mime_parser.is_mime_modified {
|
||||
let headers = if !mime_parser.decoded_data.is_empty() {
|
||||
mime_parser.decoded_data.clone()
|
||||
} else {
|
||||
@@ -1443,23 +1447,24 @@ async fn add_parts(
|
||||
|
||||
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
|
||||
|
||||
if let Some(m) = group_changes.better_msg {
|
||||
if let Some(msg) = group_changes_msgs.1 {
|
||||
match &better_msg {
|
||||
None => better_msg = Some(m),
|
||||
None => better_msg = Some(msg),
|
||||
Some(_) => {
|
||||
if !m.is_empty() {
|
||||
group_changes.extra_msgs.push((m, is_system_message))
|
||||
if !msg.is_empty() {
|
||||
group_changes_msgs.0.push(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (group_changes_msg, cmd) in group_changes.extra_msgs {
|
||||
for group_changes_msg in group_changes_msgs.0 {
|
||||
// Currently all additional group changes messages are "Member added".
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
&group_changes_msg,
|
||||
cmd,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
sort_timestamp,
|
||||
None,
|
||||
None,
|
||||
@@ -1495,9 +1500,39 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if handle_edit_delete(context, mime_parser, from_id).await? {
|
||||
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
info!(context, "Message edits/deletes existing message (TRASH).");
|
||||
if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(mut original_msg) =
|
||||
Message::load_from_db_optional(context, original_msg_id).await?
|
||||
{
|
||||
if original_msg.from_id == from_id {
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
let edit_msg_showpadlock = part
|
||||
.param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default();
|
||||
if edit_msg_showpadlock || !original_msg.get_showpadlock() {
|
||||
let new_text =
|
||||
part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
|
||||
chat::save_text_edit_to_db(context, &mut original_msg, new_text)
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Edit message: Not encrypted.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Edit message: Bad sender.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Edit message: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Edit message: rfc724_mid {rfc724_mid:?} not found."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut parts = mime_parser.parts.iter().peekable();
|
||||
@@ -1632,7 +1667,7 @@ RETURNING id
|
||||
},
|
||||
hidden,
|
||||
part.bytes as isize,
|
||||
if save_mime_modified && !(trash || hidden) {
|
||||
if (save_mime_headers || save_mime_modified) && !(trash || hidden) {
|
||||
mime_headers.clone()
|
||||
} else {
|
||||
Vec::new()
|
||||
@@ -1766,123 +1801,6 @@ RETURNING id
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks for "Chat-Edit" and "Chat-Delete" headers,
|
||||
/// and edits/deletes existing messages accordingly.
|
||||
///
|
||||
/// Returns `true` if this message is an edit/deletion request.
|
||||
async fn handle_edit_delete(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> Result<bool> {
|
||||
if let Some(rfc724_mid) = mime_parser.get_header(HeaderDef::ChatEdit) {
|
||||
if let Some((original_msg_id, _)) = rfc724_mid_exists(context, rfc724_mid).await? {
|
||||
if let Some(mut original_msg) =
|
||||
Message::load_from_db_optional(context, original_msg_id).await?
|
||||
{
|
||||
if original_msg.from_id == from_id {
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
let edit_msg_showpadlock = part
|
||||
.param
|
||||
.get_bool(Param::GuaranteeE2ee)
|
||||
.unwrap_or_default();
|
||||
if edit_msg_showpadlock || !original_msg.get_showpadlock() {
|
||||
let new_text =
|
||||
part.msg.strip_prefix(EDITED_PREFIX).unwrap_or(&part.msg);
|
||||
chat::save_text_edit_to_db(context, &mut original_msg, new_text)
|
||||
.await?;
|
||||
} else {
|
||||
warn!(context, "Edit message: Not encrypted.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Edit message: Bad sender.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Edit message: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Edit message: rfc724_mid {rfc724_mid:?} not found."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
|
||||
// deletion requests, so there's no need to support them.
|
||||
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
|
||||
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
|
||||
for rfc724_mid in rfc724_mid_vec {
|
||||
if let Some((msg_id, _)) =
|
||||
message::rfc724_mid_exists(context, rfc724_mid).await?
|
||||
{
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
if msg.from_id == from_id {
|
||||
message::delete_msg_locally(context, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
} else {
|
||||
warn!(context, "Delete message: Bad sender.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Delete message: {rfc724_mid:?} not found.");
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
|
||||
} else {
|
||||
warn!(context, "Delete message: Not encrypted.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
async fn tweak_sort_timestamp(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
silent: bool,
|
||||
chat_id: ChatId,
|
||||
sort_timestamp: i64,
|
||||
) -> Result<i64> {
|
||||
// Ensure replies to messages are sorted after the parent message.
|
||||
//
|
||||
// This is useful in a case where sender clocks are not
|
||||
// synchronized and parent message has a Date: header with a
|
||||
// timestamp higher than reply timestamp.
|
||||
//
|
||||
// This does not help if parent message arrives later than the
|
||||
// reply.
|
||||
let parent_timestamp = mime_parser.get_parent_timestamp(context).await?;
|
||||
let mut sort_timestamp = parent_timestamp.map_or(sort_timestamp, |parent_timestamp| {
|
||||
std::cmp::max(sort_timestamp, parent_timestamp)
|
||||
});
|
||||
|
||||
// If the message should be silent,
|
||||
// set the timestamp to be no more than the same as last message
|
||||
// so that the chat is not sorted to the top of the chatlist.
|
||||
if silent {
|
||||
let last_msg_timestamp = if let Some(t) = chat_id.get_timestamp(context).await? {
|
||||
t
|
||||
} else {
|
||||
chat_id.created_timestamp(context).await?
|
||||
};
|
||||
sort_timestamp = std::cmp::min(sort_timestamp, last_msg_timestamp);
|
||||
}
|
||||
Ok(sort_timestamp)
|
||||
}
|
||||
|
||||
/// Saves attached locations to the database.
|
||||
///
|
||||
/// Emits an event if at least one new location was added.
|
||||
@@ -2331,23 +2249,11 @@ async fn update_chats_contacts_timestamps(
|
||||
Ok(modified)
|
||||
}
|
||||
|
||||
/// The return type of [apply_group_changes].
|
||||
/// Contains information on which system messages
|
||||
/// should be shown in the chat.
|
||||
#[derive(Default)]
|
||||
struct GroupChangesInfo {
|
||||
/// Optional: A better message that should replace the original system message.
|
||||
/// If this is an empty string, the original system message should be trashed.
|
||||
better_msg: Option<String>,
|
||||
/// If true, the user should not be notified about the group change.
|
||||
silent: bool,
|
||||
/// A list of additional group changes messages that should be shown in the chat.
|
||||
extra_msgs: Vec<(String, SystemMessage)>,
|
||||
}
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Returns [GroupChangesInfo].
|
||||
/// Returns `Vec` of group changes messages and, optionally, a better message to replace the
|
||||
/// original system message. If the better message is empty, the original system message
|
||||
/// should be trashed.
|
||||
///
|
||||
/// * `to_ids` - contents of the `To` and `Cc` headers.
|
||||
/// * `past_ids` - contents of the `Chat-Group-Past-Members` header.
|
||||
@@ -2359,20 +2265,19 @@ async fn apply_group_changes(
|
||||
to_ids: &[ContactId],
|
||||
past_ids: &[ContactId],
|
||||
verified_encryption: &VerifiedEncryption,
|
||||
) -> Result<GroupChangesInfo> {
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
if chat_id.is_special() {
|
||||
// Do not apply group changes to the trash chat.
|
||||
return Ok(GroupChangesInfo::default());
|
||||
return Ok((Vec::new(), None));
|
||||
}
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
return Ok(GroupChangesInfo::default());
|
||||
return Ok((Vec::new(), None));
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut silent = false;
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
@@ -2410,7 +2315,6 @@ async fn apply_group_changes(
|
||||
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
|
||||
if let Some(id) = removed_id {
|
||||
better_msg = if id == from_id {
|
||||
silent = true;
|
||||
Some(stock_str::msg_group_left_local(context, from_id).await)
|
||||
} else {
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
@@ -2428,19 +2332,9 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
}
|
||||
|
||||
let group_name_timestamp = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameTimestamp)
|
||||
.and_then(|s| s.parse::<i64>().ok());
|
||||
if let Some(old_name) = mime_parser
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.map(|s| s.trim())
|
||||
.or(match group_name_timestamp {
|
||||
Some(0) => None,
|
||||
Some(_) => Some(chat.name.as_str()),
|
||||
None => None,
|
||||
})
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
@@ -2449,15 +2343,13 @@ async fn apply_group_changes(
|
||||
{
|
||||
let grpname = &sanitize_single_line(grpname);
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
|
||||
let chat_group_name_timestamp =
|
||||
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
||||
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
||||
// To provide group name consistency, compare names if timestamps are equal.
|
||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, old_name)
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
||||
.await?
|
||||
if chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
Param::GroupNameTimestamp,
|
||||
mime_parser.timestamp_sent,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
info!(context, "Updating grpname for chat {chat_id}.");
|
||||
context
|
||||
@@ -2466,18 +2358,10 @@ async fn apply_group_changes(
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
if mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.is_some()
|
||||
{
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
|
||||
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
|
||||
}
|
||||
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
// this is just an explicit message containing the group-avatar,
|
||||
@@ -2657,11 +2541,7 @@ async fn apply_group_changes(
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, chat_id);
|
||||
}
|
||||
Ok(GroupChangesInfo {
|
||||
better_msg,
|
||||
silent,
|
||||
extra_msgs: group_changes_msgs,
|
||||
})
|
||||
Ok((group_changes_msgs, better_msg))
|
||||
}
|
||||
|
||||
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
|
||||
@@ -2670,7 +2550,7 @@ async fn group_changes_msgs(
|
||||
added_ids: &HashSet<ContactId>,
|
||||
removed_ids: &HashSet<ContactId>,
|
||||
chat_id: ChatId,
|
||||
) -> Result<Vec<(String, SystemMessage)>> {
|
||||
) -> Result<Vec<String>> {
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
if !added_ids.is_empty() {
|
||||
warn!(
|
||||
@@ -2687,19 +2567,17 @@ async fn group_changes_msgs(
|
||||
group_changes_msgs.reserve(added_ids.len() + removed_ids.len());
|
||||
for contact_id in added_ids {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
group_changes_msgs.push((
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
));
|
||||
);
|
||||
}
|
||||
for contact_id in removed_ids {
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
group_changes_msgs.push((
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
SystemMessage::MemberRemovedFromGroup,
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
Ok(group_changes_msgs)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use rand::Rng;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::fs;
|
||||
@@ -1895,11 +1894,44 @@ async fn test_save_mime_headers_off() -> anyhow::Result<()> {
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), "hi!");
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
assert!(mime.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_mime_headers_on() -> anyhow::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.set_config_bool(Config::SaveMimeHeaders, true).await?;
|
||||
let bob = TestContext::new_bob().await;
|
||||
bob.set_config_bool(Config::SaveMimeHeaders, true).await?;
|
||||
|
||||
// alice sends a message to bob, bob sees full mime
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), "hi!");
|
||||
assert!(!msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&bob, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
|
||||
// another one, from bob to alice, that gets encrypted
|
||||
let chat_bob = bob.create_chat(&alice).await;
|
||||
chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?;
|
||||
let msg = alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.get_text(), "ho!");
|
||||
assert!(msg.get_showpadlock());
|
||||
let mime = message::get_mime_headers(&alice, msg.id).await?;
|
||||
let mime_str = String::from_utf8_lossy(&mime);
|
||||
assert!(mime_str.contains("Message-ID:"));
|
||||
assert!(mime_str.contains("From:"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_alias_reply(from_dc: bool, chat_request: bool, group_request: bool) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -4642,14 +4674,7 @@ async fn test_download_later() -> Result<()> {
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
|
||||
// Generate a random string so OpenPGP does not compress it.
|
||||
let text: String = rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
.take(MIN_DOWNLOAD_LIMIT as usize)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let text = String::from_utf8(vec![b'a'; MIN_DOWNLOAD_LIMIT as usize])?;
|
||||
let sent_msg = bob.send_text(bob_chat.id, &text).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
@@ -4680,10 +4705,8 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
|
||||
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
|
||||
// participate in the scenario.
|
||||
let bob = &TestContext::new().await;
|
||||
assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 0);
|
||||
bob.configure_addr("bob@example.net").await;
|
||||
imex(bob, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1);
|
||||
let malice = &TestContext::new().await;
|
||||
malice.configure_addr(alice_addr).await;
|
||||
|
||||
@@ -4691,7 +4714,6 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
|
||||
.send_recv_accept(bob, malice, "hi from bob")
|
||||
.await
|
||||
.chat_id;
|
||||
assert_eq!(crate::key::load_self_secret_keyring(bob).await?.len(), 1);
|
||||
|
||||
let sent_msg = malice.send_text(malice_chat_id, "hi from malice").await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
@@ -5403,40 +5425,6 @@ Hello!"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_rename_chat_on_missing_message() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_to_chat_contacts_table(
|
||||
&alice,
|
||||
time(),
|
||||
chat_id,
|
||||
&[Contact::create(&alice, "bob", "bob@example.net").await?],
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Bob changes the group name. NB: If Bob does this too fast, it's not guaranteed that his group
|
||||
// name wins because "Group-Name-Timestamp" may not increase.
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
chat::set_chat_name(&bob, bob_chat_id, "Renamed").await?;
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// Bob adds a new member.
|
||||
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
|
||||
let add_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice only receives the member addition.
|
||||
alice.recv_msg(&add_msg).await;
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert_eq!(chat.get_name(), "Renamed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that creating a group
|
||||
/// is preferred over assigning message to existing
|
||||
/// chat based on `In-Reply-To` and `References`.
|
||||
|
||||
@@ -141,7 +141,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vc-auth-required"
|
||||
);
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
assert_eq!(
|
||||
bob_chat.why_cant_send(&bob).await.unwrap(),
|
||||
@@ -154,7 +154,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
let bob_chat = bob.get_chat(&alice).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
|
||||
// Check Bob emitted the JoinerProgress event.
|
||||
@@ -252,7 +252,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.get_chat() explicitly below)
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
.await
|
||||
@@ -267,7 +267,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
@@ -816,13 +816,8 @@ async fn test_shared_bobs_key() -> Result<()> {
|
||||
let bob3_addr = "bob3@example.net";
|
||||
bob3.configure_addr(bob3_addr).await;
|
||||
imex(bob3, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
|
||||
let chat = bob3.create_email_chat(alice).await;
|
||||
let sent = bob3.send_text(chat.id, "hi Alice!").await;
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
let chat = alice.create_email_chat(bob3).await;
|
||||
let sent = alice.send_text(chat.id, "hi Bob3!").await;
|
||||
let msg = bob3.recv_msg(&sent).await;
|
||||
tcm.send_recv(bob3, alice, "hi Alice!").await;
|
||||
let msg = tcm.send_recv(alice, bob3, "hi Bob3!").await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mut bob_ids = HashSet::new();
|
||||
|
||||
77
src/sql.rs
77
src/sql.rs
@@ -251,7 +251,7 @@ impl Sql {
|
||||
|
||||
if recode_avatar {
|
||||
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
||||
let mut blob = BlobObject::from_path(context, Path::new(&avatar))?;
|
||||
let mut blob = BlobObject::from_name(context, avatar)?;
|
||||
match blob.recode_to_avatar_size(context).await {
|
||||
Ok(()) => {
|
||||
if let Some(path) = blob.to_abs_path().to_str() {
|
||||
@@ -458,9 +458,7 @@ impl Sql {
|
||||
/// in parallel with other transactions. NB: Creating and modifying temporary tables are also
|
||||
/// allowed with `query_only`, temporary tables aren't visible in other connections, but you
|
||||
/// need to pass `PRAGMA query_only=0;` to SQLite before that:
|
||||
/// ```text
|
||||
/// pragma_update(None, "query_only", "0")
|
||||
/// ```
|
||||
/// `pragma_update(None, "query_only", "0")`.
|
||||
/// Also temporary tables need to be dropped because the connection is returned to the pool
|
||||
/// then.
|
||||
///
|
||||
@@ -914,51 +912,42 @@ pub async fn remove_unused_files(context: &Context) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let stats = match tokio::fs::metadata(entry.path()).await {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
if let Ok(stats) = tokio::fs::metadata(entry.path()).await {
|
||||
if stats.is_dir() {
|
||||
if let Err(e) = tokio::fs::remove_dir(entry.path()).await {
|
||||
// The dir could be created not by a user, but by a desktop
|
||||
// environment f.e. So, no warning.
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Cannot rmdir {}: {:#}.",
|
||||
entry.path().display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
unreferenced_count += 1;
|
||||
let recently_created =
|
||||
stats.created().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_modified =
|
||||
stats.modified().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_accessed =
|
||||
stats.accessed().is_ok_and(|t| t > keep_files_newer_than);
|
||||
|
||||
if p == blobdir
|
||||
&& (recently_created || recently_modified || recently_accessed)
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Cannot get metadata for {}: {:#}.",
|
||||
entry.path().display(),
|
||||
err
|
||||
"Housekeeping: Keeping new unreferenced file #{}: {:?}.",
|
||||
unreferenced_count,
|
||||
entry.file_name(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(stats) => stats,
|
||||
};
|
||||
|
||||
if stats.is_dir() {
|
||||
if let Err(e) = tokio::fs::remove_dir(entry.path()).await {
|
||||
// The dir could be created not by a user, but by a desktop
|
||||
// environment f.e. So, no warning.
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Cannot rmdir {}: {:#}.",
|
||||
entry.path().display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
unreferenced_count += 1;
|
||||
}
|
||||
|
||||
unreferenced_count += 1;
|
||||
let recently_created = stats.created().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_modified =
|
||||
stats.modified().is_ok_and(|t| t > keep_files_newer_than);
|
||||
let recently_accessed =
|
||||
stats.accessed().is_ok_and(|t| t > keep_files_newer_than);
|
||||
|
||||
if p == blobdir && (recently_created || recently_modified || recently_accessed)
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Keeping new unreferenced file #{}: {:?}.",
|
||||
unreferenced_count,
|
||||
entry.file_name(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Housekeeping: Deleting unreferenced file #{}: {:?}.",
|
||||
|
||||
@@ -97,25 +97,24 @@ impl Summary {
|
||||
let prefix = if msg.state == MessageState::OutDraft {
|
||||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||||
} else if msg.from_id == ContactId::SELF {
|
||||
if msg.is_info() {
|
||||
if msg.is_info() || chat.is_self_talk() {
|
||||
None
|
||||
} else {
|
||||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||||
}
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
if msg.is_info() || contact.is_none() {
|
||||
None
|
||||
} else {
|
||||
msg.get_override_sender_name()
|
||||
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
|
||||
.map(SummaryPrefix::Username)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
match chat.typ {
|
||||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||||
if msg.is_info() || contact.is_none() {
|
||||
None
|
||||
} else {
|
||||
msg.get_override_sender_name()
|
||||
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
|
||||
.map(SummaryPrefix::Username)
|
||||
}
|
||||
}
|
||||
Chattype::Single => None,
|
||||
}
|
||||
};
|
||||
|
||||
let mut text = msg.get_summary_text(context).await;
|
||||
|
||||
27
src/sync.rs
27
src/sync.rs
@@ -17,7 +17,6 @@ use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::{message, stock_str, token};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Whether to send device sync messages. Aimed for usage in the internal API.
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -67,9 +66,6 @@ pub(crate) enum SyncData {
|
||||
src: String, // RFC724 id (i.e. "Message-Id" header)
|
||||
dest: String, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
DeleteMessages {
|
||||
msgs: Vec<String>, // RFC724 id (i.e. "Message-Id" header)
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -169,7 +165,7 @@ impl Context {
|
||||
///
|
||||
/// Mustn't be called from multiple tasks in parallel to avoid sending the same sync items twice
|
||||
/// because sync items are removed from the db only after successful sending. We guarantee this
|
||||
/// by calling `send_sync_msg()` only from the inbox loop.
|
||||
/// by calling `send_sync_msg()` only from the SMTP loop.
|
||||
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
|
||||
if let Some((json, ids)) = self.build_sync_json().await? {
|
||||
let chat_id =
|
||||
@@ -262,7 +258,6 @@ impl Context {
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
||||
SyncData::Config { key, val } => self.sync_config(key, val).await,
|
||||
SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await,
|
||||
SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
@@ -302,26 +297,6 @@ impl Context {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn sync_message_deletion(&self, msgs: &Vec<String>) -> Result<()> {
|
||||
let mut modified_chat_ids = HashSet::new();
|
||||
let mut msg_ids = Vec::new();
|
||||
for rfc724_mid in msgs {
|
||||
if let Some((msg_id, _)) = message::rfc724_mid_exists(self, rfc724_mid).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(self, msg_id).await? {
|
||||
message::delete_msg_locally(self, &msg).await?;
|
||||
msg_ids.push(msg.id);
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
} else {
|
||||
warn!(self, "Sync message delete: Database entry does not exist.");
|
||||
}
|
||||
} else {
|
||||
warn!(self, "Sync message delete: {rfc724_mid:?} not found.");
|
||||
}
|
||||
}
|
||||
message::delete_msgs_locally_done(self, &msg_ids, modified_chat_ids).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -29,11 +29,11 @@ use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::constants::DC_GCL_NO_SPECIALS;
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::{import_vcard, make_vcard, Contact, ContactId, Modifier, Origin};
|
||||
use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::key::{self, DcKey};
|
||||
use crate::key::{self, DcKey, KeyPairUse};
|
||||
use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -167,10 +167,7 @@ impl TestContextManager {
|
||||
);
|
||||
}
|
||||
|
||||
/// Executes SecureJoin protocol between `scanner` and `scanned`.
|
||||
///
|
||||
/// Returns chat ID of the 1:1 chat for `scanner`.
|
||||
pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) -> ChatId {
|
||||
pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) {
|
||||
self.section(&format!(
|
||||
"{} scans {}'s QR code",
|
||||
scanner.name(),
|
||||
@@ -178,20 +175,12 @@ impl TestContextManager {
|
||||
));
|
||||
|
||||
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
|
||||
self.exec_securejoin_qr(scanner, scanned, &qr).await
|
||||
self.exec_securejoin_qr(scanner, scanned, &qr).await;
|
||||
}
|
||||
|
||||
/// Executes SecureJoin initiated by `scanner` scanning `qr` generated by `scanned`.
|
||||
///
|
||||
/// The [`ChatId`] of the created chat is returned, for a SetupContact QR this is the 1:1
|
||||
/// chat with `scanned`, for a SecureJoin QR this is the group chat.
|
||||
pub async fn exec_securejoin_qr(
|
||||
&self,
|
||||
scanner: &TestContext,
|
||||
scanned: &TestContext,
|
||||
qr: &str,
|
||||
) -> ChatId {
|
||||
let chat_id = join_securejoin(&scanner.ctx, qr).await.unwrap();
|
||||
pub async fn exec_securejoin_qr(&self, scanner: &TestContext, scanned: &TestContext, qr: &str) {
|
||||
join_securejoin(&scanner.ctx, qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
@@ -202,7 +191,6 @@ impl TestContextManager {
|
||||
break;
|
||||
}
|
||||
}
|
||||
chat_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +271,7 @@ impl TestContextBuilder {
|
||||
|
||||
let test_context = TestContext::new_internal(Some(name), self.log_sink).await;
|
||||
test_context.configure_addr(&addr).await;
|
||||
key::store_self_keypair(&test_context, &key_pair)
|
||||
key::store_self_keypair(&test_context, &key_pair, KeyPairUse::Default)
|
||||
.await
|
||||
.expect("Failed to save key");
|
||||
test_context
|
||||
@@ -412,11 +400,11 @@ impl TestContext {
|
||||
/// Sets a name for this [`TestContext`] if one isn't yet set.
|
||||
///
|
||||
/// This will show up in events logged in the test output.
|
||||
pub fn set_name(&self, name: &str) {
|
||||
pub fn set_name(&self, name: impl Into<String>) {
|
||||
let mut context_names = CONTEXT_NAMES.write().unwrap();
|
||||
context_names
|
||||
.entry(self.ctx.get_id())
|
||||
.or_insert_with(|| name.to_string());
|
||||
.or_insert_with(|| name.into());
|
||||
}
|
||||
|
||||
/// Returns the name of this [`TestContext`].
|
||||
@@ -727,26 +715,11 @@ impl TestContext {
|
||||
|
||||
/// Creates or returns an existing 1:1 [`Chat`] with another account.
|
||||
///
|
||||
/// This first creates a contact by exporting a vCard from the `other`
|
||||
/// and importing it into `self`,
|
||||
/// then creates a 1:1 chat with this contact.
|
||||
/// 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 create_chat(&self, other: &TestContext) -> Chat {
|
||||
let vcard = make_vcard(other, &[ContactId::SELF]).await.unwrap();
|
||||
let contact_ids = import_vcard(self, &vcard).await.unwrap();
|
||||
assert_eq!(contact_ids.len(), 1);
|
||||
let contact_id = contact_ids.first().unwrap();
|
||||
let chat_id = ChatId::create_for_contact(self, *contact_id).await.unwrap();
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
}
|
||||
|
||||
/// Creates or returns an existing 1:1 [`Chat`] with another account
|
||||
/// by email address.
|
||||
///
|
||||
/// This function can be used to create unencrypted chats.
|
||||
pub async fn create_email_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_contact(other).await;
|
||||
let chat_id = ChatId::create_for_contact(self, contact.id).await.unwrap();
|
||||
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
}
|
||||
|
||||
@@ -770,15 +743,6 @@ impl TestContext {
|
||||
Chat::load_from_db(self, chat_id).await.unwrap()
|
||||
}
|
||||
|
||||
pub async fn assert_no_chat(&self, id: ChatId) {
|
||||
assert!(Chat::load_from_db(self, id).await.is_err());
|
||||
assert!(!self
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (id,))
|
||||
.await
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
/// Sends out the text message.
|
||||
///
|
||||
/// This is not hooked up to any SMTP-IMAP pipeline, so the other account must call
|
||||
|
||||
@@ -186,14 +186,14 @@ async fn test_missing_peerstate_reexecute_securejoin() -> Result<()> {
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
let bob = &tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[alice, bob]).await;
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = bob.get_chat(alice).await;
|
||||
assert!(chat.is_protected());
|
||||
bob.sql
|
||||
.execute("DELETE FROM acpeerstates WHERE addr=?", (&alice_addr,))
|
||||
.await?;
|
||||
let chat_id = tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = Chat::load_from_db(bob, chat_id).await?;
|
||||
tcm.execute_securejoin(bob, alice).await;
|
||||
let chat = bob.get_chat(alice).await;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_protection_broken());
|
||||
Ok(())
|
||||
|
||||
34
src/tools.rs
34
src/tools.rs
@@ -348,13 +348,14 @@ pub(crate) fn get_abs_path(context: &Context, path: &Path) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_filebytes(context: &Context, path: &Path) -> Result<u64> {
|
||||
let path_abs = get_abs_path(context, path);
|
||||
pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef<Path>) -> Result<u64> {
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
let meta = fs::metadata(&path_abs).await?;
|
||||
Ok(meta.len())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_file(context: &Context, path: &Path) -> Result<()> {
|
||||
pub(crate) async fn delete_file(context: &Context, path: impl AsRef<Path>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let path_abs = get_abs_path(context, path);
|
||||
if !path_abs.exists() {
|
||||
bail!("path {} does not exist", path_abs.display());
|
||||
@@ -442,8 +443,11 @@ impl AsRef<Path> for TempPathGuard {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create_folder(context: &Context, path: &Path) -> Result<(), io::Error> {
|
||||
let path_abs = get_abs_path(context, path);
|
||||
pub(crate) async fn create_folder(
|
||||
context: &Context,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<(), io::Error> {
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
if !path_abs.exists() {
|
||||
match fs::create_dir_all(path_abs).await {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -451,7 +455,7 @@ pub(crate) async fn create_folder(context: &Context, path: &Path) -> Result<(),
|
||||
warn!(
|
||||
context,
|
||||
"Cannot create directory \"{}\": {}",
|
||||
path.display(),
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err)
|
||||
@@ -465,16 +469,16 @@ pub(crate) async fn create_folder(context: &Context, path: &Path) -> Result<(),
|
||||
/// Write a the given content to provided file path.
|
||||
pub(crate) async fn write_file(
|
||||
context: &Context,
|
||||
path: &Path,
|
||||
path: impl AsRef<Path>,
|
||||
buf: &[u8],
|
||||
) -> Result<(), io::Error> {
|
||||
let path_abs = get_abs_path(context, path);
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
fs::write(&path_abs, buf).await.map_err(|err| {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot write {} bytes to \"{}\": {}",
|
||||
buf.len(),
|
||||
path.display(),
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
err
|
||||
@@ -482,8 +486,8 @@ pub(crate) async fn write_file(
|
||||
}
|
||||
|
||||
/// Reads the file and returns its context as a byte vector.
|
||||
pub async fn read_file(context: &Context, path: &Path) -> Result<Vec<u8>> {
|
||||
let path_abs = get_abs_path(context, path);
|
||||
pub async fn read_file(context: &Context, path: impl AsRef<Path>) -> Result<Vec<u8>> {
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
|
||||
match fs::read(&path_abs).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
@@ -491,7 +495,7 @@ pub async fn read_file(context: &Context, path: &Path) -> Result<Vec<u8>> {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot read \"{}\" or file is empty: {}",
|
||||
path.display(),
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
@@ -499,8 +503,8 @@ pub async fn read_file(context: &Context, path: &Path) -> Result<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open_file(context: &Context, path: &Path) -> Result<fs::File> {
|
||||
let path_abs = get_abs_path(context, path);
|
||||
pub async fn open_file(context: &Context, path: impl AsRef<Path>) -> Result<fs::File> {
|
||||
let path_abs = get_abs_path(context, path.as_ref());
|
||||
|
||||
match fs::File::open(&path_abs).await {
|
||||
Ok(bytes) => Ok(bytes),
|
||||
@@ -508,7 +512,7 @@ pub async fn open_file(context: &Context, path: &Path) -> Result<fs::File> {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot read \"{}\" or file is empty: {}",
|
||||
path.display(),
|
||||
path.as_ref().display(),
|
||||
err
|
||||
);
|
||||
Err(err.into())
|
||||
|
||||
@@ -282,22 +282,15 @@ async fn test_file_handling() {
|
||||
};
|
||||
}
|
||||
|
||||
assert!(delete_file(context, Path::new("$BLOBDIR/lkqwjelqkwlje"))
|
||||
assert!(delete_file(context, "$BLOBDIR/lkqwjelqkwlje")
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
write_file(context, Path::new("$BLOBDIR/foobar"), b"content")
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
assert!(write_file(context, "$BLOBDIR/foobar", b"content")
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(file_exist!(context, "$BLOBDIR/foobar"));
|
||||
assert!(!file_exist!(context, "$BLOBDIR/foobarx"));
|
||||
assert_eq!(
|
||||
get_filebytes(context, Path::new("$BLOBDIR/foobar"))
|
||||
.await
|
||||
.unwrap(),
|
||||
7
|
||||
);
|
||||
assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7);
|
||||
|
||||
let abs_path = context
|
||||
.get_blobdir()
|
||||
@@ -307,23 +300,19 @@ async fn test_file_handling() {
|
||||
|
||||
assert!(file_exist!(context, &abs_path));
|
||||
|
||||
assert!(delete_file(context, Path::new("$BLOBDIR/foobar"))
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(create_folder(context, Path::new("$BLOBDIR/foobar-folder"))
|
||||
assert!(delete_file(context, "$BLOBDIR/foobar").await.is_ok());
|
||||
assert!(create_folder(context, "$BLOBDIR/foobar-folder")
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(file_exist!(context, "$BLOBDIR/foobar-folder"));
|
||||
assert!(delete_file(context, Path::new("$BLOBDIR/foobar-folder"))
|
||||
assert!(delete_file(context, "$BLOBDIR/foobar-folder")
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
let fn0 = "$BLOBDIR/data.data";
|
||||
assert!(write_file(context, Path::new(fn0), b"content")
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(write_file(context, &fn0, b"content").await.is_ok());
|
||||
|
||||
assert!(delete_file(context, Path::new(fn0)).await.is_ok());
|
||||
assert!(delete_file(context, &fn0).await.is_ok());
|
||||
assert!(!file_exist!(context, &fn0));
|
||||
}
|
||||
|
||||
|
||||
@@ -213,44 +213,6 @@ mod tests {
|
||||
// Assert that the \n was correctly removed from the group name also in the system message
|
||||
assert_eq!(msg.text.contains('\n'), false);
|
||||
|
||||
// This doesn't update the name because Date is the same and name is greater.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg4@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: another name update 4\n\
|
||||
Chat-Group-Name-Changed: another name update\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
4th message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat.id).await?;
|
||||
assert_eq!(chat.name, "another name update");
|
||||
|
||||
// This updates the name because Date is the same and name is lower.
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: Bob Authname <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Message-ID: <msg5@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-ID: abcde123456\n\
|
||||
Chat-Group-Name: another name updat\n\
|
||||
Chat-Group-Name-Changed: another name update\n\
|
||||
Date: Sun, 22 Mar 2021 03:00:00 +0000\n\
|
||||
\n\
|
||||
5th message\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat.id).await?;
|
||||
assert_eq!(chat.name, "another name updat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -726,7 +726,7 @@ async fn test_send_webxdc_status_update() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends an webxdc instance and a status update
|
||||
let alice_chat = alice.create_email_chat(&bob).await;
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
let sent1 = &alice.pop_sent_msg().await;
|
||||
assert_eq!(alice_instance.viewtype, Viewtype::Webxdc);
|
||||
@@ -1022,7 +1022,7 @@ async fn test_pop_status_update() -> Result<()> {
|
||||
async fn test_draft_and_send_webxdc_status_update() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = alice.create_email_chat(&bob).await.id;
|
||||
let alice_chat_id = alice.create_chat(&bob).await.id;
|
||||
|
||||
// prepare webxdc instance,
|
||||
// status updates are not sent for drafts, therefore send_webxdc_status_update() returns Ok(None)
|
||||
@@ -1648,26 +1648,29 @@ async fn test_webxdc_no_internet_access() -> Result<()> {
|
||||
let group_id = create_group_chat(&t, ProtectionStatus::Unprotected, "chat").await?;
|
||||
let broadcast_id = create_broadcast_list(&t).await?;
|
||||
|
||||
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
||||
for internet_xdc in [true, false] {
|
||||
let mut instance = create_webxdc_instance(
|
||||
&t,
|
||||
"foo.xdc",
|
||||
if internet_xdc {
|
||||
include_bytes!("../../test-data/webxdc/request-internet-access.xdc")
|
||||
} else {
|
||||
include_bytes!("../../test-data/webxdc/minimal.xdc")
|
||||
},
|
||||
)?;
|
||||
let instance_id = send_msg(&t, chat_id, &mut instance).await?;
|
||||
t.send_webxdc_status_update(
|
||||
instance_id,
|
||||
r#"{"summary":"real summary", "payload": 42}"#,
|
||||
)
|
||||
.await?;
|
||||
let instance = Message::load_from_db(&t, instance_id).await?;
|
||||
let info = instance.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.internet_access, false);
|
||||
for e2ee in ["1", "0"] {
|
||||
t.set_config(Config::E2eeEnabled, Some(e2ee)).await?;
|
||||
for chat_id in [self_id, single_id, group_id, broadcast_id] {
|
||||
for internet_xdc in [true, false] {
|
||||
let mut instance = create_webxdc_instance(
|
||||
&t,
|
||||
"foo.xdc",
|
||||
if internet_xdc {
|
||||
include_bytes!("../../test-data/webxdc/request-internet-access.xdc")
|
||||
} else {
|
||||
include_bytes!("../../test-data/webxdc/minimal.xdc")
|
||||
},
|
||||
)?;
|
||||
let instance_id = send_msg(&t, chat_id, &mut instance).await?;
|
||||
t.send_webxdc_status_update(
|
||||
instance_id,
|
||||
r#"{"summary":"real summary", "payload": 42}"#,
|
||||
)
|
||||
.await?;
|
||||
let instance = Message::load_from_db(&t, instance_id).await?;
|
||||
let info = instance.get_webxdc_info(&t).await?;
|
||||
assert_eq!(info.internet_access, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Detect/prevent active attacks | [securejoin][] protocols
|
||||
Compare public keys | [openpgp4fpr][] URI Scheme
|
||||
Header encryption | [Header Protection for Cryptographically Protected E-mail](https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/)
|
||||
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
|
||||
Messenger functions | [Chat-over-Email](https://github.com/chatmail/core/blob/main/spec.md#chat-mail-specification)
|
||||
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification)
|
||||
Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][])
|
||||
User and chat colors | [XEP-0392][]: Consistent Color Generation
|
||||
Send and receive system messages | Multipart/Report Media Type ([RFC 6522][])
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
Chat-Group-ID: WVnDtF5azch
|
||||
Chat-Group-Name: =?utf-8?q?testgr1?=
|
||||
Chat-Group-Avatar: group-image.png
|
||||
Chat-User-Avatar: avatar.png
|
||||
Subject: =?utf-8?q?Chat=3A_testgr1=3A_hi!_?=
|
||||
Date: Thu, 12 Dec 2019 17:24:03 +0000
|
||||
X-Mailer: Delta Chat Core 1.0.0-beta.15/CLI
|
||||
@@ -12,8 +14,6 @@ Content-Type: multipart/mixed; boundary="LV8nfXkpyyn39fsVyoB1b29PKDMeb5"
|
||||
|
||||
--LV8nfXkpyyn39fsVyoB1b29PKDMeb5
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Chat-Group-Avatar: group-image.png
|
||||
Chat-User-Avatar: avatar.png
|
||||
|
||||
hi!
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user