Compare commits

..

1 Commits

Author SHA1 Message Date
Hocuri
f979e9b26d feat: In debug builds, panic on errors in C bindings 2025-05-05 18:59:33 +02:00
207 changed files with 10565 additions and 13255 deletions

View File

@@ -20,10 +20,10 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.88.0
RUST_VERSION: 1.86.0
# Minimum Supported Rust Version
MSRV: 1.85.0
MSRV: 1.82.0
jobs:
lint_rust:
@@ -36,8 +36,6 @@ 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

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.4.0
uses: dependabot/fetch-metadata@v2.3.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR

View File

@@ -95,10 +95,9 @@ jobs:
fail-fast: false
matrix:
installable:
- deltachat-rpc-server
- deltachat-rpc-server-aarch64-darwin
# Fails to bulid
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4

View File

@@ -19,7 +19,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v5
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

1
.gitignore vendored
View File

@@ -53,4 +53,3 @@ result
# direnv
.envrc
.direnv
.aider*

View File

@@ -1,320 +1,5 @@
# Changelog
## [2.10.0] - 2025-08-04
### Features / Changes
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
### Miscellaneous Tasks
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
- cargo: Bump bolero from 0.13.3 to 0.13.4.
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
- cargo: Bump criterion from 0.6.0 to 0.7.0.
- cargo: Bump strum from 0.27.1 to 0.27.2.
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
- Upgrade async-imap to 0.11.1.
## [2.9.0] - 2025-07-31
### Features / Changes
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
### Fixes
- Display correct timer value for ephemeral timer changes.
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
### Refactor
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
- Don't call add_or_lookup_key_contacts() in advance.
## [2.8.0] - 2025-07-28
### Features / Changes
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
### Fixes
- Lookup self by address if there is no fingerprint or gossip.
## [2.7.0] - 2025-07-26
### Features / Changes
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
### Fixes
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
### Tests
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
## [2.6.0] - 2025-07-23
### Fixes
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
## [2.5.0] - 2025-07-22
### Fixes
- Correctly migrate "verified by me".
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
- Do not ignore errors in add_flag_finalized_with_set.
### Documentation
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
- STYLE.md: Prefer `try_next()` over `next()`.
## [2.4.0] - 2025-07-21
### Fixes
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
### Miscellaneous Tasks
- Update async-imap to 0.11.0.
### Refactor
- Use `try_next()` when processing FETCH responses.
## [2.3.0] - 2025-07-19
### Features / Changes
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
- repl: Print errors and debug logs to stderr.
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
- Log when background fetch of all accounts finishes successfully.
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
### Fixes
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
- List e-mail contacts in repl listcontacts command.
- Save peer address for LoggingStream early.
## [2.2.0] - 2025-07-14
### API-Changes
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
### Features / Changes
- Log failed debug assertions in all configurations.
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
- Advance next UID even if connection fails while fetching.
### Fixes
- Always prefer the last header.
### Tests
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
## [2.1.0] - 2025-07-11
### Features / Changes
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
- Migrations: Use tools::Time to measure time for logging.
- Log emitted logging events with `tracing`.
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
### Fixes
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
- Don't apply chat name and avatar changes from non-members.
### Documentation
- Update showpadlock ffi.
### Miscellaneous Tasks
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
### Tests
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
## [2.0.0] - 2025-07-09
This release changes the way the core handles contact keys.
Instead of tracking OpenPGP keys corresponding to the
contacts in [Autocrypt](https://autocrypt.org/) peerstate,
the core creates a new "key-contact" for each known public key.
Reception of a message signed with a new unknown key
no longer results in warnings about setup changes,
but creates a new contact and a new 1:1 chat if necessary.
Additionally, there are "address-contacts" corresponding
to the e-mail addresses.
### Features / Changes
- Key-contacts ([#6796](https://github.com/chatmail/core/pull/6796), [#6941](https://github.com/chatmail/core/pull/6941)).
- Increase event channel size from 1000 to 10000.
- Minimize the amount of data preserved for trashed messages.
- Show broadcast channels in their own, proper "Channel" chat ([#6901](https://github.com/chatmail/core/pull/6901), [#6975](https://github.com/chatmail/core/pull/6975)).
- Check images passed as `File` before making them `Image`.
### API-Changes
- CFFI: Add dc_contact_is_key_contact() ([#6955](https://github.com/chatmail/core/pull/6955)).
- Contact::get_all(): Support listing address-contacts.
- [**breaking**] Add InBroadcastChannel, OutBroadcastChannel chattypes, add create_broadcast_channel() ([#6901](https://github.com/chatmail/core/pull/6901)).
- deltachat-rpc-client: Add Message.get_read_receipts().
### Fixes
- Remove display name from get_info(). This information usually goes at the top of the log and we don't want users to include it in bug reports.
- Wait for scheduler tasks shutdown in parallel.
- Update deltachat-repl help and autocomplete to match implementation ([#6978](https://github.com/chatmail/core/pull/6978), ([#6979](https://github.com/chatmail/core/pull/6979)).
- Send Autocrypt header in MDNs. This is needed to assign MDNs to key-contacts.
- Prefer encrypted List-Id header ([#6983](https://github.com/chatmail/core/pull/6983)).
- Treat "tgs" as Viewtype::File.
- Treat and send images that can't be decoded as Viewtype::File.
- Decide on filename used for sending depending on the original Viewtype.
- Migrate_key_contacts(): Remove "id>9" from encrypted messages SELECT.
- Save msgs to key-contacts migration state and run migration periodically ([#6956](https://github.com/chatmail/core/pull/6956)).
- Do not try to lookup key-contacts for unencrypted 1:1 messages.
- Add query to post request for account creation ([#6989](https://github.com/chatmail/core/pull/6989)).
### CI
- Update Rust to 1.88.0.
### Documentation
- Remove outdated comment that says MDNs are unencrypted.
### Refactor
- Upgrade to Rust 2024.
- Build_body_file(): Remove guessing mimetype by file extension.
### Tests
- Add online test for read receipts.
- Add a test reproducing chat assignment bug.
### Miscellaneous Tasks
- cargo: Bump smallvec from 1.15.0 to 1.15.1.
- cargo: Bump syn from 2.0.101 to 2.0.104.
- cargo: Bump hyper-util from 0.1.13 to 0.1.14.
- cargo: Bump toml from 0.8.19 to 0.8.23.
- cargo: Bump proptest from 1.6.0 to 1.7.0.
- cargo: Bump libc from 0.2.172 to 0.2.174.
## [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
@@ -518,8 +203,8 @@ to the e-mail addresses.
- Use vCard in TestContext.add_or_lookup_contact().
- Remove test_group_with_removed_message_id.
- Use add_or_lookup_address_contact() in get_chat().
- Use add_or_lookup_address_contact in test_setup_contact_ex.
- Use add_or_lookup_email_contact() in get_chat().
- Use add_or_lookup_email_contact in test_setup_contact_ex.
- Use vCards more in Python tests.
- Use TestContextManager in more tests.
- Use vCards to create contacts in more Rust tests.
@@ -6582,17 +6267,3 @@ 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
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0

1705
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "2.10.0"
edition = "2024"
version = "1.159.3"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.85"
rust-version = "1.82"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -18,9 +18,6 @@ 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>
@@ -44,9 +41,9 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
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.2", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
brotli = { version = "8", default-features=false, features = ["std"] }
@@ -59,25 +56,25 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "0.25.2"
hickory-resolver = "=0.25.0-alpha.5"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.16"
hyper-util = "0.1.11"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
iroh = { version = "0.33", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.3", default-features = false }
mail-builder = { version = "0.4.2", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.17"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
parking_lot = "0.12"
percent-encoding = "2.3"
pgp = { version = "0.16.0", default-features = false }
pgp = { version = "0.15.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
@@ -86,7 +83,7 @@ rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.12.0"
rustls-pki-types = "1.11.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
@@ -94,21 +91,20 @@ serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
smallvec = "1.15.1"
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
smallvec = "1.15.0"
strum = "0.27"
strum_macros = "0.27"
tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.1"
tokio-io-timeout = "1.2.0"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.9"
tracing = "0.1.41"
toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
@@ -116,7 +112,7 @@ blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.7.0", features = ["async_tokio"] }
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -175,7 +171,7 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.5.0"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.41", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
@@ -190,11 +186,11 @@ nu-ansi-term = "0.46"
num-traits = "0.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.36"
rusqlite = "0.32"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.20.0"
tempfile = "3.19.1"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
@@ -205,8 +201,7 @@ yerpc = "0.6.4"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl",
"async-native-tls/vendored"
"rusqlite/bundled-sqlcipher-vendored-openssl"
]
[lints.rust]

View File

@@ -80,41 +80,30 @@ Connect to your mail server (if already configured):
> connect
```
Export your public key to a vCard file:
```
> make-vcard my.vcard 1
```
Create contacts by address or vCard file:
Create a contact:
```
> addcontact yourfriends@email.org
> import-vcard key-contact.vcard
Command executed successfully.
```
List contacts:
```
> listcontacts
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
Contact#Contact#Self: Me √ <your@email.org>
2 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
Contact#10: <name unset> <yourfriends@email.org>
Contact#1: Me √ <your@email.org>
```
Create a chat with your friend and send a message:
```
> createchat 10
Single#Chat#12 created successfully.
> chat 12
Selecting chat Chat#12
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
0 messages.
81.252µs to create this list, 123.625µs to mark all messages as noticed.
Single#10 created successfully.
> chat 10
Single#10: yourfriends@email.org [yourfriends@email.org]
> send hi
Message sent.
```
List messages when inside a chat:
@@ -167,13 +156,13 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install cargo-bolero
$ cargo install cargo-bolero@0.8.0
```
Run fuzzing tests with
```sh
$ cd fuzz
$ cargo bolero test fuzz_mailparse -s NONE
$ cargo bolero test fuzz_mailparse --release=false -s NONE
```
Corpus is created at `fuzz/fuzz_targets/corpus`,
@@ -181,6 +170,11 @@ 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.

View File

@@ -78,40 +78,12 @@ All errors should be handled in one of these ways:
- With `.log_err().ok()`.
- Bubbled up with `?`.
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
```
while let Some(event) = stream.try_next().await? {
todo!();
}
```
instead of
```
while let Some(event_res) = stream.next().await {
todo!();
}
```
as it allows bubbling up the error early with `?`
with no way to accidentally skip error processing
with early `continue` or `break`.
Some streams reading from a connection
return infinite number of `Some(Err(_))`
items when connection breaks and not processing
errors may result in infinite loop.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
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.
@@ -124,17 +96,3 @@ 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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="480"
viewBox="0 -960 9600 9600"
width="480"
fill="#ffffff"
version="1.1"
id="svg1"
sodipodi:docname="icon-email.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.99091847"
inkscape:cx="263.392"
inkscape:cy="177.613"
inkscape:window-width="1884"
inkscape:window-height="1052"
inkscape:window-x="36"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<rect
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
id="rect1"
width="9951.9541"
height="9767.4756"
x="-71.697792"
y="-1012.83"
ry="0.43547946" />
<path
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
id="path1"
style="stroke-width:5.40098" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,7 +0,0 @@
BEGIN:VCARD
VERSION:4.0
EMAIL:self_reporting@testrun.org
FN:Statistics bot
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
REV:20250412T195751Z
END:VCARD

View File

@@ -1,11 +1,9 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use tempfile::tempdir;
async fn address_book_benchmark(n: u32, read_count: u32) {

View File

@@ -1,8 +1,7 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{Criterion, criterion_group, criterion_main};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::accounts::Accounts;
use tempfile::tempdir;

View File

@@ -1,13 +1,12 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;

View File

@@ -1,12 +1,11 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_list_benchmark(context: &Context) {
Chatlist::try_load(context, 0, None, None).await.unwrap();

View File

@@ -1,13 +1,12 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use deltachat::Events;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use futures_lite::future::block_on;
use tempfile::tempdir;

View File

@@ -1,15 +1,14 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{Criterion, criterion_group, criterion_main};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::{
Events,
config::Config,
context::Context,
imex::{ImexMode, imex},
imex::{imex, ImexMode},
receive_imf::receive_imf,
stock_str::StockStrings,
Events,
};
use tempfile::tempdir;

View File

@@ -1,11 +1,10 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn search_benchmark(dbfile: impl AsRef<Path>) {
let id = 100;

View File

@@ -1,17 +1,17 @@
#![recursion_limit = "256"]
use criterion::{Criterion, criterion_group, criterion_main};
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::{Event, EventType, Events};
use deltachat::{info, 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 {
context.emit_event(EventType::Info("interesting event...".to_string()));
info!(context, "interesting event...");
}
context.emit_event(EventType::Info("DONE".to_string()));
info!(context, "DONE");
loop {
match emitter.recv().await.unwrap() {

View File

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

View File

@@ -20,8 +20,6 @@ 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>,
}
@@ -46,15 +44,10 @@ 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 {
// 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());
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
@@ -62,13 +55,10 @@ 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");
}
if let Some(biography) = &c.biography {
res += &format!("NOTE:{}\r\n", escape(biography));
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
@@ -89,8 +79,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
None
}
}
/// Returns (parameters, raw value) tuple.
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
/// Returns (parameters, value) tuple.
fn vcard_property<'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`
@@ -120,25 +110,23 @@ 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_raw(line, "key")?;
let (params, value) = vcard_property(line, "key")?;
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
{
return Some(value);
}
remove_prefix(value, "data:application/pgp-keys;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
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
}
fn base64_photo(line: &str) -> Option<&str> {
let (params, value) = vcard_property_raw(line, "photo")?;
let (params, value) = vcard_property(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")
@@ -148,9 +136,13 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
{
return Some(value);
}
remove_prefix(value, "data:image/jpeg;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
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
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
@@ -194,7 +186,6 @@ 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() {
@@ -214,24 +205,18 @@ 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_default(),
&addr.unwrap_or_default(),
);
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
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),
});

View File

@@ -91,7 +91,6 @@ 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 {
@@ -99,7 +98,6 @@ fn test_make_and_parse_vcard() {
authname: "".to_string(),
key: None,
profile_image: None,
biography: None,
timestamp: Ok(0),
},
];
@@ -108,9 +106,8 @@ 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\
NOTE:Hi\\, I'm Alice\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
@@ -249,8 +246,7 @@ END:VCARD",
assert_eq!(contacts[0].profile_image, None);
}
/// Proton at some point slightly changed the format of their vcards.
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
/// Proton at some point slightly changed the format of their vcards
#[test]
fn test_protonmail_vcard2() {
let contacts = parse_vcard(

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<doxygenlayout version="2.0">
<!-- Generated by doxygen 1.13.2 -->
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.20 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
@@ -12,16 +11,10 @@
</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=""/>
@@ -42,228 +35,4 @@
</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>

View File

@@ -503,6 +503,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
* to 1 if it supports verified 1:1 chats.
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
* and when the key changes, an info message is posted into the chat.
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
@@ -1332,14 +1339,12 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* Optionally, some special markers added to the ID array may help to
* implement virtual lists.
*
* To get the concrete time of the message, use dc_array_get_timestamp().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
* @param marker1before Deprecated, set this to 0.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -2089,19 +2094,9 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Looks up a known and unblocked contact with a given e-mail address.
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
* (e.g. an address-contact and a key-contact),
* this looks up the most recently seen contact,
* i.e. which contact is returned depends on which contact last sent a message.
* If the user just clicked on a mailto: link, then this is the best thing you can do.
* But **DO NOT** internally represent contacts by their email address
* and do not use this function to look them up;
* otherwise this function will sometimes look up the wrong contact.
* Instead, you should internally represent contacts by their ids.
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
*
@@ -2123,13 +2118,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
* a bunch of addresses.
*
* This will always create or look up an address-contact,
* i.e. a contact identified by an email address,
* with all messages sent to and from this contact being unencrypted.
* If the user just clicked on an email address,
* you should first check `lookup_contact_id_by_addr`,
* and only if there is no contact yet, call this function here.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
* @memberof dc_context_t
@@ -2144,12 +2132,8 @@ 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);
// Deprecated 2025-05-20, setting this flag is a no-op.
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
#define DC_GCL_VERIFIED_ONLY 0x01
#define DC_GCL_ADD_SELF 0x02
#define DC_GCL_ADDRESS 0x04
/**
@@ -2205,13 +2189,13 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
* Returns known and unblocked contacts.
*
* To get information about a single contact, see dc_get_contact().
* By default, key-contacts are listed.
*
* @memberof dc_context_t
* @param context The context object.
* @param flags A combination of flags:
* - DC_GCL_ADD_SELF: SELF is added to the list unless filtered by other parameters
* - DC_GCL_ADDRESS: List address-contacts instead of key-contacts.
* - 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
@@ -3830,12 +3814,21 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
*
* Only verified contacts
* End-to-end encryption is guaranteed in protected chats
* and only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
* 1:1 chats become protected or unprotected automatically
* if `verified_one_on_one_chats` setting is enabled.
*
* UI should display a green checkmark
* in the chat title,
* in the chatlist item
* and in the chat profile
* if chat protection is enabled.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3844,21 +3837,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Check if the chat is encrypted.
*
* 1:1 chats with key-contacts and group chats with key-contacts
* are encrypted.
* 1:1 chats with emails contacts and ad-hoc groups
* created for email threads are not encrypted.
*
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat is encrypted, 0=chat is not encrypted.
*/
int dc_chat_is_encrypted (const dc_chat_t *chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
@@ -3872,8 +3850,6 @@ int dc_chat_is_encrypted (const dc_chat_t *chat);
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
*
* @deprecated 2025-07 chats protection cannot break any longer
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
@@ -4308,16 +4284,11 @@ int dc_msg_get_duration (const dc_msg_t* msg);
/**
* Check if message was correctly encrypted and signed.
*
* Historically, UIs showed a small padlock on the message then.
* Today, the UIs should instead
* show a small email-icon on the message if the message is not encrypted or signed,
* and nothing otherwise.
* Check if a padlock should be shown beside the message.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
@@ -4540,12 +4511,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4598,10 +4569,9 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
/**
@@ -5272,14 +5242,20 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to protected chats.
* can be added to verified chats,
* i.e. has a verified key
* and Autocrypt key matches the verified key.
*
* See dc_contact_get_verifier_id() for a guidance how to display these information.
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items,
* in chat member list items
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -5293,39 +5269,19 @@ int dc_contact_is_verified (dc_contact_t* contact);
int dc_contact_is_bot (dc_contact_t* contact);
/**
* Returns whether contact is a key-contact,
* i.e. it is identified by the public key
* rather than the email address.
*
* If so, all messages to and from this contact are encrypted.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 1 if the contact is a key-contact, 0 if it is an address-contact.
*/
int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
* If the function returns non-zero result,
* display green checkmark in the profile and "Introduced by ..." line
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr.
*
* UI should display the information in the contact's profile as follows:
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
* If this function returns a verifier,
* this does not necessarily mean
* you can add the contact to verified chats.
* Use dc_contact_is_verified() to check
* if a contact can be added to a verified chat instead.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5758,33 +5714,9 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CHAT_TYPE_MAILINGLIST 140
/**
* Outgoing broadcast channel, called "Channel" in the UI.
*
* The user can send into this chat,
* and all recipients will receive messages
* in a `DC_CHAT_TYPE_IN_BROADCAST`.
*
* Called `broadcast` here rather than `channel`,
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
* A broadcast list. See dc_chat_get_type() for details.
*/
#define DC_CHAT_TYPE_OUT_BROADCAST 160
/**
* Incoming broadcast channel, called "Channel" in the UI.
*
* This chat is read-only,
* and we do not know who the other recipients are.
*
* This is similar to `DC_CHAT_TYPE_MAILINGLIST`,
* with the main difference being that
* broadcasts are encrypted.
*
* Called `broadcast` here rather than `channel`,
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
*/
#define DC_CHAT_TYPE_IN_BROADCAST 165
#define DC_CHAT_TYPE_BROADCAST 160
/**
* @}
@@ -6391,6 +6323,7 @@ void dc_event_unref(dc_event_t* event);
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
* and dc_remove_contact_from_chat().
*
@@ -6903,7 +6836,9 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_GIF 23
/// @deprecated 2025-07, this string is no longer needed.
/// "Encrypted message"
///
/// Used in subjects of outgoing messages.
#define DC_STR_ENCRYPTEDMSG 24
/// "End-to-end encryption available."
@@ -6950,7 +6885,6 @@ void dc_event_unref(dc_event_t* event);
/// "End-to-end encryption preferred."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
/// @deprecated 2025-06-05
#define DC_STR_E2E_PREFERRED 34
/// "%1$s verified"
@@ -6963,14 +6897,12 @@ void dc_event_unref(dc_event_t* event);
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact that cannot be verified
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_NOT_VERIFIED 36
/// "Changed setup for %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact with the changed setup
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_SETUP_CHANGED 37
/// "Archived chats"
@@ -7360,7 +7292,6 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s changed their address from %2$s to %3$s"
///
/// Used as an info message to chats with contacts that changed their address.
/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
@@ -7427,7 +7358,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left."
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
@@ -7608,7 +7539,7 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are end-to-end encrypted."
/// "Messages are guaranteed to be end-to-end encrypted from now on."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
@@ -7616,7 +7547,6 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s sent a message from another device."
///
/// Used in info messages.
/// @deprecated 2025-07
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message."
@@ -7668,12 +7598,8 @@ void dc_event_unref(dc_event_t* event);
/// "The contact must be online to proceed. This process will continue automatically in background."
///
/// Used as info message.
/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.10.0"
version = "1.159.3"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -19,7 +19,6 @@ 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::{
@@ -36,6 +35,7 @@ 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};
@@ -224,14 +224,6 @@ impl CommandApi {
self.accounts.read().await.get_selected_account_id()
}
/// Set the order of accounts.
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
self.accounts.write().await.set_accounts_order(order).await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
@@ -362,20 +354,6 @@ impl CommandApi {
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
/// If there was an error while the account was opened
/// and migrated to the current version,
/// then this function returns it.
///
/// This function is useful because the key-contacts migration could fail due to bugs
/// and then the account will not work properly.
///
/// After opening an account, the UI should call this function
/// and show the error string if one is returned.
async fn get_migration_error(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_migration_error())
}
/// Copy file to blob dir.
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
let ctx = self.get_context(account_id).await?;
@@ -934,7 +912,7 @@ impl CommandApi {
/// explicitly as it may happen that oneself gets removed from a still existing
/// group
///
/// - for broadcast channels, all recipients are returned, DC_CONTACT_ID_SELF is not included
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
///
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
/// for now, the UI should not show the list for mailing lists.
@@ -953,7 +931,7 @@ impl CommandApi {
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
}
/// Create a new encrypted group chat (with key-contacts).
/// Create a new group chat.
///
/// After creation,
/// the group has one member with the ID DC_CONTACT_ID_SELF
@@ -971,52 +949,30 @@ impl CommandApi {
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
/// and end-to-end-encryption is always enabled.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_ex(&ctx, Some(protect), &name)
chat::create_group_chat(&ctx, protect, &name)
.await
.map(|id| id.to_u32())
}
/// Create a new unencrypted group chat.
/// Create a new broadcast list.
///
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_group_ex(&ctx, None, &name)
.await
.map(|id| id.to_u32())
}
/// Deprecated 2025-07 in favor of create_broadcast().
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
self.create_broadcast(account_id, "Channel".to_string())
.await
}
/// Create a new **broadcast channel**
/// (called "Channel" in the UI).
///
/// Broadcast channels are similar to groups on the sending device,
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in a read-only chat
/// and will not see who the other members are.
/// and will see who the other members are.
///
/// Called `broadcast` here rather than `channel`,
/// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it.
///
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
///
/// Returns the created chat's id.
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast(&ctx, chat_name)
chat::create_broadcast_list(&ctx)
.await
.map(|id| id.to_u32())
}
@@ -1227,10 +1183,8 @@ impl CommandApi {
}
/// Returns all messages of a particular chat.
///
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
/// If `add_daymarker` is `true`, it will return them as
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
async fn get_message_ids(
&self,
account_id: u32,
@@ -1473,14 +1427,7 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// This will always create or look up an address-contact,
/// i.e. a contact identified by an email address,
/// with all messages sent to and from this contact being unencrypted.
/// If the user just clicked on an email address,
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
/// and only if there is no contact yet, call this function here.
///
/// Returns contact id of the created or existing contact.
/// Returns contact id of the created or existing contact
async fn create_contact(
&self,
account_id: u32,
@@ -1532,14 +1479,6 @@ impl CommandApi {
Ok(contacts)
}
/// Returns ids of known and unblocked contacts.
///
/// By default, key-contacts are listed.
///
/// * `list_flags` - A combination of flags:
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
/// * `query` - A string to filter the list.
async fn get_contact_ids(
&self,
account_id: u32,
@@ -1551,10 +1490,8 @@ impl CommandApi {
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
}
/// Returns known and unblocked contacts.
///
/// Formerly called `getContacts2` in Desktop.
/// See [`Self::get_contact_ids`] for parameters and more info.
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn get_contacts(
&self,
account_id: u32,
@@ -1605,6 +1542,15 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
/// Sets display name for existing contact.
async fn change_contact_name(
&self,
@@ -1630,19 +1576,9 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Looks up a known and unblocked contact with a given e-mail address.
/// Check if an e-mail address belongs to a known and unblocked contact.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(
@@ -1983,10 +1919,12 @@ impl CommandApi {
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
if let Some(fut) =
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
{
tokio::spawn(fut);
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")
});
}
Ok(())
}

View File

@@ -21,39 +21,15 @@ pub struct FullChat {
/// True if the chat is protected.
///
/// Only verified contacts
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
/// can be added to protected chats.
///
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
/// by setting the 'protect' parameter to true.
///
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
@@ -71,9 +47,7 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -134,7 +108,6 @@ impl FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
@@ -186,30 +159,6 @@ pub struct BasicChat {
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
@@ -218,9 +167,7 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
/// Deprecated 2025-07. Chats protection cannot break any longer.
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}
@@ -240,7 +187,6 @@ impl BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,

View File

@@ -23,7 +23,6 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: u32,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
@@ -31,31 +30,6 @@ pub enum ChatListItemFetchResult {
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
/// deprecated 2025-07, use chat_type instead
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
@@ -66,6 +40,8 @@ pub enum ChatListItemFetchResult {
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// true when chat is a broadcastlist
is_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
@@ -155,14 +131,12 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
@@ -173,6 +147,7 @@ pub(crate) async fn get_chat_list_item_by_id(
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,

View File

@@ -19,23 +19,15 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
/// Is the contact a key contact.
is_key_contact: bool,
/// Is encryption available for this contact.
///
/// This can only be true for key-contacts.
/// However, it is possible to have a key-contact
/// for which encryption is not available because we don't have a key yet,
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
/// True if the contact can be added to verified groups.
///
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items,
/// in chat member list items
/// and in profiles if no chat with the contact exist.
is_verified: bool,
/// True if the contact profile title should have a green checkmark.
@@ -44,29 +36,12 @@ pub struct ContactObject {
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The contact ID that verified a contact.
/// The ID of the contact that verified this contact.
///
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
@@ -92,7 +67,6 @@ impl ContactObject {
let verifier_id = contact
.get_verifier_id(context)
.await?
.flatten()
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
@@ -106,7 +80,6 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,

View File

@@ -224,6 +224,7 @@ pub enum EventType {
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///

View File

@@ -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 },
@@ -59,13 +59,6 @@ pub struct MessageObject {
// summary - use/create another function if you need it
subject: String,
/// True if the message was correctly encrypted&signed, false otherwise.
/// Historically, UIs showed a small padlock on the message then.
///
/// Today, the UIs should instead show a small email-icon on the message
/// if `show_padlock` is `false`,
/// and nothing if it is `true`.
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
@@ -97,6 +90,8 @@ pub struct MessageObject {
file_bytes: u64,
file_name: Option<String>,
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
@@ -147,6 +142,12 @@ 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();
@@ -260,6 +261,7 @@ 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
@@ -416,9 +418,6 @@ pub enum SystemMessageType {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat is e2ee
ChatE2ee,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
@@ -453,7 +452,6 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,

View File

@@ -2,24 +2,24 @@
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^5.0.0",
"isomorphic-ws": "^4.0.1",
"yerpc": "^0.6.2"
},
"devDependencies": {
"@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",
"@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",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.25.5",
"esbuild": "^0.17.9",
"http-server": "^14.1.1",
"mocha": "^10.2.0",
"mocha": "^9.1.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"ws": "^8.5.0"
},
"exports": {
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.10.0"
"version": "1.159.3"
}

View File

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

View File

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

View File

@@ -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,10 +34,7 @@ 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) {
@@ -56,7 +53,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);
}
@@ -86,10 +83,7 @@ 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 = "";

View File

@@ -1,3 +1,4 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
@@ -31,14 +32,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);
});
@@ -84,7 +85,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;
@@ -126,7 +127,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);
});
@@ -138,7 +139,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);
});
@@ -152,7 +153,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);
});

View File

@@ -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,13 +92,11 @@ describe("online tests", function () {
accountId2,
chatIdOnAccountB,
false,
false,
false
);
// There are 2 messages in the chat:
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
expect(messageList).have.length(2);
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
expect(message.showPadlock).equal(true);
});
@@ -126,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
@@ -152,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);
});
@@ -169,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) {

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.10.0"
version = "1.159.3"
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 = "16"
rustyline = "15"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }

View File

@@ -20,6 +20,7 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
@@ -34,6 +35,14 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", ())
.await
.unwrap();
println!("(2) Peerstates reset.");
}
if 0 != bits & 4 {
context
.sql()
@@ -87,7 +96,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
eprintln!("receive_imf errored: {err:?}");
println!("receive_imf errored: {err:?}");
}
Ok(())
}
@@ -111,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() {
eprintln!("Import: No file or folder given.");
error!(context, "Import: No file or folder given.");
return false;
}
real_spec = rs.unwrap();
@@ -140,7 +149,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
} else {
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
@@ -268,7 +277,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
for contact_id in contacts {
let line2 = "".to_string();
let mut line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
@@ -287,6 +296,15 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
);
let peerstate = Peerstate::from_addr(context, addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != ContactId::SELF {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
}
println!("Contact#{}: {}{}", *contact_id, line, line2);
}
@@ -324,7 +342,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
send-backup\n\
receive-backup <qr>\n\
export-keys\n\
import-keys <key-file>\n\
import-keys\n\
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -333,6 +351,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
_ => println!(
"==========================Database commands==\n\
info\n\
open <file to open or create>\n\
close\n\
set <configuration-key> [<value>]\n\
get <configuration-key>\n\
oauth2\n\
@@ -347,24 +367,21 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
==============================Chat commands==\n\
listchats [<query>]\n\
listarchived\n\
start-realtime <msg-id>\n\
send-realtime <msg-id> <data>\n\
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast <name>\n\
createbroadcast\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
groupimage <image>\n\
groupimage [<file>]\n\
chatinfo\n\
sendlocations <seconds>\n\
setlocation <lat> <lng>\n\
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
@@ -383,7 +400,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unmute <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
blockchat <chat-id>\n\
decline <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -397,14 +414,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
addcontact [<name>] <addr>\n\
contactinfo <contact-id>\n\
delcontact <contact-id>\n\
cleanupcontacts\n\
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
import-vcard <file>\n\
make-vcard <file> <contact-id> [contact-id ...]\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
@@ -476,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?;
}
@@ -491,17 +508,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
ensure!(
!arg1.is_empty(),
"Argument <bits> missing: 4=private keys, 8=rest but server config"
);
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16.");
reset_tables(&context, bits).await;
@@ -623,7 +636,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{cnt} chats");
eprintln!("{time_needed:?} to create this list");
println!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
@@ -733,7 +746,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
eprintln!(
println!(
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
);
}
@@ -752,8 +765,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Group#{chat_id} created successfully.");
}
"createbroadcast" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
let chat_id = chat::create_broadcast_list(&context).await?;
println!("Broadcast#{chat_id} created successfully.");
}
@@ -987,7 +999,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
query,
);
eprintln!("{time_needed:?} to create this list");
println!("{time_needed:?} to create this list");
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1150,13 +1162,19 @@ 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" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).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?;
log_contactlist(&context, &contacts).await?;
println!("{} key contacts.", contacts.len());
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
log_contactlist(&context, &addrcontacts).await?;
println!("{} address contacts.", addrcontacts.len());
println!("{} contacts.", contacts.len());
}
"addcontact" => {
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
@@ -1220,24 +1238,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"import-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
let contacts = import_vcard(&context, &vcard_content).await?;
println!("vCard contacts imported:");
log_contactlist(&context, &contacts).await?;
}
"make-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
let mut contact_ids = vec![];
for x in arg2.split_whitespace() {
contact_ids.push(ContactId::new(x.parse()?))
}
let vcard_content = make_vcard(&context, &contact_ids).await?;
fs::write(&arg1.to_string(), vcard_content).await?;
println!("vCard written to: {arg1}");
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;
@@ -1247,7 +1247,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {

View File

@@ -5,6 +5,7 @@
//! 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};
@@ -40,25 +41,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!(
@@ -123,7 +124,7 @@ fn receive_event(event: EventType) {
);
}
_ => {
info!("Received {event:?}");
info!("Received {:?}", event);
}
}
}
@@ -179,11 +180,9 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 36] = [
"listchats",
"listarchived",
"start-realtime",
"send-realtime",
"chat",
"createchat",
"creategroup",
@@ -199,16 +198,13 @@ const CHAT_COMMANDS: [&str; 39] = [
"dellocations",
"getlocations",
"send",
"sendempty",
"sendimage",
"sendsticker",
"sendfile",
"sendhtml",
"sendsyncmsg",
"sendupdate",
"videochat",
"draft",
"devicemsg",
"listmedia",
"archive",
"unarchive",
@@ -216,48 +212,47 @@ const CHAT_COMMANDS: [&str; 39] = [
"unpin",
"mute",
"unmute",
"protect",
"unprotect",
"delchat",
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 10] = [
const MESSAGE_COMMANDS: [&str; 9] = [
"listmsgs",
"msginfo",
"download",
"html",
"listfresh",
"forward",
"resend",
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
"listverified",
"addcontact",
"contactinfo",
"delcontact",
"cleanupcontacts",
"block",
"unblock",
"listblocked",
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 14] = [
const MISC_COMMANDS: [&str; 12] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"setqr",
"createqrsvg",
"providerinfo",
"fileinfo",
"estimatedeletion",
"clear",
"exit",
"quit",
"help",
"estimatedeletion",
];
impl Hinter for DcHelper {
@@ -313,7 +308,7 @@ impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
eprintln!("Error: Bad arguments, expected [db-name].");
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
@@ -368,7 +363,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
eprintln!("Error: {err:#}");
println!("Error: {err:#}");
true
}
}
@@ -383,7 +378,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
eprintln!("Error: {err:#}");
println!("Error: {err:#}");
break;
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.10.0"
version = "1.159.3"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -66,9 +66,6 @@ lint.select = [
"RUF006" # asyncio-dangling-task
]
lint.ignore = [
"PLC0415" # `import` should be at the top-level of a file
]
line-length = 120
[tool.isort]

View File

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

View File

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

View File

@@ -1,5 +1,3 @@
"""Account module."""
from __future__ import annotations
from dataclasses import dataclass
@@ -36,10 +34,7 @@ class Account:
return next_event
def clear_all_events(self):
"""Remove all queued-up events for a given account.
Useful for tests.
"""
"""Removes all queued-up events for a given account. Useful for tests."""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
@@ -48,9 +43,7 @@ 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()
@@ -87,7 +80,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)
@@ -106,12 +99,10 @@ 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
@@ -126,7 +117,7 @@ class Account:
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""
"""Returns 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
@@ -167,8 +158,7 @@ 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]
@@ -185,21 +175,7 @@ class Account:
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
"""Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
@@ -225,8 +201,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.
@@ -234,9 +210,12 @@ 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
@@ -248,12 +227,12 @@ class Account:
@property
def self_contact(self) -> Contact:
"""Account's identity as a Contact."""
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Chat:
"""Account's device contact."""
"""This account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
def get_chatlist(
@@ -302,52 +281,17 @@ class Account:
def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation,
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
and is in _unpromoted_ state.
This means, you can add or remove members, change the name,
the group image and so on without messages being sent to all group members.
This changes as soon as the first message is sent to the group members
and the group becomes _promoted_.
After that, all changes are synced with all group members
by sending status message.
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
(see `get_full_snapshot()` / `get_basic_snapshot()`).
This may be useful if you want to show some help for just created groups.
:param protect: If set to 1 the function creates group with protection initially enabled.
Only verified members are allowed in these groups
and end-to-end-encryption is always enabled.
After creation, the group has only self-contact as member and is in unpromoted state.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
def create_broadcast(self, name: str) -> Chat:
"""Create a new **broadcast channel**
(called "Channel" in the UI).
Broadcast channels are similar to groups on the sending device,
however, recipients get the messages in a read-only chat
and will not see who the other members are.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
After creation, the chat contains no recipients and is in _unpromoted_ state;
see `create_group()` for more information on the unpromoted state.
Returns the created chat.
"""
return Chat(self, self._rpc.create_broadcast(self.id, name))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
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.
@@ -417,26 +361,22 @@ 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]:

View File

@@ -1,5 +1,3 @@
"""Chat module."""
from __future__ import annotations
import calendar
@@ -91,8 +89,7 @@ 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:
@@ -202,12 +199,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:

View File

@@ -48,7 +48,6 @@ 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)
@@ -78,11 +77,9 @@ 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():
@@ -201,6 +198,5 @@ 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)

View File

@@ -1,20 +1,14 @@
"""Constants module."""
from enum import Enum, IntEnum
COMMAND_PREFIX = "/"
class ContactFlag(IntEnum):
"""Bit flags for get_contacts() method."""
VERIFIED_ONLY = 0x01
ADD_SELF = 0x02
ADDRESS = 0x04
class ChatlistFlag(IntEnum):
"""Bit flags for get_chatlist() method."""
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
@@ -22,8 +16,6 @@ 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
@@ -31,7 +23,7 @@ class SpecialContactId(IntEnum):
class EventType(str, Enum):
"""Core event types."""
"""Core event types"""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
@@ -79,7 +71,7 @@ class EventType(str, Enum):
class ChatId(IntEnum):
"""Special chat IDs."""
"""Special chat ids"""
TRASH = 3
ARCHIVED_LINK = 6
@@ -88,47 +80,17 @@ class ChatId(IntEnum):
class ChatType(IntEnum):
"""Chat type."""
"""Chat types"""
UNDEFINED = 0
SINGLE = 100
"""1:1 chat, i.e. a direct chat with a single contact"""
GROUP = 120
MAILINGLIST = 140
OUT_BROADCAST = 160
"""Outgoing broadcast channel, called "Channel" in the UI.
The user can send into this channel,
and all recipients will receive messages
in an `IN_BROADCAST`.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
"""
IN_BROADCAST = 165
"""Incoming broadcast channel, called "Channel" in the UI.
This channel is read-only,
and we do not know who the other recipients are.
This is similar to a `MAILINGLIST`,
with the main difference being that
`IN_BROADCAST`s are encrypted.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
"""
BROADCAST = 160
class ChatVisibility(str, Enum):
"""Chat visibility types."""
"""Chat visibility types"""
NORMAL = "Normal"
ARCHIVED = "Archived"
@@ -136,7 +98,7 @@ class ChatVisibility(str, Enum):
class DownloadState(str, Enum):
"""Message download state."""
"""Message download state"""
DONE = "Done"
AVAILABLE = "Available"
@@ -197,14 +159,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
@@ -212,7 +174,7 @@ class CertificateChecks(IntEnum):
class Connectivity(IntEnum):
"""Connectivity states."""
"""Connectivity states"""
NOT_CONNECTED = 1000
CONNECTING = 2000
@@ -221,7 +183,7 @@ class Connectivity(IntEnum):
class KeyGenType(IntEnum):
"""Type of the key to generate."""
"""Type of the key to generate"""
DEFAULT = 0
RSA2048 = 1
@@ -231,21 +193,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
@@ -253,7 +215,7 @@ class ProviderStatus(IntEnum):
class PushNotifyState(IntEnum):
"""Push notifications state."""
"""Push notifications state"""
NOT_CONNECTED = 0
HEARTBEAT = 1
@@ -261,7 +223,7 @@ class PushNotifyState(IntEnum):
class ShowEmails(IntEnum):
"""Show emails mode."""
"""Show emails mode"""
OFF = 0
ACCEPTED_CONTACTS = 1
@@ -269,7 +231,7 @@ class ShowEmails(IntEnum):
class SocketSecurity(IntEnum):
"""Socket security."""
"""Socket security"""
AUTOMATIC = 0
SSL = 1
@@ -278,7 +240,7 @@ class SocketSecurity(IntEnum):
class VideochatType(IntEnum):
"""Video chat URL type."""
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1

View File

@@ -1,5 +1,3 @@
"""Contact module."""
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -13,7 +11,8 @@ if TYPE_CHECKING:
@dataclass
class Contact:
"""Contact API.
"""
Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
@@ -37,14 +36,17 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str:
"""Get a multi-line encryption info.
Encryption info contains your fingerprint and the fingerprint of the contact.
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
@@ -64,5 +66,4 @@ class Contact:
)
def make_vcard(self) -> str:
"""Make a vCard for the contact."""
return self.account.make_vcard([self])

View File

@@ -1,5 +1,3 @@
"""Account manager module."""
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -12,13 +10,12 @@ 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:
@@ -40,7 +37,9 @@ class DeltaChat:
self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None:
"""Indicate that the network conditions might have changed."""
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
self.rpc.maybe_network()
def get_system_info(self) -> AttrDict:

View File

@@ -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,7 +52,9 @@ class EventFilter(ABC):
@abstractmethod
def filter(self, event):
"""Return True-like value if the event passed the filter."""
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
class RawEvent(EventFilter):
@@ -80,17 +82,31 @@ 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."""
"""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.
"""
def __init__(
self,
@@ -105,25 +121,6 @@ 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
@@ -162,7 +159,6 @@ 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:
@@ -203,7 +199,6 @@ 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)
@@ -236,7 +231,6 @@ 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)
@@ -262,12 +256,13 @@ 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()

View File

@@ -1,8 +1,6 @@
"""Message module."""
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Union
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict, futuremethod
from .const import EventType
@@ -39,11 +37,6 @@ class Message:
snapshot["message"] = self
return snapshot
def get_read_receipts(self) -> List[AttrDict]:
"""Get message read receipts."""
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
return [AttrDict(read_receipt) for read_receipt in read_receipts]
def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
@@ -52,7 +45,6 @@ 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)
@@ -61,11 +53,6 @@ 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:
@@ -75,7 +62,6 @@ 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:
@@ -83,7 +69,6 @@ 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:
@@ -95,10 +80,8 @@ 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))

View File

@@ -1,5 +1,3 @@
"""Pytest plugin module."""
from __future__ import annotations
import os
@@ -13,38 +11,26 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
from ._utils import futuremethod
from .rpc import Rpc
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
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}
@@ -54,7 +40,6 @@ 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)
@@ -62,13 +47,11 @@ 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]
@@ -83,10 +66,6 @@ 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)
@@ -98,7 +77,6 @@ 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)
@@ -117,7 +95,6 @@ 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,
@@ -131,7 +108,6 @@ 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
@@ -139,7 +115,6 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
"""Return account factory fixture."""
return ACFactory(DeltaChat(rpc))
@@ -157,7 +132,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

View File

@@ -1,5 +1,3 @@
"""JSON-RPC client module."""
from __future__ import annotations
import itertools
@@ -14,19 +12,16 @@ from typing import Any, Iterator, Optional
class JsonRpcError(Exception):
"""JSON-RPC error."""
pass
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:
@@ -37,19 +32,17 @@ class RpcFuture:
class RpcMethod:
"""RPC method."""
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Call JSON-RPC method synchronously."""
"""Synchronously calls JSON-RPC method."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Call JSON-RPC method asynchronously."""
"""Asynchronously calls JSON-RPC method."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
@@ -65,13 +58,8 @@ class RpcMethod:
class Rpc:
"""RPC client."""
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""Initialize RPC client.
The given arguments will be passed to subprocess.Popen().
"""
"""The given arguments will be passed to subprocess.Popen()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -93,7 +81,6 @@ 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",
@@ -143,7 +130,6 @@ 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)
@@ -171,13 +157,12 @@ 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:
"""Request new events and distributes them between queues."""
"""Requests new events and distributes them between queues."""
try:
while True:
if self.closing:
@@ -193,12 +178,12 @@ class Rpc:
logging.exception("Exception in the event loop")
def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Wait for the next event from the given account and returns it."""
"""Waits 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):
"""Remove all queued-up events for a given account. Useful for tests."""
"""Removes all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:

View File

@@ -36,9 +36,6 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text

View File

@@ -1,4 +1,5 @@
import logging
import time
import pytest
@@ -15,14 +16,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -83,7 +84,7 @@ def test_qr_securejoin(acfactory, protect):
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
@@ -92,7 +93,7 @@ def test_qr_securejoin(acfactory, protect):
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -100,7 +101,7 @@ def test_qr_securejoin(acfactory, protect):
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
@@ -212,8 +213,8 @@ def test_setup_contact_resetup(acfactory) -> None:
bob.wait_for_securejoin_joiner_success()
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying then removing and adding a member back."""
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -226,7 +227,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
@@ -234,8 +235,6 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
ac3_contact_ac2_old = ac3.create_contact(ac2)
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -248,10 +247,85 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hi!"
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Hi!"
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert ac1_contact.get_snapshot().is_verified
# ac2 can write messages to the group.
snapshot.chat.send_text("Works again!")
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.create_contact(ac2)
ac3_chat.remove_contact(ac3_contact_ac2_old)
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
@@ -280,16 +354,19 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/chatmail/core/issues/4894>.
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
@@ -323,12 +400,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.create_contact(ac2).get_snapshot().is_verified
assert ac2.create_contact(ac3).get_snapshot().is_verified
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.create_contact(ac2))
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
# ensure ac2 receives message in VG
vg.send_text("hello")
@@ -366,7 +443,7 @@ def test_qr_new_group_unblocked(acfactory):
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.create_contact(ac2))
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
@@ -500,8 +577,30 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# Loop sending message from ac1 to ac2
# until ac2 accepts new ac1 key.
#
# This may not happen immediately because resetup of ac1
# rewinds "smeared timestamp" so Date: header for messages
# sent by new ac1 are in the past compared to the last Date:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2, "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
logging.info("ac2 received Hello!")
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
if not ac2_contact_ac1.get_snapshot().is_verified:
break
time.sleep(1)
# ac1 goes offline.
ac1.remove()

View File

@@ -11,8 +11,7 @@ from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.rpc import JsonRpcError
@@ -171,10 +170,7 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
# get_contact_by_addr() can lookup a key contact by address:
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
assert bob_contact.display_name == "Bob"
assert bob_contact.is_key_contact
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact
@@ -291,6 +287,7 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
@@ -461,12 +458,8 @@ def test_wait_next_messages(acfactory) -> None:
alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
if len(next_messages) == E2EE_INFO_MSGS:
next_messages += bot.wait_next_messages()
assert len(next_messages) == 1 + E2EE_INFO_MSGS
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
@@ -734,26 +727,6 @@ def test_markseen_contact_request(acfactory):
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acfactory):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
"""
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_contact_bob = alice.create_contact(bob)
bob.create_chat(alice) # Accept the chat
alice_chat_bob.send_text("Hello Bob!")
msg = bob.wait_for_incoming_msg()
msg.mark_seen()
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
read_receipts = read_msg.get_read_receipts()
assert len(read_receipts) == 1
assert read_receipts[0].contact_id == alice_contact_bob.id
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
@@ -874,36 +847,3 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()
def test_broadcast(acfactory):
alice, bob = acfactory.get_online_accounts(2)
alice_chat = alice.create_broadcast("My great channel")
snapshot = alice_chat.get_basic_snapshot()
assert snapshot.name == "My great channel"
assert snapshot.is_unpromoted
assert snapshot.is_encrypted
assert snapshot.chat_type == ChatType.OUT_BROADCAST
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat.add_contact(alice_contact_bob)
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
assert alice_msg.text == "hello"
assert alice_msg.show_padlock
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.text == "hello"
assert bob_msg.show_padlock
assert bob_msg.error is None
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
bob_chat_snapshot = bob_chat.get_basic_snapshot()
assert bob_chat_snapshot.name == "My great channel"
assert not bob_chat_snapshot.is_unpromoted
assert bob_chat_snapshot.is_encrypted
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert bob_chat_snapshot.is_contact_request
assert not bob_chat.can_send()

View File

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

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.10.0"
"version": "1.159.3"
}

View File

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

View File

@@ -10,6 +10,9 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# Unmaintained backoff
"RUSTSEC-2025-0012",
# Unmaintained paste
"RUSTSEC-2024-0436",
]
@@ -22,33 +25,31 @@ ignore = [
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "core-foundation", version = "0.9.4" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "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 = "lru", version = "0.12.3" },
{ name = "loom", version = "0.5.6" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nom", version = "7.1.3" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ 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 = "serdect", version = "0.2.0" },
{ name = "spin", version = "0.9.8" },
{ name = "security-framework", version = "2.11.1" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
@@ -83,7 +84,6 @@ allow = [
"MPL-2.0",
"Unicode-3.0",
"Unicode-DFS-2016",
"Unlicense",
"Zlib",
]

24
flake.lock generated
View File

@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1747291057,
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"lastModified": 1737527504,
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
"owner": "nix-community",
"repo": "fenix",
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"lastModified": 1737469691,
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"type": "github"
},
"original": {
@@ -175,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1746889290,
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"lastModified": 1737453499,
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
"type": "github"
},
"original": {

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.13.4"
bolero = "0.8"
[dependencies]
mailparse = { workspace = true }

View File

@@ -0,0 +1,49 @@
# content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline
class GroupTrackingPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.create_chat()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text(f"echoing from {addr}:\n{text}")
@account_hookimpl
def ac_outgoing_message(self, message):
print("ac_outgoing_message:", message)
@account_hookimpl
def ac_configure_completed(self, success):
print("ac_configure_completed:", success)
@account_hookimpl
def ac_chat_modified(self, chat):
print("ac_chat_modified:", chat.id, chat.get_name())
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,10 @@
import echo_and_quit
import group_tracking
import py
import pytest
from deltachat.events import FFIEventLogger
@pytest.fixture(scope="session")
def datadir():
@@ -33,3 +36,55 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("send quit sequence")
bot_chat.send_text("/quit")
botproc.wait()
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
lp.sec("creating bot test group with bot")
bot_chat = ac1.qr_setup_contact(botproc.qr)
ac1._evtracker.wait_securejoin_joiner_progress(1000)
bot_contact = bot_chat.get_contacts()[0]
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
botproc.fnmatch_lines(
"""
*ac_chat_modified*bot test group*
""",
)
lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2)
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines(
"""
*ac_member_added {}*from*{}*
""".format(
contact3.addr,
ac1.get_config("addr"),
),
)
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines(
"""
*ac_member_removed {}*from*{}*
""".format(
contact3.addr,
ac1.get_config("addr"),
),
)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.10.0"
version = "1.159.3"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
@@ -47,10 +47,6 @@ 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]

View File

@@ -293,8 +293,6 @@ class Account:
return Contact(self, contact_id)
def get_contact(self, obj) -> Optional[Contact]:
if isinstance(obj, Account):
return self.create_contact(obj)
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
@@ -330,21 +328,7 @@ class Account:
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
"""get a contact for the email address or None if it's blocked or doesn't exist."""
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
@@ -371,16 +355,20 @@ 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)

View File

@@ -417,13 +417,7 @@ class Chat:
:raises ValueError: if contact could not be added
:returns: None
"""
from .contact import Contact
if isinstance(obj, Contact):
contact = obj
else:
contact = self.account.create_contact(obj)
contact = self.account.create_contact(obj)
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError(f"could not add contact {contact!r} to chat")

View File

@@ -13,6 +13,7 @@ from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
@@ -303,15 +304,21 @@ class EventThread(threading.Thread):
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
yield ("ac_incoming_message", {"message": msg})
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
yield "ac_incoming_message", {"message": msg}
yield map_system_message(msg) or (
"ac_incoming_message",
{"message": msg},
)
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)

View File

@@ -2,6 +2,7 @@
import json
import os
import re
from datetime import datetime, timezone
from typing import Optional, Union
@@ -503,3 +504,56 @@ def get_viewtype_code_from_name(view_type_name):
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
)
#
# some helper code for turning system messages into hook events
#
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return None
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
return "ac_member_" + res[0], d
def extract_addr(text):
m = re.match(r".*\((.+@.+)\)", text)
if m:
text = m.group(1)
text = text.rstrip(".")
return text.strip()
def parse_system_add_remove(text):
"""return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple
"""
# You removed member a@b.
# You added member a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
# Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org.
text = text.lower()
m = re.match(r"member (.+) (removed|added) by (.+)", text)
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
m = re.match(r"you (removed|added) member (.+)", text)
if m:
action, affected = m.groups()
return action, extract_addr(affected), "me"
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:
return "removed", addr, addr

View File

@@ -20,12 +20,6 @@ import deltachat
from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options")
@@ -612,7 +606,7 @@ class ACFactory:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are end-to-end encrypted."
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text

View File

@@ -133,7 +133,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert "added" in msg.text.lower()
assert any(
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
for m in msg.chat.get_messages()
)
lp.sec("ac1: send message")
msg_out = chat1.send_text("hello")
@@ -186,6 +187,83 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
def test_undecipherable_group(acfactory, lp):
"""Test how group messages that cannot be decrypted are
handled.
Group name is encrypted and plaintext subject is set to "..." in
this case, so we should assign the messages to existing chat
instead of creating a new one. Since there is no existing group
chat, the messages should be assigned to 1-1 chat with the sender
of the message.
"""
lp.sec("creating and configuring three accounts")
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2, ac3])
lp.sec("ac3 reinstalls DC and generates a new key")
ac3.stop_io()
acfactory.remove_preconfigured_keys()
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
acfactory.wait_configured(ac4)
# Create contacts to make sure incoming messages are not treated as contact requests
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
ac4.start_io()
ac4._evtracker.wait_idle_inbox_ready()
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
lp.sec("ac2: checking that the chat arrived correctly")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.is_encrypted(), "Message is not encrypted"
# ac4 cannot decrypt the message.
# Error message should be assigned to the chat with ac1.
lp.sec("ac4: checking that message is assigned to the sender chat")
error_msg = ac4._evtracker.wait_next_incoming_message()
assert error_msg.error # There is an error decrypting the message
assert error_msg.chat == chat41
lp.sec("ac2: sending a reply to the chat")
msg.chat.send_text("reply")
reply = ac1._evtracker.wait_next_incoming_message()
assert reply.text == "reply"
assert reply.is_encrypted(), "Reply is not encrypted"
lp.sec("ac4: checking that reply is assigned to ac2 chat")
error_reply = ac4._evtracker.wait_next_incoming_message()
assert error_reply.error # There is an error decrypting the message
assert error_reply.chat == chat42
# Test that ac4 replies to error messages don't appear in the
# group chat on ac1 and ac2.
lp.sec("ac4: replying to ac1 and ac2")
# Otherwise reply becomes a contact request.
chat41.send_text("I can't decrypt your message, ac1!")
chat42.send_text("I can't decrypt your message, ac2!")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac1!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac1.create_chat(ac3)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac2!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac2.create_chat(ac4)
def test_ephemeral_timer(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -337,7 +415,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
@@ -356,10 +434,9 @@ 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 an unverified group with unverified members.
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the second device has all members and group verified.
- Now the seconds device has all members verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.remove_preconfigured_keys()
@@ -397,12 +474,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 == "hi"
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
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 not chat2_offl.is_protected()
assert chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
@@ -411,7 +488,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are end-to-end encrypted."
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
@@ -423,7 +500,6 @@ 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()

View File

@@ -10,7 +10,6 @@ from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
def test_basic_imap_api(acfactory, tmp_path):
@@ -409,10 +408,6 @@ def test_forward_messages(acfactory, lp):
msg_out = chat.send_text("message2")
lp.sec("ac2: wait for receive")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -627,11 +622,6 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
@@ -748,7 +738,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(chat.get_messages()) == 1
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
@@ -756,7 +746,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("wait for ac2 to receive message")
msg = ac2._evtracker.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(msg.chat.get_messages()) == 1
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
@@ -765,7 +755,7 @@ def test_mdn_asymmetric(acfactory, lp):
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
assert len(chat.get_messages()) == 1
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
@@ -777,7 +767,7 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_receive_encrypt(acfactory, lp):
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.get_device_chat().mark_noticed()
@@ -808,11 +798,12 @@ def test_send_receive_encrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
lp.sec("create group chat with two members")
lp.sec("create group chat with two members, one of which has no encrypt state")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
msg = chat.send_text("test not encrypt")
assert msg.is_encrypted()
assert not msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
@@ -1133,11 +1124,6 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_out
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -1153,9 +1139,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
lp.sec("create some chat content")
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1).create_chat()
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts()) == 1
assert len(ac1.get_contacts(query="some1")) == 1
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
@@ -1167,16 +1153,16 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts()
contacts = ac.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
assert len(messages) == 3
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
@@ -1302,6 +1288,79 @@ def test_set_get_contact_avatar(acfactory, data, lp):
assert msg6.get_sender_contact().get_profile_image() is None
def test_add_remove_member_remote_events(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1_addr = ac1.get_config("addr")
ac3_addr = ac3.get_config("addr")
# activate local plugin for ac2
in_list = queue.Queue()
class EventHolder:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
class InPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
# we immediately accept the sender because
# otherwise we won't see member_added contacts
message.create_chat()
@account_hookimpl
def ac_chat_modified(self, chat):
in_list.put(EventHolder(action="chat-modified", chat=chat))
@account_hookimpl
def ac_member_added(self, chat, contact, message):
in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message))
@account_hookimpl
def ac_member_removed(self, chat, contact, message):
in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message))
ac2.add_account_plugin(InPlugin())
lp.sec("ac1: create group chat with ac2")
chat = ac1.create_group_chat("hello", contacts=[ac2])
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get()
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
lp.sec("ac1: add address2")
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact(ac3)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == ac3_addr
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "removed"
assert ev.contact.addr == contact2.addr
assert ev.message.get_sender_contact().addr == ac1_addr
lp.sec("ac1: remove ac2 contact from chat")
chat.remove_contact(ac2)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
def test_system_group_msg_from_blocked_user(acfactory, lp):
"""
Tests that a blocked user removes you from a group.
@@ -1429,8 +1488,8 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1 + E2EE_INFO_MSGS
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
assert len(msgs) == 1
assert msgs[0].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
@@ -1440,8 +1499,8 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2 + E2EE_INFO_MSGS
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
def test_fetch_deleted_msg(acfactory, lp):
@@ -1558,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
@@ -1594,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
@@ -1701,6 +1760,44 @@ def test_configure_error_msgs_invalid_server(acfactory):
assert "configuration" not in ev.data2.lower()
def test_name_changes(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("displayname", "Account 1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
contact = None
def update_name():
"""Send a message from ac1 to ac2 to update the name"""
nonlocal contact
chat12.send_text("Hello")
msg = ac2._evtracker.wait_next_incoming_message()
contact = msg.get_sender_contact()
return contact.name
assert update_name() == "Account 1"
ac1.set_config("displayname", "Account 1 revision 2")
assert update_name() == "Account 1 revision 2"
# Explicitly rename contact on ac2 to "Renamed"
ac2.create_contact(contact, name="Renamed")
assert contact.name == "Renamed"
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
# ac1 also renames itself into "Renamed"
assert update_name() == "Renamed"
ac1.set_config("displayname", "Renamed")
assert update_name() == "Renamed"
# Contact name was set to "Renamed" explicitly before,
# so it should not be changed.
ac1.set_config("displayname", "Renamed again")
updated_name = update_name()
assert updated_name == "Renamed"
def test_status(acfactory):
"""Test that status is transferred over the network."""
ac1, ac2 = acfactory.get_online_accounts(2)

View File

@@ -1,12 +1,51 @@
import os
import time
from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat.tracker import ImexFailed
from deltachat import Account, Message
from deltachat.testplugin import E2EE_INFO_MSGS
from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
("msgtext", "res"),
[
(
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by me",
("removed", "tmp1@x.org", "me"),
),
(
"Group left by some one (tmp1@x.org).",
("removed", "tmp1@x.org", "tmp1@x.org"),
),
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
(
"Member tmp1@x.org added by tmp2@x.org.",
("added", "tmp1@x.org", "tmp2@x.org"),
),
("Member nothing bla bla", None),
("Another unknown system message", None),
],
)
def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove
out = parse_system_add_remove(msgtext)
assert out == res
class TestOfflineAccountBasic:
@@ -138,15 +177,15 @@ class TestOfflineContact:
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac2)
contact1 = ac1.create_contact("some1@example.org", name="some1")
contacts = ac1.get_contacts()
assert len(contacts) == 1
assert contact1 in contacts
assert not ac1.get_contacts(query="some2")
assert not ac1.get_contacts(query="some1")
assert ac1.get_contacts(query="some1")
assert not ac1.get_contacts(only_verified=True)
assert len(ac1.get_contacts(with_self=True)) == 2
assert contact1 in ac1.get_contacts()
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
@@ -161,9 +200,9 @@ class TestOfflineContact:
def test_create_chat_flexibility(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat1 = ac1.create_chat(ac2) # This creates a key-contact chat
chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat
assert chat1 != chat2
chat1 = ac1.create_chat(ac2)
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
assert chat1 == chat2
ac3 = acfactory.get_unconfigured_account()
with pytest.raises(ValueError):
ac1.create_chat(ac3)
@@ -221,18 +260,17 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
with pytest.raises(ValueError):
chat.add_contact(ac2.get_self_contact())
contact = chat.add_contact(ac2)
assert contact.addr == ac2.get_config("addr")
assert contact.name == ac2.get_config("displayname")
assert contact.account == ac1
chat.remove_contact(ac2)
def test_group_chat_creation(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
ac3 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac2)
contact2 = ac1.create_contact(ac3)
def test_group_chat_creation(self, ac1):
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
@@ -279,14 +317,13 @@ class TestOfflineChat:
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_removing_blocked_user_from_group(self, ac1, acfactory, lp):
def test_removing_blocked_user_from_group(self, ac1, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
ac2 = acfactory.get_pseudo_configured_account()
contact = ac1.create_contact(ac2)
contact = ac1.create_contact("some1@example.org")
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
@@ -298,6 +335,10 @@ class TestOfflineChat:
group.remove_contact(contact)
assert contact.is_blocked()
lp.sec("ac1 adding blocked contact unblocks it")
group.add_contact(contact)
assert not contact.is_blocked()
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
@@ -440,8 +481,7 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac_contact).create_chat()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -456,15 +496,15 @@ class TestOfflineChat:
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("addr")
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1"
@@ -472,9 +512,8 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac2).create_chat()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -495,15 +534,15 @@ class TestOfflineChat:
ac2.import_all(path)
# check data integrity
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
contact2_addr = contact2.addr
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
ac2.shutdown()
@@ -512,15 +551,15 @@ class TestOfflineChat:
ac2.open(passphrase2)
# check data integrity
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == contact2_addr
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase"
@@ -528,9 +567,8 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac_contact).create_chat()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -552,15 +590,15 @@ class TestOfflineChat:
ac2.import_all(path, passphrase)
# check data integrity
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("addr")
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
"""
@@ -574,8 +612,7 @@ class TestOfflineChat:
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac_contact).create_chat()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -598,15 +635,15 @@ class TestOfflineChat:
ac2.import_all(path, bak_passphrase)
# check data integrity
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("addr")
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
ac2.shutdown()
@@ -615,15 +652,15 @@ class TestOfflineChat:
ac2.open(acct_passphrase)
# check data integrity
contacts = ac2.get_contacts()
contacts = ac2.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == ac_contact.get_config("addr")
assert contact2.addr == "some1@example.org"
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
def test_set_get_draft(self, chat1):
msg1 = Message.new_empty(chat1.account, "text")
@@ -645,10 +682,78 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_audit_log_view_without_daymarker(self, acfactory, lp):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
# promote chat
chat.send_text("hello")
assert chat.is_promoted()
# activate local plugin
in_list = []
class InPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor):
in_list.append(("added", chat, contact, actor))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor):
in_list.append(("removed", chat, contact, actor))
ac1.add_account_plugin(InPlugin())
# perform add contact many times
contacts = []
for i in range(10):
lp.sec("create contact")
contact = ac1.create_contact(f"some{i}@example.org")
contacts.append(contact)
lp.sec("add contact")
chat.add_contact(contact)
assert chat.num_contacts() == 11
# let's make sure the events perform plugin hooks
def wait_events(cond):
now = time.time()
while time.time() < now + 5:
if cond():
break
time.sleep(0.1)
else:
pytest.fail("failed to get events")
wait_events(lambda: len(in_list) == 10)
assert len(in_list) == 10
chat_contacts = chat.get_contacts()
for in_cmd, in_chat, in_contact, in_actor in in_list:
assert in_cmd == "added"
assert in_chat == chat
assert in_contact in chat_contacts
assert in_actor is None
chat_contacts.remove(in_contact)
assert chat_contacts[0].id == 1 # self contact
in_list[:] = []
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert chat.num_contacts() == 9
wait_events(lambda: len(in_list) == 2)
assert len(in_list) == 2
assert in_list[0][0] == "removed"
assert in_list[0][1] == chat
assert in_list[0][2] == contacts[9]
assert in_list[1][0] == "removed"
assert in_list[1][1] == chat
assert in_list[1][2] == contacts[3]
def test_audit_log_view_without_daymarker(self, ac1, lp):
lp.sec("ac1: test audit log (show only system messages)")
chat = ac1.create_group_chat(name="audit log sample data")
@@ -657,7 +762,7 @@ class TestOfflineChat:
assert chat.is_promoted()
lp.sec("create test data")
chat.add_contact(ac2)
chat.add_contact(ac1.create_contact("some-1@example.org"))
chat.set_name("audit log test group")
chat.send_text("a message in between")

View File

@@ -1 +1 @@
2025-08-04
2025-04-24

View File

@@ -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.88.0
RUST_VERSION=1.86.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -15,7 +15,7 @@ cd "$TMP"
git checkout "$REV"
DATE=$(git show -s --format=%cs)
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
rustfmt "$CORE_ROOT/src/provider/data.rs"
rm -fr "$TMP"
cd "$CORE_ROOT"

View File

@@ -1,24 +1,25 @@
//! # Account manager module.
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail, ensure};
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, JoinSet};
use tokio::task::JoinHandle;
use uuid::Uuid;
#[cfg(not(target_os = "ios"))]
use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{Duration, sleep};
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;
@@ -53,14 +54,6 @@ impl Accounts {
Accounts::open(dir, writable).await
}
/// Get the ID used to log events.
///
/// Account manager logs events with ID 0
/// which is not used by any accounts.
fn get_id(&self) -> u32 {
0
}
/// Creates a new default structure.
async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
@@ -270,51 +263,9 @@ impl Accounts {
}
}
/// Gets a list of all account ids in the user-configured order.
/// Get a list of all account ids.
pub fn get_all(&self) -> Vec<u32> {
let mut ordered_ids = Vec::new();
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// First, add accounts in the configured order
for &id in &self.config.inner.accounts_order {
if all_ids.remove(&id) {
ordered_ids.push(id);
}
}
// Then add any accounts not in the order list (newly added accounts)
for id in all_ids {
ordered_ids.push(id);
}
ordered_ids
}
/// Sets the order of accounts.
///
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// Filter out non-existent account IDs
let mut filtered_order: Vec<u32> = order
.into_iter()
.filter(|id| existing_ids.contains(id))
.collect();
// Add any missing account IDs at the end
for &id in &existing_ids {
if !filtered_order.contains(&id) {
filtered_order.push(id);
}
}
self.config.inner.accounts_order = filtered_order;
self.config.sync().await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
self.accounts.keys().copied().collect()
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
@@ -353,28 +304,24 @@ 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) {
let n_accounts = accounts.len();
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {n_accounts} accounts."
)),
});
let mut set = JoinSet::new();
for account in accounts {
set.spawn(async move {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
});
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
set.join_all().await;
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Finished background fetch for {n_accounts} accounts."
"Starting background fetch for {} 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() {}
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -408,10 +355,7 @@ impl Accounts {
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.
pub fn background_fetch(
&self,
timeout: std::time::Duration,
) -> impl Future<Output = ()> + use<> {
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
let events = self.events.clone();
Self::background_fetch_with_timeout(accounts, events, timeout)
@@ -463,10 +407,6 @@ struct InnerConfig {
pub selected_account: u32,
pub next_id: u32,
pub accounts: Vec<AccountConfig>,
/// Ordered list of account IDs, representing the user's preferred order.
/// If an account ID is not in this list, it will be appended at the end.
#[serde(default)]
pub accounts_order: Vec<u32>,
}
impl Drop for Config {
@@ -519,9 +459,7 @@ impl Config {
Ok(())
});
if locked_rx.await.is_err() {
bail!(
"Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"
);
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
};
Ok(Some(lock_task))
}
@@ -533,7 +471,6 @@ impl Config {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
accounts_order: Vec::new(),
};
if !lock {
let cfg = Self {
@@ -566,13 +503,11 @@ impl Config {
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
#[cfg(not(target_os = "ios"))]
ensure!(
!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished()
);
ensure!(!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished());
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
@@ -666,10 +601,6 @@ impl Config {
uuid,
});
self.inner.next_id += 1;
// Add new account to the end of the order list
self.inner.accounts_order.push(id);
id
};
@@ -691,10 +622,6 @@ impl Config {
// remove account from the configs
self.inner.accounts.remove(idx);
}
// Remove from order list as well
self.inner.accounts_order.retain(|&x| x != id);
if self.inner.selected_account == id {
// reset selected account
self.inner.selected_account = self
@@ -1038,11 +965,9 @@ mod tests {
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
assert!(
tokio::time::timeout(duration, event_emitter.recv())
.await
.is_err()
);
assert!(tokio::time::timeout(duration, event_emitter.recv())
.await
.is_err());
// When account manager is dropped, event emitter is exhausted.
drop(accounts);

View File

@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use anyhow::{Context as _, Error, Result, bail};
use anyhow::{bail, Context as _, Error, Result};
use crate::key::{DcKey, SignedPublicKey};
@@ -217,9 +217,7 @@ mod tests {
let rendered = ah.to_string();
assert_eq!(rendered, fixed_header);
let ah = Aheader::from_str(&format!(
" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"
))?;
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"))?;
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
@@ -242,44 +240,38 @@ mod tests {
#[test]
fn test_display_aheader() {
assert!(
format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
assert!(format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
.contains("prefer-encrypt=mutual;")
);
)
.contains("prefer-encrypt=mutual;"));
// According to Autocrypt Level 1 specification,
// only "prefer-encrypt=mutual;" can be used.
// If the setting is nopreference, the whole attribute is omitted.
assert!(
!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
assert!(!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
)
.contains("prefer-encrypt")
);
)
.contains("prefer-encrypt"));
// Always lowercase the address in the header.
assert!(
format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
assert!(format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
.contains("test@example.com")
);
)
.contains("test@example.com"));
}
}

View File

@@ -266,6 +266,7 @@ mod tests {
use super::*;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
@@ -519,6 +520,41 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_chat = alice.create_chat(&bob).await;
let bob_alice_chat = bob.create_chat(&alice).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -548,13 +584,12 @@ Authentication-Results: dkim=";
let rcvd = bob.recv_msg(&sent).await;
// The message info should contain a warning:
assert!(
rcvd.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false")
);
assert!(rcvd
.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false"));
Ok(())
}

View File

@@ -6,11 +6,11 @@ use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, ensure, format_err};
use anyhow::{ensure, format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::ImageReader;
use image::codecs::jpeg::JpegEncoder;
use image::ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::{fs, task};
@@ -20,8 +20,7 @@ use crate::config::Config;
use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, error, info, warn};
use crate::message::Viewtype;
use crate::log::LogExt;
use crate::tools::sanitize_filename;
/// Represents a file in the blob directory.
@@ -94,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}")
};
@@ -206,7 +205,11 @@ impl<'a> BlobObject<'a> {
/// to be lowercase.
pub fn suffix(&self) -> Option<&str> {
let ext = self.name.rsplit('.').next();
if ext == Some(&self.name) { None } else { ext }
if ext == Some(&self.name) {
None
} else {
ext
}
}
/// Checks whether a name is a valid blob name.
@@ -264,30 +267,32 @@ impl<'a> BlobObject<'a> {
}
};
let viewtype = &mut Viewtype::Image;
let maybe_sticker = &mut false;
let is_avatar = true;
self.check_or_recode_to_size(
context, None, // The name of an avatar doesn't matter
viewtype, img_wh, max_bytes, is_avatar,
self.recode_to_size(
context,
None, // The name of an avatar doesn't matter
maybe_sticker,
img_wh,
max_bytes,
is_avatar,
)?;
Ok(())
}
/// Checks or recodes an image pointed by the [BlobObject] so that it fits into limits on the
/// image width, height and file size specified by the config.
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
/// height and file size specified by the config.
///
/// Recoding is only done for [`Viewtype::Image`]. For [`Viewtype::File`], if it's a correct
/// image, `*viewtype` is set to [`Viewtype::Image`].
///
/// On some platforms images are passed to Core as [`Viewtype::Sticker`]. We recheck if the
/// image is a true sticker assuming that it must have at least one fully transparent corner,
/// otherwise `*viewtype` is set to [`Viewtype::Image`].
pub async fn check_or_recode_image(
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
/// reset.
pub async fn recode_to_image_size(
&mut self,
context: &Context,
name: Option<String>,
viewtype: &mut Viewtype,
maybe_sticker: &mut bool,
) -> Result<String> {
let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
@@ -300,10 +305,13 @@ impl<'a> BlobObject<'a> {
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
let is_avatar = false;
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
let new_name =
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
Ok(new_name)
}
/// Checks or recodes the image so that it fits into limits on width/height and byte size.
/// Recodes the image so that it fits into limits on width/height and byte size.
///
/// If `!is_avatar`, then if `max_bytes` is exceeded, reduces the image to `img_wh` and proceeds
/// with the result without rechecking.
@@ -314,11 +322,11 @@ impl<'a> BlobObject<'a> {
/// then the updated user-visible filename will be returned;
/// this may be necessary because the format may be changed to JPG,
/// i.e. "image.png" -> "image.jpg".
fn check_or_recode_to_size(
fn recode_to_size(
&mut self,
context: &Context,
name: Option<String>,
viewtype: &mut Viewtype,
maybe_sticker: &mut bool,
mut img_wh: u32,
max_bytes: usize,
is_avatar: bool,
@@ -329,7 +337,6 @@ impl<'a> BlobObject<'a> {
let no_exif_ref = &mut no_exif;
let mut name = name.unwrap_or_else(|| self.name.clone());
let original_name = name.clone();
let vt = &mut *viewtype;
let res: Result<String> = tokio::task::block_in_place(move || {
let mut file = std::fs::File::open(self.to_abs_path())?;
let (nr_bytes, exif) = image_metadata(&file)?;
@@ -348,28 +355,21 @@ impl<'a> BlobObject<'a> {
)
}
};
let fmt = imgreader.format().context("Unknown format")?;
if *vt == Viewtype::File {
*vt = Viewtype::Image;
return Ok(name);
}
let fmt = imgreader.format().context("No format??")?;
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
if *vt == Viewtype::Sticker {
if *maybe_sticker {
let x_max = img.width().saturating_sub(1);
let y_max = img.height().saturating_sub(1);
if !img.in_bounds(x_max, y_max)
|| !(img.get_pixel(0, 0).0[3] == 0
*maybe_sticker = img.in_bounds(x_max, y_max)
&& (img.get_pixel(0, 0).0[3] == 0
|| img.get_pixel(x_max, 0).0[3] == 0
|| img.get_pixel(0, y_max).0[3] == 0
|| img.get_pixel(x_max, y_max).0[3] == 0)
{
*vt = Viewtype::Image;
}
|| img.get_pixel(x_max, y_max).0[3] == 0);
}
if *vt == Viewtype::Sticker && exif.is_none() {
if *maybe_sticker && exif.is_none() {
return Ok(name);
}
@@ -504,11 +504,10 @@ impl<'a> BlobObject<'a> {
Ok(_) => res,
Err(err) => {
if !is_avatar && no_exif {
error!(
warn!(
context,
"Cannot check/recode image, using original data: {err:#}.",
"Cannot recode image, using original data: {err:#}.",
);
*viewtype = Viewtype::File;
Ok(original_name)
} else {
Err(err)

View File

@@ -4,7 +4,7 @@ use super::*;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::sql;
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
use crate::test_utils::{self, TestContext};
use crate::tools::SystemTime;
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -140,9 +140,9 @@ async fn test_add_white_bg() {
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
let img_wh = 128;
let viewtype = &mut Viewtype::Image;
let maybe_sticker = &mut false;
let strict_limits = true;
blob.check_or_recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
.unwrap();
tokio::task::block_in_place(move || {
let img = ImageReader::open(blob.to_abs_path())
@@ -188,9 +188,9 @@ async fn test_selfavatar_outside_blobdir() {
);
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
let viewtype = &mut Viewtype::Image;
let maybe_sticker = &mut false;
let strict_limits = true;
blob.check_or_recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
.unwrap();
let new_file_size = file_size(&blob.to_abs_path()).await;
assert!(new_file_size <= 3000);
@@ -241,8 +241,9 @@ async fn test_selfavatar_in_blobdir() {
async fn test_selfavatar_copy_without_recode() {
let t = TestContext::new().await;
let avatar_src = t.dir.path().join("avatar.png");
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
fs::write(&avatar_src, avatar_bytes).await.unwrap();
let avatar_blob = t.get_blobdir().join("e9b6c7a78aa2e4f415644f55a553e73.png");
assert!(!avatar_blob.exists());
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
.await
@@ -250,7 +251,7 @@ async fn test_selfavatar_copy_without_recode() {
assert!(avatar_blob.exists());
assert_eq!(
fs::metadata(&avatar_blob).await.unwrap().len(),
AVATAR_64x64_BYTES.len() as u64
avatar_bytes.len() as u64
);
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
@@ -399,7 +400,7 @@ async fn test_recode_image_balanced_png() {
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
media_quality_config: "0",
@@ -579,23 +580,20 @@ impl SendImageCheckMediaquality<'_> {
}
fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
assert!(
msg.param
.get(Param::File)
.unwrap()
.ends_with(&format!(".{extension}"))
);
assert!(
msg.param
.get(Param::Filename)
.unwrap()
.ends_with(&format!(".{extension}"))
);
assert!(
msg.get_filename()
.unwrap()
.ends_with(&format!(".{extension}"))
);
assert!(msg
.param
.get(Param::File)
.unwrap()
.ends_with(&format!(".{extension}")));
assert!(msg
.param
.get(Param::Filename)
.unwrap()
.ends_with(&format!(".{extension}")));
assert!(msg
.get_filename()
.unwrap()
.ends_with(&format!(".{extension}")));
assert_eq!(
msg.get_file(context)
.unwrap()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,15 @@
//! # Chat list module.
use anyhow::{Context as _, Result, ensure};
use anyhow::{ensure, Context as _, Result};
use std::sync::LazyLock;
use crate::chat::{Chat, ChatId, ChatVisibility, update_special_chat_names};
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
};
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;
@@ -245,6 +244,9 @@ impl Chatlist {
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c
@@ -320,7 +322,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 })
@@ -406,8 +408,7 @@ impl Chatlist {
if lastmsg.from_id == ContactId::SELF {
None
} else if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
|| chat.typ == Chattype::Broadcast
|| chat.typ == Chattype::Mailinglist
|| chat.is_self_talk()
{
@@ -481,8 +482,8 @@ mod tests {
use super::*;
use crate::chat::save_msgs;
use crate::chat::{
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
remove_contact_from_chat, send_text_msg,
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
send_text_msg, ProtectionStatus,
};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -570,7 +571,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sort_self_talk_up_on_forward() {
let t = TestContext::new_alice().await;
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
.await
@@ -578,23 +579,19 @@ mod tests {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 3);
assert!(
!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk()
);
assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk());
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert_eq!(chats.len(), 2); // device chat cannot be written and is skipped on forwarding
assert!(
Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk()
);
assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
.await
.unwrap()
.is_self_talk());
remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
.await
@@ -607,7 +604,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_special_chat_names() {
let t = TestContext::new_alice().await;
let t = TestContext::new().await;
t.update_device_chats().await.unwrap();
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)

View File

@@ -4,7 +4,7 @@ use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
use serde::{Deserialize, Serialize};
@@ -17,10 +17,10 @@ use crate::configure::EnteredLoginParam;
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, info};
use crate::log::LogExt;
use crate::login_param::ConfiguredLoginParam;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{Provider, get_provider_by_id};
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
@@ -369,9 +369,6 @@ pub enum Config {
#[strum(props(default = "0"))]
DisableIdle,
/// Timestamp of the next check for donation request need.
DonationRequestNextCheck,
/// Defines the max. size (in bytes) of messages downloaded automatically.
/// 0 = no limit.
#[strum(props(default = "0"))]
@@ -417,7 +414,7 @@ pub enum Config {
#[strum(props(default = "172800"))]
GossipPeriod,
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
/// Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
@@ -816,10 +813,7 @@ impl Context {
bail!("Cannot change ConfiguredAddr");
}
if let Some(addr) = value {
info!(
self,
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
);
info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?

View File

@@ -1,7 +1,7 @@
use num_traits::FromPrimitive;
use super::*;
use crate::test_utils::{TestContext, TestContextManager, sync};
use crate::test_utils::{sync, TestContext, TestContextManager};
#[test]
fn test_to_string() {
@@ -20,11 +20,10 @@ async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(
t.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok()
);
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
@@ -264,13 +263,11 @@ async fn test_sync() -> Result<()> {
self_chat_avatar_path,
alice0.get_blobdir().join(SAVED_MESSAGES_DEDUPLICATED_FILE)
);
assert!(
alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some()
);
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
alice0.set_config(Config::Selfavatar, None).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
@@ -324,21 +321,17 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
.await?;
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
alice1.recv_msg(&sent_msg).await;
assert!(
alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some()
);
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
sync(alice1, alice0).await;
assert!(
alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some()
);
assert!(alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some());
Ok(())
}

View File

@@ -13,21 +13,21 @@ mod auto_mozilla;
mod auto_outlook;
pub(crate) mod server_params;
use anyhow::{Context as _, Result, bail, ensure, format_err};
use anyhow::{bail, ensure, format_err, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::{EmailAddress, addr_normalize};
use deltachat_contact_tools::{addr_normalize, EmailAddress};
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::utf8_percent_encode;
use server_params::{ServerParams, expand_param_vector};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::{LogExt, info, warn};
use crate::log::LogExt;
pub use crate::login_param::EnteredLoginParam;
use crate::login_param::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
@@ -39,8 +39,8 @@ use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::{EventType, stock_str};
use crate::{chat, provider};
use crate::{stock_str, EventType};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
@@ -215,9 +215,7 @@ impl Context {
/// (i.e. [EnteredLoginParam::addr]).
#[expect(clippy::unused_async)]
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
bail!(
"Adding and removing additional transports is not supported yet. Check back in a few months!"
)
bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {

View File

@@ -9,7 +9,6 @@ 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};

View File

@@ -9,7 +9,6 @@ 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};

View File

@@ -88,8 +88,8 @@ 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;
pub const DC_GCL_ADDRESS: u32 = 0x04;
// unchanged user avatars are resent to the recipients every some days
pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
@@ -127,46 +127,17 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// A 1:1 chat, i.e. a normal chat with a single contact.
///
/// Created by [`ChatId::create_for_contact`].
/// 1:1 chat.
Single = 100,
/// Group chat.
///
/// Created by [`crate::chat::create_group_chat`].
Group = 120,
/// An (unencrypted) mailing list,
/// created by an incoming mailing list email.
/// Mailing list.
Mailinglist = 140,
/// Outgoing broadcast channel, called "Channel" in the UI.
///
/// The user can send into this chat,
/// and all recipients will receive messages
/// in an `InBroadcast`.
///
/// Called `broadcast` here rather than `channel`,
/// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it.
///
/// Created by [`crate::chat::create_broadcast`].
OutBroadcast = 160,
/// Incoming broadcast channel, called "Channel" in the UI.
///
/// This chat is read-only,
/// and we do not know who the other recipients are.
///
/// This is similar to a `MailingList`,
/// with the main difference being that
/// `InBroadcast`s are encrypted.
///
/// Called `broadcast` here rather than `channel`,
/// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it.
InBroadcast = 165,
/// Broadcast list.
Broadcast = 160,
}
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
@@ -223,10 +194,6 @@ pub(crate) const WORSE_AVATAR_BYTES: usize = 20_000; // this also fits to Outloo
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
/// Limit for received images size. Bigger images become `Viewtype::File` to avoid excessive memory
/// usage by UIs.
pub const MAX_RCVD_IMAGE_PIXELS: u32 = 50_000_000;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
@@ -244,6 +211,11 @@ pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60;
/// in the group membership consistency algo to reject outdated membership changes.
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
/// How long a 1:1 chat can't be used for sending while the SecureJoin is in progress. This should
/// be 10-20 seconds so that we are reasonably sure that the app remains active and receiving also
/// on mobile devices. See also [`crate::chat::CantSendReason::SecurejoinWait`].
pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
// To make text edits clearer for Non-Delta-MUA or old Delta Chats, edited text will be prefixed by EDITED_PREFIX.
// Newer Delta Chats will remove the prefix as needed.
pub(crate) const EDITED_PREFIX: &str = "✏️";
@@ -261,9 +233,6 @@ pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
use \"Settings / Add Second Device\" instead.";
/// Period between `sql::housekeeping()` runs.
pub(crate) const HOUSEKEEPING_PERIOD: i64 = 24 * 60 * 60;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
@@ -276,7 +245,7 @@ mod tests {
assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap());
assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap());
assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap());
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
assert_eq!(Chattype::Broadcast, Chattype::from_i32(160).unwrap());
}
#[test]

View File

@@ -1,43 +1,42 @@
//! Contacts module
use std::cmp::Reverse;
use std::cmp::{min, Reverse};
use std::collections::{BinaryHeap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use base64::Engine as _;
pub use deltachat_contact_tools::may_be_valid_addr;
use deltachat_contact_tools::{
self as contact_tools, ContactAddress, VcardContact, addr_normalize, sanitize_name,
sanitize_name_and_addr,
self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
ContactAddress, VcardContact,
};
use deltachat_derive::{FromSql, ToSql};
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{Duration, timeout};
use tokio::time::{timeout, Duration};
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype};
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
self_fingerprint_opt,
};
use crate::log::{LogExt, info, warn};
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
use crate::log::LogExt;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::sync::{self, Sync::*};
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
@@ -103,16 +102,7 @@ impl ContactId {
/// for this contact will switch to the
/// contact's authorized name.
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
self.set_name_ex(context, Sync, name).await
}
pub(crate) async fn set_name_ex(
self,
context: &Context,
sync: sync::Sync,
name: &str,
) -> Result<()> {
let row = context
let addr = context
.sql
.transaction(|transaction| {
let is_changed = transaction.execute(
@@ -121,45 +111,30 @@ impl ContactId {
)? > 0;
if is_changed {
update_chat_names(context, transaction, self)?;
let (addr, fingerprint) = transaction.query_row(
"SELECT addr, fingerprint FROM contacts WHERE id=?",
let addr = transaction.query_row(
"SELECT addr FROM contacts WHERE id=?",
(self,),
|row| {
let addr: String = row.get(0)?;
let fingerprint: String = row.get(1)?;
Ok((addr, fingerprint))
Ok(addr)
},
)?;
context.emit_event(EventType::ContactsChanged(Some(self)));
Ok(Some((addr, fingerprint)))
Ok(Some(addr))
} else {
Ok(None)
}
})
.await?;
if sync.into() {
if let Some((addr, fingerprint)) = row {
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
}
if let Some(addr) = addr {
chat::sync(
context,
chat::SyncId::ContactAddr(addr.to_string()),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
Ok(())
}
@@ -221,6 +196,31 @@ impl ContactId {
.await?;
Ok(addr)
}
/// Resets encryption with the contact.
///
/// Effect is similar to receiving a message without Autocrypt header
/// from the contact, but this action is triggered manually by the user.
///
/// For example, this will result in sending the next message
/// to 1:1 chat unencrypted, but will not remove existing verified keys.
pub async fn reset_encryption(self, context: &Context) -> Result<()> {
let now = time();
let addr = self.addr(context).await?;
if let Some(mut peerstate) = Peerstate::from_addr(context, &addr).await? {
peerstate.degrade_encryption(now);
peerstate.save_to_db(&context.sql).await?;
}
// Reset 1:1 chat protection.
if let Some(chat_id) = ChatId::lookup_by_contact(context, self).await? {
chat_id
.set_protection(context, ProtectionStatus::Unprotected, now, Some(self))
.await?;
}
Ok(())
}
}
impl fmt::Display for ContactId {
@@ -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)
@@ -267,8 +267,14 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
let mut vcard_contacts = Vec::with_capacity(contacts.len());
for id in contacts {
let c = Contact::get_by_id(context, *id).await?;
let key = c.public_key(context).await?.map(|k| k.to_base64());
let profile_image = match c.get_profile_image_ex(context, false).await? {
let key = match *id {
ContactId::SELF => Some(load_self_public_key(context).await?),
_ => Peerstate::from_addr(context, &c.addr)
.await?
.and_then(|peerstate| peerstate.take_key(false)),
};
let key = key.map(|k| k.to_base64());
let profile_image = match c.get_profile_image(context).await? {
None => None,
Some(path) => tokio::fs::read(path)
.await
@@ -281,7 +287,6 @@ 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),
});
@@ -324,6 +329,15 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
// want `contact.authname` to be saved as the authname and not a locally given name.
let origin = Origin::CreateChat;
let (id, modified) =
match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
Ok(val) => val,
};
if modified != Modifier::None {
context.emit_event(EventType::ContactsChanged(Some(id)));
}
let key = contact.key.as_ref().and_then(|k| {
SignedPublicKey::from_base64(k)
.with_context(|| {
@@ -335,35 +349,50 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
.log_err(context)
.ok()
});
let fingerprint;
if let Some(public_key) = key {
fingerprint = public_key.dc_fingerprint().hex();
context
.sql
.execute(
"INSERT INTO public_keys (fingerprint, public_key)
VALUES (?, ?)
ON CONFLICT (fingerprint)
DO NOTHING",
(&fingerprint, public_key.to_bytes()),
)
.await?;
} else {
fingerprint = String::new();
}
let (id, modified) =
match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin)
let timestamp = contact
.timestamp
.as_ref()
.map_or(0, |&t| min(t, smeared_time(context)));
let aheader = Aheader {
addr: contact.addr.clone(),
public_key,
prefer_encrypt: EncryptPreference::Mutual,
};
let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
Err(e) => {
warn!(
context,
"import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
);
return Ok(id);
}
Ok(p) => p,
};
let peerstate = if let Some(mut p) = peerstate {
p.apply_gossip(&aheader, timestamp);
p
} else {
Peerstate::from_gossip(&aheader, timestamp)
};
if let Err(e) = peerstate.save_to_db(&context.sql).await {
warn!(
context,
"import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
);
return Ok(id);
}
if let Err(e) = peerstate
.handle_fingerprint_change(context, timestamp)
.await
{
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
Ok(val) => val,
};
if modified != Modifier::None {
context.emit_event(EventType::ContactsChanged(Some(id)));
warn!(
context,
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
contact.addr
);
return Ok(id);
}
}
if modified != Modifier::Created {
return Ok(id);
@@ -394,14 +423,6 @@ 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)
}
@@ -435,11 +456,6 @@ pub struct Contact {
/// E-Mail-Address of the contact. It is recommended to use `Contact::get_addr` to access this field.
addr: String,
/// OpenPGP key fingerprint.
/// Non-empty iff the contact is a key-contact,
/// identified by this fingerprint.
fingerprint: Option<String>,
/// Blocked state. Use contact_is_blocked to access this field.
pub blocked: bool,
@@ -589,7 +605,7 @@ impl Contact {
.sql
.query_row_optional(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status, c.is_bot, c.fingerprint
c.authname, c.param, c.status, c.is_bot
FROM contacts c
WHERE c.id=?;",
(contact_id,),
@@ -603,14 +619,11 @@ impl Contact {
let param: String = row.get(6)?;
let status: Option<String> = row.get(7)?;
let is_bot: bool = row.get(8)?;
let fingerprint: Option<String> =
Some(row.get(9)?).filter(|s: &String| !s.is_empty());
let contact = Self {
id: contact_id,
name,
authname,
addr,
fingerprint,
blocked: blocked.unwrap_or_default(),
last_seen,
origin,
@@ -633,9 +646,6 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
if let Some(self_fp) = self_fingerprint_opt(context).await? {
contact.fingerprint = Some(self_fp.to_string());
}
contact.status = context
.get_config(Config::Selfstatus)
.await?
@@ -755,19 +765,7 @@ impl Contact {
self.is_bot
}
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
/// Check if an e-mail address belongs to a known and unblocked contact.
///
/// Known and unblocked contacts will be returned by `get_contacts()`.
///
@@ -798,17 +796,16 @@ impl Contact {
let addr_normalized = addr_normalize(addr);
if context.is_configured().await? && context.is_self_addr(addr).await? {
if context.is_self_addr(&addr_normalized).await? {
return Ok(Some(ContactId::SELF));
}
let id = context
.sql
.query_get_value(
"SELECT id FROM contacts
WHERE addr=?1 COLLATE NOCASE
AND id>?2 AND origin>=?3 AND (? OR blocked=?)
ORDER BY last_seen DESC LIMIT 1",
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
(
&addr_normalized,
ContactId::LAST_SPECIAL,
@@ -821,19 +818,8 @@ impl Contact {
Ok(id)
}
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: &ContactAddress,
origin: Origin,
) -> Result<(ContactId, Modifier)> {
Self::add_or_lookup_ex(context, name, addr, "", origin).await
}
/// Lookup a contact and create it if it does not exist yet.
/// If `fingerprint` is non-empty, a key-contact with this fingerprint is added / looked up.
/// Otherwise, an address-contact with `addr` is added / looked up.
/// A name and an "origin" can be given.
/// The contact is identified by the email-address, a name and an "origin" can be given.
///
/// The "origin" is where the address comes from -
/// from-header, cc-header, addressbook, qr, manual-edit etc.
@@ -857,34 +843,21 @@ impl Contact {
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
pub(crate) async fn add_or_lookup_ex(
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: &str,
fingerprint: &str,
addr: &ContactAddress,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
ensure!(
!addr.is_empty() || !fingerprint.is_empty(),
"Can not add_or_lookup empty address"
);
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_configured().await? && context.is_self_addr(addr).await? {
if context.is_self_addr(addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
if !fingerprint.is_empty() && context.is_configured().await? {
let fingerprint_self = self_fingerprint(context)
.await
.context("self_fingerprint")?;
if fingerprint == fingerprint_self {
return Ok((ContactId::SELF, sth_modified));
}
}
let mut name = sanitize_name(name);
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
@@ -920,10 +893,8 @@ impl Contact {
let row = transaction
.query_row(
"SELECT id, name, addr, origin, authname
FROM contacts
WHERE fingerprint=?1 AND
(?1<>'' OR addr=?2 COLLATE NOCASE)",
(fingerprint, addr),
FROM contacts WHERE addr=? COLLATE NOCASE",
(addr,),
|row| {
let row_id: u32 = row.get(0)?;
let row_name: String = row.get(1)?;
@@ -947,7 +918,7 @@ impl Contact {
|| row_authname.is_empty());
row_id = id;
if origin >= row_origin && addr != row_addr {
if origin >= row_origin && addr.as_ref() != row_addr {
update_addr = true;
}
if update_name || update_authname || update_addr || origin > row_origin {
@@ -991,12 +962,11 @@ impl Contact {
let update_authname = !manual;
transaction.execute(
"INSERT INTO contacts (name, addr, fingerprint, origin, authname)
VALUES (?, ?, ?, ?, ?);",
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
(
if update_name { &name } else { "" },
&addr,
fingerprint,
origin,
if update_authname { &name } else { "" },
),
@@ -1004,14 +974,7 @@ impl Contact {
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
if fingerprint.is_empty() {
info!(context, "Added contact id={row_id} addr={addr}.");
} else {
info!(
context,
"Added contact id={row_id} fpr={fingerprint} addr={addr}."
);
}
info!(context, "Added contact id={row_id} addr={addr}.");
}
Ok(row_id)
})
@@ -1075,12 +1038,12 @@ impl Contact {
/// Returns known and unblocked contacts.
///
/// To get information about a single contact, see get_contact().
/// By default, key-contacts are listed.
///
/// * `listflags` - A combination of flags:
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
/// * `query` - A string to filter the list.
/// `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.
pub async fn get_all(
context: &Context,
listflags: u32,
@@ -1093,31 +1056,32 @@ impl Contact {
.collect::<HashSet<_>>();
let mut add_self = false;
let mut ret = Vec::new();
let flag_add_self = (listflags & constants::DC_GCL_ADD_SELF) != 0;
let flag_address = (listflags & constants::DC_GCL_ADDRESS) != 0;
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 query.is_some() {
if flag_verified_only || query.is_some() {
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
context
.sql
.query_map(
"SELECT c.id, c.addr FROM contacts c
LEFT JOIN acpeerstates ps ON c.addr=ps.addr \
WHERE c.id>?
AND (c.fingerprint='')=?
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,
flag_address,
minimal_origin,
&s3str_like_cmd,
&s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
),
|row| {
let id: ContactId = row.get(0)?;
@@ -1164,11 +1128,10 @@ impl Contact {
.query_map(
"SELECT id, addr FROM contacts
WHERE id>?
AND (fingerprint='')=?
AND origin>=?
AND blocked=0
ORDER BY last_seen DESC, id DESC;",
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
(ContactId::LAST_SPECIAL, minimal_origin),
|row| {
let id: ContactId = row.get(0)?;
let addr: String = row.get(1)?;
@@ -1194,7 +1157,7 @@ impl Contact {
Ok(ret)
}
/// Adds blocked mailinglists and broadcast channels as pseudo-contacts
/// Adds blocked mailinglists as contacts
/// to allow unblocking them as if they are contacts
/// (this way, only one unblock-ffi is needed and only one set of ui-functions,
/// from the users perspective,
@@ -1203,20 +1166,15 @@ impl Contact {
context
.sql
.transaction(move |transaction| {
let mut stmt = transaction.prepare(
"SELECT name, grpid, type FROM chats WHERE (type=? OR type=?) AND blocked=?",
)?;
let rows = stmt.query_map(
(Chattype::Mailinglist, Chattype::InBroadcast, Blocked::Yes),
|row| {
let name: String = row.get(0)?;
let grpid: String = row.get(1)?;
let typ: Chattype = row.get(2)?;
Ok((name, grpid, typ))
},
)?;
let mut stmt = transaction
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
let name: String = row.get(0)?;
let grpid: String = row.get(1)?;
Ok((name, grpid))
})?;
let blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?;
for (name, grpid, typ) in blocked_mailinglists {
for (name, grpid) in blocked_mailinglists {
let count = transaction.query_row(
"SELECT COUNT(id) FROM contacts WHERE addr=?",
[&grpid],
@@ -1229,17 +1187,10 @@ impl Contact {
transaction.execute("INSERT INTO contacts (addr) VALUES (?)", [&grpid])?;
}
let fingerprint = if typ == Chattype::InBroadcast {
// Set some fingerprint so that is_pgp_contact() returns true,
// and the contact isn't marked with a letter icon.
"Blocked_broadcast"
} else {
""
};
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
(&name, Origin::MailinglistAddress, &grpid),
)?;
}
Ok(())
@@ -1297,16 +1248,17 @@ impl Contact {
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let Some(fingerprint_other) = contact.fingerprint() else {
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
else {
return Ok(stock_str::encr_none(context).await);
};
let fingerprint_other = fingerprint_other.to_string();
let stock_message = if contact.public_key(context).await?.is_some() {
stock_str::e2e_available(context).await
} else {
stock_str::encr_none(context).await
let stock_message = match peerstate.prefer_encrypt {
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
EncryptPreference::Reset => stock_str::encr_none(context).await,
};
let finger_prints = stock_str::finger_prints(context).await;
@@ -1316,31 +1268,43 @@ impl Contact {
.await?
.dc_fingerprint()
.to_string();
if addr < contact.addr {
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.dc_fingerprint().to_string())
.unwrap_or_default();
if addr < peerstate.addr {
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
"",
);
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&contact.addr,
&fingerprint_other,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
} else {
cat_fingerprint(
&mut ret,
contact.get_display_name(),
&contact.addr,
&fingerprint_other,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(
&mut ret,
&stock_str::self_msg(context).await,
&addr,
&fingerprint_self,
"",
);
}
@@ -1413,59 +1377,6 @@ impl Contact {
&self.addr
}
/// Returns true if the contact is a key-contact.
/// Otherwise it is an addresss-contact.
pub fn is_key_contact(&self) -> bool {
self.fingerprint.is_some()
}
/// Returns OpenPGP fingerprint of a contact.
///
/// `None` for address-contacts.
pub fn fingerprint(&self) -> Option<Fingerprint> {
if let Some(fingerprint) = &self.fingerprint {
fingerprint.parse().ok()
} else {
None
}
}
/// Returns OpenPGP public key of a contact.
///
/// Returns `None` if the contact is not a key-contact
/// or if the key is not available.
/// It is possible for a key-contact to not have a key,
/// e.g. if only the fingerprint is known from a QR-code.
pub async fn public_key(&self, context: &Context) -> Result<Option<SignedPublicKey>> {
if self.id == ContactId::SELF {
return Ok(Some(load_self_public_key(context).await?));
}
if let Some(fingerprint) = &self.fingerprint {
if let Some(public_key_bytes) = context
.sql
.query_row_optional(
"SELECT public_key
FROM public_keys
WHERE fingerprint=?",
(fingerprint,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
Ok(Some(public_key))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
/// Get name authorized by the contact.
pub fn get_authname(&self) -> &str {
&self.authname
@@ -1531,28 +1442,11 @@ impl Contact {
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
self.get_profile_image_ex(context, true).await
}
/// Get the contact's profile image.
/// This is the image set by each remote user on their own
/// using set_config(context, "selfavatar", image).
async fn get_profile_image_ex(
&self,
context: &Context,
show_fallback_icon: bool,
) -> Result<Option<PathBuf>> {
if self.id == ContactId::SELF {
if let Some(p) = context.get_config(Config::Selfavatar).await? {
return Ok(Some(PathBuf::from(p))); // get_config() calls get_abs_path() internally already
}
} else if self.id == ContactId::DEVICE {
return Ok(Some(chat::get_device_icon(context).await?));
}
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
return Ok(Some(chat::get_address_contact_icon(context).await?));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
@@ -1578,21 +1472,25 @@ impl Contact {
/// Returns whether end-to-end encryption to the contact is available.
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
if self.id == ContactId::SELF {
// We don't need to check if we have our own key.
return Ok(true);
}
Ok(self.public_key(context).await?.is_some())
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
return Ok(false);
};
Ok(peerstate.peek_key(false).is_some())
}
/// Returns true if the contact
/// can be added to verified chats.
/// can be added to verified chats,
/// i.e. has a verified key
/// and Autocrypt key matches the verified key.
///
/// If contact is verified
/// UI should display green checkmark after the contact name
/// in contact list items and
/// in chat member list items.
///
/// In contact profile view, use this function only if there is no chat with the contact,
/// In contact profile view, us this function only if there is no chat with the contact,
/// otherwise use is_chat_protected().
/// Use [Self::get_verifier_id] to display the verifier contact
/// in the info section of the contact profile.
@@ -1603,31 +1501,64 @@ impl Contact {
return Ok(true);
}
Ok(self.get_verifier_id(context).await?.is_some())
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
return Ok(false);
};
let forward_verified = peerstate.is_using_verified_key();
let backward_verified = peerstate.is_backward_verified(context).await?;
Ok(forward_verified && backward_verified)
}
/// Returns true if we have a verified key for the contact
/// and it is the same as Autocrypt key.
/// This is enough to send messages to the contact in verified chat
/// and verify received messages, but not enough to display green checkmark
/// or add the contact to verified groups.
pub async fn is_forward_verified(&self, context: &Context) -> Result<bool> {
if self.id == ContactId::SELF {
return Ok(true);
}
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
return Ok(false);
};
Ok(peerstate.is_using_verified_key())
}
/// Returns the `ContactId` that verified the contact.
///
/// If this returns Some(_),
/// If the function returns non-zero result,
/// display green checkmark in the profile and "Introduced by ..." line
/// with the name and address of the contact
/// formatted by [Self::get_name_n_addr].
///
/// If this returns `Some(None)`, then the contact is verified,
/// but it's unclear by whom.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<Option<ContactId>>> {
let verifier_id: u32 = context
.sql
.query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,))
/// If this function returns a verifier,
/// this does not necessarily mean
/// you can add the contact to verified chats.
/// Use [Self::is_verified] to check
/// if a contact can be added to a verified chat instead.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
.await?
.with_context(|| format!("Contact {} does not exist", self.id))?;
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
else {
return Ok(None);
};
if verifier_id == 0 {
Ok(None)
} else if verifier_id == self.id.to_u32() {
Ok(Some(None))
} else {
Ok(Some(Some(ContactId::new(verifier_id))))
if addr_cmp(&verifier_addr, &self.addr) {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::Unknown).await? {
Some(contact_id) => Ok(Some(contact_id)),
None => {
let addr = &self.addr;
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
Ok(None)
}
}
}
@@ -1799,16 +1730,14 @@ WHERE type=? AND id IN (
true => chat::SyncAction::Block,
false => chat::SyncAction::Unblock,
};
let sync_id = if let Some(fingerprint) = contact.fingerprint() {
chat::SyncId::ContactFingerprint(fingerprint.hex())
} else {
chat::SyncId::ContactAddr(contact.addr.clone())
};
chat::sync(context, sync_id, action)
.await
.log_err(context)
.ok();
chat::sync(
context,
chat::SyncId::ContactAddr(contact.addr.clone()),
action,
)
.await
.log_err(context)
.ok();
}
}
@@ -1928,52 +1857,29 @@ pub(crate) async fn update_last_seen(
Ok(())
}
/// Marks contact `contact_id` as verified by `verifier_id`.
pub(crate) async fn mark_contact_id_as_verified(
context: &Context,
contact_id: ContactId,
verifier_id: ContactId,
) -> Result<()> {
ensure_and_debug_assert_ne!(
contact_id,
verifier_id,
"Contact cannot be verified by self",
fn cat_fingerprint(
ret: &mut String,
name: &str,
addr: &str,
fingerprint_verified: &str,
fingerprint_unverified: &str,
) {
*ret += &format!(
"\n\n{} ({}):\n{}",
name,
addr,
if !fingerprint_verified.is_empty() {
fingerprint_verified
} else {
fingerprint_unverified
},
);
context
.sql
.transaction(|transaction| {
let contact_fingerprint: String = transaction.query_row(
"SELECT fingerprint FROM contacts WHERE id=?",
(contact_id,),
|row| row.get(0),
)?;
if contact_fingerprint.is_empty() {
bail!("Non-key-contact {contact_id} cannot be verified");
}
if verifier_id != ContactId::SELF {
let verifier_fingerprint: String = transaction.query_row(
"SELECT fingerprint FROM contacts WHERE id=?",
(verifier_id,),
|row| row.get(0),
)?;
if verifier_fingerprint.is_empty() {
bail!(
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
);
}
}
transaction.execute(
"UPDATE contacts SET verifier=? WHERE id=?",
(verifier_id, contact_id),
)?;
Ok(())
})
.await?;
Ok(())
}
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
if !fingerprint_verified.is_empty()
&& !fingerprint_unverified.is_empty()
&& fingerprint_verified != fingerprint_unverified
{
*ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
}
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {

View File

@@ -1,7 +1,7 @@
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
use deltachat_contact_tools::may_be_valid_addr;
use super::*;
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
@@ -56,83 +56,64 @@ fn test_split_address_book() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_contacts() -> Result<()> {
let mut tcm = TestContextManager::new();
let context = tcm.bob().await;
let alice = tcm.alice().await;
alice
.set_config(Config::Displayname, Some("MyName"))
.await?;
let context = TestContext::new().await;
// Alice is not in the contacts yet.
let contacts = Contact::get_all(&context.ctx, 0, Some("Alice")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&context.ctx, 0, Some("MyName")).await?;
assert!(context.get_all_self_addrs().await?.is_empty());
// Bob is not in the contacts yet.
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
assert_eq!(contacts.len(), 0);
let claire_id = Contact::create(&context, "someone", "claire@example.org").await?;
let dave_id = Contact::create(&context, "", "dave@example.org").await?;
let id = context.add_or_lookup_contact_id(&alice).await;
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
&ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
assert_ne!(id, ContactId::UNDEFINED);
let contact = Contact::get_by_id(&context, id).await.unwrap();
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_display_name(), "MyName");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "bob");
// Search by name.
let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.first(), Some(&id));
// Search by address.
let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.first(), Some(&id));
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?;
assert_eq!(contacts.len(), 0);
// Set Alice name to "someone" manually.
id.set_name(&context, "someone").await?;
// Set Bob name to "someone" manually.
let (contact_bob_id, modified) = Contact::add_or_lookup(
&context.ctx,
"someone",
&ContactAddress::new("user@example.org")?,
Origin::ManuallyCreated,
)
.await?;
assert_eq!(contact_bob_id, id);
assert_eq!(modified, Modifier::Modified);
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "MyName");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "someone");
// Not searchable by authname, because it is not displayed.
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
assert_eq!(contacts.len(), 0);
for add_self in [0, constants::DC_GCL_ADD_SELF] {
info!(&context, "add_self={add_self}");
// Search key-contacts by display name (same as manually set name).
let contacts = Contact::get_all(&context.ctx, add_self, Some("someone")).await?;
assert_eq!(contacts, vec![id]);
// Get all key-contacts.
let contacts = Contact::get_all(&context, add_self, None).await?;
match add_self {
0 => assert_eq!(contacts, vec![id]),
_ => assert_eq!(contacts, vec![id, ContactId::SELF]),
}
}
// Search address-contacts by display name.
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, Some("someone")).await?;
assert_eq!(contacts, vec![claire_id]);
// Get all address-contacts. Newer contacts go first.
let contacts = Contact::get_all(&context, constants::DC_GCL_ADDRESS, None).await?;
assert_eq!(contacts, vec![dave_id, claire_id]);
let contacts = Contact::get_all(
&context,
constants::DC_GCL_ADDRESS | constants::DC_GCL_ADD_SELF,
None,
)
.await?;
assert_eq!(contacts, vec![dave_id, claire_id, ContactId::SELF]);
// Search by display name (same as manually set name).
let contacts = Contact::get_all(&context.ctx, 0, Some("someone")).await?;
assert_eq!(contacts.len(), 1);
assert_eq!(contacts.first(), Some(&id));
Ok(())
}
@@ -152,7 +133,7 @@ async fn test_is_self_addr() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
let t = TestContext::new_alice().await;
let t = TestContext::new().await;
let book = concat!(
" Name one \n one@eins.org \n",
"Name two\ntwo@deux.net\n",
@@ -266,7 +247,7 @@ async fn test_add_or_lookup() {
// check SELF
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), "alice@example.org");
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
}
@@ -301,7 +282,7 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "f@example.org");
assert_eq!(contact.get_name_n_addr(), "f@example.org");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
// second message inits the name
receive_imf(
@@ -327,9 +308,9 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Flobbyfoo");
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
// third message changes the name
receive_imf(
@@ -357,11 +338,11 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Foo Flobby");
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
assert_eq!(contacts.len(), 0);
let contacts = Contact::get_all(&t, 0, Some("Foo Flobby")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
// change name manually
let test_id = Contact::create(&t, "Falk", "f@example.org").await?;
@@ -375,9 +356,9 @@ async fn test_contact_name_changes() -> Result<()> {
assert_eq!(contact.get_display_name(), "Falk");
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
assert_eq!(contacts.len(), 0);
assert_eq!(contacts.len(), 1);
Ok(())
}
@@ -385,13 +366,20 @@ async fn test_contact_name_changes() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
assert!(Contact::delete(&alice, ContactId::SELF).await.is_err());
// Create Bob contact
let contact_id = alice.add_or_lookup_contact_id(&bob).await;
let chat = alice.create_chat(&bob).await;
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
@@ -428,57 +416,30 @@ async fn test_delete() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_recreate_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = tcm.alice().await;
let bob = tcm.bob().await;
let t = TestContext::new_alice().await;
// test recreation after physical deletion
let contact_id1 = t.add_or_lookup_contact_id(&bob).await;
assert_eq!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
1
);
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
assert_eq!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
0
);
let contact_id2 = t.add_or_lookup_contact_id(&bob).await;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
assert_eq!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
1
);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
// test recreation after hiding
t.create_chat(&bob).await;
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::get_by_id(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
0
);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = t.add_or_lookup_contact_id(&bob).await;
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
let contact = Contact::get_by_id(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::CreateChat);
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(
Contact::get_all(&t, 0, Some("bob@example.net"))
.await?
.len(),
1
);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Ok(())
}
@@ -705,28 +666,20 @@ async fn test_name_in_address() {
assert_eq!(contact.get_name(), "name1");
assert_eq!(contact.get_addr(), "dave@example.org");
assert!(
Contact::create(&t, "", "<dskjfdslk@sadklj.dk")
.await
.is_err()
);
assert!(
Contact::create(&t, "", "<dskjf>dslk@sadklj.dk>")
.await
.is_err()
);
assert!(Contact::create(&t, "", "<dskjfdslk@sadklj.dk")
.await
.is_err());
assert!(Contact::create(&t, "", "<dskjf>dslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjfdslksadklj.dk").await.is_err());
assert!(
Contact::create(&t, "", "dskjfdslk@sadklj.dk>")
.await
.is_err()
);
assert!(Contact::create(&t, "", "dskjfdslk@sadklj.dk>")
.await
.is_err());
assert!(Contact::create(&t, "", "dskjf dslk@d.e").await.is_err());
assert!(
Contact::create(&t, "", "<dskjf dslk@sadklj.dk")
.await
.is_err()
);
assert!(Contact::create(&t, "", "<dskjf dslk@sadklj.dk")
.await
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -775,59 +728,51 @@ async fn test_contact_get_color() -> Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_get_encrinfo() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice = TestContext::new_alice().await;
// Return error for special IDs
let encrinfo = Contact::get_encrinfo(alice, ContactId::SELF).await;
let encrinfo = Contact::get_encrinfo(&alice, ContactId::SELF).await;
assert!(encrinfo.is_err());
let encrinfo = Contact::get_encrinfo(alice, ContactId::DEVICE).await;
let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await;
assert!(encrinfo.is_err());
let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?;
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(!contact.e2ee_avail(&alice).await?);
let contact = Contact::get_by_id(alice, address_contact_bob_id).await?;
assert!(!contact.e2ee_avail(alice).await?);
let bob = TestContext::new_bob().await;
let chat_alice = bob
.create_chat_with_contact("Alice", "alice@example.org")
.await;
send_text_msg(&bob, chat_alice.id, "Hello".to_string()).await?;
let msg = bob.pop_sent_msg().await;
alice.recv_msg(&msg).await;
let contact_bob_id = alice.add_or_lookup_contact_id(bob).await;
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
assert_eq!(
encrinfo,
"End-to-end encryption available.
"End-to-end encryption preferred.
Fingerprints:
Me (alice@example.org):
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
bob@example.net (bob@example.net):
Bob (bob@example.net):
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
assert!(contact.e2ee_avail(alice).await?);
alice.sql.execute("DELETE FROM public_keys", ()).await?;
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
assert_eq!(
encrinfo,
"No encryption.
Fingerprints:
Me (alice@example.org):
2E6F A2CB 23B5 32D7 2863
4B58 64B0 8F61 A9ED 9443
bob@example.net (bob@example.net):
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
assert!(!contact.e2ee_avail(alice).await?);
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(contact.e2ee_avail(&alice).await?);
Ok(())
}
@@ -835,24 +780,24 @@ CCCB 5AA9 F6E1 141C 9431
/// synchronized when the message is not encrypted.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_synchronize_status() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice has two devices.
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let alice1 = TestContext::new_alice().await;
let alice2 = TestContext::new_alice().await;
// Bob has one device.
let bob = &tcm.bob().await;
let bob = TestContext::new_bob().await;
let default_status = alice1.get_config(Config::Selfstatus).await?;
alice1
.set_config(Config::Selfstatus, Some("New status"))
.await?;
let chat = alice1.create_email_chat(bob).await;
let chat = alice1
.create_chat_with_contact("Bob", "bob@example.net")
.await;
// Alice sends a message to Bob from the first device.
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
@@ -868,9 +813,18 @@ async fn test_synchronize_status() -> Result<()> {
// Message was not encrypted, so status is not copied.
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
// Alice sends encrypted message.
let chat = alice1.create_chat(bob).await;
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
// Bob replies.
let chat = bob
.create_chat_with_contact("Alice", "alice@example.org")
.await;
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
alice1.recv_msg(&sent_msg).await;
alice2.recv_msg(&sent_msg).await;
// Alice sends second message.
send_text_msg(&alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
@@ -891,14 +845,12 @@ async fn test_synchronize_status() -> Result<()> {
/// Tests that DC_EVENT_SELFAVATAR_CHANGED is emitted on avatar changes.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_changed_event() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice has two devices.
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
let alice1 = TestContext::new_alice().await;
let alice2 = TestContext::new_alice().await;
// Bob has one device.
let bob = &tcm.bob().await;
let bob = TestContext::new_bob().await;
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
@@ -914,9 +866,20 @@ async fn test_selfavatar_changed_event() -> Result<()> {
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
.await;
// Bob sends a message so that Alice can encrypt to him.
let chat = bob
.create_chat_with_contact("Alice", "alice@example.org")
.await;
send_text_msg(&bob, chat.id, "Reply".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
alice1.recv_msg(&sent_msg).await;
alice2.recv_msg(&sent_msg).await;
// Alice sends a message.
let alice1_chat_id = alice1.create_chat(bob).await.id;
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
let alice1_chat_id = alice1.get_last_msg().await.chat_id;
alice1_chat_id.accept(&alice1).await?;
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
@@ -1045,7 +1008,7 @@ async fn test_verified_by_none() -> Result<()> {
let contact = Contact::get_by_id(&alice, contact_id).await?;
assert!(contact.get_verifier_id(&alice).await?.is_none());
// Receive a message from Bob to save the public key.
// Receive a message from Bob to create a peerstate.
let chat = bob.create_chat(&alice).await;
let sent_msg = bob.send_text(chat.id, "moin").await;
alice.recv_msg(&sent_msg).await;
@@ -1072,7 +1035,6 @@ async fn test_sync_create() -> Result<()> {
.unwrap();
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob");
assert_eq!(a1b_contact.is_key_contact(), false);
Contact::create(alice0, "Bob Renamed", "bob@example.net").await?;
test_utils::sync(alice0, alice1).await;
@@ -1082,7 +1044,6 @@ async fn test_sync_create() -> Result<()> {
assert_eq!(id, a1b_contact_id);
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
assert_eq!(a1b_contact.name, "Bob Renamed");
assert_eq!(a1b_contact.is_key_contact(), false);
Ok(())
}
@@ -1093,8 +1054,6 @@ 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);
@@ -1102,12 +1061,16 @@ 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;
let bob_id = alice.recv_msg(&sent_msg).await.from_id;
let bob_contact = Contact::get_by_id(alice, bob_id).await?;
let key_base64 = bob_contact.public_key(alice).await?.unwrap().to_base64();
alice.recv_msg(&sent_msg).await;
let bob_id = Contact::create(alice, "Some Bob", &bob_addr).await?;
let key_base64 = Peerstate::from_addr(alice, &bob_addr)
.await?
.unwrap()
.peek_key(false)
.unwrap()
.to_base64();
let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
assert_eq!(make_vcard(alice, &[]).await?, "".to_string());
@@ -1123,14 +1086,12 @@ 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);
@@ -1153,7 +1114,6 @@ 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());
@@ -1185,16 +1145,13 @@ 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(())
}
/// Tests importing a vCard with the same email address,
/// but a new key.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_vcard_key_change() -> Result<()> {
async fn test_import_vcard_updates_only_key() -> Result<()> {
let alice = &TestContext::new_alice().await;
let bob = &TestContext::new_bob().await;
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
@@ -1212,34 +1169,28 @@ async fn test_import_vcard_key_change() -> Result<()> {
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
let bob1 = &TestContext::new().await;
bob1.configure_addr(bob_addr).await;
bob1.set_config(Config::Displayname, Some("New Bob"))
.await?;
let avatar_path = bob1.dir.path().join("avatar.png");
let bob = &TestContext::new().await;
bob.configure_addr(bob_addr).await;
bob.set_config(Config::Displayname, Some("Not Bob")).await?;
let avatar_path = bob.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
tokio::fs::write(&avatar_path, avatar_bytes).await?;
bob1.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
.await?;
SystemTime::shift(Duration::from_secs(1));
let vcard1 = make_vcard(bob1, &[ContactId::SELF]).await?;
let alice_bob_id1 = import_vcard(alice, &vcard1).await?[0];
assert_ne!(alice_bob_id1, alice_bob_id);
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
assert_eq!(alice_bob_contact.get_authname(), "Bob");
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
let alice_bob_contact1 = Contact::get_by_id(alice, alice_bob_id1).await?;
assert_eq!(alice_bob_contact1.get_authname(), "New Bob");
assert!(alice_bob_contact1.get_profile_image(alice).await?.is_some());
// Last message is still the same,
// no new messages are added.
let msg = alice.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), "moin");
let chat_id1 = ChatId::create_for_contact(alice, alice_bob_id1).await?;
let sent_msg = alice.send_text(chat_id1, "moin").await;
let msg = bob1.recv_msg(&sent_msg).await;
assert!(msg.is_info());
assert_eq!(
msg.get_text(),
stock_str::contact_setup_changed(alice, bob_addr).await
);
let sent_msg = alice.send_text(chat_id, "moin").await;
let msg = bob.recv_msg(&sent_msg).await;
assert!(msg.get_showpadlock());
// The old vCard is imported, but doesn't change Bob's key for Alice.
@@ -1251,6 +1202,63 @@ async fn test_import_vcard_key_change() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg = tcm.send_recv_accept(bob, alice, "Hi!").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reset_verified_encryption() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, alice).await;
let msg = tcm.send_recv(bob, alice, "Encrypted").await;
assert_eq!(msg.get_showpadlock(), true);
let alice_bob_chat_id = msg.chat_id;
let alice_bob_contact_id = msg.from_id;
alice_bob_contact_id.reset_encryption(alice).await?;
// Check that the contact is still verified after resetting encryption.
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_contact_id).await?;
assert_eq!(alice_bob_contact.is_verified(alice).await?, true);
// 1:1 chat and profile is no longer verified.
assert_eq!(alice_bob_contact.is_profile_verified(alice).await?, false);
let info_msg = alice.get_last_msg_in(alice_bob_chat_id).await;
assert_eq!(
info_msg.text,
"bob@example.net sent a message from another device."
);
let sent = alice.send_text(alice_bob_chat_id, "Unencrypted").await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.get_showpadlock(), false);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_is_verified() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -1260,87 +1268,9 @@ async fn test_self_is_verified() -> Result<()> {
assert_eq!(contact.is_verified(&alice).await?, true);
assert!(contact.is_profile_verified(&alice).await?);
assert!(contact.get_verifier_id(&alice).await?.is_none());
assert!(contact.is_key_contact());
let chat_id = ChatId::get_for_contact(&alice, ContactId::SELF).await?;
assert!(chat_id.is_protected(&alice).await.unwrap() == ProtectionStatus::Protected);
Ok(())
}
/// Tests that importing a vCard with a key creates a key-contact.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_vcard_creates_key_contact() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
let contact_ids = import_vcard(alice, &vcard).await?;
assert_eq!(contact_ids.len(), 1);
let contact_id = contact_ids.first().unwrap();
let contact = Contact::get_by_id(alice, *contact_id).await?;
assert!(contact.is_key_contact());
Ok(())
}
/// Tests changing display name by sending a message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_name_changes() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
alice
.set_config(Config::Displayname, Some("Alice Revision 1"))
.await?;
let alice_bob_chat = alice.create_chat(bob).await;
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
let bob_alice_id = bob.recv_msg(&sent_msg).await.from_id;
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 1");
alice
.set_config(Config::Displayname, Some("Alice Revision 2"))
.await?;
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
bob.recv_msg(&sent_msg).await;
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
assert_eq!(bob_alice_contact.get_display_name(), "Alice Revision 2");
// Explicitly rename contact to "Renamed".
bob.evtracker.clear_events();
bob_alice_contact.id.set_name(bob, "Renamed").await?;
let event = bob
.evtracker
.get_matching(|e| matches!(e, EventType::ContactsChanged { .. }))
.await;
assert_eq!(
event,
EventType::ContactsChanged(Some(bob_alice_contact.id))
);
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
// Alice also renames self into "Renamed".
alice
.set_config(Config::Displayname, Some("Renamed"))
.await?;
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
bob.recv_msg(&sent_msg).await;
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
// Contact name was set to "Renamed" explicitly before,
// so it should not be changed.
alice
.set_config(Config::Displayname, Some("Renamed again"))
.await?;
let sent_msg = alice.send_text(alice_bob_chat.id, "Hello").await;
bob.recv_msg(&sent_msg).await;
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_id).await?;
assert_eq!(bob_alice_contact.get_display_name(), "Renamed");
Ok(())
}

View File

@@ -5,36 +5,37 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use pgp::types::PublicKeyTrait;
use pgp::SignedPublicKey;
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
use crate::aheader::EncryptPreference;
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
self, DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT, DC_CHAT_ID_TRASH, DC_VERSION_STR,
};
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
use crate::contact::{Contact, ContactId};
use crate::debug_logging::DebugLogging;
use crate::download::DownloadState;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
use crate::key::{load_self_secret_key, self_fingerprint};
use crate::log::{info, warn};
use crate::logged_debug_assert;
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
use crate::message::{self, Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::peer_channels::Iroh;
use crate::peerstate::Peerstate;
use crate::push::PushSubscriber;
use crate::quota::QuotaInfo;
use crate::scheduler::{SchedulerState, convert_folder_meaning};
use crate::scheduler::{convert_folder_meaning, SchedulerState};
use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
@@ -277,13 +278,6 @@ pub struct InnerContext {
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: parking_lot::RwLock<String>,
/// It's not possible to emit migration errors as an event,
/// because at the time of the migration, there is no event emitter yet.
/// So, this holds the error that happened during migration, if any.
/// This is necessary for the possibly-failible PGP migration,
/// which happened 2025-05, and can be removed a few releases later.
pub(crate) migration_error: parking_lot::RwLock<Option<String>>,
/// If debug logging is enabled, this contains all necessary information
///
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
@@ -299,11 +293,6 @@ pub struct InnerContext {
/// Iroh for realtime peer channels.
pub(crate) iroh: Arc<RwLock<Option<Iroh>>>,
/// The own fingerprint, if it was computed already.
/// tokio::sync::OnceCell would be possible to use, but overkill for our usecase;
/// the standard library's OnceLock is enough, and it's a lot smaller in memory.
pub(crate) self_fingerprint: OnceLock<String>,
}
/// The state of ongoing process.
@@ -333,15 +322,6 @@ impl Default for RunningState {
/// about the context on top of the information here.
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();
#[cfg(debug_assertions)]
res.insert(
"debug_assertions",
"On - DO NOT RELEASE THIS BUILD".to_string(),
);
#[cfg(not(debug_assertions))]
res.insert("debug_assertions", "Off".to_string());
res.insert("deltachat_core_version", format!("v{}", &*DC_VERSION_STR));
res.insert("sqlite_version", rusqlite::version().to_string());
res.insert("arch", (std::mem::size_of::<usize>() * 8).to_string());
@@ -467,12 +447,10 @@ impl Context {
creation_time: tools::Time::now(),
last_full_folder_scan: Mutex::new(None),
last_error: parking_lot::RwLock::new("".to_string()),
migration_error: parking_lot::RwLock::new(None),
debug_logging: std::sync::RwLock::new(None),
push_subscriber,
push_subscribed: AtomicBool::new(false),
iroh: Arc::new(RwLock::new(None)),
self_fingerprint: OnceLock::new(),
};
let ctx = Context {
@@ -670,16 +648,8 @@ impl Context {
/// or [`Self::emit_msgs_changed_without_msg_id`] should be used
/// instead of this function.
pub fn emit_msgs_changed(&self, chat_id: ChatId, msg_id: MsgId) {
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed: chat_id is unset."
);
logged_debug_assert!(
self,
!msg_id.is_unset(),
"emit_msgs_changed: msg_id is unset."
);
debug_assert!(!chat_id.is_unset());
debug_assert!(!msg_id.is_unset());
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
chatlist_events::emit_chatlist_changed(self);
@@ -688,11 +658,7 @@ impl Context {
/// Emits a MsgsChanged event with specified chat and without message id.
pub fn emit_msgs_changed_without_msg_id(&self, chat_id: ChatId) {
logged_debug_assert!(
self,
!chat_id.is_unset(),
"emit_msgs_changed_without_msg_id: chat_id is unset."
);
debug_assert!(!chat_id.is_unset());
self.emit_event(EventType::MsgsChanged {
chat_id,
@@ -807,11 +773,13 @@ 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;
@@ -838,10 +806,10 @@ impl Context {
let pub_key_cnt = self
.sql
.count("SELECT COUNT(*) FROM public_keys;", ())
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match self_fingerprint(self).await {
Ok(fp) => fp.to_string(),
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.dc_fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -890,6 +858,7 @@ 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)
@@ -1052,7 +1021,7 @@ impl Context {
self.get_config_int(Config::GossipPeriod).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats", // deprecated 2025-07
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
@@ -1063,19 +1032,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"donation_request_next_check",
self.get_config_i64(Config::DonationRequestNextCheck)
.await?
.to_string(),
);
res.insert(
"first_key_contacts_msg_id",
self.sql
.get_raw_config("first_key_contacts_msg_id")
.await?
.unwrap_or_default(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));
@@ -1087,6 +1043,7 @@ impl Context {
#[derive(Default)]
struct ChatNumbers {
protected: u32,
protection_broken: u32,
opportunistic_dc: u32,
opportunistic_mua: u32,
unencrypted_dc: u32,
@@ -1104,24 +1061,25 @@ impl Context {
)
.await?
.unwrap_or_default();
res += &format!("num_msgs {num_msgs}\n");
res += &format!("num_msgs {}\n", num_msgs);
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 {num_chats}\n");
res += &format!("num_chats {}\n", num_chats);
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
res += &format!("db_size_bytes {db_size}\n");
res += &format!("db_size_bytes {}\n", db_size);
let secret_key = &load_self_secret_key(self).await?.primary_key;
let key_created = secret_key.public_key().created_at().timestamp();
res += &format!("key_created {key_created}\n");
let key_created = secret_key.created_at().timestamp();
res += &format!("key_created {}\n", key_created);
// how many of the chats active in the last months are:
// - protected
// - protection-broken
// - opportunistic-encrypted and the contact uses Delta Chat
// - opportunistic-encrypted and the contact uses a classical MUA
// - unencrypted and the contact uses Delta Chat
@@ -1164,6 +1122,8 @@ impl Context {
if protected == ProtectionStatus::Protected {
chats.protected += 1;
} else if protected == ProtectionStatus::ProtectionBroken {
chats.protection_broken += 1;
} else if encrypted {
if is_dc_message {
chats.opportunistic_dc += 1;
@@ -1181,6 +1141,7 @@ impl Context {
)
.await?;
res += &format!("chats_protected {}\n", chats.protected);
res += &format!("chats_protection_broken {}\n", chats.protection_broken);
res += &format!("chats_opportunistic_dc {}\n", chats.opportunistic_dc);
res += &format!("chats_opportunistic_mua {}\n", chats.opportunistic_mua);
res += &format!("chats_unencrypted_dc {}\n", chats.unencrypted_dc);
@@ -1194,7 +1155,7 @@ impl Context {
id
}
};
res += &format!("self_reporting_id {self_reporting_id}");
res += &format!("self_reporting_id {}", self_reporting_id);
Ok(res)
}
@@ -1205,14 +1166,32 @@ impl Context {
/// On the other end, a bot will receive the message and make it available
/// to Delta Chat's developers.
pub async fn draft_self_report(&self) -> Result<ChatId> {
const SELF_REPORTING_BOT_VCARD: &str = include_str!("../assets/self-reporting-bot.vcf");
let contact_id: ContactId = *import_vcard(self, SELF_REPORTING_BOT_VCARD)
.await?
.first()
.context("Self reporting bot vCard does not contain a contact")?;
mark_contact_id_as_verified(self, contact_id, ContactId::SELF).await?;
const SELF_REPORTING_BOT: &str = "self_reporting@testrun.org";
let contact_id = Contact::create(self, "Statistics bot", SELF_REPORTING_BOT).await?;
let chat_id = ChatId::create_for_contact(self, contact_id).await?;
// We're including the bot's public key in Delta Chat
// so that the first message to the bot can directly be encrypted:
let public_key = SignedPublicKey::from_base64(
"xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCM\
PNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUI\
CQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+Nq\
I4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARl\
t8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGB\
YIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj\
2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ\
4=",
)?;
let mut peerstate = Peerstate::from_public_key(
SELF_REPORTING_BOT,
0,
EncryptPreference::Mutual,
&public_key,
);
let fingerprint = public_key.dc_fingerprint();
peerstate.set_verified(public_key, fingerprint, "".to_string())?;
peerstate.save_to_db(&self.sql).await?;
chat_id
.set_protection(self, ProtectionStatus::Protected, time(), Some(contact_id))
.await?;

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