Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
0b0ad62a4d feat(http): revalidate .json every ten minutes rather than 1 hour 2025-02-16 09:28:06 +00:00
137 changed files with 13736 additions and 7558 deletions

View File

@@ -35,6 +35,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -59,6 +60,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -110,6 +112,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -137,6 +140,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
@@ -248,10 +252,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

View File

@@ -37,6 +37,9 @@ jobs:
run: npm run test
env:
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
- name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc
run: cargo build --bin deltachat-jsonrpc-server --features webserver
- name: Run linter
working-directory: deltachat-jsonrpc/typescript
run: npm run prettier:check

View File

@@ -24,6 +24,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix fmt
# Check that formatting does not change anything.
@@ -55,9 +56,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
@@ -86,6 +85,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,4 +105,5 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build .#${{ matrix.installable }}

View File

@@ -19,6 +19,7 @@ jobs:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -37,6 +37,7 @@ jobs:
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -56,6 +57,7 @@ jobs:
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

4
.gitignore vendored
View File

@@ -1,9 +1,7 @@
target/
/target
**/*.rs.bk
/build
/dist
/fuzz/fuzz_targets/corpus/
/fuzz/fuzz_targets/crashes/
# ignore vi temporaries
*~

File diff suppressed because it is too large Load Diff

View File

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

1686
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "deltachat"
version = "1.157.3"
version = "1.155.5"
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"] }
@@ -50,24 +50,26 @@ brotli = { version = "7", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
data-encoding = "2.7.0"
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = "0.2"
escaper = "0.1"
fast-socks5 = "0.10"
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"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
mail-builder = { version = "0.4.2", default-features = false }
mailparse = { workspace = true }
mailparse = "0.16"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
@@ -93,14 +95,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"
strum = "0.27"
strum_macros = "0.27"
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 }
@@ -109,7 +111,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.
@@ -134,7 +136,6 @@ members = [
"deltachat-time",
"format-flowed",
"deltachat-contact-tools",
"fuzz",
]
[[bench]]
@@ -158,10 +159,6 @@ harness = false
name = "get_chat_msgs"
harness = false
[[bench]]
name = "marknoticed_chat"
harness = false
[[bench]]
name = "get_chatlist"
harness = false
@@ -174,7 +171,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 }
@@ -182,7 +179,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"

View File

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

View File

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

View File

@@ -1,94 +0,0 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use futures_lite::future::block_on;
use tempfile::tempdir;
async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) {
for c in chats.iter().take(20) {
chat::marknoticed_chat(context, *c).await.unwrap();
}
}
fn criterion_benchmark(c: &mut Criterion) {
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
// messages, such as your primary account.
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
let rt = tokio::runtime::Runtime::new().unwrap();
let chats: Vec<_> = rt.block_on(async {
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
.await
.unwrap();
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
let len = chatlist.len();
(1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
});
// This mainly tests the performance of marknoticed_chat()
// when nothing has to be done
c.bench_function(
"chat::marknoticed_chat (mark 20 chats as noticed repeatedly)",
|b| {
let dir = tempdir().unwrap();
let dir = dir.path();
let new_db = dir.join("dc.db");
std::fs::copy(&path, &new_db).unwrap();
let context = block_on(async {
Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new())
.await
.unwrap()
});
b.to_async(&rt)
.iter(|| marknoticed_chat_benchmark(&context, black_box(&chats)))
},
);
// If the first 20 chats contain fresh messages or reactions,
// this tests the performance of marking them as noticed.
c.bench_function(
"chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)",
|b| {
b.to_async(&rt).iter_batched(
|| {
let dir = tempdir().unwrap();
let new_db = dir.path().join("dc.db");
std::fs::copy(&path, &new_db).unwrap();
let context = block_on(async {
Context::new(
Path::new(&new_db),
100,
Events::new(),
StockStrings::new(),
)
.await
.unwrap()
});
(dir, context)
},
|(_dir, context)| {
let chats = &chats;
async move {
marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await
}
},
BatchSize::PerIteration,
);
},
);
} else {
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
}
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

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

View File

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

View File

@@ -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.
@@ -964,7 +975,7 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
* ~~~
* dc_msg_t* msg = dc_msg_new(context, DC_MSG_IMAGE);
*
* dc_msg_set_file_and_deduplicate(msg, "/file/to/send.jpg", NULL, NULL);
* dc_msg_set_file(msg, "/file/to/send.jpg", NULL);
* dc_send_msg(context, chat_id, msg);
*
* dc_msg_unref(msg);
@@ -1028,38 +1039,6 @@ 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);
/**
* Send chat members a request to edit the given message's text.
*
* Only outgoing messages sent by self can be edited.
* Edited messages should be flagged as such in the UI, see dc_msg_is_edited().
* UI is informed about changes using the event #DC_EVENT_MSGS_CHANGED.
* If the text is not changed, no event and no edit request message are sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The message ID of the message to edit.
* @param new_text The new text.
* This must not be NULL nor empty.
*/
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 +1929,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 +2459,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 +2507,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().
@@ -4440,20 +4432,6 @@ int dc_msg_is_sent (const dc_msg_t* msg);
int dc_msg_is_forwarded (const dc_msg_t* msg);
/**
* Check if the message was edited.
*
* Edited messages should be marked by the UI as such,
* e.g. by the text "Edited" beside the time.
* To edit messages, use dc_send_edit_request().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message is edited, 0=message not edited.
*/
int dc_msg_is_edited (const dc_msg_t* msg);
/**
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
@@ -4763,6 +4741,22 @@ void dc_msg_set_subject (dc_msg_t* msg, const char* subjec
void dc_msg_set_override_sender_name(dc_msg_t* msg, const char* name);
/**
* Set the file associated with a message object.
* This does not alter any information in the database
* nor copy or move the file or checks if the file exist.
* All this can be done with dc_send_msg() later.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param file If the message object is used in dc_send_msg() later,
* this must be the full path of the image file to send.
* @param filemime The MIME type of the file. NULL if you don't know or don't care.
* @deprecated 2025-01-21 Use dc_msg_set_file_and_deduplicate instead
*/
void dc_msg_set_file (dc_msg_t* msg, const char* file, const char* filemime);
/**
* Sets the file associated with a message.
*
@@ -4790,7 +4784,7 @@ void dc_msg_set_file_and_deduplicate(dc_msg_t* msg, const char* file,
/**
* Set the dimensions associated with message object.
* Typically this is the width and the height of an image or video associated using dc_msg_set_file_and_deduplicate().
* Typically this is the width and the height of an image or video associated using dc_msg_set_file().
* This does not alter any information in the database; this may be done by dc_send_msg() later.
*
* @memberof dc_msg_t
@@ -4803,7 +4797,7 @@ void dc_msg_set_dimension (dc_msg_t* msg, int width, int hei
/**
* Set the duration associated with message object.
* Typically this is the duration of an audio or video associated using dc_msg_set_file_and_deduplicate().
* Typically this is the duration of an audio or video associated using dc_msg_set_file().
* This does not alter any information in the database; this may be done by dc_send_msg() later.
*
* @memberof dc_msg_t
@@ -5442,7 +5436,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
* If you want to define the type of a dc_msg_t object for sending,
* use dc_msg_new().
* Depending on the type, you will set more properties using e.g.
* dc_msg_set_text() or dc_msg_set_file_and_deduplicate().
* dc_msg_set_text() or dc_msg_set_file().
* To finally send the message, use dc_send_msg().
*
* To get the types of dc_msg_t objects received, use dc_msg_get_viewtype().
@@ -5463,7 +5457,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Image message.
* If the image is an animated GIF, the type #DC_MSG_GIF should be used.
* File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
* File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension()
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
*
* Before sending, the image is recoded to an reasonable size,
@@ -5476,7 +5470,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Animated GIF message.
* File, width, and height are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension()
* File, width, and height are set via dc_msg_set_file(), dc_msg_set_dimension()
* and retrieved via dc_msg_get_file(), dc_msg_get_width(), and dc_msg_get_height().
*/
#define DC_MSG_GIF 21
@@ -5494,7 +5488,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing an audio file.
* File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
* File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
*/
#define DC_MSG_AUDIO 40
@@ -5503,7 +5497,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* A voice message that was directly recorded by the user.
* For all other audio messages, the type #DC_MSG_AUDIO should be used.
* File and duration are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_duration()
* File and duration are set via dc_msg_set_file(), dc_msg_set_duration()
* and retrieved via dc_msg_get_file(), and dc_msg_get_duration().
*/
#define DC_MSG_VOICE 41
@@ -5512,7 +5506,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Video messages.
* File, width, height, and duration
* are set via dc_msg_set_file_and_deduplicate(), dc_msg_set_dimension(), dc_msg_set_duration()
* are set via dc_msg_set_file(), dc_msg_set_dimension(), dc_msg_set_duration()
* and retrieved via
* dc_msg_get_file(), dc_msg_get_width(),
* dc_msg_get_height(), and dc_msg_get_duration().
@@ -5522,7 +5516,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message containing any file, e.g. a PDF.
* The file is set via dc_msg_set_file_and_deduplicate()
* The file is set via dc_msg_set_file()
* and retrieved via dc_msg_get_file().
*/
#define DC_MSG_FILE 60
@@ -6286,18 +6280,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 +6520,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
*

View File

@@ -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,42 +1041,6 @@ pub unsafe extern "C" fn dc_send_text_msg(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_edit_request(
context: *mut dc_context_t,
msg_id: u32,
new_text: *const libc::c_char,
) {
if context.is_null() || new_text.is_null() {
eprintln!("ignoring careless call to dc_send_edit_request()");
return;
}
let ctx = &*context;
let new_text = to_string_lossy(new_text);
block_on(chat::send_edit_request(ctx, MsgId::new(msg_id), new_text))
.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 +1618,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 +1913,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 +2964,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 +3031,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 +3052,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 +3118,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
}
@@ -3700,16 +3683,6 @@ pub unsafe extern "C" fn dc_msg_is_forwarded(msg: *mut dc_msg_t) -> libc::c_int
ffi_msg.message.is_forwarded().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_edited(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_is_edited()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.is_edited().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_info(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
@@ -3845,6 +3818,23 @@ pub unsafe extern "C" fn dc_msg_set_override_sender_name(
.set_override_sender_name(to_opt_string_lossy(name))
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_file(
msg: *mut dc_msg_t,
file: *const libc::c_char,
filemime: *const libc::c_char,
) {
if msg.is_null() || file.is_null() {
eprintln!("ignoring careless call to dc_msg_set_file()");
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.set_file(
to_string_lossy(file),
to_opt_string_lossy(filemime).as_deref(),
)
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_file_and_deduplicate(
msg: *mut dc_msg_t,

View File

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

View File

@@ -1,17 +1,23 @@
[package]
name = "deltachat-jsonrpc"
version = "1.157.3"
version = "1.155.5"
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"
path = "src/webserver.rs"
required-features = ["webserver"]
[dependencies]
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 }
@@ -25,10 +31,15 @@ sanitize-filename = { workspace = true }
walkdir = "2.5.0"
base64 = { workspace = true }
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.6", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
[features]
default = ["vendored"]
webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"]
vendored = ["deltachat/vendored"]

View File

@@ -4,16 +4,46 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
The JSON-RPC API is exposed in two fashions:
* A executable `deltachat-rpc-server` that exposes the JSON-RPC API through stdio.
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). It exposes the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder.
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
## Usage
#### Running the WebSocket server
From within this folder, you can start the WebSocket server with the following command:
```sh
cargo run --features webserver
```
If you want to use the server in a production setup, first build it in release mode:
```sh
cargo build --features webserver --release
```
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
The server can be configured with environment variables:
|variable|default|description|
|-|-|-|
|`DC_PORT`|`20808`|port to listen on|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
```sh
cross build --features=webserver --target armv7-linux-androideabi --release
```
#### Using the TypeScript/JavaScript client
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder.
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
```sh
@@ -22,7 +52,15 @@ npm install
npm run build
```
The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
```typescript
import { DeltaChat } from './deltachat.bundle.js'
const dc = new DeltaChat('ws://localhost:20808/ws')
const accounts = await dc.rpc.getAllAccounts()
console.log('accounts', accounts)
```
A script is included to build autogenerated documentation, which includes all RPC methods:
```sh
@@ -35,6 +73,18 @@ Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
#### Running the example app
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
```sh
cd typescript
npm run build
npm run example:build
npm run example:start
```
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
Run `npm run example:dev` to live-rebuild the example app when files changes.
### Testing
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
@@ -54,12 +104,14 @@ cd typescript
npm run test
```
This will build the `deltachat-jsonrpc-server` binary and then run a test suite.
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
```
CHATMAIL_DOMAIN=ci-chatmail.testrun.org npm run test
CHATMAIL_DOMAIN=chat.example.org npm run test
```
#### Test Coverage

28
deltachat-jsonrpc/TODO.md Normal file
View File

@@ -0,0 +1,28 @@
# TODO
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
## MVP - Websocket server&client
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
- [ ] coverage for a majority of the API
- [ ] Blobs served
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
### Other Ideas for the Websocket server
- [ ] make sure there can only be one connection at a time to the ws
- why? , it could give problems if its commanded from multiple connections
- [ ] encrypted connection?
- [ ] authenticated connection?
- [ ] Look into unit-testing for the proc macros?
- [ ] proc macro taking over doc comments to generated typescript file
## Desktop Apis
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default

View File

@@ -1,5 +1,5 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::path::Path;
use std::str;
use std::sync::Arc;
use std::time::Duration;
@@ -7,7 +7,6 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
@@ -22,7 +21,7 @@ use deltachat::ephemeral::Timer;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -39,8 +38,6 @@ use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use types::basic_message::{BasicMessageLoadResult, BasicMessageObject};
use types::login_param::EnteredLoginParam;
use walkdir::WalkDir;
use yerpc::rpc;
@@ -344,19 +341,11 @@ impl CommandApi {
ctx.get_info().await
}
/// Get the blob dir.
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
/// Copy file to blob dir.
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
let ctx = self.get_context(account_id).await?;
let file = Path::new(&path);
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
@@ -433,9 +422,6 @@ impl CommandApi {
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
///
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
async fn configure(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
@@ -450,69 +436,6 @@ impl CommandApi {
Ok(())
}
/// Configures a new email account using the provided parameters
/// and adds it as a transport.
///
/// If the email address is the same as an existing transport,
/// then this existing account will be reconfigured instead of a new one being added.
///
/// This function stops and starts IO as needed.
///
/// Usually it will be enough to only set `addr` and `imap.password`,
/// and all the other settings will be autoconfigured.
///
/// During configuration, ConfigureProgress events are emitted;
/// they indicate a successful configuration as well as errors
/// and may be used to create a progress bar.
/// This function will return after configuration is finished.
///
/// If configuration is successful,
/// the working server parameters will be saved
/// and used for connecting to the server.
/// The parameters entered by the user will be saved separately
/// so that they can be prefilled when the user opens the server-configuration screen again.
///
/// See also:
/// - [Self::is_configured()] to check whether there is
/// at least one working transport.
/// - [Self::add_transport_from_qr()] to add a transport
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport(&param.try_into()?).await
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.add_transport_from_qr(&qr).await
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
let ctx = self.get_context(account_id).await?;
let res = ctx
.list_transports()
.await?
.into_iter()
.map(|t| t.into())
.collect();
Ok(res)
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.delete_transport(&addr).await
}
/// Signal an ongoing process to stop.
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1267,48 +1190,6 @@ impl CommandApi {
Ok(messages)
}
async fn basic_get_message(&self, account_id: u32, msg_id: u32) -> Result<BasicMessageObject> {
let ctx = self.get_context(account_id).await?;
let msg_id = MsgId::new(msg_id);
let message_object = BasicMessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))?
.with_context(|| format!("Message {msg_id} does not exist for account {account_id}"))?;
Ok(message_object)
}
/// get multiple messages in one call (but only basic properties)
///
/// This is for optimized performance, the result of [get_messages] is more complete, but is more expensive.
///
/// if loading one message fails the error is stored in the result object in it's place.
///
/// this is the batch variant of [basic_get_message]
async fn basic_get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, BasicMessageLoadResult>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, BasicMessageLoadResult> = HashMap::new();
for message_id in message_ids {
let message_result = BasicMessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
messages.insert(
message_id,
match message_result {
Ok(Some(message)) => BasicMessageLoadResult::Message(message),
Ok(None) => BasicMessageLoadResult::LoadingError {
error: "Message does not exist".to_string(),
},
Err(error) => BasicMessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
},
);
}
Ok(messages)
}
/// Fetch info desktop needs for creating a notification for a message
async fn get_message_notification_info(
&self,
@@ -1324,15 +1205,7 @@ impl CommandApi {
async fn delete_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ex(&ctx, &msgs, false).await
}
/// Delete messages. The messages are deleted on the current device,
/// on the IMAP server and also for all chat members
async fn delete_messages_for_all(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let msgs: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
delete_msgs_ex(&ctx, &msgs, true).await
delete_msgs(&ctx, &msgs).await
}
/// Get an informational text for a single message. The text is multiline and may
@@ -1432,12 +1305,6 @@ impl CommandApi {
Ok(results)
}
async fn save_msgs(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
chat::save_msgs(&ctx, &message_ids).await
}
// ---------------------------------------------
// contact
// ---------------------------------------------
@@ -1645,18 +1512,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?;
@@ -2089,7 +1944,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Sticker);
msg.set_file_and_deduplicate(&ctx, Path::new(&sticker_path), None, None)?;
msg.set_file(&sticker_path, None);
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
msg.force_sticker();
@@ -2143,16 +1998,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?;
@@ -2319,14 +2164,12 @@ impl CommandApi {
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
#[expect(clippy::too_many_arguments)]
async fn misc_send_msg(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
filename: Option<String>,
location: Option<(f64, f64)>,
quoted_message_id: Option<u32>,
) -> Result<(u32, MessageObject)> {
@@ -2338,7 +2181,7 @@ impl CommandApi {
});
message.set_text(text.unwrap_or_default());
if let Some(file) = file {
message.set_file_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?;
message.set_file(file, None);
}
if let Some((latitude, longitude)) = location {
message.set_location(latitude, longitude);
@@ -2366,14 +2209,12 @@ impl CommandApi {
// the better version should support:
// - changing viewtype to enable/disable compression
// - keeping same message id as long as attachment does not change for webxdc messages
#[expect(clippy::too_many_arguments)]
async fn misc_set_draft(
&self,
account_id: u32,
chat_id: u32,
text: Option<String>,
file: Option<String>,
filename: Option<String>,
quoted_message_id: Option<u32>,
view_type: Option<MessageViewtype>,
) -> Result<()> {
@@ -2390,7 +2231,7 @@ impl CommandApi {
));
draft.set_text(text.unwrap_or_default());
if let Some(file) = file {
draft.set_file_and_deduplicate(&ctx, Path::new(&file), filename.as_deref(), None)?;
draft.set_file(file, None);
}
if let Some(id) = quoted_message_id {
draft

View File

@@ -1,166 +0,0 @@
use anyhow::{Context as _, Result};
use deltachat::context::Context;
use deltachat::message::Message;
use deltachat::message::MsgId;
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::message::DownloadState;
use super::message::MessageViewtype;
use super::message::SystemMessageType;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum BasicMessageLoadResult {
Message(BasicMessageObject),
LoadingError { error: String },
}
/// Message that only has basic properties that doen't require additional db calls
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "BasicMessage", rename_all = "camelCase")]
pub struct BasicMessageObject {
id: u32,
chat_id: u32,
from_id: u32,
text: String,
is_edited: bool,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
state: u32,
/// An error text, if there is one.
error: Option<String>,
timestamp: i64,
sort_timestamp: i64,
received_timestamp: i64,
has_deviating_timestamp: bool,
// summary - use/create another function if you need it
subject: String,
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
/// True if the message was sent by a bot.
is_bot: bool,
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
duration: i32,
dimensions_height: i32,
dimensions_width: i32,
videochat_type: Option<u32>,
videochat_url: Option<String>,
override_sender_name: Option<String>,
setup_code_begin: Option<String>,
file: Option<String>,
file_mime: Option<String>,
file_name: Option<String>,
webxdc_href: Option<String>,
download_state: DownloadState,
original_msg_id: Option<u32>,
saved_message_id: Option<u32>,
}
impl BasicMessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
return Ok(None);
};
let override_sender_name = message.get_override_sender_name();
let download_state = message.download_state().into();
let message_object = Self {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
from_id: message.get_from_id().to_u32(),
text: message.get_text(),
is_edited: message.is_edited(),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),
state: message
.get_state()
.to_u32()
.context("state conversion to number failed")?,
error: message.error(),
timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(),
received_timestamp: message.get_received_timestamp(),
has_deviating_timestamp: message.has_deviating_timestamp(),
subject: message.get_subject().to_owned(),
show_padlock: message.get_showpadlock(),
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
is_bot: message.is_bot(),
system_message_type: message.get_info_type().into(),
duration: message.get_duration(),
dimensions_height: message.get_height(),
dimensions_width: message.get_width(),
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.context("videochat type conversion to number failed")?,
),
None => None,
},
videochat_url: message.get_videochat_url(),
override_sender_name,
setup_code_begin: message.get_setupcodebegin(context).await,
file: match message.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
}, //BLOBS
file_mime: message.get_filemime(),
file_name: message.get_filename(),
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
webxdc_href: message.get_webxdc_href(),
download_state,
original_msg_id: message
.get_original_msg_id(context)
.await?
.map(|id| id.to_u32()),
saved_message_id: message
.get_saved_msg_id(context)
.await?
.map(|id| id.to_u32()),
};
Ok(Some(message_object))
}
}

View File

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

View File

@@ -1,179 +0,0 @@
use anyhow::Result;
use deltachat::login_param as dc;
use serde::Deserialize;
use serde::Serialize;
use yerpc::TypeDef;
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EnteredServerLoginParam {
/// Server hostname or IP address.
pub server: String,
/// Server port.
///
/// 0 if not specified.
pub port: u16,
/// Socket security.
pub security: Socket,
/// Username.
///
/// Empty string if not specified.
pub user: String,
/// Password.
pub password: String,
}
impl From<dc::EnteredServerLoginParam> for EnteredServerLoginParam {
fn from(param: dc::EnteredServerLoginParam) -> Self {
Self {
server: param.server,
port: param.port,
security: param.security.into(),
user: param.user,
password: param.password,
}
}
}
impl From<EnteredServerLoginParam> for dc::EnteredServerLoginParam {
fn from(param: EnteredServerLoginParam) -> Self {
Self {
server: param.server,
port: param.port,
security: param.security.into(),
user: param.user,
password: param.password,
}
}
}
/// Login parameters entered by the user.
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EnteredLoginParam {
/// Email address.
pub addr: String,
/// IMAP settings.
pub imap: EnteredServerLoginParam,
/// SMTP settings.
pub smtp: EnteredServerLoginParam,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: EnteredCertificateChecks,
/// If true, login via OAUTH2 (not recommended anymore)
pub oauth2: bool,
}
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
fn from(param: dc::EnteredLoginParam) -> Self {
Self {
addr: param.addr,
imap: param.imap.into(),
smtp: param.smtp.into(),
certificate_checks: param.certificate_checks.into(),
oauth2: param.oauth2,
}
}
}
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
type Error = anyhow::Error;
fn try_from(param: EnteredLoginParam) -> Result<Self> {
Ok(Self {
addr: param.addr,
imap: param.imap.into(),
smtp: param.smtp.into(),
certificate_checks: param.certificate_checks.into(),
oauth2: param.oauth2,
})
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum Socket {
/// Unspecified socket security, select automatically.
Automatic,
/// TLS connection.
Ssl,
/// STARTTLS connection.
Starttls,
/// No TLS, plaintext connection.
Plain,
}
impl From<dc::Socket> for Socket {
fn from(value: dc::Socket) -> Self {
match value {
dc::Socket::Automatic => Self::Automatic,
dc::Socket::Ssl => Self::Ssl,
dc::Socket::Starttls => Self::Starttls,
dc::Socket::Plain => Self::Plain,
}
}
}
impl From<Socket> for dc::Socket {
fn from(value: Socket) -> Self {
match value {
Socket::Automatic => Self::Automatic,
Socket::Ssl => Self::Ssl,
Socket::Starttls => Self::Starttls,
Socket::Plain => Self::Plain,
}
}
}
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum EnteredCertificateChecks {
/// `Automatic` means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// check certificates strictly.
Automatic,
/// Ensure that TLS certificate is valid for the server hostname.
Strict,
/// Accept certificates that are expired, self-signed
/// or otherwise not valid for the server hostname.
AcceptInvalidCertificates,
}
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
fn from(value: dc::EnteredCertificateChecks) -> Self {
match value {
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
dc::EnteredCertificateChecks::Strict => Self::Strict,
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
Self::AcceptInvalidCertificates
}
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
Self::AcceptInvalidCertificates
}
}
}
}
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
fn from(value: EnteredCertificateChecks) -> Self {
match value {
EnteredCertificateChecks::Automatic => Self::Automatic,
EnteredCertificateChecks::Strict => Self::Strict,
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
}
}
}

View File

@@ -1,5 +1,3 @@
use std::path::Path;
use crate::api::VcardContact;
use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
@@ -39,8 +37,6 @@ pub struct MessageObject {
text: String,
is_edited: bool,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
@@ -93,10 +89,6 @@ pub struct MessageObject {
download_state: DownloadState,
original_msg_id: Option<u32>,
saved_message_id: Option<u32>,
reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
@@ -206,7 +198,6 @@ impl MessageObject {
quote,
parent_id,
text: message.get_text(),
is_edited: message.is_edited(),
has_location: message.has_location(),
has_html: message.has_html(),
view_type: message.get_viewtype().into(),
@@ -262,16 +253,6 @@ impl MessageObject {
download_state,
original_msg_id: message
.get_original_msg_id(context)
.await?
.map(|id| id.to_u32()),
saved_message_id: message
.get_saved_msg_id(context)
.await?
.map(|id| id.to_u32()),
reactions,
vcard_contact: vcard_contacts.first().cloned(),
@@ -612,7 +593,6 @@ pub struct MessageData {
pub html: Option<String>,
pub viewtype: Option<MessageViewtype>,
pub file: Option<String>,
pub filename: Option<String>,
pub location: Option<(f64, f64)>,
pub override_sender_name: Option<String>,
/// Quoted message id. Takes preference over `quoted_text` (see below).
@@ -637,12 +617,7 @@ impl MessageData {
message.set_override_sender_name(self.override_sender_name);
}
if let Some(file) = self.file {
message.set_file_and_deduplicate(
context,
Path::new(&file),
self.filename.as_deref(),
None,
)?;
message.set_file(file, None);
}
if let Some((latitude, longitude)) = self.location {
message.set_location(latitude, longitude);

View File

@@ -1,12 +1,10 @@
pub mod account;
pub mod basic_message;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod events;
pub mod http;
pub mod location;
pub mod login_param;
pub mod message;
pub mod provider_info;
pub mod qr;

View File

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

View File

@@ -0,0 +1,47 @@
#![recursion_limit = "256"]
use std::net::SocketAddr;
use std::path::PathBuf;
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808;
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), std::io::Error> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
let port = std::env::var("DC_PORT")
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()
.route("/ws", get(handler))
.layer(Extension(state.clone()));
tokio::spawn(async move {
state.accounts.write().await.start_io().await;
});
let addr = SocketAddr::from(([127, 0, 0, 1], port));
log::info!("JSON-RPC WebSocket server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
handle_ws_rpc(ws, out_receiver, session).await
}

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>DeltaChat JSON-RPC example</title>
<style>
body {
font-family: monospace;
background: black;
color: grey;
}
.grid {
display: grid;
grid-template-columns: 3fr 1fr;
grid-template-areas: "a a" "b c";
}
.message {
color: red;
}
#header {
grid-area: a;
color: white;
font-size: 1.2rem;
}
#header a {
color: white;
font-weight: bold;
}
#main {
grid-area: b;
color: green;
}
#main h2,
#main h3 {
color: blue;
}
#side {
grid-area: c;
color: #777;
overflow-y: auto;
}
</style>
<script type="module" src="dist/example.bundle.js"></script>
</head>
<body>
<h1>DeltaChat JSON-RPC example</h1>
<div class="grid">
<div id="header"></div>
<div id="main"></div>
<div id="side"><h2>log</h2></div>
</div>
<p>
Tip: open the dev console and use the client with
<code>window.client</code>
</p>
</body>
</html>

View File

@@ -0,0 +1,109 @@
import { DcEvent, DeltaChat } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;
window.addEventListener("DOMContentLoaded", (_event) => {
(window as any).selectDeltaAccount = (id: string) => {
SELECTED_ACCOUNT = Number(id);
window.dispatchEvent(new Event("account-changed"));
};
console.log("launch run script...");
run().catch((err) => console.error("run failed", err));
});
async function run() {
const $main = document.getElementById("main")!;
const $side = document.getElementById("side")!;
const $head = document.getElementById("header")!;
const client = new DeltaChat("ws://localhost:20808/ws");
(window as any).client = client.rpc;
client.on("ALL", (accountId, event) => {
onIncomingEvent(accountId, event);
});
window.addEventListener("account-changed", async (_event: Event) => {
listChatsForSelectedAccount();
});
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
async function loadAccountsInHeader() {
console.log("load accounts");
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
for (const account of accounts) {
if (account.kind === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
${account.id}: ${account.addr!}
</a>&nbsp;`
);
} else {
write(
$head,
`<a href="#">
${account.id}: (unconfigured)
</a>&nbsp;`
);
}
}
}
async function listChatsForSelectedAccount() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.kind !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
const chats = await client.rpc.getChatlistEntries(
selectedAccount,
0,
null,
null
);
for (const chatId of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds(
selectedAccount,
chatId,
false,
false
);
const messages = await client.rpc.getMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
}
}
}
function onIncomingEvent(accountId: number, event: DcEvent) {
write(
$side,
`
<p class="message">
[<strong>${event.kind}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { kind: undefined })
)}
</p>`
);
}
}
function write(el: HTMLElement, html: string) {
el.innerHTML += html;
}
function clear(el: HTMLElement) {
el.innerHTML = "";
}

View File

@@ -0,0 +1,29 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat("ws://localhost:20808/ws");
delta.on("event", (event) => {
console.log("event", event.data);
});
const email = process.argv[2];
const password = process.argv[3];
if (!email || !password)
throw new Error(
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
);
console.log(`creating account for ${email}`);
const id = await delta.rpc.addAccount();
console.log(`created account id ${id}`);
await delta.rpc.setConfig(id, "addr", email);
await delta.rpc.setConfig(id, "mail_pw", password);
console.log("configuration updated");
await delta.rpc.configure(id);
console.log("account configured!");
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
}

View File

@@ -0,0 +1,14 @@
import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat();
delta.on("event", (event) => {
console.log("event", event.data);
});
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
}

View File

@@ -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",
@@ -42,6 +42,10 @@
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
"example": "run-s build example:build example:start",
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check .",
@@ -54,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.157.3"
"version": "1.155.5"
}

View File

@@ -2,7 +2,7 @@ import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { BaseTransport, Request } from "yerpc";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
@@ -74,6 +74,34 @@ export class BaseDeltaChat<
}
}
export type Opts = {
url: string;
startEventLoop: boolean;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
startEventLoop: true,
};
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
close() {
this.transport.close();
}
constructor(opts?: Opts | string) {
if (typeof opts === "string") {
opts = { ...DEFAULT_OPTS, url: opts };
} else if (opts) {
opts = { ...DEFAULT_OPTS, ...opts };
} else {
opts = { ...DEFAULT_OPTS };
}
const transport = new WebsocketTransport(opts.url);
super(transport, opts.startEventLoop);
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
close() {}
constructor(input: any, output: any, startEventLoop: boolean) {

View File

@@ -15,6 +15,6 @@
"noImplicitAny": true,
"isolatedModules": true
},
"include": ["*.ts", "test/*.ts"],
"include": ["*.ts", "example/*.ts", "test/*.ts"],
"compileOnSave": false
}

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat-repl"
version = "1.157.3"
version = "1.155.5"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
anyhow = { workspace = true }

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.157.3"
version = "1.155.5"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -26,12 +26,9 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
def wait_for_event(self, event_type=None) -> AttrDict:
def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
while True:
next_event = AttrDict(self._rpc.wait_for_event(self.id))
if event_type is None or next_event.kind == event_type:
return next_event
return AttrDict(self._rpc.wait_for_event(self.id))
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
@@ -41,16 +38,6 @@ class Account:
"""Remove the account."""
self._rpc.remove_account(self.id)
def clone(self) -> "Account":
"""Clone given account.
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
future = self._rpc.provide_backup.future(self.id)
qr = self._rpc.get_backup_qr(self.id)
new_account = self.manager.add_account()
new_account._rpc.get_backup(new_account.id, qr)
future()
return new_account
def start_io(self) -> None:
"""Start the account I/O."""
self._rpc.start_io(self.id)
@@ -96,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):
@@ -113,7 +96,10 @@ class Account:
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
while True:
event = self.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -132,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)
@@ -214,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,
@@ -332,15 +296,10 @@ class Account:
def wait_for_incoming_msg_event(self):
"""Wait for incoming message event and return it."""
return self.wait_for_event(EventType.INCOMING_MSG)
def wait_for_msgs_changed_event(self):
"""Wait for messages changed event and return it."""
return self.wait_for_event(EventType.MSGS_CHANGED)
def wait_for_msgs_noticed_event(self):
"""Wait for messages noticed event and return it."""
return self.wait_for_event(EventType.MSGS_NOTICED)
while True:
event = self.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
@@ -361,7 +320,10 @@ class Account:
break
def wait_for_reactions_changed(self):
return self.wait_for_event(EventType.REACTIONS_CHANGED)
while True:
event = self.wait_for_event()
if event.kind == EventType.REACTIONS_CHANGED:
return event
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
@@ -390,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)

View File

@@ -124,7 +124,6 @@ class Chat:
html: Optional[str] = None,
viewtype: Optional[ViewType] = None,
file: Optional[str] = None,
filename: Optional[str] = None,
location: Optional[tuple[float, float]] = None,
override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None,
@@ -138,7 +137,6 @@ class Chat:
"html": html,
"viewtype": viewtype,
"file": file,
"filename": filename,
"location": location,
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
@@ -174,14 +172,13 @@ class Chat:
self,
text: Optional[str] = None,
file: Optional[str] = None,
filename: Optional[str] = None,
quoted_msg: Optional[int] = None,
viewtype: Optional[str] = None,
) -> None:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype)
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
def remove_draft(self) -> None:
"""Remove draft message."""

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import os
import random
from typing import AsyncGenerator, Optional
import py
import pytest
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
@@ -125,50 +124,3 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))
@pytest.fixture
def data():
"""Test data."""
class Data:
def __init__(self) -> None:
for path in reversed(py.path.local(__file__).parts()):
datadir = path.join("test-data")
if datadir.isdir():
self.path = datadir
return
raise Exception("Data path cannot be found")
def get_path(self, bn):
"""return path of file or None if it doesn't exist."""
fn = os.path.join(self.path, *bn.split("/"))
assert os.path.exists(fn)
return fn
def read_path(self, bn, mode="r"):
fn = self.get_path(bn)
if fn is not None:
with open(fn, mode) as f:
return f.read()
return None
return Data()
@pytest.fixture
def log():
"""Log printer fixture."""
class Printer:
def section(self, msg: str) -> None:
print()
print("=" * 10, msg, "=" * 10)
def step(self, msg: str) -> None:
print("-" * 5, "step " + msg, "-" * 5)
def indent(self, msg: str) -> None:
print(" " + msg)
return Printer()

View File

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

View File

@@ -175,11 +175,17 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
threading.Thread(target=thread_run, daemon=True).start()
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
n = int(bytes(event.data).decode())
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
n = int(bytes(event.data).decode())
break
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
assert int(bytes(event.data).decode()) > n
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert int(bytes(event.data).decode()) > n
break
def test_no_reordering(acfactory, path_to_webxdc):
@@ -223,5 +229,8 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
ac2_hello_msg_snapshot.chat.accept()
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
assert event.msg_id == ac1_webxdc_msg.id
while 1:
event = ac1.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
assert event.msg_id == ac1_webxdc_msg.id
break

View File

@@ -1,53 +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()
alice2.bring_online()
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()
alice2.bring_online()
# 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)

View File

@@ -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)
@@ -76,11 +74,17 @@ def test_qr_securejoin(acfactory, protect):
bob.secure_join(qr_code)
# Alice deletes "vg-request".
alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
while True:
event = alice.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
for ac in [alice, bob]:
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
@@ -457,7 +461,6 @@ def test_qr_new_group_unblocked(acfactory):
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
@pytest.mark.skip(reason="AEAP is disabled for now")
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -650,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.")

View File

@@ -110,9 +110,12 @@ def test_account(acfactory) -> None:
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
@@ -284,59 +287,6 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()
log.section("Alice adds a second device")
alice2 = alice.clone()
log.section("Second device goes online")
alice2.start_io()
log.section("First device changes avatar")
image = data.get_path("image/avatar1000x1000.jpg")
alice.set_config("selfavatar", image)
avatar_config = alice.get_config("selfavatar")
avatar_hash = os.path.basename(avatar_config)
print("Info: avatar hash is ", avatar_hash)
log.section("First device receives avatar change")
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
avatar_config2 = alice2.get_config("selfavatar")
avatar_hash2 = os.path.basename(avatar_config2)
print("Info: avatar hash on second device is ", avatar_hash2)
assert avatar_hash == avatar_hash2
assert avatar_config != avatar_config2
def test_reaction_seen_on_another_dev(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice2 = alice.clone()
alice2.start_io()
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot.chat.accept()
message.send_reaction("😎")
for a in [alice, alice2]:
a.wait_for_event(EventType.INCOMING_REACTION)
alice2.clear_all_events()
alice_chat_bob.mark_noticed()
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
alice2_chat_bob = alice2_contact_bob.create_chat()
assert chat_id == alice2_chat_bob.id
def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
@@ -349,12 +299,16 @@ def test_is_bot(acfactory) -> None:
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
def test_bot(acfactory) -> None:
@@ -515,7 +469,10 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
# Alice reads Bob's message.
message.mark_seen()
bob.wait_for_event(EventType.MSG_READ)
while True:
event = bob.wait_for_event()
if event.kind == EventType.MSG_READ:
break
# Bob sends a message to Alice, it should also be encrypted.
bob_chat_alice.send_text("Hi Alice!")
@@ -666,7 +623,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.
@@ -674,7 +631,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)
@@ -685,7 +645,10 @@ def test_markseen_contact_request(acfactory):
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
bob2.wait_for_event(EventType.MSGS_NOTICED)
while True:
event = bob2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN
@@ -715,30 +678,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
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
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

View File

@@ -1,3 +1,6 @@
from deltachat_rpc_client import EventType
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -6,9 +9,12 @@ def test_webxdc(acfactory) -> None:
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
event = bob.wait_for_incoming_msg_event()
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
while True:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break
webxdc_info = message.get_webxdc_info()
assert webxdc_info == {

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.157.3"
version = "1.155.5"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

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

View File

@@ -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.3"
"version": "1.155.5"
}

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
[advisories]
ignore = [
"RUSTSEC-2020-0071",
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
# and this requires precise measurement of the decryption time by the attacker.
@@ -7,14 +9,14 @@ ignore = [
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained encoding
"RUSTSEC-2021-0153",
# 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,29 +32,30 @@ 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 = "heck", version = "0.4.1" },
{ name = "getrandom", version = "<0.2" },
{ 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 = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rtnetlink", version = "0.13.1" },
{ name = "security-framework", version = "2.11.1" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "time", version = "<0.3" },
{ 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 = "wasi", version = "<0.11" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
{ name = "windows_aarch64_msvc" },
@@ -69,7 +72,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 +86,7 @@ allow = [
"ISC",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
@@ -95,3 +98,9 @@ expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[sources.allow-org]
# Organisations which we allow git sources from.
github = [
"deltachat",
]

View File

@@ -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,10 @@
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"email-0.0.20" = "sha256-cfR3D5jFQpw32bGsgapK2Uwuxmht+rRK/n1ZUmCb2WA=";
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
};
};
mkRustPackage = packageName:
naersk'.buildPackage {
@@ -309,41 +312,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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View 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) => {}
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.157.3"
version = "1.155.5"
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"

View File

@@ -118,7 +118,7 @@ class Message:
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path):
raise ValueError(f"path does not exist: {path!r}")
lib.dc_msg_set_file_and_deduplicate(self._dc_msg, as_dc_charpointer(path), ffi.NULL, mtype)
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
@props.with_doc
def basename(self) -> str:
@@ -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."""

View File

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

View File

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

View File

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

View File

@@ -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()
@@ -432,11 +436,11 @@ class TestOfflineChat:
assert msg.id > 0
assert msg.is_file()
assert os.path.exists(msg.filename)
assert msg.filename.endswith(".txt") == fn.endswith(".txt")
assert msg.filename.endswith(msg.basename)
assert msg.filemime == typeout
msg2 = chat1.send_file(fp, typein)
assert msg2 != msg
assert msg2.filename == msg.filename
assert msg2.filename != msg.filename
def test_create_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()

View File

@@ -1 +1 @@
2025-03-19
2025-02-14

View File

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

View File

@@ -67,6 +67,7 @@ def main():
parser.add_argument("newversion")
json_list = [
"package.json",
"deltachat-jsonrpc/typescript/package.json",
"deltachat-rpc-server/npm-package/package.json",
]

View File

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

View File

@@ -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;
@@ -13,7 +14,8 @@ use image::codecs::jpeg::JpegEncoder;
use image::ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::{fs, task};
use tokio::io::AsyncWriteExt;
use tokio::{fs, io, task};
use tokio_stream::wrappers::ReadDirStream;
use crate::config::Config;
@@ -46,6 +48,73 @@ enum ImageOutputFormat {
}
impl<'a> BlobObject<'a> {
/// Creates a new file, returning a tuple of the name and the handle.
async fn create_new_file(
context: &Context,
dir: &Path,
stem: &str,
ext: &str,
) -> Result<(String, fs::File)> {
const MAX_ATTEMPT: u32 = 16;
let mut attempt = 0;
let mut name = format!("{stem}{ext}");
loop {
attempt += 1;
let path = dir.join(&name);
match fs::OpenOptions::new()
// Using `create_new(true)` in order to avoid race conditions
// when creating multiple files with the same name.
.create_new(true)
.write(true)
.open(&path)
.await
{
Ok(file) => return Ok((name, file)),
Err(err) => {
if attempt >= MAX_ATTEMPT {
return Err(err).context("failed to create file");
} else if attempt == 1 && !dir.exists() {
fs::create_dir_all(dir).await.log_err(context).ok();
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
}
}
}
}
/// Creates a new blob object with unique name by copying an existing file.
///
/// This creates a new blob
/// and copies an existing file into it. This is done in a
/// in way which avoids race-conditions when multiple files are
/// concurrently created.
pub async fn create_and_copy(context: &'a Context, src: &Path) -> Result<BlobObject<'a>> {
let mut src_file = fs::File::open(src)
.await
.with_context(|| format!("failed to open file {}", src.display()))?;
let (stem, ext) = BlobObject::sanitize_name_and_split_extension(&src.to_string_lossy());
let (name, mut dst_file) =
BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?;
let name_for_err = name.clone();
if let Err(err) = io::copy(&mut src_file, &mut dst_file).await {
// Attempt to remove the failed file, swallow errors resulting from that.
let path = context.get_blobdir().join(&name_for_err);
fs::remove_file(path).await.ok();
return Err(err).context("failed to copy file");
}
// Ensure that all buffered bytes are written
dst_file.flush().await?;
let blob = BlobObject {
blobdir: context.get_blobdir(),
name: format!("$BLOBDIR/{name}"),
};
context.emit_event(EventType::NewBlobFile(blob.as_name().to_string()));
Ok(blob)
}
/// Creates a blob object by copying or renaming an existing file.
/// If the source file is already in the blobdir, it will be renamed,
/// otherwise it will be copied to the blobdir first.
@@ -140,6 +209,27 @@ impl<'a> BlobObject<'a> {
})
}
/// Creates a blob from a file, possibly copying it to the blobdir.
///
/// If the source file is not a path to into the blob directory
/// the file will be copied into the blob directory first. If the
/// source file is already in the blobdir it will not be copied
/// and only be created if it is a valid blobname, that is no
/// subdirectory is used and [BlobObject::sanitize_name_and_split_extension] does not
/// modify the filename.
///
/// Paths into the blob directory may be either defined by an absolute path
/// or by the relative prefix `$BLOBDIR`.
pub async fn new_from_path(context: &'a Context, src: &Path) -> Result<BlobObject<'a>> {
if src.starts_with(context.get_blobdir()) {
BlobObject::from_path(context, src)
} else if src.starts_with("$BLOBDIR/") {
BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string())
} else {
BlobObject::create_and_copy(context, src).await
}
}
/// Returns a [BlobObject] for an existing blob from a path.
///
/// The path must designate a file directly in the blobdir and
@@ -150,11 +240,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 +253,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(),
@@ -193,12 +283,21 @@ impl<'a> BlobObject<'a> {
/// Note that this is NOT the user-visible filename,
/// which is only stored in Param::Filename on the message.
///
#[allow(rustdoc::private_intra_doc_links)]
/// [Params]: crate::param::Params
pub fn as_name(&self) -> &str {
&self.name
}
/// Returns the filename of the blob.
pub fn as_file_name(&self) -> &str {
self.name.rsplit('/').next().unwrap_or_default()
}
/// The path relative in the blob directory.
pub fn as_rel_path(&self) -> &Path {
Path::new(self.as_file_name())
}
/// Returns the extension of the blob.
///
/// If a blob's filename has an extension, it is always guaranteed
@@ -212,21 +311,67 @@ impl<'a> BlobObject<'a> {
}
}
/// The name is returned as a tuple, the first part
/// being the stem or basename and the second being an extension,
/// including the dot. E.g. "foo.txt" is returned as `("foo",
/// ".txt")` while "bar" is returned as `("bar", "")`.
///
/// The extension part will always be lowercased.
fn sanitize_name_and_split_extension(name: &str) -> (String, String) {
let name = sanitize_filename(name);
// Let's take a tricky filename,
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
// Assume that the extension is 32 chars maximum.
let ext: String = name
.chars()
.rev()
.take_while(|c| {
(!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control()
})
.take(33)
.collect::<Vec<_>>()
.iter()
.rev()
.collect();
// ext == "nd_point_and_double_ending.tar.gz"
// Split it into "nd_point_and_double_ending" and "tar.gz":
let mut iter = ext.splitn(2, '.');
iter.next();
let ext = iter.next().unwrap_or_default();
let ext = if ext.is_empty() {
String::new()
} else {
format!(".{ext}")
// ".tar.gz"
};
let stem = name
.strip_suffix(&ext)
.unwrap_or_default()
.chars()
.take(64)
.collect();
(stem, ext.to_lowercase())
}
/// Checks whether a name is a valid blob name.
///
/// 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,30 +397,26 @@ impl<'a> BlobObject<'a> {
Ok(blob.as_name().to_string())
}
/// Recode image to avatar size.
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(())
@@ -304,17 +445,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.
///
@@ -329,10 +474,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());
@@ -403,7 +548,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) {
@@ -440,7 +585,7 @@ impl<'a> BlobObject<'a> {
ofmt.clone(),
max_bytes,
&mut encoded,
)? && is_avatar
)? && strict_limits
{
if img_wh < 20 {
return Err(format_err!(
@@ -490,7 +635,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:#}.",

View File

@@ -2,7 +2,6 @@ use std::time::Duration;
use super::*;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::sql;
use crate::test_utils::{self, TestContext};
use crate::tools::SystemTime;
@@ -45,6 +44,20 @@ async fn test_lowercase_ext() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_as_file_name() {
let t = TestContext::new().await;
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
assert_eq!(blob.as_file_name(), FILE_DEDUPLICATED);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_as_rel_path() {
let t = TestContext::new().await;
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
assert_eq!(blob.as_rel_path(), Path::new(FILE_DEDUPLICATED));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_suffix() {
let t = TestContext::new().await;
@@ -100,19 +113,59 @@ 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);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_and_copy() {
let t = TestContext::new().await;
let src = t.dir.path().join("src");
fs::write(&src, b"boo").await.unwrap();
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/src");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let whoops = t.dir.path().join("whoops");
assert!(BlobObject::create_and_copy(&t, whoops.as_ref())
.await
.is_err());
let whoops = t.get_blobdir().join("whoops");
assert!(!whoops.exists());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_from_path() {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("external");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/external");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
let src_int = t.get_blobdir().join("internal");
fs::write(&src_int, b"boo").await.unwrap();
let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap();
assert_eq!(blob.as_name(), "$BLOBDIR/internal");
let data = fs::read(blob.to_abs_path()).await.unwrap();
assert_eq!(data, b"boo");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_from_name_long() {
let t = TestContext::new().await;
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
fs::write(&src_ext, b"boo").await.unwrap();
let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap();
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html"
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
);
}
@@ -120,12 +173,70 @@ 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"));
}
#[test]
fn test_sanitise_name() {
let (stem, ext) = BlobObject::sanitize_name_and_split_extension(
"Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt",
);
assert_eq!(ext, ".txt");
assert!(!stem.is_empty());
// the extensions are kept together as between stem and extension a number may be added -
// and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz`
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("wot.tar.gz");
assert_eq!(stem, "wot");
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitize_name_and_split_extension(".foo.bar");
assert_eq!(stem, "file");
assert_eq!(ext, ".foo.bar");
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("foo?.bar");
assert!(stem.contains("foo"));
assert!(!stem.contains('?'));
assert_eq!(ext, ".bar");
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("no-extension");
assert_eq!(stem, "no-extension");
assert_eq!(ext, "");
let (stem, ext) =
BlobObject::sanitize_name_and_split_extension("path/ignored\\this: is* forbidden?.c");
assert_eq!(ext, ".c");
assert!(!stem.contains("path"));
assert!(!stem.contains("ignored"));
assert!(stem.contains("this"));
assert!(stem.contains("forbidden"));
assert!(!stem.contains('/'));
assert!(!stem.contains('\\'));
assert!(!stem.contains(':'));
assert!(!stem.contains('*'));
assert!(!stem.contains('?'));
let (stem, ext) = BlobObject::sanitize_name_and_split_extension(
"file.with_lots_of_characters_behind_point_and_double_ending.tar.gz",
);
assert_eq!(
stem,
"file.with_lots_of_characters_behind_point_and_double_ending"
);
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("a. tar.tar.gz");
assert_eq!(stem, "a. tar");
assert_eq!(ext, ".tar.gz");
let (stem, ext) = BlobObject::sanitize_name_and_split_extension("Guia_uso_GNB (v0.8).pdf");
assert_eq!(stem, "Guia_uso_GNB (v0.8)");
assert_eq!(ext, ".pdf");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_white_bg() {
let t = TestContext::new().await;
@@ -138,7 +249,7 @@ async fn test_add_white_bg() {
let avatar_src = t.dir.path().join("avatar.png");
fs::write(&avatar_src, bytes).await.unwrap();
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
let img_wh = 128;
let maybe_sticker = &mut false;
let strict_limits = true;
@@ -174,7 +285,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;
@@ -187,7 +298,7 @@ async fn test_selfavatar_outside_blobdir() {
constants::BALANCED_AVATAR_SIZE,
);
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap();
let maybe_sticker = &mut false;
let strict_limits = true;
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
@@ -226,7 +337,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("9e7f409ac5c92b942cc4f31cee2770a.png"),
"Avatar file name {avatar_cfg} should end with its hash"
);
@@ -544,12 +655,6 @@ impl SendImageCheckMediaquality<'_> {
alice_msg.save_file(&alice, &file_saved).await?;
check_image_size(file_saved, compressed_width, compressed_height);
if original_width == compressed_width {
assert_extension(&alice, alice_msg, extension);
} else {
assert_extension(&alice, alice_msg, "jpg");
}
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
@@ -568,52 +673,10 @@ impl SendImageCheckMediaquality<'_> {
assert!(exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);
if original_width == compressed_width {
assert_extension(&bob, bob_msg, extension);
} else {
assert_extension(&bob, bob_msg, "jpg");
}
Ok(img)
}
}
fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
assert!(msg
.param
.get(Param::File)
.unwrap()
.ends_with(&format!(".{extension}")));
assert!(msg
.param
.get(Param::Filename)
.unwrap()
.ends_with(&format!(".{extension}")));
assert!(msg
.get_filename()
.unwrap()
.ends_with(&format!(".{extension}")));
assert_eq!(
msg.get_file(context)
.unwrap()
.extension()
.unwrap()
.to_str()
.unwrap(),
extension
);
assert_eq!(
msg.param
.get_file_blob(context)
.unwrap()
.unwrap()
.suffix()
.unwrap(),
extension
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_big_gif_as_image() -> Result<()> {
let bytes = include_bytes!("../../test-data/image/screenshot.gif");

View File

@@ -3,7 +3,6 @@
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::io::Cursor;
use std::marker::Sync;
use std::path::{Path, PathBuf};
use std::str::FromStr;
@@ -12,7 +11,6 @@ use std::time::Duration;
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
use deltachat_derive::{FromSql, ToSql};
use mail_builder::mime::MimePart;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
@@ -20,11 +18,12 @@ use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS, EDITED_PREFIX,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
TIMESTAMP_SENT_TOLERANCE,
};
use crate::contact::{self, Contact, ContactId, Origin};
@@ -33,6 +32,7 @@ use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::log::LogExt;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
@@ -41,6 +41,7 @@ use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::securejoin::BobState;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
@@ -50,7 +51,6 @@ use crate::tools::{
truncate_msg_text, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -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(())
}
@@ -914,6 +890,12 @@ impl ChatId {
}
}
_ => {
let blob = msg
.param
.get_blob(Param::File, context)
.await?
.context("no file stored in params")?;
msg.param.set(Param::File, blob.as_name());
if msg.viewtype == Viewtype::File {
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
// We do not do an automatic conversion to other viewtypes here so that
@@ -926,10 +908,6 @@ impl ChatId {
}
}
if msg.viewtype == Viewtype::Vcard {
let blob = msg
.param
.get_file_blob(context)?
.context("no file stored in params")?;
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
}
}
@@ -1072,14 +1050,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 +1283,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 +1300,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 +1313,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())
@@ -2178,18 +2157,14 @@ impl Chat {
} else {
None
};
let new_mime_headers: Option<String> = new_mime_headers.map(|s| {
let html_part = MimePart::new("text/html", s);
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
html_part.write_part(cursor).ok();
String::from_utf8_lossy(&buffer).to_string()
});
let new_mime_headers = new_mime_headers.map(|s| new_html_mimepart(s).build().as_string());
let new_mime_headers = new_mime_headers.or_else(|| match was_truncated {
// We need to add some headers so that they are stripped before formatting HTML by
// `MsgId::get_html()`, not a part of the actual text. Let's add "Content-Type", it's
// anyway a useful metadata about the stored text.
true => Some("Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text),
true => Some(
"Content-Type: text/plain; charset=utf-8\r\n\r\n".to_string() + &msg.text + "\r\n",
),
false => None,
});
let new_mime_headers = match new_mime_headers {
@@ -2323,10 +2298,6 @@ impl Chat {
async fn get_sync_id(&self, context: &Context) -> Result<Option<SyncId>> {
match self.typ {
Chattype::Single => {
if self.is_device_talk() {
return Ok(Some(SyncId::Device));
}
let mut r = None;
for contact_id in get_chat_contacts(context, self.id).await? {
if contact_id == ContactId::SELF && !self.is_self_talk() {
@@ -2589,27 +2560,19 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
/// unblocking the chat and notifying the user accordingly.
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
let chat_ids: Vec<ChatId> = context
.sql
.query_map(
"SELECT chat_id FROM bobstate",
(),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
},
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
let Some(bobstate) = BobState::from_db(&context.sql).await? else {
return Ok(());
};
if !bobstate.in_progress() {
return Ok(());
}
let chat_id = bobstate.alice_chat();
let chat = Chat::load_from_db(context, chat_id).await?;
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
for chat_id in chat_ids {
let chat = Chat::load_from_db(context, chat_id).await?;
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
chat_id.spawn_securejoin_wait(context, timeout);
}
if timeout > 0 {
chat_id.spawn_securejoin_wait(context, timeout);
}
Ok(())
}
@@ -2773,7 +2736,8 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
} else if msg.viewtype.has_file() {
let mut blob = msg
.param
.get_file_blob(context)?
.get_blob(Param::File, context)
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let send_as_is = msg.viewtype == Viewtype::File;
@@ -2821,12 +2785,20 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.recode_to_image_size(context, msg.get_filename(), &mut maybe_sticker)
.await?;
msg.param.set(Param::Filename, new_name);
msg.param.set(Param::File, blob.as_name());
if !maybe_sticker {
msg.viewtype = Viewtype::Image;
}
}
msg.param.set(Param::File, blob.as_name());
if let (Some(filename), Some(blob_ext)) = (msg.param.get(Param::Filename), blob.suffix()) {
let stem = match filename.rsplit_once('.') {
Some((stem, _)) => stem,
None => filename,
};
msg.param
.set(Param::Filename, stem.to_string() + "." + blob_ext);
}
if !msg.param.exists(Param::MimeType) {
if let Some((_, mime)) = message::guess_msgtype_from_suffix(msg) {
@@ -3029,10 +3001,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
// disabled by default is fine.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
recipients.retain(|x| x.to_lowercase() != lowercase_from);
if (context.get_config_bool(Config::BccSelf).await?
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
if context.get_config_bool(Config::BccSelf).await?
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
{
recipients.push(from);
}
@@ -3048,17 +3020,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
context,
"Message {} has no recipient, skipping smtp-send.", msg.id
);
msg.param.set_int(Param::GuaranteeE2ee, 1);
msg.update_param(context).await?;
msg.id.set_delivered(context).await?;
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,66 +3124,6 @@ pub async fn send_text_msg(
send_msg(context, chat_id, &mut msg).await
}
/// 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<()> {
let mut original_msg = Message::load_from_db(context, msg_id).await?;
ensure!(
original_msg.from_id == ContactId::SELF,
"Can edit only own messages"
);
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(());
}
save_text_edit_to_db(context, &mut original_msg, &new_text).await?;
let mut edit_msg = Message::new_text(EDITED_PREFIX.to_owned() + &new_text); // prefix only set for nicer display in Non-Delta-MUAs
edit_msg.set_quote(context, Some(&original_msg)).await?; // quote only set for nicer display in Non-Delta-MUAs
if original_msg.get_showpadlock() {
edit_msg.param.set_int(Param::GuaranteeE2ee, 1);
}
edit_msg
.param
.set(Param::TextEditFor, original_msg.rfc724_mid);
edit_msg.hidden = true;
send_msg(context, original_msg.chat_id, &mut edit_msg).await?;
Ok(())
}
pub(crate) async fn save_text_edit_to_db(
context: &Context,
original_msg: &mut Message,
new_text: &str,
) -> Result<()> {
original_msg.param.set_int(Param::IsEdited, 1);
context
.sql
.execute(
"UPDATE msgs SET txt=?, txt_normalized=?, param=? WHERE id=?",
(
new_text,
message::normalize_text(new_text),
original_msg.param.to_string(),
original_msg.id,
),
)
.await?;
context.emit_msgs_changed(original_msg.chat_id, original_msg.id);
Ok(())
}
/// Sends invitation to a videochat.
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId> {
ensure!(
@@ -3423,7 +3328,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
} else {
start_chat_ephemeral_timers(context, chat_id).await?;
let noticed_msgs_count = context
if context
.sql
.execute(
"UPDATE msgs
@@ -3433,36 +3338,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?;
// This is to trigger emitting `MsgsNoticed` on other devices when reactions are noticed
// locally (i.e. when the chat was opened locally).
let hidden_messages = context
.sql
.query_map(
"SELECT id, rfc724_mid FROM msgs
WHERE state=?
AND hidden=1
AND chat_id=?
ORDER BY id LIMIT 100", // LIMIT to 100 in order to avoid blocking the UI too long, usually there will be less than 100 messages anyway
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
|row| {
let msg_id: MsgId = row.get(0)?;
let rfc724_mid: String = row.get(1)?;
Ok((msg_id, rfc724_mid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (msg_id, rfc724_mid) in &hidden_messages {
message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
imap::markseen_on_imap_table(context, rfc724_mid).await?;
}
if noticed_msgs_count == 0 {
.await?
== 0
{
return Ok(());
}
}
@@ -3615,8 +3493,6 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
}
/// Returns a vector of contact IDs for given chat ID that are no longer part of the group.
///
/// Members that have been removed recently are in the beginning of the list.
pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec<ContactId>> {
let now = time();
let list = context
@@ -3629,7 +3505,7 @@ pub async fn get_past_chat_contacts(context: &Context, chat_id: ChatId) -> Resul
WHERE cc.chat_id=?
AND cc.add_timestamp < cc.remove_timestamp
AND ? < cc.remove_timestamp
ORDER BY c.id=1, cc.remove_timestamp DESC, c.id DESC",
ORDER BY c.id=1, c.last_seen DESC, c.id DESC",
(chat_id, now.saturating_sub(60 * 24 * 3600)),
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
@@ -4394,7 +4270,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
})
.await?;
}
context.scheduler.interrupt_inbox().await;
context.send_sync_msg().await?;
Ok(())
}
@@ -4872,9 +4748,6 @@ pub(crate) enum SyncId {
Grpid(String),
/// "Message-ID"-s, from oldest to latest. Used for ad-hoc groups.
Msgids(Vec<String>),
// Special id for device chat.
Device,
}
/// An action synchronised to other devices.
@@ -4890,7 +4763,6 @@ pub(crate) enum SyncAction {
Rename(String),
/// Set chat contacts by their addresses.
SetContacts(Vec<String>),
Delete,
}
impl Context {
@@ -4937,7 +4809,6 @@ impl Context {
ChatId::lookup_by_message(&msg)
.with_context(|| format!("No chat found for Message-IDs {msgids:?}"))?
}
SyncId::Device => ChatId::get_for_contact(self, ContactId::DEVICE).await?,
};
match action {
SyncAction::Block => chat_id.block_ex(self, Nosync).await,
@@ -4950,7 +4821,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,
}
}

View File

@@ -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(())
}
@@ -796,21 +774,6 @@ async fn test_self_talk() -> Result<()> {
Ok(())
}
/// Tests that when BCC-self is disabled
/// and no messages are actually sent
/// in a self-chat, they have a padlock.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_talk_no_bcc_padlock() -> Result<()> {
let t = &TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat = &t.get_self_chat().await;
let msg_id = send_text_msg(t, chat.id, "Foobar".to_string()).await?;
let msg = Message::load_from_db(t, msg_id).await?;
assert!(msg.get_showpadlock());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_device_msg_unlabelled() {
let t = TestContext::new().await;
@@ -1876,6 +1839,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 +2637,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;
@@ -2964,24 +2932,15 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
let a0b_chat_id = rcvd_msg.chat_id;
let a0b_contact_id = rcvd_msg.from_id;
assert_eq!(
Chat::load_from_db(alice0, a0b_chat_id).await?.blocked,
Blocked::Request
);
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::CreateChat);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
sync(alice0, alice1).await;
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
assert_eq!(alice1_contacts.len(), 1);
let a1b_contact_id = alice1_contacts[0];
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.get_addr(), "bob@example.net");
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::CreateChat);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Not);
@@ -3004,22 +2963,22 @@ async fn test_sync_block_before_first_msg() -> Result<()> {
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let rcvd_msg = alice0.recv_msg(&sent_msg).await;
let a0b_chat_id = rcvd_msg.chat_id;
let a0b_contact_id = rcvd_msg.from_id;
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.block(alice0).await?;
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
let a0b_contact = alice0.add_or_lookup_contact(&bob).await;
assert_eq!(a0b_contact.origin, Origin::IncomingUnknownFrom);
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Yes);
sync(alice0, alice1).await;
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
assert_eq!(alice1_contacts.len(), 0);
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::Hidden);
assert!(ChatIdBlocked::lookup_by_contact(alice1, a1b_contact.id)
.await?
.is_none());
let rcvd_msg = alice1.recv_msg(&sent_msg).await;
let a1b_contact_id = rcvd_msg.from_id;
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
let a1b_contact = alice1.add_or_lookup_contact(&bob).await;
assert_eq!(a1b_contact.origin, Origin::IncomingUnknownFrom);
let a1b_chat = alice1.get_chat(&bob).await;
assert_eq!(a1b_chat.blocked, Blocked::Yes);
@@ -3027,48 +2986,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;
@@ -3142,34 +3059,6 @@ async fn test_sync_visibility() -> Result<()> {
Ok(())
}
/// Tests syncing of chat visibility on device message chat.
///
/// Previously due to a bug pinning "Device Messages"
/// chat resulted in creation of `device@localhost` chat
/// on another device.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_device_messages_visibility() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let device_chat_id0 = ChatId::get_for_contact(alice0, ContactId::DEVICE).await?;
device_chat_id0
.set_visibility(alice0, ChatVisibility::Pinned)
.await?;
sync(alice0, alice1).await;
let device_chat_id1 = ChatId::get_for_contact(alice1, ContactId::DEVICE).await?;
let device_chat1 = Chat::load_from_db(alice1, device_chat_id1).await?;
assert_eq!(device_chat1.get_visibility(), ChatVisibility::Pinned);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_muted() -> Result<()> {
let alice0 = &TestContext::new_alice().await;
@@ -3251,10 +3140,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(())
}
@@ -3533,61 +3418,6 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
Ok(())
}
/// Test that past members are ordered by the timestamp of their removal.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_past_members_order() -> Result<()> {
let t = &TestContext::new_alice().await;
let bob_contact_id = Contact::create(t, "Bob", "bob@example.net").await?;
let charlie_contact_id = Contact::create(t, "Charlie", "charlie@example.org").await?;
let fiona_contact_id = Contact::create(t, "Fiona", "fiona@example.net").await?;
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
add_contact_to_chat(t, chat_id, charlie_contact_id).await?;
add_contact_to_chat(t, chat_id, fiona_contact_id).await?;
t.send_text(chat_id, "Hi! I created a group.").await;
assert_eq!(get_past_chat_contacts(t, chat_id).await?.len(), 0);
remove_contact_from_chat(t, chat_id, charlie_contact_id).await?;
let past_contacts = get_past_chat_contacts(t, chat_id).await?;
assert_eq!(past_contacts.len(), 1);
assert_eq!(past_contacts[0], charlie_contact_id);
SystemTime::shift(Duration::from_secs(5));
remove_contact_from_chat(t, chat_id, bob_contact_id).await?;
let past_contacts = get_past_chat_contacts(t, chat_id).await?;
assert_eq!(past_contacts.len(), 2);
assert_eq!(past_contacts[0], bob_contact_id);
assert_eq!(past_contacts[1], charlie_contact_id);
SystemTime::shift(Duration::from_secs(5));
remove_contact_from_chat(t, chat_id, fiona_contact_id).await?;
let past_contacts = get_past_chat_contacts(t, chat_id).await?;
assert_eq!(past_contacts.len(), 3);
assert_eq!(past_contacts[0], fiona_contact_id);
assert_eq!(past_contacts[1], bob_contact_id);
assert_eq!(past_contacts[2], charlie_contact_id);
// Adding and removing Bob
// moves him to the top of past member list.
SystemTime::shift(Duration::from_secs(5));
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
remove_contact_from_chat(t, chat_id, bob_contact_id).await?;
let past_contacts = get_past_chat_contacts(t, chat_id).await?;
assert_eq!(past_contacts.len(), 3);
assert_eq!(past_contacts[0], bob_contact_id);
assert_eq!(past_contacts[1], fiona_contact_id);
assert_eq!(past_contacts[2], charlie_contact_id);
Ok(())
}
/// Test the case when Alice restores a backup older than 60 days
/// with outdated member list.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -3717,189 +3547,3 @@ async fn test_one_to_one_chat_no_group_member_timestamps() {
let payload = sent.payload;
assert!(!payload.contains("Chat-Group-Member-Timestamps:"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_edit_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;
// Alice sends a message with typos, followed by a correction message
let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await;
let alice_msg = sent1.load_from_db().await;
assert_eq!(alice_msg.text, "zext me in delra.cat");
send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
let sent2 = alice.pop_sent_msg().await;
let test = Message::load_from_db(alice, alice_msg.id).await?;
assert_eq!(test.text, "Text me on Delta.Chat");
// Bob receives both messages and has the correct text at the end
let bob_msg = bob.recv_msg(&sent1).await;
assert_eq!(bob_msg.text, "zext me in delra.cat");
bob.recv_msg_opt(&sent2).await;
let test = Message::load_from_db(bob, bob_msg.id).await?;
assert_eq!(test.text, "Text me on Delta.Chat");
// alice has another device, and sees the correction also there
let alice2 = tcm.alice().await;
let alice2_msg = alice2.recv_msg(&sent1).await;
assert_eq!(alice2_msg.text, "zext me in delra.cat");
alice2.recv_msg_opt(&sent2).await;
let test = Message::load_from_db(&alice2, alice2_msg.id).await?;
assert_eq!(test.text, "Text me on Delta.Chat");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_receive_edit_request_after_removal() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
// Alice sends a messag with typos, followed by a correction message
let sent1 = alice.send_text(alice_chat.id, "zext me in delra.cat").await;
let alice_msg = sent1.load_from_db().await;
send_edit_request(alice, alice_msg.id, "Text me on Delta.Chat".to_string()).await?;
let sent2 = alice.pop_sent_msg().await;
// Bob receives first message, deletes it and then ignores the correction
let bob_msg = bob.recv_msg(&sent1).await;
let bob_chat_id = bob_msg.chat_id;
assert_eq!(bob_msg.text, "zext me in delra.cat");
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 1);
delete_msgs(bob, &[bob_msg.id]).await?;
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
bob.recv_msg_trash(&sent2).await;
assert_eq!(bob_chat_id.get_msg_cnt(bob).await?, 0);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_cannot_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;
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;
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())
.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(())
}

View File

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

View File

@@ -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
@@ -955,4 +963,397 @@ fn get_config_keys_string() -> String {
}
#[cfg(test)]
mod config_tests;
mod tests {
use num_traits::FromPrimitive;
use super::*;
use crate::test_utils::{sync, TestContext, TestContextManager};
#[test]
fn test_to_string() {
assert_eq!(Config::MailServer.to_string(), "mail_server");
assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer));
assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys");
assert_eq!(
Config::from_str("sys.config_keys"),
Ok(Config::SysConfigKeys)
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {
let t = TestContext::new().await;
assert!(t.set_config(Config::Bot, None).await.is_ok());
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ui_config() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
t.set_ui_config("ui.android.screen_security", Some("safe"))
.await?;
assert_eq!(
t.get_ui_config("ui.android.screen_security").await?,
Some("safe".to_string())
);
t.set_ui_config("ui.android.screen_security", None).await?;
assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
Ok(())
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bool() -> Result<()> {
let t = TestContext::new().await;
// We need some config that defaults to true
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);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_addrs() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(alice.is_self_addr("alice@example.org").await?);
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
assert!(!alice.is_self_addr("alice@alice.com").await?);
// Test adding the same primary address
alice.set_primary_self_addr("alice@example.org").await?;
alice.set_primary_self_addr("Alice@Example.Org").await?;
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
// Test adding a new (primary) self address
// The address is trimmed during configure by `LoginParam::from_database()`,
// so `set_primary_self_addr()` doesn't have to trim it.
alice.set_primary_self_addr("Alice@alice.com").await?;
assert!(alice.is_self_addr("aliCe@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["Alice@alice.com", "Alice@Example.Org"]
);
// Check that the entry is not duplicated
alice.set_primary_self_addr("alice@alice.com").await?;
alice.set_primary_self_addr("alice@alice.com").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.com", "Alice@Example.Org"]
);
// Test switching back
alice.set_primary_self_addr("alice@example.org").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@example.org", "alice@alice.com"]
);
// Test setting a new primary self address, the previous self address
// should be kept as a secondary self address
alice.set_primary_self_addr("alice@alice.xyz").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
);
assert!(alice.is_self_addr("alice@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
assert!(t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
// The setting should be displayed correctly.
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
t.set_config_bool(Config::Bot, true).await?;
assert!(!t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_server_after_default() -> Result<()> {
let t = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
// does).
t.set_config_bool(Config::BccSelf, false).await?;
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
Ok(())
}
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> 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 mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
// Alice1 has a different config value.
alice1
.set_config_bool(Config::MdnsEnabled, !mdns_enabled)
.await?;
// This changes nothing, but still sends a sync message.
alice0
.set_config_bool(Config::MdnsEnabled, mdns_enabled)
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config_bool(Config::MdnsEnabled).await?,
mdns_enabled
);
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
for key in [Config::ShowEmails, Config::MvboxMove] {
let val = alice0.get_config_bool(key).await?;
alice0.set_config_bool(key, !val).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(key).await?, !val);
}
// `Config::SyncMsgs` mustn't be synced.
alice0.set_config_bool(Config::SyncMsgs, false).await?;
alice0.set_config_bool(Config::SyncMsgs, true).await?;
alice0.set_config_bool(Config::MdnsEnabled, true).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
// Usual sync scenario.
async fn test_config_str(
alice0: &TestContext,
alice1: &TestContext,
key: Config,
val: &str,
) -> Result<()> {
alice0.set_config(key, Some(val)).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
Ok(())
}
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
sync(&alice0, &alice1).await;
// There was a bug that a sync message creates the self-chat with the user avatar instead of
// the special icon and that remains so when the self-chat becomes user-visible. Let's check
// this.
let self_chat = alice0.get_self_chat().await;
let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
assert_eq!(
self_chat_avatar_path,
alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
);
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
alice0.set_config(Config::Selfavatar, None).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
Ok(())
}
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status.to_string())
);
sync(alice1, alice0).await;
assert_eq!(
alice0.get_config(Config::Selfstatus).await?,
Some(status1.to_string())
);
// Need a chat with another contact to send self-avatar.
let bob = &tcm.bob().await;
let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
alice1
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
alice1.recv_msg(&sent_msg).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
sync(alice1, alice0).await;
assert!(alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_event_config_synced() -> 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?;
}
alice0
.set_config(Config::Displayname, Some("Alice Sync"))
.await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some("Alice Sync".to_string())
);
alice1
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
alice0.set_config(Config::Displayname, None).await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
Ok(())
}
}

View File

@@ -1,392 +0,0 @@
use num_traits::FromPrimitive;
use super::*;
use crate::test_utils::{sync, TestContext, TestContextManager};
#[test]
fn test_to_string() {
assert_eq!(Config::MailServer.to_string(), "mail_server");
assert_eq!(Config::from_str("mail_server"), Ok(Config::MailServer));
assert_eq!(Config::SysConfigKeys.to_string(), "sys.config_keys");
assert_eq!(
Config::from_str("sys.config_keys"),
Ok(Config::SysConfigKeys)
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {
let t = TestContext::new().await;
assert!(t.set_config(Config::Bot, None).await.is_ok());
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_media_quality_config_option() {
let t = TestContext::new().await;
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 0);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Balanced);
t.set_config(Config::MediaQuality, Some("1")).await.unwrap();
let media_quality = t.get_config_int(Config::MediaQuality).await.unwrap();
assert_eq!(media_quality, 1);
assert_eq!(constants::MediaQuality::Worse as i32, 1);
let media_quality = constants::MediaQuality::from_i32(media_quality).unwrap_or_default();
assert_eq!(media_quality, constants::MediaQuality::Worse);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ui_config() -> Result<()> {
let t = TestContext::new().await;
assert_eq!(t.get_ui_config("ui.desktop.linux.systray").await?, None);
t.set_ui_config("ui.android.screen_security", Some("safe"))
.await?;
assert_eq!(
t.get_ui_config("ui.android.screen_security").await?,
Some("safe".to_string())
);
t.set_ui_config("ui.android.screen_security", None).await?;
assert_eq!(t.get_ui_config("ui.android.screen_security").await?, None);
assert!(t.set_ui_config("configured", Some("bar")).await.is_err());
Ok(())
}
/// Regression test for https://github.com/deltachat/deltachat-core-rust/issues/3012
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bool() -> Result<()> {
let t = TestContext::new().await;
// We need some config that defaults to true
let c = Config::MdnsEnabled;
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);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_addrs() -> Result<()> {
let alice = TestContext::new_alice().await;
assert!(alice.is_self_addr("alice@example.org").await?);
assert_eq!(alice.get_all_self_addrs().await?, vec!["alice@example.org"]);
assert!(!alice.is_self_addr("alice@alice.com").await?);
// Test adding the same primary address
alice.set_primary_self_addr("alice@example.org").await?;
alice.set_primary_self_addr("Alice@Example.Org").await?;
assert_eq!(alice.get_all_self_addrs().await?, vec!["Alice@Example.Org"]);
// Test adding a new (primary) self address
// The address is trimmed during configure by `LoginParam::from_database()`,
// so `set_primary_self_addr()` doesn't have to trim it.
alice.set_primary_self_addr("Alice@alice.com").await?;
assert!(alice.is_self_addr("aliCe@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["Alice@alice.com", "Alice@Example.Org"]
);
// Check that the entry is not duplicated
alice.set_primary_self_addr("alice@alice.com").await?;
alice.set_primary_self_addr("alice@alice.com").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.com", "Alice@Example.Org"]
);
// Test switching back
alice.set_primary_self_addr("alice@example.org").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@example.org", "alice@alice.com"]
);
// Test setting a new primary self address, the previous self address
// should be kept as a secondary self address
alice.set_primary_self_addr("alice@alice.xyz").await?;
assert_eq!(
alice.get_all_self_addrs().await?,
vec!["alice@alice.xyz", "alice@example.org", "alice@alice.com"]
);
assert!(alice.is_self_addr("alice@example.org").await?);
assert!(alice.is_self_addr("alice@alice.com").await?);
assert!(alice.is_self_addr("Alice@alice.xyz").await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
assert!(t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
// The setting should be displayed correctly.
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
t.set_config_bool(Config::Bot, true).await?;
assert!(!t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool_opt(Config::MdnsEnabled).await?.is_none());
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_server_after_default() -> Result<()> {
let t = &TestContext::new_alice().await;
// Check that the settings are displayed correctly.
assert_eq!(t.get_config(Config::BccSelf).await?, Some("1".to_string()));
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
// Leaving emails on the server even w/o `BccSelf` is a good default at least because other
// MUAs do so even if the server doesn't save sent messages to some sentbox (like Gmail
// does).
t.set_config_bool(Config::BccSelf, false).await?;
assert_eq!(
t.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())
);
Ok(())
}
const SAVED_MESSAGES_DEDUPLICATED_FILE: &str = "969142cb84015bc135767bc2370934a.png";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> 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 mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
// Alice1 has a different config value.
alice1
.set_config_bool(Config::MdnsEnabled, !mdns_enabled)
.await?;
// This changes nothing, but still sends a sync message.
alice0
.set_config_bool(Config::MdnsEnabled, mdns_enabled)
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config_bool(Config::MdnsEnabled).await?,
mdns_enabled
);
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
for key in [Config::ShowEmails, Config::MvboxMove] {
let val = alice0.get_config_bool(key).await?;
alice0.set_config_bool(key, !val).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(key).await?, !val);
}
// `Config::SyncMsgs` mustn't be synced.
alice0.set_config_bool(Config::SyncMsgs, false).await?;
alice0.set_config_bool(Config::SyncMsgs, true).await?;
alice0.set_config_bool(Config::MdnsEnabled, true).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
// Usual sync scenario.
async fn test_config_str(
alice0: &TestContext,
alice1: &TestContext,
key: Config,
val: &str,
) -> Result<()> {
alice0.set_config(key, Some(val)).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
Ok(())
}
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
sync(&alice0, &alice1).await;
// There was a bug that a sync message creates the self-chat with the user avatar instead of
// the special icon and that remains so when the self-chat becomes user-visible. Let's check
// this.
let self_chat = alice0.get_self_chat().await;
let self_chat_avatar_path = self_chat.get_profile_image(&alice0).await?.unwrap();
assert_eq!(
self_chat_avatar_path,
alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
);
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
alice0.set_config(Config::Selfavatar, None).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
Ok(())
}
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status.to_string())
);
sync(alice1, alice0).await;
assert_eq!(
alice0.get_config(Config::Selfstatus).await?,
Some(status1.to_string())
);
// Need a chat with another contact to send self-avatar.
let bob = &tcm.bob().await;
let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
alice1
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
alice1.recv_msg(&sent_msg).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
sync(alice1, alice0).await;
assert!(alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_event_config_synced() -> 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?;
}
alice0
.set_config(Config::Displayname, Some("Alice Sync"))
.await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some("Alice Sync".to_string())
);
alice1
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
alice0.set_config(Config::Displayname, None).await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
Ok(())
}

View File

@@ -28,19 +28,17 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::LogExt;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
};
use crate::message::Message;
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::qr::set_account_from_qr;
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;
@@ -66,59 +64,8 @@ impl Context {
self.sql.get_raw_config_bool("configured").await
}
/// Configures this account with the currently provided parameters.
///
/// Deprecated since 2025-02; use `add_transport_from_qr()`
/// or `add_transport()` instead.
/// Configures this account with the currently set parameters.
pub async fn configure(&self) -> Result<()> {
let param = EnteredLoginParam::load(self).await?;
self.add_transport_inner(&param).await
}
/// Configures a new email account using the provided parameters
/// and adds it as a transport.
///
/// If the email address is the same as an existing transport,
/// then this existing account will be reconfigured instead of a new one being added.
///
/// This function stops and starts IO as needed.
///
/// Usually it will be enough to only set `addr` and `imap.password`,
/// and all the other settings will be autoconfigured.
///
/// During configuration, ConfigureProgress events are emitted;
/// they indicate a successful configuration as well as errors
/// and may be used to create a progress bar.
/// This function will return after configuration is finished.
///
/// If configuration is successful,
/// the working server parameters will be saved
/// and used for connecting to the server.
/// The parameters entered by the user will be saved separately
/// so that they can be prefilled when the user opens the server-configuration screen again.
///
/// See also:
/// - [Self::is_configured()] to check whether there is
/// at least one working transport.
/// - [Self::add_transport_from_qr()] to add a transport
/// from a server encoded in a QR code.
/// - [Self::list_transports()] to get a list of all configured transports.
/// - [Self::delete_transport()] to remove a transport.
pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> {
self.stop_io().await;
let result = self.add_transport_inner(param).await;
if result.is_err() {
if let Ok(true) = self.is_configured().await {
self.start_io().await;
}
return result;
}
self.start_io().await;
Ok(())
}
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
ensure!(
!self.scheduler.is_running().await,
"cannot configure, already running"
@@ -127,63 +74,42 @@ impl Context {
self.sql.is_open().await,
"cannot configure, database not opened."
);
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), &param.addr) {
bail!("Adding a new transport is not supported right now. Check back in a few months!");
}
let cancel_channel = self.alloc_ongoing().await?;
let res = self
.inner_configure(param)
.inner_configure()
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.await;
self.free_ongoing().await;
if let Err(err) = res.as_ref() {
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
progress!(self, 0, Some(error_msg));
progress!(
self,
0,
Some(
stock_str::configuration_failed(
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
&format!("{err:#}"),
)
.await
)
);
} else {
param.save(self).await?;
progress!(self, 1000);
}
res
}
/// Adds a new email account as a transport
/// using the server encoded in the QR code.
/// See [Self::add_transport].
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
set_account_from_qr(self, qr).await?;
self.configure().await?;
Ok(())
}
/// Returns the list of all email accounts that are used as a transport in the current profile.
/// Use [Self::add_transport()] to add or change a transport
/// and [Self::delete_transport()] to delete a transport.
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
let param = EnteredLoginParam::load(self).await?;
Ok(vec![param])
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
#[expect(clippy::unused_async)]
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
async fn inner_configure(&self) -> Result<()> {
info!(self, "Configure ...");
let param = EnteredLoginParam::load(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let configured_param = configure(self, param).await?;
let configured_param = configure(self, &param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, configured_param, old_addr).await?;
@@ -259,7 +185,8 @@ async fn get_configured_param(
param.smtp.password.clone()
};
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
let proxy_config = param.proxy_config.clone();
let proxy_enabled = proxy_config.is_some();
let mut addr = param.addr.clone();
if param.oauth2 {
@@ -418,7 +345,7 @@ async fn get_configured_param(
.collect(),
smtp_user: param.smtp.user.clone(),
smtp_password,
proxy_config: ProxyConfig::load(ctx).await?,
proxy_config: param.proxy_config.clone(),
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
@@ -515,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;
@@ -545,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;

View File

@@ -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;
@@ -217,10 +234,6 @@ pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX.
// Newer Delta Chats will remove the prefix as needed.
pub(crate) const EDITED_PREFIX: &str = "✏️";
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -236,6 +249,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

View File

@@ -247,16 +247,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
timestamp: Ok(now),
});
}
// XXX: newline at the end of vCard is trimmed
// for compatibility with core <=1.155.3
// Newer core should be able to deal with
// trailing CRLF as the fix
// <https://github.com/deltachat/deltachat-core-rust/pull/6522>
// is merged.
Ok(contact_tools::make_vcard(&vcard_contacts)
.trim_end()
.to_string())
Ok(contact_tools::make_vcard(&vcard_contacts))
}
/// Imports contacts from the given vCard.
@@ -505,11 +496,7 @@ pub enum Origin {
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
SecurejoinInvited = 0x0100_0000,
/// Set on Bob's side for contacts scanned from a QR code.
/// Only means the contact has been scanned from the QR code,
/// but does not mean that securejoin succeeded
/// or the key has not changed since the last scan.
/// Getting the current key verification status requires calling contact_is_verified() !
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
@@ -1435,7 +1422,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() {

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ use crate::tools::time;
use crate::webxdc::StatusUpdateItem;
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;
use tokio::task;
#[derive(Debug)]
@@ -99,7 +100,9 @@ pub async fn maybe_set_logging_xdc(
context,
msg.get_viewtype(),
chat_id,
msg.param.get(Param::Filename),
msg.param
.get_path(Param::Filename, context)
.unwrap_or_default(),
msg.get_id(),
)
.await?;
@@ -112,16 +115,18 @@ pub async fn maybe_set_logging_xdc_inner(
context: &Context,
viewtype: Viewtype,
chat_id: ChatId,
filename: Option<&str>,
filename: Option<PathBuf>,
msg_id: MsgId,
) -> anyhow::Result<()> {
if viewtype == Viewtype::Webxdc {
if let Some(filename) = filename {
if filename.starts_with("debug_logging")
&& filename.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
if let Some(file) = filename {
if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) {
if file_name.starts_with("debug_logging")
&& file_name.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
}
}
}
}

View File

@@ -1,9 +1,6 @@
//! End-to-end encryption support.
use std::io::Cursor;
use anyhow::{format_err, Context as _, Result};
use mail_builder::mime::MimePart;
use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
@@ -54,13 +51,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;
}
@@ -89,7 +95,7 @@ impl EncryptHelper {
self,
context: &Context,
verified: bool,
mail_to_encrypt: MimePart<'static>,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, String)>,
compress: bool,
) -> Result<String> {
@@ -130,9 +136,7 @@ impl EncryptHelper {
let sign_key = load_self_secret_key(context).await?;
let mut raw_message = Vec::new();
let cursor = Cursor::new(&mut raw_message);
mail_to_encrypt.clone().write_part(cursor).ok();
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
@@ -141,13 +145,15 @@ impl EncryptHelper {
/// Signs the passed-in `mail` using the private key from `context`.
/// Returns the payload and the signature.
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<String> {
pub async fn sign(
self,
context: &Context,
mail: lettre_email::PartBuilder,
) -> Result<(lettre_email::MimeMessage, String)> {
let sign_key = load_self_secret_key(context).await?;
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
mail.clone().write_part(cursor).ok();
let signature = pgp::pk_calc_signature(&buffer, &sign_key)?;
Ok(signature)
let mime_message = mail.build();
let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
Ok((mime_message, signature))
}
}
@@ -167,7 +173,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 +225,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 +326,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 +349,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();

View File

@@ -1,11 +1,7 @@
use super::*;
use crate::chat::{
add_contact_to_chat, marknoticed_chat, remove_contact_from_chat, set_muted, ChatVisibility,
MuteDuration,
};
use crate::chat::{marknoticed_chat, set_muted, ChatVisibility, MuteDuration};
use crate::config::Config;
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
use crate::contact::Contact;
use crate::download::DownloadState;
use crate::location;
use crate::message::markseen_msgs;
@@ -783,39 +779,3 @@ async fn test_archived_ephemeral_timer() -> Result<()> {
Ok(())
}
/// Tests that non-members cannot change ephemeral timer settings.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ephemeral_timer_non_member() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
let alice_chat_id =
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
send_text_msg(alice, alice_chat_id, "Hi!".to_string()).await?;
let sent = alice.pop_sent_msg().await;
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
// Bob wants to modify the timer.
bob_chat_id.accept(bob).await?;
bob_chat_id
.set_ephemeral_timer(bob, Timer::Enabled { duration: 60 })
.await?;
let sent_ephemeral_timer_change = bob.pop_sent_msg().await;
// Alice removes Bob before receiving the timer change.
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
alice.recv_msg(&sent_ephemeral_timer_change).await;
// Timer is not changed because Bob is not a member.
assert_eq!(
alice_chat_id.get_ephemeral_timer(alice).await?,
Timer::Disabled
);
Ok(())
}

View File

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

View File

@@ -57,7 +57,6 @@ pub enum HeaderDef {
ChatGroupId,
ChatGroupName,
ChatGroupNameChanged,
ChatGroupNameTimestamp,
ChatVerified,
ChatGroupAvatar,
ChatUserAvatar,
@@ -81,12 +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,
/// [Autocrypt](https://autocrypt.org/) header.
Autocrypt,
AutocryptGossip,

View File

@@ -11,8 +11,9 @@ use std::mem;
use anyhow::{Context as _, Result};
use base64::Engine as _;
use lettre_email::mime::Mime;
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use mime::Mime;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
@@ -276,6 +277,16 @@ impl MsgId {
}
}
/// Wraps HTML text into a new text/html mimepart structure.
///
/// Used on forwarding messages to avoid leaking the original mime structure
/// and also to avoid sending too much, maybe large data.
pub fn new_html_mimepart(html: String) -> PartBuilder {
PartBuilder::new()
.content_type(&"text/html; charset=utf-8".parse::<mime::Mime>().unwrap())
.body(html)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -15,15 +15,15 @@ use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_tar::Archive;
use crate::blob::BlobDirContents;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chat::{self, delete_and_reset_all_device_msgs};
use crate::config::Config;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::message::{Message, Viewtype};
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 +139,20 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
}
}
async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = "It seems you are using multiple devices with Delta Chat. Great!\n\n\
If you also want to synchronize outgoing messages across all devices, \
go to \"Settings → Advanced\" and enable \"Send Copy to Self\"."
.to_string();
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
}
Ok(())
}
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 +184,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 +223,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 +415,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 +613,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 +638,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 +657,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 +801,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 +838,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 +885,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 +907,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 +934,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);

View File

@@ -8,6 +8,7 @@ use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::imex::maybe_add_bcc_self_device_msg;
use crate::imex::set_self_key;
use crate::key::{load_self_secret_key, DcKey};
use crate::message::{Message, MsgId, Viewtype};
@@ -46,11 +47,12 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
msg.force_plaintext();
msg.param.set_int(Param::SkipAutocrypt, 1);
// Enable BCC-self, because transferring a key
// means we have a multi-device setup.
context.set_config_bool(Config::BccSelf, true).await?;
chat::send_msg(context, chat_id, &mut msg).await?;
// no maybe_add_bcc_self_device_msg() here.
// the ui shows the dialog with the setup code on this device,
// it would be too much noise to have two things popping up at the same time.
// maybe_add_bcc_self_device_msg() is called on the other device
// once the transfer is completed.
Ok(setup_code)
}
@@ -75,8 +77,8 @@ 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?;
context.set_config_bool(Config::BccSelf, true).await?;
set_self_key(context, &armored_key, true).await?;
maybe_add_bcc_self_device_msg(context).await?;
Ok(())
} else {
@@ -299,12 +301,8 @@ mod tests {
async fn test_key_transfer() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config(Config::BccSelf, Some("0")).await?;
let setup_code = initiate_key_transfer(&alice).await?;
// Test that sending Autocrypt Setup Message enables `bcc_self`.
assert_eq!(alice.get_config_bool(Config::BccSelf).await?, true);
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
@@ -315,24 +313,21 @@ 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;
let rcvd_msg = alice.recv_msg(&sent).await;
assert_eq!(rcvd_msg.get_text(), "Test");
alice.recv_msg(&sent).await;
assert_eq!(alice.get_last_msg().await.get_text(), "Test");
Ok(())
}

View File

@@ -45,7 +45,7 @@ use crate::message::Message;
use crate::qr::Qr;
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::{create_id, time, TempPathGuard};
use crate::{e2ee, EventType};
use crate::EventType;
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
@@ -109,11 +109,6 @@ impl BackupProvider {
.parent()
.context("Context dir not found")?;
// before we export, make sure the private key exists
e2ee::ensure_secret_key_exists(context)
.await
.context("Cannot create private key or private key not available")?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;

View File

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

View File

@@ -52,7 +52,7 @@ pub(crate) mod events;
pub use events::*;
mod aheader;
pub mod blob;
mod blob;
pub mod chat;
pub mod chatlist;
pub mod config;
@@ -68,7 +68,7 @@ mod imap;
pub mod imex;
pub mod key;
pub mod location;
pub mod login_param;
mod login_param;
pub mod message;
mod mimefactory;
pub mod mimeparser;
@@ -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;

Some files were not shown because too many files have changed in this diff Show More