mirror of
https://github.com/chatmail/core.git
synced 2026-10-11 23:50:16 +03:00
Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e486693d | ||
|
|
7ac04d0204 | ||
|
|
a40337f4e0 | ||
|
|
b45d9aa464 | ||
|
|
48b2e2bc1f | ||
|
|
545007aca5 | ||
|
|
07ce319839 | ||
|
|
0d36c85568 | ||
|
|
139fbfae85 | ||
|
|
0568393157 | ||
|
|
7ec732977a | ||
|
|
a8a7cec376 | ||
|
|
7f6beeeecb | ||
|
|
15092407ea | ||
|
|
bd70d48cdf | ||
|
|
ce04e904e2 | ||
|
|
026ddbf9f1 | ||
|
|
628b178076 | ||
|
|
823a16e8e9 | ||
|
|
407ec1311e | ||
|
|
b9667aae6b | ||
|
|
806b437209 | ||
|
|
1a5232f863 | ||
|
|
7ad119f126 | ||
|
|
1682f4b252 | ||
|
|
6a320d545b | ||
|
|
e7aebd6fbc | ||
|
|
8189abd660 | ||
|
|
5ded153ae4 | ||
|
|
2fd5507c00 | ||
|
|
becb83faf1 | ||
|
|
32263b4574 | ||
|
|
fd3e48dcb2 | ||
|
|
69573cd735 | ||
|
|
5c2af42cdd | ||
|
|
42975b2ff3 | ||
|
|
c7063c00f7 | ||
|
|
89df9536e9 | ||
|
|
0e45c2246f | ||
|
|
81a6afde15 | ||
|
|
adcc8a919c | ||
|
|
a24e6d4278 | ||
|
|
776b2247dd | ||
|
|
37dc1f5ca0 | ||
|
|
a68ddab703 | ||
|
|
877f873910 | ||
|
|
53fa0147ae | ||
|
|
7655c5b150 | ||
|
|
235b625f71 | ||
|
|
014b0024a0 | ||
|
|
b0508e661a | ||
|
|
ab3cd6a8f7 | ||
|
|
85461204c5 | ||
|
|
3abf2b5227 | ||
|
|
0d5d7032fe | ||
|
|
c48b04ab99 | ||
|
|
eaa30dbe21 | ||
|
|
bb0f812f71 | ||
|
|
4c287075da | ||
|
|
09d18f9097 | ||
|
|
47b9bfc8bf | ||
|
|
21d13e8a9c | ||
|
|
079260a7cf | ||
|
|
fdec78c092 | ||
|
|
259ffef0bb | ||
|
|
6661a0803e | ||
|
|
c1471bdbd9 | ||
|
|
a981573e48 | ||
|
|
8fb3a7514e |
@@ -20,10 +20,10 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.86.0
|
||||
RUST_VERSION: 1.87.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.82.0
|
||||
MSRV: 1.85.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt and clippy
|
||||
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
- name: Run rustfmt
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.3.0
|
||||
uses: dependabot/fetch-metadata@v2.4.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
@@ -95,9 +95,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server-aarch64-darwin
|
||||
- deltachat-rpc-server
|
||||
|
||||
# Fails to bulid
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
@@ -53,3 +53,4 @@ result
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
.aider*
|
||||
|
||||
@@ -1,5 +1,91 @@
|
||||
# Changelog
|
||||
|
||||
## [1.160.0] - 2025-06-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] jsonrpc: remove webxdc info from MessageObject.
|
||||
Users need to call `get_webxdc_info` separately now
|
||||
and expect that the call may fail e.g. if WebXDC is not a valid ZIP archive.
|
||||
- [**breaking**] Deprecate `DC_GCL_VERIFIED_ONLY`.
|
||||
- [**breaking**] Make logging macros private.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add more IMAP logging.
|
||||
- Sort apps by recently-updated ([#6875](https://github.com/chatmail/core/pull/6875)).
|
||||
- Better error for quoting a message from another chat.
|
||||
- Put "biography" in the vCard ([#6819](https://github.com/chatmail/core/pull/6819)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not allow chat creation if decryption failed.
|
||||
- Remove faulty test ([#6880](https://github.com/chatmail/core/pull/6880)).
|
||||
- Reduce the scope of the last_full_folder_scan lock in scan_folders.
|
||||
- Ignore verification error if the chat is not protected yet.
|
||||
- Create group chats unprotected on verification error.
|
||||
- `fetch_url`: return err on non 2xx reponses.
|
||||
- Sort multiple saved messages by timestamp ([#6862](https://github.com/chatmail/core/pull/6862)).
|
||||
- contact-tools: Escape commas in vCards' FN, KEY, PHOTO, NOTE ([#6912](https://github.com/chatmail/core/pull/6912)).
|
||||
- Don't change ConfiguredAddr when adding a transport ([#6804](https://github.com/chatmail/core/pull/6804)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Increase MSRV to 1.85.0.
|
||||
- Update Doxygen config and layout file.
|
||||
- Update to rPGP 0.16.0 ([#6719](https://github.com/chatmail/core/pull/6719)).
|
||||
- Enable async-native-tls/vendored feature.
|
||||
- Update rusqlite to 0.36.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.87.0.
|
||||
- nix: Test build on macOS without cross-compilation.
|
||||
- Use installed toolchain to lint Rust.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove explicit lock drop at the end of scope.
|
||||
- Use CancellationToken instead of a 1-message channel.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add more code style guide references.
|
||||
|
||||
## [1.159.5] - 2025-05-14
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't change webxdc self-addr when saving and loading draft ([#6854](https://github.com/chatmail/core/pull/6854)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove duplicate miniz_oxide dependency.
|
||||
- Update async-smtp to 0.10.2.
|
||||
|
||||
## [1.159.4] - 2025-05-13
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add missing documentation to deltachat-rpc-client.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Better avatar quality ([#6822](https://github.com/chatmail/core/pull/6822)).
|
||||
- Update iroh from 0.33.0 to 0.35.0 ([#6687](https://github.com/chatmail/core/pull/6687)).
|
||||
- Other dependency updates.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Emit progress(0) in case AEAP is tried.
|
||||
- Replace `FuturesUnordered` from `futures` with `JoinSet` from `tokio`.
|
||||
- Fix order of operations when handling "vc-request-with-auth" ([#6850](https://github.com/chatmail/core/pull/6850)).
|
||||
- Generate rfc724_mid when creating Message ([#6704](https://github.com/chatmail/core/pull/6704))
|
||||
|
||||
### Tests
|
||||
|
||||
- Profile data is attached to group leave messages.
|
||||
|
||||
## [1.159.3] - 2025-04-24
|
||||
|
||||
### CI
|
||||
@@ -6267,3 +6353,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1
|
||||
[1.159.2]: https://github.com/chatmail/core/compare/v1.159.1..v1.159.2
|
||||
[1.159.3]: https://github.com/chatmail/core/compare/v1.159.2..v1.159.3
|
||||
[1.159.4]: https://github.com/chatmail/core/compare/v1.159.3..v1.159.4
|
||||
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
|
||||
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
|
||||
|
||||
Generated
+691
-685
File diff suppressed because it is too large
Load Diff
+21
-17
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.85"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -18,6 +18,9 @@ opt-level = 1
|
||||
debug = 1
|
||||
opt-level = 0
|
||||
|
||||
[profile.fuzz]
|
||||
inherits = "test"
|
||||
|
||||
# Always optimize dependencies.
|
||||
# This does not apply to crates in the workspace.
|
||||
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
||||
@@ -43,7 +46,7 @@ async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", 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-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||
@@ -56,25 +59,25 @@ 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.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.11"
|
||||
hyper-util = "0.1.13"
|
||||
image = { version = "0.25.6", 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.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.4.2", default-features = false }
|
||||
mail-builder = { version = "0.4.3", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num_cpus = "1.17"
|
||||
num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.15.0", default-features = false }
|
||||
pgp = { version = "0.16.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -83,7 +86,7 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.11.0"
|
||||
rustls-pki-types = "1.12.0"
|
||||
rustls = { version = "0.23.22", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -91,7 +94,7 @@ serde_urlencoded = "0.7.1"
|
||||
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"] }
|
||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.0"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
@@ -112,7 +115,7 @@ blake3 = "1.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -186,11 +189,11 @@ nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.32"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.19.1"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.14"
|
||||
@@ -201,7 +204,8 @@ yerpc = "0.6.4"
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"async-native-tls/vendored"
|
||||
]
|
||||
|
||||
[lints.rust]
|
||||
|
||||
@@ -156,13 +156,13 @@ $ 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
|
||||
```sh
|
||||
$ cd fuzz
|
||||
$ cargo bolero test fuzz_mailparse --release=false -s NONE
|
||||
$ cargo bolero test fuzz_mailparse -s NONE
|
||||
```
|
||||
|
||||
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
||||
@@ -170,11 +170,6 @@ you can add initial inputs there.
|
||||
For `fuzz_mailparse` target corpus can be populated with
|
||||
`../test-data/message/*.eml`.
|
||||
|
||||
To run with AFL instead of libFuzzer:
|
||||
```sh
|
||||
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
|
||||
@@ -84,6 +84,13 @@ This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||
and get a backtrace with line numbers in resultified tests
|
||||
which return `anyhow::Result`.
|
||||
|
||||
`unwrap` and `expect` are not used in the library
|
||||
because panics are difficult to debug on user devices.
|
||||
However, in the tests `.expect` may be used.
|
||||
Follow
|
||||
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
||||
for `.expect` message style.
|
||||
|
||||
## Logging
|
||||
|
||||
For logging, use `info!`, `warn!` and `error!` macros.
|
||||
@@ -96,3 +103,17 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||
```
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
```
|
||||
|
||||
## Documentation comments
|
||||
|
||||
All public modules, methods and fields should be documented.
|
||||
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
|
||||
|
||||
Private items do not have to be documented,
|
||||
but CI uses `cargo doc --document-private-items`
|
||||
to build the documentation,
|
||||
so it is preferred that new items
|
||||
are documented.
|
||||
|
||||
Follow Rust guidelines for the documentation comments:
|
||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::accounts::Accounts;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::{
|
||||
config::Config,
|
||||
context::Context,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
@@ -3,15 +3,15 @@ use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{info, Event, EventType, Events};
|
||||
use deltachat::{Event, EventType, Events};
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn send_events_benchmark(context: &Context) {
|
||||
let emitter = context.get_event_emitter();
|
||||
for _i in 0..1_000_000 {
|
||||
info!(context, "interesting event...");
|
||||
context.emit_event(EventType::Info("interesting event...".to_string()));
|
||||
}
|
||||
info!(context, "DONE");
|
||||
context.emit_event(EventType::Info("DONE".to_string()));
|
||||
|
||||
loop {
|
||||
match emitter.recv().await.unwrap() {
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ContactAddress {
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
@@ -282,7 +282,7 @@ impl EmailAddress {
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct VcardContact {
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The biography, stored in the vcard property `note`
|
||||
pub biography: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<i64>,
|
||||
}
|
||||
@@ -44,10 +46,15 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace(',', "\\,")
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||
let addr = escape(&c.addr);
|
||||
let display_name = escape(c.display_name());
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
@@ -55,10 +62,13 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
FN:{display_name}\r\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(biography) = &c.biography {
|
||||
res += &format!("NOTE:{}\r\n", escape(biography));
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
@@ -79,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Returns (parameters, value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
/// Returns (parameters, raw value) tuple.
|
||||
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
let remainder = remove_prefix(line, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
@@ -110,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
Some((params, value))
|
||||
}
|
||||
/// Returns (parameters, unescaped value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||
let (params, value) = vcard_property_raw(line, property)?;
|
||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||
Some((params, value.replace("\\,", ",")))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "key")?;
|
||||
let (params, value) = vcard_property_raw(line, "key")?;
|
||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
||||
}
|
||||
fn base64_photo(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "photo")?;
|
||||
let (params, value) = vcard_property_raw(line, "photo")?;
|
||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||
@@ -136,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:image/jpeg;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
@@ -186,6 +194,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut biography = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for mut line in lines.by_ref() {
|
||||
@@ -205,18 +214,24 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = base64_photo(line) {
|
||||
photo.get_or_insert(p);
|
||||
} else if let Some((_params, bio)) = vcard_property(line, "note") {
|
||||
biography.get_or_insert(bio);
|
||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
let (authname, addr) = sanitize_name_and_addr(
|
||||
&display_name.unwrap_or_default(),
|
||||
&addr.unwrap_or_default(),
|
||||
);
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
biography,
|
||||
timestamp: datetime
|
||||
.as_deref()
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
|
||||
@@ -91,6 +91,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
biography: Some("Hi, I'm Alice".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
@@ -98,6 +99,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
biography: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
@@ -106,8 +108,9 @@ fn test_make_and_parse_vcard() {
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||
NOTE:Hi\\, I'm Alice\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
@@ -246,7 +249,8 @@ END:VCARD",
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
/// Proton at some point slightly changed the format of their vcards
|
||||
/// Proton at some point slightly changed the format of their vcards.
|
||||
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
||||
#[test]
|
||||
fn test_protonmail_vcard2() {
|
||||
let contacts = parse_vcard(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
+1001
-445
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.20 -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doxygenlayout version="2.0">
|
||||
<!-- Generated by doxygen 1.13.2 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
@@ -11,10 +12,16 @@
|
||||
</tab>
|
||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="modules" visible="yes" title="" intro="">
|
||||
<tab type="modulelist" visible="yes" title="" intro=""/>
|
||||
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="concepts" visible="yes" title="">
|
||||
</tab>
|
||||
<tab type="interfaces" visible="yes" title="">
|
||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
@@ -35,4 +42,228 @@
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<inheritancegraph visible="yes"/>
|
||||
<collaborationgraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicmethods visible="yes" title=""/>
|
||||
<publicstaticmethods visible="yes" title=""/>
|
||||
<publicattributes visible="yes" title=""/>
|
||||
<publicstaticattributes visible="yes" title=""/>
|
||||
<protectedtypes visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<protectedmethods visible="yes" title=""/>
|
||||
<protectedstaticmethods visible="yes" title=""/>
|
||||
<protectedattributes visible="yes" title=""/>
|
||||
<protectedstaticattributes visible="yes" title=""/>
|
||||
<packagetypes visible="yes" title=""/>
|
||||
<packagemethods visible="yes" title=""/>
|
||||
<packagestaticmethods visible="yes" title=""/>
|
||||
<packageattributes visible="yes" title=""/>
|
||||
<packagestaticattributes visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<privatetypes visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<privatemethods visible="yes" title=""/>
|
||||
<privatestaticmethods visible="yes" title=""/>
|
||||
<privateattributes visible="yes" title=""/>
|
||||
<privatestaticattributes visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<related visible="yes" title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<constructors visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<related visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes" visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a concept page -->
|
||||
<concept>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<definition visible="yes" title=""/>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<authorsection visible="yes"/>
|
||||
</concept>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="yes"/>
|
||||
<includedbygraph visible="yes"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes" visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<groupgraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<modules visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a C++20 module page -->
|
||||
<module>
|
||||
<briefdescription visible="yes"/>
|
||||
<exportedmodules visible="yes"/>
|
||||
<memberdecl>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<membergroups visible="yes" title=""/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdecl>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
</module>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
|
||||
@@ -2132,7 +2132,10 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
|
||||
|
||||
|
||||
#define DC_GCL_VERIFIED_ONLY 0x01
|
||||
|
||||
// Deprecated 2025-05-20, setting this flag is a no-op.
|
||||
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
|
||||
|
||||
#define DC_GCL_ADD_SELF 0x02
|
||||
|
||||
|
||||
@@ -2194,8 +2197,6 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
|
||||
* @param context The context object.
|
||||
* @param flags A combination of flags:
|
||||
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
* - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
|
||||
* if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
|
||||
* @param query A string to filter the list. Typically used to implement an
|
||||
* incremental search. NULL for no filtering.
|
||||
* @return An array containing all contact IDs. Must be dc_array_unref()'d
|
||||
|
||||
+163
-213
@@ -235,7 +235,10 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
} else {
|
||||
match config::Config::from_str(&key) {
|
||||
match config::Config::from_str(&key)
|
||||
.context("Invalid config key")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
.set_config(key, value.as_deref())
|
||||
.await
|
||||
@@ -244,10 +247,7 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
})
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_set_config(): invalid key");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -276,7 +276,10 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
} else {
|
||||
match config::Config::from_str(&key) {
|
||||
match config::Config::from_str(&key)
|
||||
.with_context(|| format!("Invalid key {:?}", &key))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
.get_config(key)
|
||||
.await
|
||||
@@ -285,10 +288,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.strdup(),
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
|
||||
"".strdup()
|
||||
}
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -308,18 +308,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match StockMessage::from_u32(stock_id) {
|
||||
Some(id) => match ctx.set_stock_translation(id, msg).await {
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
warn!(ctx, "set_stock_translation failed: {err:#}");
|
||||
0
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!(ctx, "invalid stock message id {stock_id}");
|
||||
0
|
||||
}
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.await
|
||||
.context("set_stock_translation failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -336,15 +335,10 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
|
||||
let qr = to_string_lossy(qr);
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match qr::set_config_from_qr(ctx, &qr).await {
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to create account from QR code: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(qr::set_config_from_qr(ctx, &qr))
|
||||
.context("Failed to create account from QR code")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -354,15 +348,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
match ctx.get_info().await {
|
||||
Ok(info) => render_info(info).unwrap_or_default().strdup(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "failed to get info: {err:#}");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(ctx.get_info())
|
||||
.context("Failed to get info")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(info) => render_info(info).unwrap_or_default().strdup(),
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_info(
|
||||
@@ -395,15 +387,13 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
match ctx.get_connectivity_html().await {
|
||||
Ok(html) => html.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to get connectivity html: {err:#}");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(ctx.get_connectivity_html())
|
||||
.context("Failed to get connectivity html")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(html) => html.strdup(),
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1255,22 +1245,19 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match ChatId::new(chat_id).get_draft(ctx).await {
|
||||
Ok(Some(draft)) => {
|
||||
let ffi_msg = MessageWrapper {
|
||||
context,
|
||||
message: draft,
|
||||
};
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to get draft for chat #{chat_id}: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
match block_on(ChatId::new(chat_id).get_draft(ctx))
|
||||
.with_context(|| format!("Failed to get draft for chat #{chat_id}"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(draft) => {
|
||||
let ffi_msg = MessageWrapper {
|
||||
context,
|
||||
message: draft,
|
||||
};
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
})
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1526,10 +1513,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
1 => ChatVisibility::Archived,
|
||||
2 => ChatVisibility::Pinned,
|
||||
_ => {
|
||||
warn!(
|
||||
ctx,
|
||||
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
|
||||
);
|
||||
eprintln!("ignoring careless call to dc_set_chat_visibility(): unknown archived state");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -1683,10 +1667,11 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
||||
s
|
||||
} else {
|
||||
warn!(ctx, "bad protect-value for dc_create_group_chat()");
|
||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
||||
.context("Bad protect-value for dc_create_group_chat()")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -1832,23 +1817,20 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let muteDuration = match duration {
|
||||
let mute_duration = match duration {
|
||||
0 => MuteDuration::NotMuted,
|
||||
-1 => MuteDuration::Forever,
|
||||
n if n > 0 => SystemTime::now()
|
||||
.checked_add(Duration::from_secs(duration as u64))
|
||||
.map_or(MuteDuration::Forever, MuteDuration::Until),
|
||||
_ => {
|
||||
warn!(
|
||||
ctx,
|
||||
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
|
||||
);
|
||||
eprintln!("dc_chat_set_mute_duration(): Can not use negative duration other than -1");
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
|
||||
chat::set_muted(ctx, ChatId::new(chat_id), mute_duration)
|
||||
.await
|
||||
.map(|_| 1)
|
||||
.unwrap_or_log_default(ctx, "Failed to set mute duration")
|
||||
@@ -1866,16 +1848,10 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.get_encryption_info(ctx)
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
error!(ctx, "{e:#}");
|
||||
ptr::null_mut()
|
||||
})
|
||||
})
|
||||
block_on(ChatId::new(chat_id).get_encryption_info(ctx))
|
||||
.map(|s| s.strdup())
|
||||
.log_err(ctx)
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2032,12 +2008,10 @@ pub unsafe extern "C" fn dc_resend_msgs(
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
|
||||
error!(ctx, "Resending failed: {err:#}");
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
block_on(chat::resend_msgs(ctx, &msg_ids))
|
||||
.context("Resending failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2067,26 +2041,22 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)).await {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
|
||||
// C-core API returns empty messages, do the same
|
||||
warn!(
|
||||
ctx,
|
||||
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
|
||||
);
|
||||
message::Message::new(Viewtype::default())
|
||||
} else {
|
||||
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let message = match block_on(message::Message::load_from_db(ctx, MsgId::new(msg_id)))
|
||||
.with_context(|| format!("dc_get_msg could not rectieve msg_id {msg_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(_) => {
|
||||
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
|
||||
// C-core API returns empty messages, do the same
|
||||
message::Message::new(Viewtype::default())
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let ffi_msg = MessageWrapper { context, message };
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
})
|
||||
}
|
||||
};
|
||||
let ffi_msg = MessageWrapper { context, message };
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2316,15 +2286,10 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
error!(ctx, "{e:#}");
|
||||
ptr::null_mut()
|
||||
})
|
||||
})
|
||||
block_on(Contact::get_encrinfo(ctx, ContactId::new(contact_id)))
|
||||
.map(|s| s.strdup())
|
||||
.log_err(ctx)
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2339,15 +2304,10 @@ pub unsafe extern "C" fn dc_delete_contact(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match Contact::delete(ctx, contact_id).await {
|
||||
Ok(_) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "cannot delete contact: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(Contact::delete(ctx, contact_id))
|
||||
.context("Cannot delete contact")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2418,17 +2378,13 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::has_backup(ctx, to_string_lossy(dir).as_ref()).await {
|
||||
Ok(res) => res.strdup(),
|
||||
Err(err) => {
|
||||
// do not bubble up error to the user,
|
||||
// the ui will expect that the file does not exist or cannot be accessed
|
||||
warn!(ctx, "dc_imex_has_backup: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(imex::has_backup(ctx, to_string_lossy(dir).as_ref()))
|
||||
.context("dc_imex_has_backup")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2439,15 +2395,13 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::initiate_key_transfer(ctx).await {
|
||||
Ok(res) => res.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_initiate_key_transfer(): {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(imex::initiate_key_transfer(ctx))
|
||||
.context("dc_initiate_key_transfer()")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2462,17 +2416,14 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
|
||||
.await
|
||||
{
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
warn!(ctx, "dc_continue_key_transfer: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(imex::continue_key_transfer(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(setup_code),
|
||||
))
|
||||
.context("dc_continue_key_transfer")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2916,12 +2867,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_chat_id(index) {
|
||||
match ffi_list
|
||||
.list
|
||||
.get_chat_id(index)
|
||||
.context("get_chat_id failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(chat_id) => chat_id.to_u32(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_chat_id failed: {err:#}");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2936,12 +2889,14 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_msg_id(index) {
|
||||
match ffi_list
|
||||
.list
|
||||
.get_msg_id(index)
|
||||
.context("get_msg_id failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_msg_id failed: {err:#}");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3095,13 +3050,16 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
let ffi_chat = &*chat;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(&ffi_chat.context).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:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
match ffi_chat
|
||||
.chat
|
||||
.get_profile_image(&ffi_chat.context)
|
||||
.await
|
||||
.context("Failed to get profile image")
|
||||
.log_err(&ffi_chat.context)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(p) => p.to_string_lossy().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3258,22 +3216,20 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
|
||||
Ok(chat) => chat,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_get_chat_info_json() failed to load chat: {err:#}");
|
||||
return "".strdup();
|
||||
}
|
||||
let Ok(chat) = chat::Chat::load_from_db(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.context("dc_get_chat_info_json() failed to load chat")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
let info = match chat.get_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(
|
||||
ctx,
|
||||
"dc_get_chat_info_json() failed to get chat info: {err:#}"
|
||||
);
|
||||
return "".strdup();
|
||||
}
|
||||
let Ok(info) = chat
|
||||
.get_info(ctx)
|
||||
.await
|
||||
.context("dc_get_chat_info_json() failed to get chat info")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
|
||||
@@ -3533,18 +3489,15 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(async move {
|
||||
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {err:#}");
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
})
|
||||
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(ctx))
|
||||
.context("dc_msg_get_webxdc_info() failed to get info")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4574,13 +4527,10 @@ trait ResultExt<T, E> {
|
||||
|
||||
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T {
|
||||
match self {
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
error!(context, "{message}: {err:#}");
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
self.map_err(|err| anyhow::anyhow!("{err:#}"))
|
||||
.with_context(|| message.to_string())
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub enum Meaning {
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<Cow<str>> {
|
||||
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
@@ -66,7 +66,7 @@ impl Lot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
@@ -35,7 +36,6 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use deltachat::{imex, info};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -1919,12 +1919,10 @@ impl CommandApi {
|
||||
instance_msg_id: u32,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
if let Some(fut) = fut {
|
||||
tokio::spawn(async move {
|
||||
fut.await.ok();
|
||||
info!(ctx, "send_webxdc_realtime_advertisement done")
|
||||
});
|
||||
if let Some(fut) =
|
||||
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
|
||||
{
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
pub enum MessageLoadResult {
|
||||
Message(MessageObject),
|
||||
LoadingError { error: String },
|
||||
@@ -90,8 +90,6 @@ pub struct MessageObject {
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
@@ -142,12 +140,6 @@ impl MessageObject {
|
||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||
|
||||
let download_state = message.download_state().into();
|
||||
@@ -261,7 +253,6 @@ impl MessageObject {
|
||||
file_mime: message.get_filemime(),
|
||||
file_bytes,
|
||||
file_name: message.get_filename(),
|
||||
webxdc_info,
|
||||
|
||||
// On a WebxdcInfoMessage this might include a hash holding
|
||||
// information about a specific position or state in a webxdc app
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||
"dependencies": {
|
||||
"@deltachat/tiny-emitter": "3.0.0",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"yerpc": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/ws": "^7.2.4",
|
||||
"c8": "^7.10.0",
|
||||
"@types/chai": "^4.3.10",
|
||||
"@types/chai-as-promised": "^7.1.8",
|
||||
"@types/mocha": "^10.0.4",
|
||||
"@types/ws": "^8.5.9",
|
||||
"c8": "^8.0.1",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esbuild": "^0.17.9",
|
||||
"esbuild": "^0.25.5",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^9.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.23.2",
|
||||
"typescript": "^4.5.5",
|
||||
"prettier": "^3.5.3",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"exports": {
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "1.160.0"
|
||||
}
|
||||
|
||||
@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
|
||||
const jsonCoverage =
|
||||
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
||||
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
||||
(key) => jsonCoverage.fnMap[key]
|
||||
(key) => jsonCoverage.fnMap[key],
|
||||
);
|
||||
const htmlCoverage = readFileSync(
|
||||
"./coverage/" + generatedFile + ".html",
|
||||
"utf8"
|
||||
"utf8",
|
||||
);
|
||||
const uncoveredLines = htmlCoverage
|
||||
.split("\n")
|
||||
.filter((line) => line.includes(`"function not covered"`));
|
||||
const uncoveredFunctions = uncoveredLines.map(
|
||||
(line) => />([\w_]+)\(/.exec(line)[1]
|
||||
(line) => />([\w_]+)\(/.exec(line)[1],
|
||||
);
|
||||
console.log(
|
||||
"\nUncovered api functions:\n" +
|
||||
uncoveredFunctions
|
||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||
.map(
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
|
||||
)
|
||||
.join("\n")
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
|
||||
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1;
|
||||
@@ -50,5 +50,5 @@ const constants = data
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../generated/constants.ts"),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
||||
);
|
||||
|
||||
@@ -8,13 +8,13 @@ import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||
>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
Transport extends BaseTransport<any>,
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
@@ -34,7 +34,10 @@ export class BaseDeltaChat<
|
||||
//@ts-ignore
|
||||
private eventTask: Promise<void>;
|
||||
|
||||
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||
constructor(
|
||||
public transport: Transport,
|
||||
startEventLoop: boolean,
|
||||
) {
|
||||
super();
|
||||
this.rpc = new RawClient(this.transport);
|
||||
if (startEventLoop) {
|
||||
@@ -53,7 +56,7 @@ export class BaseDeltaChat<
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
}
|
||||
@@ -83,7 +86,10 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||
}
|
||||
|
||||
export class StdioTransport extends BaseTransport {
|
||||
constructor(public input: any, public output: any) {
|
||||
constructor(
|
||||
public input: any,
|
||||
public output: any,
|
||||
) {
|
||||
super();
|
||||
|
||||
var buffer = "";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { strictEqual } from "assert";
|
||||
import chai, { assert, expect } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
chai.use(chaiAsPromised);
|
||||
@@ -32,14 +31,14 @@ describe("basic tests", () => {
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||
),
|
||||
).to.not.contain(false);
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||
),
|
||||
).to.not.contain(true);
|
||||
});
|
||||
|
||||
@@ -85,7 +84,7 @@ describe("basic tests", () => {
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId,
|
||||
"example@delta.chat",
|
||||
null
|
||||
null,
|
||||
);
|
||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||
.false;
|
||||
@@ -127,7 +126,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
@@ -139,7 +138,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
@@ -153,7 +152,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
|
||||
@@ -17,12 +17,12 @@ describe("online tests", function () {
|
||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||
console.error(
|
||||
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -36,7 +36,7 @@ describe("online tests", function () {
|
||||
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||
if (!account1 || !account1.email || !account1.password) {
|
||||
console.log(
|
||||
"We didn't got back an account from the api, skip integration tests"
|
||||
"We didn't got back an account from the api, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -44,7 +44,7 @@ describe("online tests", function () {
|
||||
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||
if (!account2 || !account2.email || !account2.password) {
|
||||
console.log(
|
||||
"We didn't got back an account2 from the api, skip integration tests"
|
||||
"We didn't got back an account2 from the api, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -92,7 +92,7 @@ describe("online tests", function () {
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
@@ -124,11 +124,11 @@ describe("online tests", function () {
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
);
|
||||
const message = await dc.rpc.getMessage(
|
||||
accountId2,
|
||||
messageList.reverse()[0]
|
||||
messageList.reverse()[0],
|
||||
);
|
||||
expect(message.text).equal("Hello2");
|
||||
// Send message back from B to A
|
||||
@@ -150,7 +150,7 @@ describe("online tests", function () {
|
||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
expect(info).to.be.not.null;
|
||||
expect(info?.overviewPage).to.equal(
|
||||
"https://providers.delta.chat/example-com"
|
||||
"https://providers.delta.chat/example-com",
|
||||
);
|
||||
expect(info?.status).to.equal(3);
|
||||
});
|
||||
@@ -167,12 +167,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
dc: DeltaChat,
|
||||
eventType: T,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
timeout: number = EVENT_TIMEOUT,
|
||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error("Timeout reached before event came in")),
|
||||
timeout
|
||||
timeout,
|
||||
);
|
||||
const callback = (contextId: number, event: DcEvent) => {
|
||||
if (contextId == accountId) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||
|
||||
const pathToServerBinary = resolve(
|
||||
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
||||
join(await getTargetDir(), "debug/deltachat-rpc-server"),
|
||||
);
|
||||
|
||||
const server = spawn(pathToServerBinary, {
|
||||
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
||||
throw new Error(
|
||||
"Failed to start server executable " +
|
||||
pathToServerBinary +
|
||||
", make sure you built it first."
|
||||
", make sure you built it first.",
|
||||
);
|
||||
});
|
||||
let shouldClose = false;
|
||||
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
@@ -13,7 +13,7 @@ log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "15"
|
||||
rustyline = "16"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
eprintln!("Import: No file or folder given.");
|
||||
return false;
|
||||
}
|
||||
real_spec = rs.unwrap();
|
||||
@@ -149,7 +149,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"send-backup" => {
|
||||
let provider = BackupProvider::prepare(&context).await?;
|
||||
let qr = format_backup(&provider.qr())?;
|
||||
println!("QR code: {}", qr);
|
||||
println!("QR code: {qr}");
|
||||
qr2term::print_qr(qr.as_str())?;
|
||||
provider.await?;
|
||||
}
|
||||
@@ -1162,17 +1162,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let reaction = arg2;
|
||||
send_reaction(&context, msg_id, reaction).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
if arg0 == "listverified" {
|
||||
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
||||
} else {
|
||||
DC_GCL_ADD_SELF
|
||||
},
|
||||
Some(arg1),
|
||||
)
|
||||
.await?;
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||
//! All further options can be set using the set-command (type ? for help).
|
||||
|
||||
#[macro_use]
|
||||
extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
@@ -41,25 +40,25 @@ fn receive_event(event: EventType) {
|
||||
match event {
|
||||
EventType::Info(msg) => {
|
||||
/* do not show the event as this would fill the screen */
|
||||
info!("{}", msg);
|
||||
info!("{msg}");
|
||||
}
|
||||
EventType::SmtpConnected(msg) => {
|
||||
info!("[SMTP_CONNECTED] {}", msg);
|
||||
info!("[SMTP_CONNECTED] {msg}");
|
||||
}
|
||||
EventType::ImapConnected(msg) => {
|
||||
info!("[IMAP_CONNECTED] {}", msg);
|
||||
info!("[IMAP_CONNECTED] {msg}");
|
||||
}
|
||||
EventType::SmtpMessageSent(msg) => {
|
||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||
info!("[SMTP_MESSAGE_SENT] {msg}");
|
||||
}
|
||||
EventType::Warning(msg) => {
|
||||
warn!("{}", msg);
|
||||
warn!("{msg}");
|
||||
}
|
||||
EventType::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
error!("{msg}");
|
||||
}
|
||||
EventType::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
error!("[SELF_NOT_IN_GROUP] {msg}");
|
||||
}
|
||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||
info!(
|
||||
@@ -124,7 +123,7 @@ fn receive_event(event: EventType) {
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
info!("Received {:?}", event);
|
||||
info!("Received {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -66,6 +66,9 @@ lint.select = [
|
||||
|
||||
"RUF006" # asyncio-dangling-task
|
||||
]
|
||||
lint.ignore = [
|
||||
"PLC0415" # `import` should be at the top-level of a file
|
||||
]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
"""Delta Chat JSON-RPC high-level API."""
|
||||
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
|
||||
@@ -115,7 +115,7 @@ def _run_cli(
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
"""extract email address from the given text."""
|
||||
"""Extract email address from the given text."""
|
||||
match = re.match(r".*\((.+@.+)\)", text)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
@@ -124,7 +124,7 @@ def extract_addr(text: str) -> str:
|
||||
|
||||
|
||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||
"""return image changed/deleted info from parsing the given system message text."""
|
||||
"""Return image changed/deleted info from parsing the given system message text."""
|
||||
text = text.lower()
|
||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||
if match:
|
||||
@@ -143,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
||||
|
||||
|
||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
"""Return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) tuple.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Account module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -34,7 +36,10 @@ class Account:
|
||||
return next_event
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
"""Remove all queued-up events for a given account.
|
||||
|
||||
Useful for tests.
|
||||
"""
|
||||
self._rpc.clear_all_events(self.id)
|
||||
|
||||
def remove(self) -> None:
|
||||
@@ -43,7 +48,9 @@ class Account:
|
||||
|
||||
def clone(self) -> "Account":
|
||||
"""Clone given account.
|
||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
|
||||
|
||||
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()
|
||||
@@ -80,7 +87,7 @@ class Account:
|
||||
return self._rpc.get_config(self.id, key)
|
||||
|
||||
def update_config(self, **kwargs) -> None:
|
||||
"""update config values."""
|
||||
"""Update config values."""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, value)
|
||||
|
||||
@@ -99,10 +106,12 @@ class Account:
|
||||
"""Parse QR code contents.
|
||||
|
||||
This function takes the raw text scanned
|
||||
and checks what can be done with it."""
|
||||
and checks what can be done with it.
|
||||
"""
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
"""Set configuration values from a QR code."""
|
||||
self._rpc.set_config_from_qr(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
@@ -117,7 +126,7 @@ class Account:
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Returns the list of all email accounts that are used as a transport in the current profile."""
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
transports = yield self._rpc.list_transports.future(self.id)
|
||||
return transports
|
||||
|
||||
@@ -158,7 +167,8 @@ class Account:
|
||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||
"""Import vCard.
|
||||
|
||||
Return created or modified contacts in the order they appear in 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]
|
||||
|
||||
@@ -201,8 +211,8 @@ class Account:
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
*,
|
||||
with_self: bool = False,
|
||||
verified_only: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[list[Contact], list[AttrDict]]:
|
||||
"""Get a filtered list of contacts.
|
||||
@@ -210,12 +220,9 @@ class Account:
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
:param with_self: if True the self-contact is also included if it matches the query.
|
||||
:param only_verified: if True only return verified contacts.
|
||||
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
||||
"""
|
||||
flags = 0
|
||||
if verified_only:
|
||||
flags |= ContactFlag.VERIFIED_ONLY
|
||||
if with_self:
|
||||
flags |= ContactFlag.ADD_SELF
|
||||
|
||||
@@ -227,12 +234,12 @@ class Account:
|
||||
|
||||
@property
|
||||
def self_contact(self) -> Contact:
|
||||
"""This account's identity as a Contact."""
|
||||
"""Account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
"""This account's device contact."""
|
||||
"""Account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
def get_chatlist(
|
||||
@@ -290,8 +297,7 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||
another device.
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
@@ -361,22 +367,26 @@ class Account:
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event."""
|
||||
Consumes all events before the next incoming message event.
|
||||
"""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_securejoin_joiner_success(self):
|
||||
"""Wait until SecureJoin process finishes successfully on the joiner side."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_reactions_changed(self):
|
||||
"""Wait for reaction change event."""
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Chat module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
@@ -89,7 +91,8 @@ class Chat:
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat in seconds.
|
||||
|
||||
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||
0 means the timer is disabled, use 1 for immediate deletion.
|
||||
"""
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
@@ -199,12 +202,12 @@ class Chat:
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
"""Get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
"""Get number of fresh messages in this chat"""
|
||||
"""Get number of fresh messages in this chat."""
|
||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
|
||||
def mark_noticed(self) -> None:
|
||||
|
||||
@@ -48,6 +48,7 @@ class Client:
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
"""Register multiple hooks."""
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
@@ -77,9 +78,11 @@ class Client:
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True if the client is configured."""
|
||||
return self.account.is_configured()
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
"""Configure the client."""
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
@@ -198,5 +201,6 @@ class Bot(Client):
|
||||
"""Simple bot implementation that listens to events of a single account."""
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
"""Configure the bot."""
|
||||
kwargs.setdefault("bot", "1")
|
||||
super().configure(email, password, **kwargs)
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
"""Constants module."""
|
||||
|
||||
from enum import Enum, IntEnum
|
||||
|
||||
COMMAND_PREFIX = "/"
|
||||
|
||||
|
||||
class ContactFlag(IntEnum):
|
||||
VERIFIED_ONLY = 0x01
|
||||
"""Bit flags for get_contacts() method."""
|
||||
|
||||
ADD_SELF = 0x02
|
||||
|
||||
|
||||
class ChatlistFlag(IntEnum):
|
||||
"""Bit flags for get_chatlist() method."""
|
||||
|
||||
ARCHIVED_ONLY = 0x01
|
||||
NO_SPECIALS = 0x02
|
||||
ADD_ALLDONE_HINT = 0x04
|
||||
@@ -16,6 +21,8 @@ class ChatlistFlag(IntEnum):
|
||||
|
||||
|
||||
class SpecialContactId(IntEnum):
|
||||
"""Special contact IDs."""
|
||||
|
||||
SELF = 1
|
||||
INFO = 2 # centered messages as "member added", used in all chats
|
||||
DEVICE = 5 # messages "update info" in the device-chat
|
||||
@@ -23,7 +30,7 @@ class SpecialContactId(IntEnum):
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""Core event types"""
|
||||
"""Core event types."""
|
||||
|
||||
INFO = "Info"
|
||||
SMTP_CONNECTED = "SmtpConnected"
|
||||
@@ -71,7 +78,7 @@ class EventType(str, Enum):
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
"""Special chat ids"""
|
||||
"""Special chat IDs."""
|
||||
|
||||
TRASH = 3
|
||||
ARCHIVED_LINK = 6
|
||||
@@ -80,7 +87,7 @@ class ChatId(IntEnum):
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
"""Chat types"""
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
SINGLE = 100
|
||||
@@ -90,7 +97,7 @@ class ChatType(IntEnum):
|
||||
|
||||
|
||||
class ChatVisibility(str, Enum):
|
||||
"""Chat visibility types"""
|
||||
"""Chat visibility types."""
|
||||
|
||||
NORMAL = "Normal"
|
||||
ARCHIVED = "Archived"
|
||||
@@ -98,7 +105,7 @@ class ChatVisibility(str, Enum):
|
||||
|
||||
|
||||
class DownloadState(str, Enum):
|
||||
"""Message download state"""
|
||||
"""Message download state."""
|
||||
|
||||
DONE = "Done"
|
||||
AVAILABLE = "Available"
|
||||
@@ -159,14 +166,14 @@ class MessageState(IntEnum):
|
||||
|
||||
|
||||
class MessageId(IntEnum):
|
||||
"""Special message ids"""
|
||||
"""Special message IDs."""
|
||||
|
||||
DAYMARKER = 9
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class CertificateChecks(IntEnum):
|
||||
"""Certificate checks mode"""
|
||||
"""Certificate checks mode."""
|
||||
|
||||
AUTOMATIC = 0
|
||||
STRICT = 1
|
||||
@@ -174,7 +181,7 @@ class CertificateChecks(IntEnum):
|
||||
|
||||
|
||||
class Connectivity(IntEnum):
|
||||
"""Connectivity states"""
|
||||
"""Connectivity states."""
|
||||
|
||||
NOT_CONNECTED = 1000
|
||||
CONNECTING = 2000
|
||||
@@ -183,7 +190,7 @@ class Connectivity(IntEnum):
|
||||
|
||||
|
||||
class KeyGenType(IntEnum):
|
||||
"""Type of the key to generate"""
|
||||
"""Type of the key to generate."""
|
||||
|
||||
DEFAULT = 0
|
||||
RSA2048 = 1
|
||||
@@ -193,21 +200,21 @@ class KeyGenType(IntEnum):
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags"""
|
||||
"""Authorization flags."""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting"""
|
||||
"""Media quality setting."""
|
||||
|
||||
BALANCED = 0
|
||||
WORSE = 1
|
||||
|
||||
|
||||
class ProviderStatus(IntEnum):
|
||||
"""Provider status according to manual testing"""
|
||||
"""Provider status according to manual testing."""
|
||||
|
||||
OK = 1
|
||||
PREPARATION = 2
|
||||
@@ -215,7 +222,7 @@ class ProviderStatus(IntEnum):
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state"""
|
||||
"""Push notifications state."""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
@@ -223,7 +230,7 @@ class PushNotifyState(IntEnum):
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode"""
|
||||
"""Show emails mode."""
|
||||
|
||||
OFF = 0
|
||||
ACCEPTED_CONTACTS = 1
|
||||
@@ -231,7 +238,7 @@ class ShowEmails(IntEnum):
|
||||
|
||||
|
||||
class SocketSecurity(IntEnum):
|
||||
"""Socket security"""
|
||||
"""Socket security."""
|
||||
|
||||
AUTOMATIC = 0
|
||||
SSL = 1
|
||||
@@ -240,7 +247,7 @@ class SocketSecurity(IntEnum):
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type"""
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Contact module."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -11,8 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""
|
||||
Contact API.
|
||||
"""Contact API.
|
||||
|
||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||
"""
|
||||
@@ -45,8 +46,9 @@ class Contact:
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Get a multi-line encryption info, containing your fingerprint and
|
||||
the fingerprint of the contact.
|
||||
"""Get a multi-line encryption info.
|
||||
|
||||
Encryption info contains your fingerprint and the fingerprint of the contact.
|
||||
"""
|
||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
|
||||
@@ -66,4 +68,5 @@ class Contact:
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
"""Make a vCard for the contact."""
|
||||
return self.account.make_vcard([self])
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Account manager module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,12 +12,13 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class DeltaChat:
|
||||
"""
|
||||
Delta Chat accounts manager.
|
||||
"""Delta Chat accounts manager.
|
||||
|
||||
This is the root of the object oriented API.
|
||||
"""
|
||||
|
||||
def __init__(self, rpc: "Rpc") -> None:
|
||||
"""Initialize account manager."""
|
||||
self.rpc = rpc
|
||||
|
||||
def add_account(self) -> Account:
|
||||
@@ -37,9 +40,7 @@ class DeltaChat:
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network likely has come back or just that the network
|
||||
conditions might have changed.
|
||||
"""
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
def get_system_info(self) -> AttrDict:
|
||||
|
||||
@@ -36,7 +36,7 @@ class EventFilter(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def __hash__(self) -> int:
|
||||
"""Object's unique hash"""
|
||||
"""Object's unique hash."""
|
||||
|
||||
@abstractmethod
|
||||
def __eq__(self, other) -> bool:
|
||||
@@ -52,9 +52,7 @@ class EventFilter(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, event):
|
||||
"""Return True-like value if the event passed the filter and should be
|
||||
used, or False-like value otherwise.
|
||||
"""
|
||||
"""Return True-like value if the event passed the filter."""
|
||||
|
||||
|
||||
class RawEvent(EventFilter):
|
||||
@@ -82,31 +80,17 @@ class RawEvent(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Filter an event.
|
||||
|
||||
Return true if the event should be processed.
|
||||
"""
|
||||
if self.types and event.kind not in self.types:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class NewMessage(EventFilter):
|
||||
"""Matches whenever a new message arrives.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||
content.
|
||||
:param command: If set, only match messages with the given command (ex. /help).
|
||||
Setting this property implies `is_info==False`.
|
||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||
match messages from bots and users. If omitted or set to False
|
||||
only messages from users will be matched.
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
"""Matches whenever a new message arrives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -121,6 +105,25 @@ class NewMessage(EventFilter):
|
||||
is_info: Optional[bool] = None,
|
||||
func: Optional[Callable[["AttrDict"], bool]] = None,
|
||||
) -> None:
|
||||
"""Initialize a new message filter.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||
content.
|
||||
:param command: If set, only match messages with the given command (ex. /help).
|
||||
Setting this property implies `is_info==False`.
|
||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||
match messages from bots and users. If omitted or set to False
|
||||
only messages from users will be matched.
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
super().__init__(func=func)
|
||||
self.is_bot = is_bot
|
||||
self.is_info = is_info
|
||||
@@ -159,6 +162,7 @@ class NewMessage(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return true if if the event is a new message event."""
|
||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||
return False
|
||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||
@@ -199,6 +203,7 @@ class MemberListChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return true if if the event is a member addition event."""
|
||||
if self.added is not None and self.added != event.member_added:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
@@ -231,6 +236,7 @@ class GroupImageChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return True if event is matched."""
|
||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
@@ -256,13 +262,12 @@ class GroupNameChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return True if event is matched."""
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
"""
|
||||
Helper class to collect event hooks that can later be added to a Delta Chat client.
|
||||
"""
|
||||
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Message module."""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
@@ -45,6 +47,7 @@ class Message:
|
||||
return None
|
||||
|
||||
def get_sender_contact(self) -> Contact:
|
||||
"""Return sender contact."""
|
||||
from_id = self.get_snapshot().from_id
|
||||
return self.account.get_contact_by_id(from_id)
|
||||
|
||||
@@ -53,6 +56,11 @@ class Message:
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
This function can be called on received Autocrypt Setup Message
|
||||
to import the key encrypted with the provided setup code.
|
||||
"""
|
||||
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:
|
||||
@@ -62,6 +70,7 @@ class Message:
|
||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
"""Return a list of Webxdc status updates for Webxdc instance message."""
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
def get_info(self) -> str:
|
||||
@@ -69,6 +78,7 @@ class Message:
|
||||
return self._rpc.get_message_info(self.account.id, self.id)
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
"""Get info from a Webxdc message in JSON format."""
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
def wait_until_delivered(self) -> None:
|
||||
@@ -80,8 +90,10 @@ class Message:
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
"""Send an advertisement to join the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Pytest plugin module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -13,24 +15,30 @@ from .rpc import Rpc
|
||||
|
||||
|
||||
class ACFactory:
|
||||
"""Test account factory."""
|
||||
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
"""Create a new unconfigured account."""
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
"""Create a new unconfigured bot."""
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
"""Create a new configured account."""
|
||||
addr, password = self.get_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
params = {"addr": addr, "password": password}
|
||||
@@ -40,6 +48,7 @@ class ACFactory:
|
||||
return account
|
||||
|
||||
def new_configured_bot(self) -> Bot:
|
||||
"""Create a new configured bot."""
|
||||
addr, password = self.get_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(addr, password)
|
||||
@@ -47,11 +56,13 @@ class ACFactory:
|
||||
|
||||
@futuremethod
|
||||
def get_online_account(self):
|
||||
"""Create a new account and start I/O."""
|
||||
account = yield self.new_configured_account.future()
|
||||
account.bring_online()
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> list[Account]:
|
||||
"""Create multiple online accounts."""
|
||||
futures = [self.get_online_account.future() for _ in range(num)]
|
||||
return [f() for f in futures]
|
||||
|
||||
@@ -66,6 +77,10 @@ class ACFactory:
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||
|
||||
Returned chat is a chat with ac2 from ac1 point of view.
|
||||
"""
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -77,6 +92,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
"""Send a message."""
|
||||
if not from_account:
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account)
|
||||
@@ -95,6 +111,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
"""Send a message and wait until recipient processes it."""
|
||||
self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
@@ -108,6 +125,7 @@ class ACFactory:
|
||||
|
||||
@pytest.fixture
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
"""RPC client fixture."""
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
@@ -115,6 +133,7 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
"""Return account factory fixture."""
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
|
||||
@@ -132,7 +151,7 @@ def data():
|
||||
raise Exception("Data path cannot be found")
|
||||
|
||||
def get_path(self, bn):
|
||||
"""return path of file or None if it doesn't exist."""
|
||||
"""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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""JSON-RPC client module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
@@ -12,16 +14,19 @@ from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
"""JSON-RPC error."""
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
"""RPC future waiting for RPC call result."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
"""Wait for the future to return the result."""
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
@@ -32,17 +37,19 @@ class RpcFuture:
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
"""RPC method."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", name: str):
|
||||
self.rpc = rpc
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
"""Synchronously calls JSON-RPC method."""
|
||||
"""Call JSON-RPC method synchronously."""
|
||||
future = self.future(*args)
|
||||
return future()
|
||||
|
||||
def future(self, *args) -> Any:
|
||||
"""Asynchronously calls JSON-RPC method."""
|
||||
"""Call JSON-RPC method asynchronously."""
|
||||
request_id = next(self.rpc.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -58,8 +65,13 @@ class RpcMethod:
|
||||
|
||||
|
||||
class Rpc:
|
||||
"""RPC client."""
|
||||
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
"""Initialize RPC client.
|
||||
|
||||
The given arguments will be passed to subprocess.Popen().
|
||||
"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
**kwargs.get("env", os.environ),
|
||||
@@ -81,6 +93,7 @@ class Rpc:
|
||||
self.events_thread: Thread
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start RPC server subprocess."""
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
@@ -130,6 +143,7 @@ class Rpc:
|
||||
self.close()
|
||||
|
||||
def reader_loop(self) -> None:
|
||||
"""Process JSON-RPC responses from the RPC server process output."""
|
||||
try:
|
||||
while line := self.process.stdout.readline():
|
||||
response = json.loads(line)
|
||||
@@ -157,12 +171,13 @@ class Rpc:
|
||||
logging.exception("Exception in the writer loop")
|
||||
|
||||
def get_queue(self, account_id: int) -> Queue:
|
||||
"""Get event queue corresponding to the given account ID."""
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
"""Request new events and distributes them between queues."""
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
@@ -178,12 +193,12 @@ class Rpc:
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
"""Wait for the next event from the given account and returns it."""
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def clear_all_events(self, account_id: int):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
"""Remove all queued-up events for a given account. Useful for tests."""
|
||||
queue = self.get_queue(account_id)
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "1.160.0"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn main_impl() -> Result<()> {
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||
|
||||
@@ -97,7 +97,7 @@ async fn main_impl() -> Result<()> {
|
||||
Some(message) => serde_json::to_string(&message)?,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC send {}", message);
|
||||
log::trace!("RPC send {message}");
|
||||
println!("{message}");
|
||||
}
|
||||
Ok(())
|
||||
@@ -141,7 +141,7 @@ async fn main_impl() -> Result<()> {
|
||||
Some(message) => message,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC recv {}", message);
|
||||
log::trace!("RPC recv {message}");
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
session.handle_incoming(&message).await;
|
||||
|
||||
@@ -10,9 +10,6 @@ ignore = [
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# Unmaintained backoff
|
||||
"RUSTSEC-2025-0012",
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
]
|
||||
@@ -25,26 +22,29 @@ ignore = [
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.14.5" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.4.1" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
@@ -84,6 +84,7 @@ allow = [
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
|
||||
Generated
+12
-12
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1737527504,
|
||||
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
|
||||
"lastModified": 1747291057,
|
||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
|
||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1737469691,
|
||||
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +175,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +202,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1737453499,
|
||||
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
|
||||
"lastModified": 1746889290,
|
||||
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
|
||||
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.8"
|
||||
bolero = "0.13.3"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "1.160.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
@@ -47,6 +47,10 @@ line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
lint.ignore = [
|
||||
"PLC0415", # `import` should be at the top-level of a file
|
||||
"PLW1641" # Object does not implement `__hash__` method
|
||||
]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -355,20 +355,16 @@ class Account:
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
:param only_verified: if true only return verified contacts.
|
||||
:param with_self: if true the self-contact is also returned.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
"""
|
||||
flags = 0
|
||||
query_c = as_dc_charpointer(query)
|
||||
if only_verified:
|
||||
flags |= const.DC_GCL_VERIFIED_ONLY
|
||||
if with_self:
|
||||
flags |= const.DC_GCL_ADD_SELF
|
||||
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
|
||||
|
||||
@@ -434,9 +434,10 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
|
||||
- Second device comes online, receives these new messages.
|
||||
The result is an unverified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the seconds device has all members verified.
|
||||
- Now the second device has all members and group verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
@@ -474,12 +475,12 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
|
||||
assert msg_in.text == "hi"
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert chat2_offl.is_protected()
|
||||
assert not chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
@@ -500,6 +501,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.chat.is_protected()
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
|
||||
@@ -1617,7 +1617,7 @@ def test_immediate_autodelete(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: wait for close/expunge on autodelete")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
|
||||
|
||||
lp.sec("ac2: check that message was autodeleted on server")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 0
|
||||
@@ -1653,7 +1653,7 @@ def test_delete_multiple_messages(acfactory, lp):
|
||||
lp.sec("ac2: test that only one message is left")
|
||||
while 1:
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
|
||||
ac2.direct_imap.select_config_folder("inbox")
|
||||
nr_msgs = len(ac2.direct_imap.get_all_messages())
|
||||
assert nr_msgs > 0
|
||||
|
||||
@@ -184,7 +184,6 @@ class TestOfflineContact:
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert not ac1.get_contacts(only_verified=True)
|
||||
assert len(ac1.get_contacts(with_self=True)) == 2
|
||||
|
||||
assert ac1.delete_contact(contact1)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2025-04-24
|
||||
2025-06-22
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.86.0
|
||||
RUST_VERSION=1.87.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
+11
-14
@@ -5,12 +5,10 @@ use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::task::{JoinHandle, JoinSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
@@ -20,6 +18,7 @@ use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::context::{Context, ContextBuilder};
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::log::{info, warn};
|
||||
use crate::push::PushSubscriber;
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
@@ -304,12 +303,6 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
async fn background_fetch_and_log_error(account: Context) {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
}
|
||||
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
@@ -317,11 +310,15 @@ impl Accounts {
|
||||
accounts.len()
|
||||
)),
|
||||
});
|
||||
let mut futures_unordered: FuturesUnordered<_> = accounts
|
||||
.into_iter()
|
||||
.map(background_fetch_and_log_error)
|
||||
.collect();
|
||||
while futures_unordered.next().await.is_some() {}
|
||||
let mut set = JoinSet::new();
|
||||
for account in accounts {
|
||||
set.spawn(async move {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
}
|
||||
|
||||
/// Auxiliary function for [Accounts::background_fetch].
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ use crate::config::Config;
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{error, info, warn, LogExt};
|
||||
use crate::tools::sanitize_filename;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -93,7 +93,7 @@ impl<'a> BlobObject<'a> {
|
||||
if let Some(extension) = original_name.extension().filter(|e| e.len() <= 32) {
|
||||
let extension = extension.to_string_lossy().to_lowercase();
|
||||
let extension = sanitize_filename(&extension);
|
||||
format!("$BLOBDIR/{hash}.{}", extension)
|
||||
format!("$BLOBDIR/{hash}.{extension}")
|
||||
} else {
|
||||
format!("$BLOBDIR/{hash}")
|
||||
};
|
||||
|
||||
+78
-38
@@ -34,7 +34,7 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{start_chat_ephemeral_timers, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{error, info, warn, LogExt};
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
@@ -653,7 +653,7 @@ impl ChatId {
|
||||
) -> Result<()> {
|
||||
let chat_id = ChatId::create_for_contact_with_blocked(context, contact_id, Blocked::Yes)
|
||||
.await
|
||||
.with_context(|| format!("can't create chat for {}", contact_id))?;
|
||||
.with_context(|| format!("can't create chat for {contact_id}"))?;
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
@@ -995,6 +995,7 @@ impl ChatId {
|
||||
transaction.execute(
|
||||
"INSERT INTO msgs (
|
||||
chat_id,
|
||||
rfc724_mid,
|
||||
from_id,
|
||||
timestamp,
|
||||
type,
|
||||
@@ -1004,9 +1005,10 @@ impl ChatId {
|
||||
param,
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
&msg.rfc724_mid,
|
||||
ContactId::SELF,
|
||||
time(),
|
||||
msg.viewtype,
|
||||
@@ -1506,7 +1508,7 @@ impl std::fmt::Display for ChatId {
|
||||
/// This allows you to directly store [ChatId] into the database as
|
||||
/// well as query for a [ChatId].
|
||||
impl rusqlite::types::ToSql for ChatId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
@@ -2378,7 +2380,7 @@ pub enum ChatVisibility {
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for ChatVisibility {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Integer(*self as i64);
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
@@ -2941,7 +2943,10 @@ async fn prepare_send_msg(
|
||||
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
|
||||
if let Some(quoted_message) = msg.quoted_message(context).await? {
|
||||
if quoted_message.chat_id != chat_id {
|
||||
bail!("Bad quote reply");
|
||||
bail!(
|
||||
"Quote of message from {} cannot be sent to {chat_id}",
|
||||
quoted_message.chat_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3532,37 +3537,62 @@ pub async fn get_chat_media(
|
||||
msg_type2: Viewtype,
|
||||
msg_type3: Viewtype,
|
||||
) -> Result<Vec<MsgId>> {
|
||||
// TODO This query could/should be converted to `AND type IN (?, ?, ?)`.
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id
|
||||
let list = if msg_type == Viewtype::Webxdc
|
||||
&& msg_type2 == Viewtype::Unknown
|
||||
&& msg_type3 == Viewtype::Unknown
|
||||
{
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE (1=? OR chat_id=?)
|
||||
AND chat_id != ?
|
||||
AND (type=? OR type=? OR type=?)
|
||||
AND type = ?
|
||||
AND hidden=0
|
||||
ORDER BY max(timestamp, timestamp_rcvd), id;",
|
||||
(
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
DC_CHAT_ID_TRASH,
|
||||
Viewtype::Webxdc,
|
||||
),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| Ok(ids.flatten().collect()),
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE (1=? OR chat_id=?)
|
||||
AND chat_id != ?
|
||||
AND type IN (?, ?, ?)
|
||||
AND hidden=0
|
||||
ORDER BY timestamp, id;",
|
||||
(
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
DC_CHAT_ID_TRASH,
|
||||
msg_type,
|
||||
if msg_type2 != Viewtype::Unknown {
|
||||
msg_type2
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
if msg_type3 != Viewtype::Unknown {
|
||||
msg_type3
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| Ok(ids.flatten().collect()),
|
||||
)
|
||||
.await?;
|
||||
(
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
DC_CHAT_ID_TRASH,
|
||||
msg_type,
|
||||
if msg_type2 != Viewtype::Unknown {
|
||||
msg_type2
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
if msg_type3 != Viewtype::Unknown {
|
||||
msg_type3
|
||||
} else {
|
||||
msg_type
|
||||
},
|
||||
),
|
||||
|row| row.get::<_, MsgId>(0),
|
||||
|ids| Ok(ids.flatten().collect()),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
@@ -3996,7 +4026,7 @@ pub enum MuteDuration {
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for MuteDuration {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let duration: i64 = match &self {
|
||||
MuteDuration::NotMuted => 0,
|
||||
MuteDuration::Forever => -1,
|
||||
@@ -4313,7 +4343,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
.sql
|
||||
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
|
||||
.await?
|
||||
.context("No message {id}")?;
|
||||
.with_context(|| format!("No message {id}"))?;
|
||||
msgs.push((ts, *id));
|
||||
}
|
||||
msgs.sort_unstable();
|
||||
@@ -4368,7 +4398,17 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
/// Save a copy of the message in "Saved Messages"
|
||||
/// and send a sync messages so that other devices save the message as well, unless deleted there.
|
||||
pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
for src_msg_id in msg_ids {
|
||||
let mut msgs = Vec::with_capacity(msg_ids.len());
|
||||
for id in msg_ids {
|
||||
let ts: i64 = context
|
||||
.sql
|
||||
.query_get_value("SELECT timestamp FROM msgs WHERE id=?", (id,))
|
||||
.await?
|
||||
.with_context(|| format!("No message {id}"))?;
|
||||
msgs.push((ts, *id));
|
||||
}
|
||||
msgs.sort_unstable();
|
||||
for (_, src_msg_id) in msgs {
|
||||
let dest_rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let src_rfc724_mid = save_copy_in_self_talk(context, src_msg_id, &dest_rfc724_mid).await?;
|
||||
context
|
||||
@@ -4389,11 +4429,11 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
/// Returns data needed to add a `SaveMessage` sync item.
|
||||
pub(crate) async fn save_copy_in_self_talk(
|
||||
context: &Context,
|
||||
src_msg_id: &MsgId,
|
||||
src_msg_id: MsgId,
|
||||
dest_rfc724_mid: &String,
|
||||
) -> Result<String> {
|
||||
let dest_chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
|
||||
let mut msg = Message::load_from_db(context, *src_msg_id).await?;
|
||||
let mut msg = Message::load_from_db(context, src_msg_id).await?;
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::WebxdcDocument);
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
@@ -4431,7 +4471,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
.await?;
|
||||
let dest_msg_id = MsgId::new(row_id.try_into()?);
|
||||
|
||||
context.emit_msgs_changed(msg.chat_id, *src_msg_id);
|
||||
context.emit_msgs_changed(msg.chat_id, src_msg_id);
|
||||
context.emit_msgs_changed(dest_chat_id, dest_msg_id);
|
||||
chatlist_events::emit_chatlist_changed(context);
|
||||
chatlist_events::emit_chatlist_item_changed(context, dest_chat_id);
|
||||
|
||||
+92
-1
@@ -2286,6 +2286,40 @@ async fn test_save_msgs() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_msgs_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
for a in [alice, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Protected, "grp").await?;
|
||||
let sent = [
|
||||
alice.send_text(chat_id, "0").await,
|
||||
alice.send_text(chat_id, "1").await,
|
||||
];
|
||||
// NB: This doesn't work if sync messages are fetched earlier than the referenced ones.
|
||||
for msg in &sent {
|
||||
alice1.recv_msg(msg).await;
|
||||
}
|
||||
save_msgs(alice, &[sent[1].sender_msg_id, sent[0].sender_msg_id]).await?;
|
||||
sync(alice, alice1).await;
|
||||
|
||||
for a in [alice, alice1] {
|
||||
let self_chat = a.get_self_chat().await;
|
||||
let msgs = get_chat_msgs(a, self_chat.id).await?;
|
||||
for i in [0, 1] {
|
||||
let ChatItem::Message { msg_id } = msgs[msgs.len() - 2 + i] else {
|
||||
panic!("Wrong item type");
|
||||
};
|
||||
let msg = Message::load_from_db(a, msg_id).await.unwrap();
|
||||
assert_eq!(msg.get_text(), format!("{i}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_saved_msgs_not_added_to_shared_chats() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -2924,6 +2958,61 @@ async fn test_get_chat_media() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_media_webxdc_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
|
||||
let mut instance1 = Message::new(Viewtype::Webxdc);
|
||||
instance1.set_file_from_bytes(
|
||||
&alice,
|
||||
"test1.xdc",
|
||||
include_bytes!("../../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)?;
|
||||
let instance1_id = send_msg(&alice, chat.id, &mut instance1).await?;
|
||||
|
||||
let mut instance2 = Message::new(Viewtype::Webxdc);
|
||||
instance2.set_file_from_bytes(
|
||||
&alice,
|
||||
"test2.xdc",
|
||||
include_bytes!("../../test-data/webxdc/minimal.xdc"),
|
||||
None,
|
||||
)?;
|
||||
let instance2_id = send_msg(&alice, chat.id, &mut instance2).await?;
|
||||
|
||||
// list is ordered oldest to newest, check that
|
||||
let media = get_chat_media(
|
||||
&alice,
|
||||
Some(chat.id),
|
||||
Viewtype::Webxdc,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(media.first().unwrap(), &instance1_id);
|
||||
assert_eq!(media.get(1).unwrap(), &instance2_id);
|
||||
|
||||
// add a status update for the oder instance; that resorts the list
|
||||
alice
|
||||
.send_webxdc_status_update(instance1_id, r#"{"payload": {"foo": "bar"}}"#)
|
||||
.await?;
|
||||
let media = get_chat_media(
|
||||
&alice,
|
||||
Some(chat.id),
|
||||
Viewtype::Webxdc,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(media.first().unwrap(), &instance2_id);
|
||||
assert_eq!(media.get(1).unwrap(), &instance1_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -3868,8 +3957,10 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
alice.recv_msg(&bob_sent_text).await;
|
||||
fiona.recv_msg(&bob_sent_text).await;
|
||||
|
||||
// Alice did not knew that Charlie is not part of the group
|
||||
// when sending a message, so sent it to Charlie.
|
||||
bob.recv_msg(&alice_sent_text).await;
|
||||
fiona.recv_msg(&alice_sent_text).await;
|
||||
charlie.recv_msg(&alice_sent_text).await;
|
||||
|
||||
// Alice should have learned about Charlie not being part of the group
|
||||
// by receiving Bob's message.
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ use crate::constants::{
|
||||
};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::message::{Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::stock_str;
|
||||
@@ -322,7 +323,7 @@ impl Chatlist {
|
||||
(chat_id, MessageState::OutDraft),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
|
||||
.with_context(|| format!("failed to get msg ID for chat {chat_id}"))?;
|
||||
ids.push((chat_id, msg_id));
|
||||
}
|
||||
Ok(Chatlist { ids })
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ use crate::configure::EnteredLoginParam;
|
||||
use crate::constants;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, LogExt};
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ use crate::config::{self, Config};
|
||||
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn, LogExt};
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
|
||||
@@ -9,6 +9,7 @@ use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use quick_xml::events::Event;
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
|
||||
@@ -88,7 +88,6 @@ pub const DC_GCL_NO_SPECIALS: usize = 0x02;
|
||||
pub const DC_GCL_ADD_ALLDONE_HINT: usize = 0x04;
|
||||
pub const DC_GCL_FOR_FORWARDING: usize = 0x08;
|
||||
|
||||
pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01;
|
||||
pub const DC_GCL_ADD_SELF: u32 = 0x02;
|
||||
|
||||
// unchanged user avatars are resent to the recipients every some days
|
||||
|
||||
+15
-10
@@ -25,11 +25,11 @@ use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, warn, LogExt};
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
@@ -243,7 +243,7 @@ impl fmt::Display for ContactId {
|
||||
|
||||
/// Allow converting [`ContactId`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactId {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Integer(i64::from(self.0));
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
@@ -287,6 +287,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
authname: c.authname,
|
||||
key,
|
||||
profile_image,
|
||||
biography: Some(c.status).filter(|s| !s.is_empty()),
|
||||
// Use the current time to not reveal our or contact's online time.
|
||||
timestamp: Ok(now),
|
||||
});
|
||||
@@ -423,6 +424,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(biography) = &contact.biography {
|
||||
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
@@ -1041,9 +1050,8 @@ impl Contact {
|
||||
///
|
||||
/// `listflags` is a combination of flags:
|
||||
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
|
||||
/// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
|
||||
/// `query` is a string to filter the list.
|
||||
///
|
||||
/// `query` is a string to filter the list.
|
||||
pub async fn get_all(
|
||||
context: &Context,
|
||||
listflags: u32,
|
||||
@@ -1056,14 +1064,13 @@ impl Contact {
|
||||
.collect::<HashSet<_>>();
|
||||
let mut add_self = false;
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
|
||||
Origin::Unknown
|
||||
} else {
|
||||
Origin::IncomingReplyTo
|
||||
};
|
||||
if flag_verified_only || query.is_some() {
|
||||
if query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
|
||||
context
|
||||
.sql
|
||||
@@ -1074,14 +1081,12 @@ impl Contact {
|
||||
AND c.origin>=? \
|
||||
AND c.blocked=0 \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
(
|
||||
ContactId::LAST_SPECIAL,
|
||||
minimal_origin,
|
||||
&s3str_like_cmd,
|
||||
&s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 },
|
||||
),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
|
||||
@@ -1054,6 +1054,8 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
bob.set_config(Config::Selfstatus, Some("It's me, bob"))
|
||||
.await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
|
||||
@@ -1061,6 +1063,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
|
||||
let chat = bob.create_chat(alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
@@ -1086,12 +1089,14 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
|
||||
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
assert_eq!(contacts[1].authname, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert_eq!(contacts[1].biography, None);
|
||||
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
|
||||
@@ -1114,6 +1119,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
|
||||
@@ -1145,6 +1151,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
assert_eq!(contacts[0].authname, "".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(contacts[0].biography, None);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
|
||||
Ok(())
|
||||
|
||||
+8
-10
@@ -10,8 +10,8 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::composed::SignedPublicKey;
|
||||
use pgp::types::PublicKeyTrait;
|
||||
use pgp::SignedPublicKey;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::download::DownloadState;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
|
||||
use crate::log::{info, warn};
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -773,13 +774,11 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let unset = "0";
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await?;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
@@ -858,7 +857,6 @@ impl Context {
|
||||
);
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert("displayname", displayname.unwrap_or_else(|| unset.into()));
|
||||
res.insert(
|
||||
"selfavatar",
|
||||
self.get_config(Config::Selfavatar)
|
||||
@@ -1061,21 +1059,21 @@ impl Context {
|
||||
)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_msgs {}\n", num_msgs);
|
||||
res += &format!("num_msgs {num_msgs}\n");
|
||||
|
||||
let num_chats: u32 = self
|
||||
.sql
|
||||
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_chats {}\n", num_chats);
|
||||
res += &format!("num_chats {num_chats}\n");
|
||||
|
||||
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
|
||||
res += &format!("db_size_bytes {}\n", db_size);
|
||||
res += &format!("db_size_bytes {db_size}\n");
|
||||
|
||||
let secret_key = &load_self_secret_key(self).await?.primary_key;
|
||||
let key_created = secret_key.created_at().timestamp();
|
||||
res += &format!("key_created {}\n", key_created);
|
||||
let key_created = secret_key.public_key().created_at().timestamp();
|
||||
res += &format!("key_created {key_created}\n");
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
@@ -1155,7 +1153,7 @@ impl Context {
|
||||
id
|
||||
}
|
||||
};
|
||||
res += &format!("self_reporting_id {}", self_reporting_id);
|
||||
res += &format!("self_reporting_id {self_reporting_id}");
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
@@ -310,6 +310,13 @@ async fn test_get_info_completeness() {
|
||||
"'{key}' missing in get_info() output"
|
||||
);
|
||||
}
|
||||
|
||||
if skip_from_get_info.contains(&&*key) {
|
||||
assert!(
|
||||
!info.contains_key(&*key),
|
||||
"'{key}' should not be in get_info() output"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{error, info};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::tools::time;
|
||||
|
||||
+5
-4
@@ -9,16 +9,17 @@ use mailparse::ParsedMail;
|
||||
use crate::aheader::Aheader;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::log::info;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an Autocrypt message.
|
||||
///
|
||||
/// If successful and the message is encrypted, returns decrypted body.
|
||||
pub fn try_decrypt(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: &[SignedSecretKey],
|
||||
) -> Result<Option<::pgp::composed::Message>> {
|
||||
pub fn try_decrypt<'a>(
|
||||
mail: &'a ParsedMail<'a>,
|
||||
private_keyring: &'a [SignedSecretKey],
|
||||
) -> Result<Option<::pgp::composed::Message<'static>>> {
|
||||
let Some(encrypted_data_part) = get_encrypted_mime(mail) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::info;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::tools::time;
|
||||
|
||||
+4
-4
@@ -11,6 +11,7 @@ use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
|
||||
use crate::log::warn;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
@@ -143,7 +144,7 @@ impl EncryptHelper {
|
||||
let cursor = Cursor::new(&mut raw_message);
|
||||
mail_to_encrypt.clone().write_part(cursor).ok();
|
||||
|
||||
let ctext = pgp::pk_encrypt(&raw_message, keyring, Some(sign_key), compress).await?;
|
||||
let ctext = pgp::pk_encrypt(raw_message, keyring, Some(sign_key), compress).await?;
|
||||
|
||||
Ok(ctext)
|
||||
}
|
||||
@@ -153,9 +154,8 @@ impl EncryptHelper {
|
||||
pub async fn sign(self, context: &Context, mail: &MimePart<'static>) -> Result<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)?;
|
||||
mail.clone().write_part(&mut buffer)?;
|
||||
let signature = pgp::pk_calc_signature(buffer, &sign_key)?;
|
||||
Ok(signature)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ use crate::context::Context;
|
||||
use crate::download::MIN_DELETE_SERVER_AFTER;
|
||||
use crate::events::EventType;
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{error, info, warn, LogExt};
|
||||
use crate::message::{Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::stock_str;
|
||||
@@ -146,7 +146,7 @@ impl FromStr for Timer {
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for Timer {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Integer(match self {
|
||||
Self::Disabled => 0,
|
||||
Self::Enabled { duration } => i64::from(*duration),
|
||||
|
||||
+2
-2
@@ -134,14 +134,14 @@ pub trait HeaderDefMap {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String>;
|
||||
|
||||
/// Returns requested header if it exists.
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader>;
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader<'_>>;
|
||||
}
|
||||
|
||||
impl HeaderDefMap for [MailHeader<'_>] {
|
||||
fn get_header_value(&self, headerdef: HeaderDef) -> Option<String> {
|
||||
self.get_first_value(headerdef.get_headername())
|
||||
}
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader> {
|
||||
fn get_header(&self, headerdef: HeaderDef) -> Option<&MailHeader<'_>> {
|
||||
self.get_first_header(headerdef.get_headername())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use mime::Mime;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::warn;
|
||||
use crate::message::{self, Message, MsgId};
|
||||
use crate::mimeparser::parse_message_id;
|
||||
use crate::param::Param::SendHtml;
|
||||
|
||||
+9
-11
@@ -32,7 +32,7 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{error, info, warn, LogExt};
|
||||
use crate::login_param::{
|
||||
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
};
|
||||
@@ -285,9 +285,7 @@ impl Imap {
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
/// Connects or reconnects if needed.
|
||||
///
|
||||
/// It is safe to call this function if already connected, actions are performed only as needed.
|
||||
/// Connects to IMAP server and returns a new IMAP session.
|
||||
///
|
||||
/// Calling this function is not enough to perform IMAP operations. Use [`Imap::prepare`]
|
||||
/// instead if you are going to actually use connection rather than trying connection
|
||||
@@ -326,7 +324,7 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Connecting to IMAP server");
|
||||
info!(context, "Connecting to IMAP server.");
|
||||
self.connectivity.set_connecting(context).await;
|
||||
|
||||
self.conn_last_try = tools::Time::now();
|
||||
@@ -411,7 +409,7 @@ impl Imap {
|
||||
lp.user
|
||||
)));
|
||||
self.connectivity.set_preparing(context).await;
|
||||
info!(context, "Successfully logged into IMAP server");
|
||||
info!(context, "Successfully logged into IMAP server.");
|
||||
return Ok(session);
|
||||
}
|
||||
|
||||
@@ -459,10 +457,10 @@ impl Imap {
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
}
|
||||
|
||||
/// Prepare for IMAP operation.
|
||||
/// Prepare a new IMAP session.
|
||||
///
|
||||
/// Ensure that IMAP client is connected, folders are created and IMAP capabilities are
|
||||
/// determined.
|
||||
/// This creates a new IMAP connection and ensures
|
||||
/// that folders are created and IMAP capabilities are determined.
|
||||
pub(crate) async fn prepare(&mut self, context: &Context) -> Result<Session> {
|
||||
let configuring = false;
|
||||
let mut session = match self.connect(context, configuring).await {
|
||||
@@ -1048,7 +1046,7 @@ impl Session {
|
||||
// Expunge folder if needed, e.g. if some jobs have
|
||||
// deleted messages on the server.
|
||||
if let Err(err) = self.maybe_close_folder(context).await {
|
||||
warn!(context, "failed to close folder: {:?}", err);
|
||||
warn!(context, "Failed to close folder: {err:#}.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2165,7 +2163,7 @@ pub(crate) fn get_folder_meaning(folder: &Name) -> FolderMeaning {
|
||||
}
|
||||
|
||||
/// Parses the headers from the FETCH result.
|
||||
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>> {
|
||||
fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader<'_>>> {
|
||||
match prefetch_msg.header() {
|
||||
Some(header_bytes) => {
|
||||
let (headers, _) = mailparse::parse_headers(header_bytes)?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
|
||||
@@ -8,6 +8,7 @@ use tokio::time::timeout;
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
use crate::net::TIMEOUT;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
|
||||
@@ -56,8 +56,7 @@ fn test_build_sequence_sets() {
|
||||
}
|
||||
|
||||
let has_number = |(uids, s): &(Vec<u32>, String), number| {
|
||||
uids.iter().any(|&n| n == number)
|
||||
&& s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
uids.contains(&number) && s.split(',').any(|n| n.parse::<u32>().unwrap() == number)
|
||||
};
|
||||
|
||||
let numbers: Vec<_> = (2..=500).step_by(2).collect();
|
||||
|
||||
+17
-10
@@ -5,7 +5,7 @@ use anyhow::{Context as _, Result};
|
||||
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
|
||||
use crate::config::Config;
|
||||
use crate::imap::{session::Session, Imap};
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, LogExt};
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{context::Context, imap::FolderMeaning};
|
||||
|
||||
@@ -17,16 +17,24 @@ impl Imap {
|
||||
session: &mut Session,
|
||||
) -> Result<bool> {
|
||||
// First of all, debounce to once per minute:
|
||||
let mut last_scan = context.last_full_folder_scan.lock().await;
|
||||
if let Some(last_scan) = *last_scan {
|
||||
let elapsed_secs = time_elapsed(&last_scan).as_secs();
|
||||
let debounce_secs = context
|
||||
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
|
||||
.await?;
|
||||
{
|
||||
let mut last_scan = context.last_full_folder_scan.lock().await;
|
||||
if let Some(last_scan) = *last_scan {
|
||||
let elapsed_secs = time_elapsed(&last_scan).as_secs();
|
||||
let debounce_secs = context
|
||||
.get_config_u64(Config::ScanAllFoldersDebounceSecs)
|
||||
.await?;
|
||||
|
||||
if elapsed_secs < debounce_secs {
|
||||
return Ok(false);
|
||||
if elapsed_secs < debounce_secs {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the timestamp before scanning the folders
|
||||
// to avoid holding the lock for too long.
|
||||
// This means next scan is delayed even if
|
||||
// the current one fails.
|
||||
last_scan.replace(tools::Time::now());
|
||||
}
|
||||
info!(context, "Starting full folder scan");
|
||||
|
||||
@@ -94,7 +102,6 @@ impl Imap {
|
||||
}
|
||||
|
||||
info!(context, "Found folders: {folder_names:?}.");
|
||||
last_scan.replace(tools::Time::now());
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::Context as _;
|
||||
use super::session::Session as ImapSession;
|
||||
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
|
||||
use crate::context::Context;
|
||||
use crate::log::{info, warn};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -33,10 +34,10 @@ impl ImapSession {
|
||||
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Some(folder) = &self.selected_folder {
|
||||
if self.selected_folder_needs_expunge {
|
||||
info!(context, "Expunge messages in \"{}\".", folder);
|
||||
info!(context, "Expunge messages in {folder:?}.");
|
||||
|
||||
self.close().await.context("IMAP close/expunge failed")?;
|
||||
info!(context, "close/expunge succeeded");
|
||||
info!(context, "Close/expunge succeeded.");
|
||||
self.selected_folder = None;
|
||||
self.selected_folder_needs_expunge = false;
|
||||
self.new_mail = false;
|
||||
@@ -73,6 +74,7 @@ impl ImapSession {
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
info!(context, "Selected folder {folder:?}.");
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
@@ -94,7 +96,7 @@ impl ImapSession {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => {
|
||||
info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder);
|
||||
info!(context, "Failed to select folder {folder:?} because it does not exist, trying to create it.");
|
||||
let create_res = self.create(folder).await;
|
||||
if let Err(ref err) = create_res {
|
||||
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
|
||||
@@ -129,7 +131,7 @@ impl ImapSession {
|
||||
let newly_selected = if create {
|
||||
self.select_or_create_folder(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to select or create folder {folder}"))?
|
||||
.with_context(|| format!("Failed to select or create folder {folder:?}"))?
|
||||
} else {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => newly_selected,
|
||||
@@ -137,7 +139,7 @@ impl ImapSession {
|
||||
Error::NoFolder(..) => return Ok(false),
|
||||
_ => {
|
||||
return Err(err)
|
||||
.with_context(|| format!("failed to select folder {folder}"))?
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -145,14 +147,14 @@ impl ImapSession {
|
||||
let mailbox = self
|
||||
.selected_mailbox
|
||||
.as_mut()
|
||||
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
|
||||
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
|
||||
|
||||
let old_uid_validity = get_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get old UID validity for folder {folder}"))?;
|
||||
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
|
||||
let old_uid_next = get_uid_next(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
|
||||
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
|
||||
|
||||
let new_uid_validity = mailbox
|
||||
.uid_validity
|
||||
|
||||
+16
-5
@@ -4,7 +4,6 @@ use std::ffi::OsStr;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
|
||||
use ::pgp::types::PublicKeyTrait;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use futures::TryStreamExt;
|
||||
use futures_lite::FutureExt;
|
||||
@@ -18,20 +17,21 @@ use crate::blob::BlobDirContents;
|
||||
use crate::chat::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::log::{error, info, warn, LogExt};
|
||||
use crate::pgp;
|
||||
use crate::qr::DCBACKUP_VERSION;
|
||||
use crate::sql;
|
||||
use crate::tools::{
|
||||
create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard,
|
||||
};
|
||||
use crate::{e2ee, stock_str};
|
||||
|
||||
mod key_transfer;
|
||||
mod transfer;
|
||||
|
||||
use ::pgp::types::KeyDetails;
|
||||
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
|
||||
pub use transfer::{get_backup, BackupProvider};
|
||||
|
||||
@@ -102,7 +102,14 @@ pub async fn imex(
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
|
||||
error!(context, "IMEX failed to complete: {:#}", err);
|
||||
if err
|
||||
.to_string()
|
||||
.contains("Backup too new, please update Delta Chat")
|
||||
{
|
||||
error!(context, "{}", stock_str::old_backup(context));
|
||||
} else {
|
||||
error!(context, "IMEX failed to complete: {:#}", err);
|
||||
}
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
} else {
|
||||
info!(context, "IMEX successfully completed");
|
||||
@@ -173,7 +180,11 @@ async fn set_self_key(context: &Context, armored: &str) -> Result<()> {
|
||||
};
|
||||
key::store_self_keypair(context, &keypair).await?;
|
||||
|
||||
info!(context, "stored self key: {:?}", keypair.secret.key_id());
|
||||
info!(
|
||||
context,
|
||||
"stored self key: {:?}",
|
||||
keypair.secret.public_key().key_id()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+15
-15
@@ -1,4 +1,6 @@
|
||||
//! # Key transfer via Autocrypt Setup Message.
|
||||
use std::io::BufReader;
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
@@ -71,7 +73,7 @@ pub async fn continue_key_transfer(
|
||||
if let Some(filename) = msg.get_file(context) {
|
||||
let file = open_file_std(context, filename)?;
|
||||
let sc = normalize_setup_code(setup_code);
|
||||
let armored_key = decrypt_setup_file(&sc, file).await?;
|
||||
let armored_key = decrypt_setup_file(&sc, BufReader::new(file)).await?;
|
||||
set_self_key(context, &armored_key).await?;
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
@@ -96,7 +98,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
|
||||
};
|
||||
let private_key_asc = private_key.to_asc(ac_headers);
|
||||
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes())
|
||||
let encr = pgp::symm_encrypt(passphrase, private_key_asc.into_bytes())
|
||||
.await?
|
||||
.replace('\n', "\r\n");
|
||||
|
||||
@@ -155,7 +157,7 @@ fn create_setup_code(_context: &Context) -> String {
|
||||
ret
|
||||
}
|
||||
|
||||
async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
|
||||
async fn decrypt_setup_file<T: std::fmt::Debug + std::io::BufRead + Send + 'static>(
|
||||
passphrase: &str,
|
||||
file: T,
|
||||
) -> Result<String> {
|
||||
@@ -258,11 +260,10 @@ mod tests {
|
||||
|
||||
assert!(!base64.is_empty());
|
||||
|
||||
let setup_file = S_EM_SETUPFILE.to_string();
|
||||
let decrypted =
|
||||
decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
|
||||
.await
|
||||
.unwrap();
|
||||
let setup_file = S_EM_SETUPFILE;
|
||||
let decrypted = decrypt_setup_file(S_EM_SETUPCODE, setup_file.as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
|
||||
|
||||
@@ -278,14 +279,13 @@ mod tests {
|
||||
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decrypt_plaintext_autocrypt_setup_message() {
|
||||
let setup_file = S_PLAINTEXT_SETUPFILE.to_string();
|
||||
let setup_file = S_PLAINTEXT_SETUPFILE;
|
||||
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
|
||||
assert!(decrypt_setup_file(
|
||||
incorrect_setupcode,
|
||||
std::io::Cursor::new(setup_file.as_bytes()),
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
decrypt_setup_file(incorrect_setupcode, setup_file.as_bytes(),)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -41,6 +41,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::Message;
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
@@ -95,6 +96,7 @@ impl BackupProvider {
|
||||
pub async fn prepare(context: &Context) -> Result<Self> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
let endpoint = Endpoint::builder()
|
||||
.tls_x509() // For compatibility with iroh <0.34.0
|
||||
.alpns(vec![BACKUP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind()
|
||||
@@ -301,7 +303,11 @@ pub async fn get_backup2(
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
|
||||
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind().await?;
|
||||
let endpoint = Endpoint::builder()
|
||||
.tls_x509() // For compatibility with iroh <0.34.0
|
||||
.relay_mode(relay_mode)
|
||||
.bind()
|
||||
.await?;
|
||||
|
||||
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
|
||||
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
|
||||
|
||||
+29
-9
@@ -10,12 +10,12 @@ use deltachat_contact_tools::EmailAddress;
|
||||
use pgp::composed::Deserializable;
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::{PublicKeyTrait, SecretKeyTrait};
|
||||
use pgp::types::{KeyDetails, KeyId, Password};
|
||||
use rand::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{info, LogExt};
|
||||
use crate::pgp::KeyPair;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::tools::{self, time_elapsed};
|
||||
/// This trait is implemented for rPGP's [SignedPublicKey] and
|
||||
/// [SignedSecretKey] types and makes working with them a little
|
||||
/// easier in the deltachat world.
|
||||
pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
pub(crate) trait DcKey: Serialize + Deserializable + Clone {
|
||||
/// Create a key from some bytes.
|
||||
fn from_slice(bytes: &[u8]) -> Result<Self> {
|
||||
let res = <Self as Deserializable>::from_bytes(Cursor::new(bytes));
|
||||
@@ -78,7 +78,7 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
let bytes = data.as_bytes();
|
||||
let res = Self::from_armor_single(Cursor::new(bytes));
|
||||
let (key, headers) = match res {
|
||||
Err(pgp::errors::Error::NoMatchingPacket) => match Self::is_private() {
|
||||
Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() {
|
||||
true => bail!("No private key packet found"),
|
||||
false => bail!("No public key packet found"),
|
||||
},
|
||||
@@ -123,11 +123,10 @@ pub(crate) trait DcKey: Serialize + Deserializable + PublicKeyTrait + Clone {
|
||||
fn to_asc(&self, header: Option<(&str, &str)>) -> String;
|
||||
|
||||
/// The fingerprint for the key.
|
||||
fn dc_fingerprint(&self) -> Fingerprint {
|
||||
PublicKeyTrait::fingerprint(self).into()
|
||||
}
|
||||
fn dc_fingerprint(&self) -> Fingerprint;
|
||||
|
||||
fn is_private() -> bool;
|
||||
fn key_id(&self) -> KeyId;
|
||||
}
|
||||
|
||||
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
|
||||
@@ -230,6 +229,14 @@ impl DcKey for SignedPublicKey {
|
||||
fn is_private() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dc_fingerprint(&self) -> Fingerprint {
|
||||
self.fingerprint().into()
|
||||
}
|
||||
|
||||
fn key_id(&self) -> KeyId {
|
||||
KeyDetails::key_id(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DcKey for SignedSecretKey {
|
||||
@@ -249,6 +256,14 @@ impl DcKey for SignedSecretKey {
|
||||
fn is_private() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn dc_fingerprint(&self) -> Fingerprint {
|
||||
self.fingerprint().into()
|
||||
}
|
||||
|
||||
fn key_id(&self) -> KeyId {
|
||||
KeyDetails::key_id(&**self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deltachat extension trait for secret keys.
|
||||
@@ -262,9 +277,14 @@ pub(crate) trait DcSecretKey {
|
||||
impl DcSecretKey for SignedSecretKey {
|
||||
fn split_public_key(&self) -> Result<SignedPublicKey> {
|
||||
self.verify()?;
|
||||
let unsigned_pubkey = SecretKeyTrait::public_key(self);
|
||||
let unsigned_pubkey = self.public_key();
|
||||
let mut rng = thread_rng();
|
||||
let signed_pubkey = unsigned_pubkey.sign(&mut rng, self, || "".into())?;
|
||||
let signed_pubkey = unsigned_pubkey.sign(
|
||||
&mut rng,
|
||||
&self.primary_key,
|
||||
self.primary_key.public_key(),
|
||||
&Password::empty(),
|
||||
)?;
|
||||
Ok(signed_pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::{info, warn};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
|
||||
+26
-15
@@ -4,7 +4,6 @@
|
||||
|
||||
use crate::context::Context;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! info {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
info!($ctx, $msg,)
|
||||
@@ -19,22 +18,30 @@ macro_rules! info {
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! warn {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
warn!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
$ctx.emit_event($crate::EventType::Warning(full));
|
||||
}};
|
||||
pub(crate) use info;
|
||||
|
||||
// Workaround for <https://github.com/rust-lang/rust/issues/133708>.
|
||||
#[macro_use]
|
||||
mod warn_macro_mod {
|
||||
macro_rules! warn_macro {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
warn_macro!($ctx, $msg,)
|
||||
};
|
||||
($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
|
||||
let formatted = format!($msg, $($args),*);
|
||||
let full = format!("{file}:{line}: {msg}",
|
||||
file = file!(),
|
||||
line = line!(),
|
||||
msg = &formatted);
|
||||
$ctx.emit_event($crate::EventType::Warning(full));
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use warn_macro;
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
pub(crate) use warn_macro_mod::warn_macro as warn;
|
||||
|
||||
macro_rules! error {
|
||||
($ctx:expr, $msg:expr) => {
|
||||
error!($ctx, $msg,)
|
||||
@@ -46,6 +53,8 @@ macro_rules! error {
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use error;
|
||||
|
||||
impl Context {
|
||||
/// Set last error string.
|
||||
/// Implemented as blocking as used from macros in different, not always async blocks.
|
||||
@@ -102,6 +111,8 @@ impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
+14
-13
@@ -809,20 +809,14 @@ impl ConfiguredLoginParam {
|
||||
entered_param: &EnteredLoginParam,
|
||||
) -> Result<()> {
|
||||
let addr = addr_normalize(&self.addr);
|
||||
let provider_id = self.provider.map(|provider| provider.id);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
if let Some(configured_addr) = configured_addr {
|
||||
if let Some(configured_addr) = &configured_addr {
|
||||
ensure!(
|
||||
addr_cmp(&configured_addr, &addr,),
|
||||
addr_cmp(configured_addr, &addr),
|
||||
"Adding a second transport is not supported right now."
|
||||
);
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(
|
||||
Config::ConfiguredProvider.as_ref(),
|
||||
self.provider.map(|provider| provider.id),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -837,10 +831,17 @@ impl ConfiguredLoginParam {
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
if configured_addr.is_none() {
|
||||
// If there is no transport yet, set the new transport as the primary one
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredProvider.as_ref(), provider_id)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user