mirror of
https://github.com/chatmail/core.git
synced 2026-04-04 22:42:11 +03:00
Compare commits
161 Commits
shadowsock
...
v2.8.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d80b749dec | ||
|
|
039a8b7c36 | ||
|
|
779f58ab16 | ||
|
|
b9183fe5eb | ||
|
|
9d342671d5 | ||
|
|
4e47ebd5fc | ||
|
|
d5c418e909 | ||
|
|
85414558c5 | ||
|
|
d6af8d2526 | ||
|
|
1209e95e34 | ||
|
|
51f9279e67 | ||
|
|
f27d54f7fa | ||
|
|
7f3648f8ae | ||
|
|
49fc258578 | ||
|
|
0c51b4fe41 | ||
|
|
dbad714539 | ||
|
|
edd8008650 | ||
|
|
615a1b3f4e | ||
|
|
fe6044e1aa | ||
|
|
46b275bfab | ||
|
|
25f44c517a | ||
|
|
cac04f8ee4 | ||
|
|
45d8566ec0 | ||
|
|
29a98ba13b | ||
|
|
e3973f6448 | ||
|
|
7b41425fe4 | ||
|
|
2c7d51f98f | ||
|
|
a2df29515a | ||
|
|
6df1d165dd | ||
|
|
e03e2d9a68 | ||
|
|
8fc6ea19b4 | ||
|
|
c5c947e175 | ||
|
|
6d8dff54a7 | ||
|
|
a0f6bdffeb | ||
|
|
e6fd52afff | ||
|
|
0142515887 | ||
|
|
d45ec7f34d | ||
|
|
752f45f0f0 | ||
|
|
0299543a86 | ||
|
|
d3908d6b36 | ||
|
|
2cf979de53 | ||
|
|
f5e8c8083d | ||
|
|
58b99f59f7 | ||
|
|
402e42f858 | ||
|
|
fbae0739a6 | ||
|
|
0359481ba4 | ||
|
|
6406f305b8 | ||
|
|
e5e0f0cdd7 | ||
|
|
0bac4acdd8 | ||
|
|
ce5697c5f7 | ||
|
|
22258f7269 | ||
|
|
5ab107866a | ||
|
|
374a5ef687 | ||
|
|
1a2e355bb8 | ||
|
|
192a6a2b9d | ||
|
|
4ca0ce2fb2 | ||
|
|
ab4cb01065 | ||
|
|
661a8864b9 | ||
|
|
67f00fbb84 | ||
|
|
389649ea8a | ||
|
|
a87ee030fc | ||
|
|
3f66ae91cd | ||
|
|
75b7bea78f | ||
|
|
acba27a328 | ||
|
|
cba9eb98d6 | ||
|
|
da9b24d191 | ||
|
|
c9c5d94666 | ||
|
|
aad8f698dd | ||
|
|
35e107e87d | ||
|
|
d9b361f066 | ||
|
|
94e75cb3b8 | ||
|
|
c7fb64e2f3 | ||
|
|
ebddabe958 | ||
|
|
b81f7cfcab | ||
|
|
9197ef04f7 | ||
|
|
7e4d4cf680 | ||
|
|
0a73c2b7ab | ||
|
|
2ee3f58b69 | ||
|
|
f60af72a5e | ||
|
|
95125d30ef | ||
|
|
48a9fafe6c | ||
|
|
c4cc2fe731 | ||
|
|
3df0bd8890 | ||
|
|
2a5a0717aa | ||
|
|
ee8364913b | ||
|
|
3267126a33 | ||
|
|
2ee3675ba2 | ||
|
|
faf4fd1ca6 | ||
|
|
53ebf2ca27 | ||
|
|
f3eea9937c | ||
|
|
5c3de759d3 | ||
|
|
0ffd4d9f87 | ||
|
|
416131b4a2 | ||
|
|
7ac04d0204 | ||
|
|
a40337f4e0 | ||
|
|
b45d9aa464 | ||
|
|
48b2e2bc1f | ||
|
|
545007aca5 | ||
|
|
07ce319839 | ||
|
|
0d36c85568 | ||
|
|
139fbfae85 | ||
|
|
0568393157 | ||
|
|
7ec732977a | ||
|
|
a8a7cec376 | ||
|
|
7f6beeeecb | ||
|
|
15092407ea | ||
|
|
bd70d48cdf | ||
|
|
ce04e904e2 | ||
|
|
026ddbf9f1 | ||
|
|
628b178076 | ||
|
|
823a16e8e9 | ||
|
|
407ec1311e | ||
|
|
b9667aae6b | ||
|
|
806b437209 | ||
|
|
1a5232f863 | ||
|
|
7ad119f126 | ||
|
|
1682f4b252 | ||
|
|
6a320d545b | ||
|
|
e7aebd6fbc | ||
|
|
8189abd660 | ||
|
|
5ded153ae4 | ||
|
|
2fd5507c00 | ||
|
|
becb83faf1 | ||
|
|
32263b4574 | ||
|
|
fd3e48dcb2 | ||
|
|
69573cd735 | ||
|
|
5c2af42cdd | ||
|
|
42975b2ff3 | ||
|
|
c7063c00f7 | ||
|
|
89df9536e9 | ||
|
|
0e45c2246f | ||
|
|
81a6afde15 | ||
|
|
adcc8a919c | ||
|
|
a24e6d4278 | ||
|
|
776b2247dd | ||
|
|
37dc1f5ca0 | ||
|
|
a68ddab703 | ||
|
|
877f873910 | ||
|
|
53fa0147ae | ||
|
|
7655c5b150 | ||
|
|
235b625f71 | ||
|
|
014b0024a0 | ||
|
|
b0508e661a | ||
|
|
ab3cd6a8f7 | ||
|
|
85461204c5 | ||
|
|
3abf2b5227 | ||
|
|
0d5d7032fe | ||
|
|
c48b04ab99 | ||
|
|
eaa30dbe21 | ||
|
|
bb0f812f71 | ||
|
|
4c287075da | ||
|
|
09d18f9097 | ||
|
|
47b9bfc8bf | ||
|
|
21d13e8a9c | ||
|
|
079260a7cf | ||
|
|
fdec78c092 | ||
|
|
259ffef0bb | ||
|
|
6661a0803e | ||
|
|
c1471bdbd9 | ||
|
|
a981573e48 | ||
|
|
8fb3a7514e |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -20,10 +20,10 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.86.0
|
||||
RUST_VERSION: 1.88.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.82.0
|
||||
MSRV: 1.85.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt and clippy
|
||||
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
||||
- run: rustup override set $RUST_VERSION
|
||||
shell: bash
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
- name: Run rustfmt
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v2.3.0
|
||||
uses: dependabot/fetch-metadata@v2.4.0
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
|
||||
3
.github/workflows/nix.yml
vendored
3
.github/workflows/nix.yml
vendored
@@ -95,9 +95,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
installable:
|
||||
- deltachat-rpc-server-aarch64-darwin
|
||||
- deltachat-rpc-server
|
||||
|
||||
# Fails to bulid
|
||||
# - deltachat-rpc-server-aarch64-darwin
|
||||
# - deltachat-rpc-server-x86_64-darwin
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
2
.github/workflows/zizmor-scan.yml
vendored
2
.github/workflows/zizmor-scan.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Run zizmor
|
||||
run: uvx zizmor --format sarif . > results.sarif
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -53,3 +53,4 @@ result
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
.aider*
|
||||
|
||||
298
CHANGELOG.md
298
CHANGELOG.md
@@ -1,5 +1,287 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -203,8 +485,8 @@
|
||||
|
||||
- Use vCard in TestContext.add_or_lookup_contact().
|
||||
- Remove test_group_with_removed_message_id.
|
||||
- Use add_or_lookup_email_contact() in get_chat().
|
||||
- Use add_or_lookup_email_contact in test_setup_contact_ex.
|
||||
- Use add_or_lookup_address_contact() in get_chat().
|
||||
- Use add_or_lookup_address_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.
|
||||
@@ -6267,3 +6549,15 @@ 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
|
||||
|
||||
1617
Cargo.lock
generated
1617
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
47
Cargo.toml
47
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
edition = "2021"
|
||||
version = "2.8.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.85"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -18,6 +18,9 @@ opt-level = 1
|
||||
debug = 1
|
||||
opt-level = 0
|
||||
|
||||
[profile.fuzz]
|
||||
inherits = "test"
|
||||
|
||||
# Always optimize dependencies.
|
||||
# This does not apply to crates in the workspace.
|
||||
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
||||
@@ -41,9 +44,9 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.11.0", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
||||
@@ -56,25 +59,25 @@ fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.5"
|
||||
hickory-resolver = "0.25.2"
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.11"
|
||||
hyper-util = "0.1.14"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.33", default-features = false }
|
||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.35", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
libc = { workspace = true }
|
||||
mail-builder = { version = "0.4.2", default-features = false }
|
||||
mail-builder = { version = "0.4.3", default-features = false }
|
||||
mailparse = { workspace = true }
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num_cpus = "1.17"
|
||||
num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
parking_lot = "0.12.4"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.15.0", default-features = false }
|
||||
pgp = { version = "0.16.0", default-features = false }
|
||||
pin-project = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.37"
|
||||
@@ -83,7 +86,7 @@ rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustls-pki-types = "1.11.0"
|
||||
rustls-pki-types = "1.12.0"
|
||||
rustls = { version = "0.23.22", default-features = false }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -91,20 +94,21 @@ serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.0"
|
||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.15.1"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-io-timeout = "1.2.1"
|
||||
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.8"
|
||||
tracing = "0.1.41"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
@@ -112,7 +116,7 @@ blake3 = "1.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
@@ -186,11 +190,11 @@ nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.32"
|
||||
rusqlite = "0.36"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.19.1"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.14"
|
||||
@@ -201,7 +205,8 @@ yerpc = "0.6.4"
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"async-native-tls/vendored"
|
||||
]
|
||||
|
||||
[lints.rust]
|
||||
|
||||
26
README.md
26
README.md
@@ -84,26 +84,29 @@ Create a contact:
|
||||
|
||||
```
|
||||
> addcontact yourfriends@email.org
|
||||
Command executed successfully.
|
||||
```
|
||||
|
||||
List contacts:
|
||||
|
||||
```
|
||||
> listcontacts
|
||||
Contact#10: <name unset> <yourfriends@email.org>
|
||||
Contact#1: Me √√ <your@email.org>
|
||||
Contact#Contact#Self: Me √ <your@email.org>
|
||||
1 key contacts.
|
||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
||||
1 address contacts.
|
||||
```
|
||||
|
||||
Create a chat with your friend and send a message:
|
||||
|
||||
```
|
||||
> createchat 10
|
||||
Single#10 created successfully.
|
||||
> chat 10
|
||||
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||
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.
|
||||
> send hi
|
||||
Message sent.
|
||||
```
|
||||
|
||||
List messages when inside a chat:
|
||||
@@ -156,13 +159,13 @@ $ cargo test -- --ignored
|
||||
|
||||
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
|
||||
```sh
|
||||
$ cargo install cargo-bolero@0.8.0
|
||||
$ cargo install cargo-bolero
|
||||
```
|
||||
|
||||
Run fuzzing tests with
|
||||
```sh
|
||||
$ cd fuzz
|
||||
$ cargo bolero test fuzz_mailparse --release=false -s NONE
|
||||
$ cargo bolero test fuzz_mailparse -s NONE
|
||||
```
|
||||
|
||||
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
||||
@@ -170,11 +173,6 @@ you can add initial inputs there.
|
||||
For `fuzz_mailparse` target corpus can be populated with
|
||||
`../test-data/message/*.eml`.
|
||||
|
||||
To run with AFL instead of libFuzzer:
|
||||
```sh
|
||||
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||
|
||||
42
STYLE.md
42
STYLE.md
@@ -78,12 +78,40 @@ 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.
|
||||
@@ -96,3 +124,17 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||
```
|
||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||
```
|
||||
|
||||
## Documentation comments
|
||||
|
||||
All public modules, methods and fields should be documented.
|
||||
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
|
||||
|
||||
Private items do not have to be documented,
|
||||
but CI uses `cargo doc --document-private-items`
|
||||
to build the documentation,
|
||||
so it is preferred that new items
|
||||
are documented.
|
||||
|
||||
Follow Rust guidelines for the documentation comments:
|
||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
||||
|
||||
BIN
assets/icon-address-contact.png
Normal file
BIN
assets/icon-address-contact.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
47
assets/icon-address-contact.svg
Normal file
47
assets/icon-address-contact.svg
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
7
assets/self-reporting-bot.vcf
Normal file
7
assets/self-reporting-bot.vcf
Normal file
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -1,9 +1,11 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
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) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::accounts::Accounts;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
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();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::{
|
||||
Events,
|
||||
config::Config,
|
||||
context::Context,
|
||||
imex::{imex, ImexMode},
|
||||
imex::{ImexMode, imex},
|
||||
receive_imf::receive_imf,
|
||||
stock_str::StockStrings,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#![recursion_limit = "256"]
|
||||
use std::hint::black_box;
|
||||
use std::path::Path;
|
||||
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use deltachat::Events;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#![recursion_limit = "256"]
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{info, Event, EventType, Events};
|
||||
use deltachat::{Event, EventType, Events};
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn send_events_benchmark(context: &Context) {
|
||||
let emitter = context.get_event_emitter();
|
||||
for _i in 0..1_000_000 {
|
||||
info!(context, "interesting event...");
|
||||
context.emit_event(EventType::Info("interesting event...".to_string()));
|
||||
}
|
||||
info!(context, "DONE");
|
||||
context.emit_event(EventType::Info("DONE".to_string()));
|
||||
|
||||
loop {
|
||||
match emitter.recv().await.unwrap() {
|
||||
|
||||
@@ -76,7 +76,7 @@ impl ContactAddress {
|
||||
|
||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
||||
impl rusqlite::types::ToSql for ContactAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
@@ -282,7 +282,7 @@ impl EmailAddress {
|
||||
}
|
||||
|
||||
impl rusqlite::types::ToSql for EmailAddress {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let val = rusqlite::types::Value::Text(self.to_string());
|
||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
||||
Ok(out)
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct VcardContact {
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The biography, stored in the vcard property `note`
|
||||
pub biography: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<i64>,
|
||||
}
|
||||
@@ -44,10 +46,15 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
fn escape(s: &str) -> String {
|
||||
s.replace(',', "\\,")
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
||||
let addr = escape(&c.addr);
|
||||
let display_name = escape(c.display_name());
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
@@ -55,10 +62,13 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
FN:{display_name}\r\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(biography) = &c.biography {
|
||||
res += &format!("NOTE:{}\r\n", escape(biography));
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
@@ -79,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Returns (parameters, value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
/// Returns (parameters, raw value) tuple.
|
||||
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
let remainder = remove_prefix(line, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
@@ -110,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
Some((params, value))
|
||||
}
|
||||
/// Returns (parameters, unescaped value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
||||
let (params, value) = vcard_property_raw(line, property)?;
|
||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
||||
Some((params, value.replace("\\,", ",")))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "key")?;
|
||||
let (params, value) = vcard_property_raw(line, "key")?;
|
||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
||||
}
|
||||
fn base64_photo(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "photo")?;
|
||||
let (params, value) = vcard_property_raw(line, "photo")?;
|
||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||
@@ -136,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
remove_prefix(value, "data:image/jpeg;base64\\,")
|
||||
// Old Delta Chat format.
|
||||
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
@@ -186,6 +194,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut biography = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for mut line in lines.by_ref() {
|
||||
@@ -205,18 +214,24 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = base64_photo(line) {
|
||||
photo.get_or_insert(p);
|
||||
} else if let Some((_params, bio)) = vcard_property(line, "note") {
|
||||
biography.get_or_insert(bio);
|
||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
let (authname, addr) = sanitize_name_and_addr(
|
||||
&display_name.unwrap_or_default(),
|
||||
&addr.unwrap_or_default(),
|
||||
);
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
biography,
|
||||
timestamp: datetime
|
||||
.as_deref()
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
|
||||
@@ -91,6 +91,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
biography: Some("Hi, I'm Alice".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
@@ -98,6 +99,7 @@ fn test_make_and_parse_vcard() {
|
||||
authname: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
biography: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
@@ -106,8 +108,9 @@ fn test_make_and_parse_vcard() {
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
||||
NOTE:Hi\\, I'm Alice\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
@@ -246,7 +249,8 @@ END:VCARD",
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
/// Proton at some point slightly changed the format of their vcards
|
||||
/// Proton at some point slightly changed the format of their vcards.
|
||||
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
||||
#[test]
|
||||
fn test_protonmail_vcard2() {
|
||||
let contacts = parse_vcard(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
<doxygenlayout version="1.0">
|
||||
<!-- Generated by doxygen 1.8.20 -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doxygenlayout version="2.0">
|
||||
<!-- Generated by doxygen 1.13.2 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
@@ -11,10 +12,16 @@
|
||||
</tab>
|
||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="modules" visible="yes" title="" intro="">
|
||||
<tab type="modulelist" visible="yes" title="" intro=""/>
|
||||
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="namespaces" visible="yes" title="">
|
||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="concepts" visible="yes" title="">
|
||||
</tab>
|
||||
<tab type="interfaces" visible="yes" title="">
|
||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
@@ -35,4 +42,228 @@
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<inheritancegraph visible="yes"/>
|
||||
<collaborationgraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicmethods visible="yes" title=""/>
|
||||
<publicstaticmethods visible="yes" title=""/>
|
||||
<publicattributes visible="yes" title=""/>
|
||||
<publicstaticattributes visible="yes" title=""/>
|
||||
<protectedtypes visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<protectedmethods visible="yes" title=""/>
|
||||
<protectedstaticmethods visible="yes" title=""/>
|
||||
<protectedattributes visible="yes" title=""/>
|
||||
<protectedstaticattributes visible="yes" title=""/>
|
||||
<packagetypes visible="yes" title=""/>
|
||||
<packagemethods visible="yes" title=""/>
|
||||
<packagestaticmethods visible="yes" title=""/>
|
||||
<packageattributes visible="yes" title=""/>
|
||||
<packagestaticattributes visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<privatetypes visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<privatemethods visible="yes" title=""/>
|
||||
<privatestaticmethods visible="yes" title=""/>
|
||||
<privateattributes visible="yes" title=""/>
|
||||
<privatestaticattributes visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<related visible="yes" title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<constructors visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<related visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes" visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a concept page -->
|
||||
<concept>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<definition visible="yes" title=""/>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<authorsection visible="yes"/>
|
||||
</concept>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="yes"/>
|
||||
<includedbygraph visible="yes"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes" visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<groupgraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<modules visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a C++20 module page -->
|
||||
<module>
|
||||
<briefdescription visible="yes"/>
|
||||
<exportedmodules visible="yes"/>
|
||||
<memberdecl>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<membergroups visible="yes" title=""/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdecl>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
</module>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
|
||||
@@ -503,13 +503,6 @@ 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.
|
||||
@@ -2132,8 +2125,12 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
|
||||
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
|
||||
|
||||
|
||||
#define DC_GCL_VERIFIED_ONLY 0x01
|
||||
|
||||
// Deprecated 2025-05-20, setting this flag is a no-op.
|
||||
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
|
||||
|
||||
#define DC_GCL_ADD_SELF 0x02
|
||||
#define DC_GCL_ADDRESS 0x04
|
||||
|
||||
|
||||
/**
|
||||
@@ -2189,13 +2186,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:
|
||||
* - 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.
|
||||
* - 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.
|
||||
* @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
|
||||
@@ -3814,21 +3811,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* 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.
|
||||
@@ -3837,6 +3825,21 @@ 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.
|
||||
*
|
||||
@@ -3850,6 +3853,8 @@ int dc_chat_is_protected (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.
|
||||
@@ -4284,11 +4289,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a padlock should be shown beside the message.
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
|
||||
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
|
||||
*/
|
||||
int dc_msg_get_showpadlock (const dc_msg_t* msg);
|
||||
|
||||
@@ -4511,12 +4521,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 now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is 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.
|
||||
@@ -4569,9 +4579,10 @@ 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
|
||||
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
|
||||
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_CHAT_E2EE 50
|
||||
|
||||
|
||||
/**
|
||||
@@ -5242,20 +5253,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* 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()).
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @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; in the UI typically checkmarks are shown.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
@@ -5269,19 +5274,39 @@ 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.
|
||||
*
|
||||
* 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.
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* 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.
|
||||
* 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
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5714,9 +5739,33 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
#define DC_CHAT_TYPE_MAILINGLIST 140
|
||||
|
||||
/**
|
||||
* A broadcast list. See dc_chat_get_type() for details.
|
||||
* 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.
|
||||
*/
|
||||
#define DC_CHAT_TYPE_BROADCAST 160
|
||||
#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
|
||||
|
||||
/**
|
||||
* @}
|
||||
@@ -6323,7 +6372,6 @@ 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().
|
||||
*
|
||||
@@ -6836,9 +6884,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in summaries.
|
||||
#define DC_STR_GIF 23
|
||||
|
||||
/// "Encrypted message"
|
||||
///
|
||||
/// Used in subjects of outgoing messages.
|
||||
/// @deprecated 2025-07, this string is no longer needed.
|
||||
#define DC_STR_ENCRYPTEDMSG 24
|
||||
|
||||
/// "End-to-end encryption available."
|
||||
@@ -6885,6 +6931,7 @@ 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"
|
||||
@@ -6897,12 +6944,14 @@ 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"
|
||||
@@ -7292,6 +7341,7 @@ 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.
|
||||
@@ -7358,7 +7408,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||
|
||||
/// "You left the group."
|
||||
/// "You left."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
||||
@@ -7539,7 +7589,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 guaranteed to be end-to-end encrypted from now on."
|
||||
/// "Messages are end-to-end encrypted."
|
||||
///
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||
@@ -7547,6 +7597,7 @@ 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."
|
||||
@@ -7598,8 +7649,12 @@ 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
|
||||
|
||||
|
||||
@@ -235,7 +235,10 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
} else {
|
||||
match config::Config::from_str(&key) {
|
||||
match config::Config::from_str(&key)
|
||||
.context("Invalid config key")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
.set_config(key, value.as_deref())
|
||||
.await
|
||||
@@ -244,10 +247,7 @@ pub unsafe extern "C" fn dc_set_config(
|
||||
})
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_set_config(): invalid key");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -276,7 +276,10 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
} else {
|
||||
match config::Config::from_str(&key) {
|
||||
match config::Config::from_str(&key)
|
||||
.with_context(|| format!("Invalid key {:?}", &key))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(key) => ctx
|
||||
.get_config(key)
|
||||
.await
|
||||
@@ -285,10 +288,7 @@ pub unsafe extern "C" fn dc_get_config(
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.strdup(),
|
||||
Err(_) => {
|
||||
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
|
||||
"".strdup()
|
||||
}
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -308,18 +308,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match StockMessage::from_u32(stock_id) {
|
||||
Some(id) => match ctx.set_stock_translation(id, msg).await {
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
warn!(ctx, "set_stock_translation failed: {err:#}");
|
||||
0
|
||||
}
|
||||
},
|
||||
None => {
|
||||
warn!(ctx, "invalid stock message id {stock_id}");
|
||||
0
|
||||
}
|
||||
match StockMessage::from_u32(stock_id)
|
||||
.with_context(|| format!("Invalid stock message ID {stock_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(id) => ctx
|
||||
.set_stock_translation(id, msg)
|
||||
.await
|
||||
.context("set_stock_translation failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int,
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -336,15 +335,10 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
|
||||
let qr = to_string_lossy(qr);
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match qr::set_config_from_qr(ctx, &qr).await {
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to create account from QR code: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(qr::set_config_from_qr(ctx, &qr))
|
||||
.context("Failed to create account from QR code")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -354,15 +348,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
match ctx.get_info().await {
|
||||
Ok(info) => render_info(info).unwrap_or_default().strdup(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "failed to get info: {err:#}");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(ctx.get_info())
|
||||
.context("Failed to get info")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(info) => render_info(info).unwrap_or_default().strdup(),
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_info(
|
||||
@@ -395,15 +387,13 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
match ctx.get_connectivity_html().await {
|
||||
Ok(html) => html.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to get connectivity html: {err:#}");
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(ctx.get_connectivity_html())
|
||||
.context("Failed to get connectivity html")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(html) => html.strdup(),
|
||||
Err(_) => "".strdup(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1255,22 +1245,19 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match ChatId::new(chat_id).get_draft(ctx).await {
|
||||
Ok(Some(draft)) => {
|
||||
let ffi_msg = MessageWrapper {
|
||||
context,
|
||||
message: draft,
|
||||
};
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed to get draft for chat #{chat_id}: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
match block_on(ChatId::new(chat_id).get_draft(ctx))
|
||||
.with_context(|| format!("Failed to get draft for chat #{chat_id}"))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(draft) => {
|
||||
let ffi_msg = MessageWrapper {
|
||||
context,
|
||||
message: draft,
|
||||
};
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
})
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1526,10 +1513,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||
1 => ChatVisibility::Archived,
|
||||
2 => ChatVisibility::Pinned,
|
||||
_ => {
|
||||
warn!(
|
||||
ctx,
|
||||
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
|
||||
);
|
||||
eprintln!("ignoring careless call to dc_set_chat_visibility(): unknown archived state");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -1683,10 +1667,11 @@ pub unsafe extern "C" fn dc_create_group_chat(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
||||
s
|
||||
} else {
|
||||
warn!(ctx, "bad protect-value for dc_create_group_chat()");
|
||||
let Some(protect) = ProtectionStatus::from_i32(protect)
|
||||
.context("Bad protect-value for dc_create_group_chat()")
|
||||
.log_err(ctx)
|
||||
.ok()
|
||||
else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
@@ -1707,8 +1692,8 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
block_on(chat::create_broadcast_list(ctx))
|
||||
.context("Failed to create broadcast list")
|
||||
block_on(chat::create_broadcast(ctx, "Channel".to_string()))
|
||||
.context("Failed to create broadcast channel")
|
||||
.log_err(ctx)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
@@ -1832,23 +1817,20 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let muteDuration = match duration {
|
||||
let mute_duration = match duration {
|
||||
0 => MuteDuration::NotMuted,
|
||||
-1 => MuteDuration::Forever,
|
||||
n if n > 0 => SystemTime::now()
|
||||
.checked_add(Duration::from_secs(duration as u64))
|
||||
.map_or(MuteDuration::Forever, MuteDuration::Until),
|
||||
_ => {
|
||||
warn!(
|
||||
ctx,
|
||||
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
|
||||
);
|
||||
eprintln!("dc_chat_set_mute_duration(): Can not use negative duration other than -1");
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
|
||||
chat::set_muted(ctx, ChatId::new(chat_id), mute_duration)
|
||||
.await
|
||||
.map(|_| 1)
|
||||
.unwrap_or_log_default(ctx, "Failed to set mute duration")
|
||||
@@ -1866,16 +1848,10 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.get_encryption_info(ctx)
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
error!(ctx, "{e:#}");
|
||||
ptr::null_mut()
|
||||
})
|
||||
})
|
||||
block_on(ChatId::new(chat_id).get_encryption_info(ctx))
|
||||
.map(|s| s.strdup())
|
||||
.log_err(ctx)
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2032,12 +2008,10 @@ pub unsafe extern "C" fn dc_resend_msgs(
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
|
||||
error!(ctx, "Resending failed: {err:#}");
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
block_on(chat::resend_msgs(ctx, &msg_ids))
|
||||
.context("Resending failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2067,26 +2041,22 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)).await {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
|
||||
// C-core API returns empty messages, do the same
|
||||
warn!(
|
||||
ctx,
|
||||
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
|
||||
);
|
||||
message::Message::new(Viewtype::default())
|
||||
} else {
|
||||
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let message = match block_on(message::Message::load_from_db(ctx, MsgId::new(msg_id)))
|
||||
.with_context(|| format!("dc_get_msg could not rectieve msg_id {msg_id}"))
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(msg) => msg,
|
||||
Err(_) => {
|
||||
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
|
||||
// C-core API returns empty messages, do the same
|
||||
message::Message::new(Viewtype::default())
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
let ffi_msg = MessageWrapper { context, message };
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
})
|
||||
}
|
||||
};
|
||||
let ffi_msg = MessageWrapper { context, message };
|
||||
Box::into_raw(Box::new(ffi_msg))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2316,15 +2286,10 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
error!(ctx, "{e:#}");
|
||||
ptr::null_mut()
|
||||
})
|
||||
})
|
||||
block_on(Contact::get_encrinfo(ctx, ContactId::new(contact_id)))
|
||||
.map(|s| s.strdup())
|
||||
.log_err(ctx)
|
||||
.unwrap_or(ptr::null_mut())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2339,15 +2304,10 @@ pub unsafe extern "C" fn dc_delete_contact(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match Contact::delete(ctx, contact_id).await {
|
||||
Ok(_) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "cannot delete contact: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(Contact::delete(ctx, contact_id))
|
||||
.context("Cannot delete contact")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2418,17 +2378,13 @@ pub unsafe extern "C" fn dc_imex_has_backup(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::has_backup(ctx, to_string_lossy(dir).as_ref()).await {
|
||||
Ok(res) => res.strdup(),
|
||||
Err(err) => {
|
||||
// do not bubble up error to the user,
|
||||
// the ui will expect that the file does not exist or cannot be accessed
|
||||
warn!(ctx, "dc_imex_has_backup: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(imex::has_backup(ctx, to_string_lossy(dir).as_ref()))
|
||||
.context("dc_imex_has_backup")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2439,15 +2395,13 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::initiate_key_transfer(ctx).await {
|
||||
Ok(res) => res.strdup(),
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_initiate_key_transfer(): {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
})
|
||||
match block_on(imex::initiate_key_transfer(ctx))
|
||||
.context("dc_initiate_key_transfer()")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(res) => res.strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2462,17 +2416,14 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
|
||||
.await
|
||||
{
|
||||
Ok(()) => 1,
|
||||
Err(err) => {
|
||||
warn!(ctx, "dc_continue_key_transfer: {err:#}");
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
block_on(imex::continue_key_transfer(
|
||||
ctx,
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(setup_code),
|
||||
))
|
||||
.context("dc_continue_key_transfer")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2916,12 +2867,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_chat_id(index) {
|
||||
match ffi_list
|
||||
.list
|
||||
.get_chat_id(index)
|
||||
.context("get_chat_id failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(chat_id) => chat_id.to_u32(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_chat_id failed: {err:#}");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2936,12 +2889,14 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_msg_id(index) {
|
||||
match ffi_list
|
||||
.list
|
||||
.get_msg_id(index)
|
||||
.context("get_msg_id failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_msg_id failed: {err:#}");
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3095,13 +3050,16 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
|
||||
let ffi_chat = &*chat;
|
||||
|
||||
block_on(async move {
|
||||
match ffi_chat.chat.get_profile_image(&ffi_chat.context).await {
|
||||
Ok(Some(p)) => p.to_string_lossy().strdup(),
|
||||
Ok(None) => ptr::null_mut(),
|
||||
Err(err) => {
|
||||
error!(ffi_chat.context, "failed to get profile image: {err:#}");
|
||||
ptr::null_mut()
|
||||
}
|
||||
match ffi_chat
|
||||
.chat
|
||||
.get_profile_image(&ffi_chat.context)
|
||||
.await
|
||||
.context("Failed to get profile image")
|
||||
.log_err(&ffi_chat.context)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(p) => p.to_string_lossy().strdup(),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -3195,6 +3153,18 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
|
||||
ffi_chat.chat.is_protected() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_is_encrypted()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
|
||||
block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context))
|
||||
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||
if chat.is_null() {
|
||||
@@ -3258,22 +3228,20 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
|
||||
Ok(chat) => chat,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_get_chat_info_json() failed to load chat: {err:#}");
|
||||
return "".strdup();
|
||||
}
|
||||
let Ok(chat) = chat::Chat::load_from_db(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.context("dc_get_chat_info_json() failed to load chat")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
let info = match chat.get_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(
|
||||
ctx,
|
||||
"dc_get_chat_info_json() failed to get chat info: {err:#}"
|
||||
);
|
||||
return "".strdup();
|
||||
}
|
||||
let Ok(info) = chat
|
||||
.get_info(ctx)
|
||||
.await
|
||||
.context("dc_get_chat_info_json() failed to get chat info")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
|
||||
@@ -3533,18 +3501,15 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(async move {
|
||||
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {err:#}");
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
})
|
||||
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(ctx))
|
||||
.context("dc_msg_get_webxdc_info() failed to get info")
|
||||
.log_err(ctx)
|
||||
else {
|
||||
return "".strdup();
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4338,6 +4303,15 @@ pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::
|
||||
(*contact).contact.is_bot() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_key_contact()");
|
||||
return 0;
|
||||
}
|
||||
(*contact).contact.is_key_contact() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
@@ -4350,6 +4324,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
|
||||
.context("failed to get verifier")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
verifier_contact_id.to_u32()
|
||||
@@ -4574,13 +4549,10 @@ trait ResultExt<T, E> {
|
||||
|
||||
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
|
||||
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T {
|
||||
match self {
|
||||
Ok(t) => t,
|
||||
Err(err) => {
|
||||
error!(context, "{message}: {err:#}");
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
self.map_err(|err| anyhow::anyhow!("{err:#}"))
|
||||
.with_context(|| message.to_string())
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ pub enum Meaning {
|
||||
}
|
||||
|
||||
impl Lot {
|
||||
pub fn get_text1(&self) -> Option<Cow<str>> {
|
||||
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => match &summary.prefix {
|
||||
None => None,
|
||||
@@ -66,7 +66,7 @@ impl Lot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -19,6 +19,7 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||
use deltachat::context::get_info;
|
||||
use deltachat::ephemeral::Timer;
|
||||
use deltachat::imex;
|
||||
use deltachat::location;
|
||||
use deltachat::message::get_msg_read_receipts;
|
||||
use deltachat::message::{
|
||||
@@ -35,7 +36,6 @@ use deltachat::securejoin;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::EventEmitter;
|
||||
use deltachat::{imex, info};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
@@ -224,6 +224,14 @@ 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();
|
||||
@@ -354,6 +362,20 @@ 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?;
|
||||
@@ -912,7 +934,7 @@ impl CommandApi {
|
||||
/// explicitly as it may happen that oneself gets removed from a still existing
|
||||
/// group
|
||||
///
|
||||
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||
/// - for broadcast channels, 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.
|
||||
@@ -931,7 +953,7 @@ impl CommandApi {
|
||||
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
|
||||
}
|
||||
|
||||
/// Create a new group chat.
|
||||
/// Create a new encrypted group chat (with key-contacts).
|
||||
///
|
||||
/// After creation,
|
||||
/// the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
@@ -949,30 +971,52 @@ 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_chat(&ctx, protect, &name)
|
||||
chat::create_group_ex(&ctx, Some(protect), &name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
|
||||
/// Create a new broadcast list.
|
||||
/// Create a new unencrypted group chat.
|
||||
///
|
||||
/// Broadcast lists are similar to groups on the sending device,
|
||||
/// however, recipients get the messages in a read-only chat
|
||||
/// and will see who the other members are.
|
||||
///
|
||||
/// 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> {
|
||||
/// 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_broadcast_list(&ctx)
|
||||
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,
|
||||
/// 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 [`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> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_broadcast(&ctx, chat_name)
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
}
|
||||
@@ -1479,6 +1523,14 @@ 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,
|
||||
@@ -1490,8 +1542,10 @@ impl CommandApi {
|
||||
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
|
||||
}
|
||||
|
||||
/// Get a list of contacts.
|
||||
/// (formerly called getContacts2 in desktop)
|
||||
/// Returns known and unblocked contacts.
|
||||
///
|
||||
/// Formerly called `getContacts2` in Desktop.
|
||||
/// See [`Self::get_contact_ids`] for parameters and more info.
|
||||
async fn get_contacts(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1542,15 +1596,6 @@ 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,
|
||||
@@ -1919,12 +1964,10 @@ impl CommandApi {
|
||||
instance_msg_id: u32,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
if let Some(fut) = fut {
|
||||
tokio::spawn(async move {
|
||||
fut.await.ok();
|
||||
info!(ctx, "send_webxdc_realtime_advertisement done")
|
||||
});
|
||||
if let Some(fut) =
|
||||
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
|
||||
{
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,15 +21,39 @@ pub struct FullChat {
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// 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.
|
||||
/// 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
|
||||
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,
|
||||
@@ -47,7 +71,9 @@ 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,
|
||||
@@ -108,6 +134,7 @@ 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,
|
||||
@@ -159,6 +186,30 @@ 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,
|
||||
@@ -167,7 +218,9 @@ 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,
|
||||
}
|
||||
@@ -187,6 +240,7 @@ 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,
|
||||
|
||||
@@ -23,6 +23,7 @@ pub enum ChatListItemFetchResult {
|
||||
name: String,
|
||||
avatar_path: Option<String>,
|
||||
color: String,
|
||||
chat_type: u32,
|
||||
last_updated: Option<i64>,
|
||||
summary_text1: String,
|
||||
summary_text2: String,
|
||||
@@ -30,6 +31,31 @@ 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,
|
||||
@@ -40,8 +66,6 @@ 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,
|
||||
@@ -131,12 +155,14 @@ 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(),
|
||||
@@ -147,7 +173,6 @@ 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,
|
||||
|
||||
@@ -19,15 +19,23 @@ 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 verified groups.
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// 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.
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
@@ -36,12 +44,29 @@ pub struct ContactObject {
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The ID of the contact that verified this contact.
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
/// 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.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
@@ -67,6 +92,7 @@ impl ContactObject {
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
@@ -80,6 +106,7 @@ 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,
|
||||
|
||||
@@ -224,7 +224,6 @@ 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().
|
||||
///
|
||||
|
||||
@@ -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,6 +59,13 @@ 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,
|
||||
@@ -90,8 +97,6 @@ pub struct MessageObject {
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
webxdc_href: Option<String>,
|
||||
|
||||
download_state: DownloadState,
|
||||
@@ -142,12 +147,6 @@ impl MessageObject {
|
||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||
|
||||
let download_state = message.download_state().into();
|
||||
@@ -261,7 +260,6 @@ impl MessageObject {
|
||||
file_mime: message.get_filemime(),
|
||||
file_bytes,
|
||||
file_name: message.get_filename(),
|
||||
webxdc_info,
|
||||
|
||||
// On a WebxdcInfoMessage this might include a hash holding
|
||||
// information about a specific position or state in a webxdc app
|
||||
@@ -418,6 +416,9 @@ pub enum SystemMessageType {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat is e2ee
|
||||
ChatE2ee,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
@@ -452,6 +453,7 @@ 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,
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||
"dependencies": {
|
||||
"@deltachat/tiny-emitter": "3.0.0",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"yerpc": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/ws": "^7.2.4",
|
||||
"c8": "^7.10.0",
|
||||
"@types/chai": "^4.3.10",
|
||||
"@types/chai-as-promised": "^7.1.8",
|
||||
"@types/mocha": "^10.0.4",
|
||||
"@types/ws": "^8.5.9",
|
||||
"c8": "^8.0.1",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esbuild": "^0.17.9",
|
||||
"esbuild": "^0.25.5",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^9.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.23.2",
|
||||
"typescript": "^4.5.5",
|
||||
"prettier": "^3.5.3",
|
||||
"typedoc": "^0.28.5",
|
||||
"typescript": "^5.8.3",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"exports": {
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "2.8.0"
|
||||
}
|
||||
|
||||
@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
|
||||
const jsonCoverage =
|
||||
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
||||
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
||||
(key) => jsonCoverage.fnMap[key]
|
||||
(key) => jsonCoverage.fnMap[key],
|
||||
);
|
||||
const htmlCoverage = readFileSync(
|
||||
"./coverage/" + generatedFile + ".html",
|
||||
"utf8"
|
||||
"utf8",
|
||||
);
|
||||
const uncoveredLines = htmlCoverage
|
||||
.split("\n")
|
||||
.filter((line) => line.includes(`"function not covered"`));
|
||||
const uncoveredFunctions = uncoveredLines.map(
|
||||
(line) => />([\w_]+)\(/.exec(line)[1]
|
||||
(line) => />([\w_]+)\(/.exec(line)[1],
|
||||
);
|
||||
console.log(
|
||||
"\nUncovered api functions:\n" +
|
||||
uncoveredFunctions
|
||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||
.map(
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
|
||||
)
|
||||
.join("\n")
|
||||
.join("\n"),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
|
||||
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1;
|
||||
@@ -50,5 +50,5 @@ const constants = data
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../generated/constants.ts"),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
|
||||
);
|
||||
|
||||
@@ -8,13 +8,13 @@ import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
accountId: number,
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||
[Property in EventType["kind"]]: (
|
||||
event: Extract<EventType, { kind: Property }>
|
||||
event: Extract<EventType, { kind: Property }>,
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||
>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
Transport extends BaseTransport<any>,
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
@@ -34,7 +34,10 @@ export class BaseDeltaChat<
|
||||
//@ts-ignore
|
||||
private eventTask: Promise<void>;
|
||||
|
||||
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||
constructor(
|
||||
public transport: Transport,
|
||||
startEventLoop: boolean,
|
||||
) {
|
||||
super();
|
||||
this.rpc = new RawClient(this.transport);
|
||||
if (startEventLoop) {
|
||||
@@ -53,7 +56,7 @@ export class BaseDeltaChat<
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.kind,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
event.event as any,
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||
}
|
||||
@@ -83,7 +86,10 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||
}
|
||||
|
||||
export class StdioTransport extends BaseTransport {
|
||||
constructor(public input: any, public output: any) {
|
||||
constructor(
|
||||
public input: any,
|
||||
public output: any,
|
||||
) {
|
||||
super();
|
||||
|
||||
var buffer = "";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { strictEqual } from "assert";
|
||||
import chai, { assert, expect } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
chai.use(chaiAsPromised);
|
||||
@@ -32,14 +31,14 @@ describe("basic tests", () => {
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||
),
|
||||
).to.not.contain(false);
|
||||
|
||||
expect(
|
||||
await Promise.all(
|
||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||
)
|
||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
||||
),
|
||||
).to.not.contain(true);
|
||||
});
|
||||
|
||||
@@ -85,7 +84,7 @@ describe("basic tests", () => {
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId,
|
||||
"example@delta.chat",
|
||||
null
|
||||
null,
|
||||
);
|
||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||
.false;
|
||||
@@ -127,7 +126,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
@@ -139,7 +138,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
@@ -153,7 +152,7 @@ describe("basic tests", () => {
|
||||
await dc.rpc.batchSetConfig(accountId, config);
|
||||
const retrieved = await dc.rpc.batchGetConfig(
|
||||
accountId,
|
||||
Object.keys(config)
|
||||
Object.keys(config),
|
||||
);
|
||||
expect(retrieved).to.deep.equal(config);
|
||||
});
|
||||
|
||||
@@ -17,12 +17,12 @@ describe("online tests", function () {
|
||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||
console.error(
|
||||
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(
|
||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -36,7 +36,7 @@ describe("online tests", function () {
|
||||
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||
if (!account1 || !account1.email || !account1.password) {
|
||||
console.log(
|
||||
"We didn't got back an account from the api, skip integration tests"
|
||||
"We didn't got back an account from the api, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -44,7 +44,7 @@ describe("online tests", function () {
|
||||
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||
if (!account2 || !account2.email || !account2.password) {
|
||||
console.log(
|
||||
"We didn't got back an account2 from the api, skip integration tests"
|
||||
"We didn't got back an account2 from the api, skip integration tests",
|
||||
);
|
||||
this.skip();
|
||||
}
|
||||
@@ -92,11 +92,13 @@ describe("online tests", function () {
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
// 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(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
@@ -124,11 +126,11 @@ describe("online tests", function () {
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
false,
|
||||
false
|
||||
false,
|
||||
);
|
||||
const message = await dc.rpc.getMessage(
|
||||
accountId2,
|
||||
messageList.reverse()[0]
|
||||
messageList.reverse()[0],
|
||||
);
|
||||
expect(message.text).equal("Hello2");
|
||||
// Send message back from B to A
|
||||
@@ -150,7 +152,7 @@ describe("online tests", function () {
|
||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||
expect(info).to.be.not.null;
|
||||
expect(info?.overviewPage).to.equal(
|
||||
"https://providers.delta.chat/example-com"
|
||||
"https://providers.delta.chat/example-com",
|
||||
);
|
||||
expect(info?.status).to.equal(3);
|
||||
});
|
||||
@@ -167,12 +169,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
|
||||
dc: DeltaChat,
|
||||
eventType: T,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
timeout: number = EVENT_TIMEOUT,
|
||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error("Timeout reached before event came in")),
|
||||
timeout
|
||||
timeout,
|
||||
);
|
||||
const callback = (contextId: number, event: DcEvent) => {
|
||||
if (contextId == accountId) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||
|
||||
const pathToServerBinary = resolve(
|
||||
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
||||
join(await getTargetDir(), "debug/deltachat-rpc-server"),
|
||||
);
|
||||
|
||||
const server = spawn(pathToServerBinary, {
|
||||
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
||||
throw new Error(
|
||||
"Failed to start server executable " +
|
||||
pathToServerBinary +
|
||||
", make sure you built it first."
|
||||
", make sure you built it first.",
|
||||
);
|
||||
});
|
||||
let shouldClose = false;
|
||||
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
@@ -13,7 +13,7 @@ log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
qr2term = "0.3.3"
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "15"
|
||||
rustyline = "16"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ 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;
|
||||
@@ -35,14 +34,6 @@ 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()
|
||||
@@ -96,7 +87,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 {
|
||||
println!("receive_imf errored: {err:?}");
|
||||
eprintln!("receive_imf errored: {err:?}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -120,7 +111,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
} else {
|
||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||
if rs.is_none() {
|
||||
error!(context, "Import: No file or folder given.");
|
||||
eprintln!("Import: No file or folder given.");
|
||||
return false;
|
||||
}
|
||||
real_spec = rs.unwrap();
|
||||
@@ -149,7 +140,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -277,7 +268,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 mut line2 = "".to_string();
|
||||
let line2 = "".to_string();
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
@@ -296,15 +287,6 @@ 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);
|
||||
}
|
||||
@@ -342,7 +324,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\n\
|
||||
import-keys <key-file>\n\
|
||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||
reset <flags>\n\
|
||||
stop\n\
|
||||
@@ -351,8 +333,6 @@ 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\
|
||||
@@ -367,21 +347,24 @@ 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\n\
|
||||
createbroadcast <name>\n\
|
||||
createprotected <name>\n\
|
||||
addmember <contact-id>\n\
|
||||
removemember <contact-id>\n\
|
||||
groupname <name>\n\
|
||||
groupimage [<file>]\n\
|
||||
groupimage <image>\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\
|
||||
@@ -400,7 +383,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\
|
||||
decline <chat-id>\n\
|
||||
blockchat <chat-id>\n\
|
||||
===========================Message commands==\n\
|
||||
listmsgs <query>\n\
|
||||
msginfo <msg-id>\n\
|
||||
@@ -414,11 +397,9 @@ 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\
|
||||
@@ -493,7 +474,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?;
|
||||
}
|
||||
@@ -508,13 +489,17 @@ 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: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||
ensure!(
|
||||
!arg1.is_empty(),
|
||||
"Argument <bits> missing: 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;
|
||||
@@ -636,7 +621,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Location streaming enabled.");
|
||||
}
|
||||
println!("{cnt} chats");
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"start-realtime" => {
|
||||
if arg1.is_empty() {
|
||||
@@ -746,7 +731,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();
|
||||
|
||||
println!(
|
||||
eprintln!(
|
||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||
);
|
||||
}
|
||||
@@ -765,7 +750,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
println!("Group#{chat_id} created successfully.");
|
||||
}
|
||||
"createbroadcast" => {
|
||||
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
||||
|
||||
println!("Broadcast#{chat_id} created successfully.");
|
||||
}
|
||||
@@ -999,7 +985,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
},
|
||||
query,
|
||||
);
|
||||
println!("{time_needed:?} to create this list");
|
||||
eprintln!("{time_needed:?} to create this list");
|
||||
}
|
||||
"draft" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
@@ -1162,19 +1148,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let reaction = arg2;
|
||||
send_reaction(&context, msg_id, reaction).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
if arg0 == "listverified" {
|
||||
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
||||
} else {
|
||||
DC_GCL_ADD_SELF
|
||||
},
|
||||
Some(arg1),
|
||||
)
|
||||
.await?;
|
||||
"listcontacts" | "contacts" => {
|
||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts.", contacts.len());
|
||||
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());
|
||||
}
|
||||
"addcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||
@@ -1247,7 +1227,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) => println!("Cannot set config from QR code: {err:?}"),
|
||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
||||
}
|
||||
}
|
||||
"createqrsvg" => {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||
//! All further options can be set using the set-command (type ? for help).
|
||||
|
||||
#[macro_use]
|
||||
extern crate deltachat;
|
||||
|
||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
@@ -41,25 +40,25 @@ fn receive_event(event: EventType) {
|
||||
match event {
|
||||
EventType::Info(msg) => {
|
||||
/* do not show the event as this would fill the screen */
|
||||
info!("{}", msg);
|
||||
info!("{msg}");
|
||||
}
|
||||
EventType::SmtpConnected(msg) => {
|
||||
info!("[SMTP_CONNECTED] {}", msg);
|
||||
info!("[SMTP_CONNECTED] {msg}");
|
||||
}
|
||||
EventType::ImapConnected(msg) => {
|
||||
info!("[IMAP_CONNECTED] {}", msg);
|
||||
info!("[IMAP_CONNECTED] {msg}");
|
||||
}
|
||||
EventType::SmtpMessageSent(msg) => {
|
||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||
info!("[SMTP_MESSAGE_SENT] {msg}");
|
||||
}
|
||||
EventType::Warning(msg) => {
|
||||
warn!("{}", msg);
|
||||
warn!("{msg}");
|
||||
}
|
||||
EventType::Error(msg) => {
|
||||
error!("{}", msg);
|
||||
error!("{msg}");
|
||||
}
|
||||
EventType::ErrorSelfNotInGroup(msg) => {
|
||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||
error!("[SELF_NOT_IN_GROUP] {msg}");
|
||||
}
|
||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||
info!(
|
||||
@@ -124,7 +123,7 @@ fn receive_event(event: EventType) {
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
info!("Received {:?}", event);
|
||||
info!("Received {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,9 +179,11 @@ const DB_COMMANDS: [&str; 11] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
"send-realtime",
|
||||
"chat",
|
||||
"createchat",
|
||||
"creategroup",
|
||||
@@ -198,13 +199,16 @@ const CHAT_COMMANDS: [&str; 36] = [
|
||||
"dellocations",
|
||||
"getlocations",
|
||||
"send",
|
||||
"sendempty",
|
||||
"sendimage",
|
||||
"sendsticker",
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"devicemsg",
|
||||
"listmedia",
|
||||
"archive",
|
||||
"unarchive",
|
||||
@@ -212,47 +216,46 @@ const CHAT_COMMANDS: [&str; 36] = [
|
||||
"unpin",
|
||||
"mute",
|
||||
"unmute",
|
||||
"protect",
|
||||
"unprotect",
|
||||
"delchat",
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||
const MESSAGE_COMMANDS: [&str; 10] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"download",
|
||||
"html",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"resend",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
const CONTACT_COMMANDS: [&str; 7] = [
|
||||
"listcontacts",
|
||||
"listverified",
|
||||
"addcontact",
|
||||
"contactinfo",
|
||||
"delcontact",
|
||||
"cleanupcontacts",
|
||||
"block",
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
const MISC_COMMANDS: [&str; 14] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"setqr",
|
||||
"createqrsvg",
|
||||
"providerinfo",
|
||||
"fileinfo",
|
||||
"estimatedeletion",
|
||||
"clear",
|
||||
"exit",
|
||||
"quit",
|
||||
"help",
|
||||
"estimatedeletion",
|
||||
];
|
||||
|
||||
impl Hinter for DcHelper {
|
||||
@@ -308,7 +311,7 @@ impl Validator for DcHelper {}
|
||||
|
||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
if args.len() < 2 {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = ContextBuilder::new(args[1].clone().into())
|
||||
@@ -363,7 +366,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
false
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -378,7 +381,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error: {err:#}");
|
||||
eprintln!("Error: {err:#}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -66,6 +66,9 @@ lint.select = [
|
||||
|
||||
"RUF006" # asyncio-dangling-task
|
||||
]
|
||||
lint.ignore = [
|
||||
"PLC0415" # `import` should be at the top-level of a file
|
||||
]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
"""Delta Chat JSON-RPC high-level API."""
|
||||
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
|
||||
@@ -115,7 +115,7 @@ def _run_cli(
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
"""extract email address from the given text."""
|
||||
"""Extract email address from the given text."""
|
||||
match = re.match(r".*\((.+@.+)\)", text)
|
||||
if match:
|
||||
text = match.group(1)
|
||||
@@ -124,7 +124,7 @@ def extract_addr(text: str) -> str:
|
||||
|
||||
|
||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||
"""return image changed/deleted info from parsing the given system message text."""
|
||||
"""Return image changed/deleted info from parsing the given system message text."""
|
||||
text = text.lower()
|
||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||
if match:
|
||||
@@ -143,7 +143,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
||||
|
||||
|
||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||
"""return add/remove info from parsing the given system message text.
|
||||
"""Return add/remove info from parsing the given system message text.
|
||||
|
||||
returns a (action, affected, actor) tuple.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Account module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
@@ -34,7 +36,10 @@ class Account:
|
||||
return next_event
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
"""Remove all queued-up events for a given account.
|
||||
|
||||
Useful for tests.
|
||||
"""
|
||||
self._rpc.clear_all_events(self.id)
|
||||
|
||||
def remove(self) -> None:
|
||||
@@ -43,7 +48,9 @@ class Account:
|
||||
|
||||
def clone(self) -> "Account":
|
||||
"""Clone given account.
|
||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
|
||||
|
||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
|
||||
"""
|
||||
future = self._rpc.provide_backup.future(self.id)
|
||||
qr = self._rpc.get_backup_qr(self.id)
|
||||
new_account = self.manager.add_account()
|
||||
@@ -80,7 +87,7 @@ class Account:
|
||||
return self._rpc.get_config(self.id, key)
|
||||
|
||||
def update_config(self, **kwargs) -> None:
|
||||
"""update config values."""
|
||||
"""Update config values."""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, value)
|
||||
|
||||
@@ -99,10 +106,12 @@ class Account:
|
||||
"""Parse QR code contents.
|
||||
|
||||
This function takes the raw text scanned
|
||||
and checks what can be done with it."""
|
||||
and checks what can be done with it.
|
||||
"""
|
||||
return self._rpc.check_qr(self.id, qr)
|
||||
|
||||
def set_config_from_qr(self, qr: str):
|
||||
"""Set configuration values from a QR code."""
|
||||
self._rpc.set_config_from_qr(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
@@ -117,7 +126,7 @@ class Account:
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Returns the list of all email accounts that are used as a transport in the current profile."""
|
||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
||||
transports = yield self._rpc.list_transports.future(self.id)
|
||||
return transports
|
||||
|
||||
@@ -158,7 +167,8 @@ class Account:
|
||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
||||
"""Import vCard.
|
||||
|
||||
Return created or modified contacts in the order they appear in vCard."""
|
||||
Return created or modified contacts in the order they appear in vCard.
|
||||
"""
|
||||
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
@@ -201,8 +211,8 @@ class Account:
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
*,
|
||||
with_self: bool = False,
|
||||
verified_only: bool = False,
|
||||
snapshot: bool = False,
|
||||
) -> Union[list[Contact], list[AttrDict]]:
|
||||
"""Get a filtered list of contacts.
|
||||
@@ -210,12 +220,9 @@ class Account:
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
:param with_self: if True the self-contact is also included if it matches the query.
|
||||
:param only_verified: if True only return verified contacts.
|
||||
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
||||
"""
|
||||
flags = 0
|
||||
if verified_only:
|
||||
flags |= ContactFlag.VERIFIED_ONLY
|
||||
if with_self:
|
||||
flags |= ContactFlag.ADD_SELF
|
||||
|
||||
@@ -227,12 +234,12 @@ class Account:
|
||||
|
||||
@property
|
||||
def self_contact(self) -> Contact:
|
||||
"""This account's identity as a Contact."""
|
||||
"""Account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
"""This account's device contact."""
|
||||
"""Account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
def get_chatlist(
|
||||
@@ -281,17 +288,52 @@ 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 and is in unpromoted state.
|
||||
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.
|
||||
"""
|
||||
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.
|
||||
@@ -361,22 +403,26 @@ class Account:
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event."""
|
||||
Consumes all events before the next incoming message event.
|
||||
"""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_securejoin_joiner_success(self):
|
||||
"""Wait until SecureJoin process finishes successfully on the joiner side."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
def wait_for_reactions_changed(self):
|
||||
"""Wait for reaction change event."""
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Chat module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
@@ -89,7 +91,8 @@ class Chat:
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat in seconds.
|
||||
|
||||
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||
0 means the timer is disabled, use 1 for immediate deletion.
|
||||
"""
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
def get_encryption_info(self) -> str:
|
||||
@@ -199,12 +202,12 @@ class Chat:
|
||||
return snapshot
|
||||
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
"""Get the list of messages in this chat."""
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
def get_fresh_message_count(self) -> int:
|
||||
"""Get number of fresh messages in this chat"""
|
||||
"""Get number of fresh messages in this chat."""
|
||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
|
||||
def mark_noticed(self) -> None:
|
||||
|
||||
@@ -48,6 +48,7 @@ class Client:
|
||||
self.add_hooks(hooks or [])
|
||||
|
||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||
"""Register multiple hooks."""
|
||||
for hook, event in hooks:
|
||||
self.add_hook(hook, event)
|
||||
|
||||
@@ -77,9 +78,11 @@ class Client:
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True if the client is configured."""
|
||||
return self.account.is_configured()
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
"""Configure the client."""
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
@@ -198,5 +201,6 @@ class Bot(Client):
|
||||
"""Simple bot implementation that listens to events of a single account."""
|
||||
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
"""Configure the bot."""
|
||||
kwargs.setdefault("bot", "1")
|
||||
super().configure(email, password, **kwargs)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
"""Constants module."""
|
||||
|
||||
from enum import Enum, IntEnum
|
||||
|
||||
COMMAND_PREFIX = "/"
|
||||
|
||||
|
||||
class ContactFlag(IntEnum):
|
||||
VERIFIED_ONLY = 0x01
|
||||
"""Bit flags for get_contacts() method."""
|
||||
|
||||
ADD_SELF = 0x02
|
||||
ADDRESS = 0x04
|
||||
|
||||
|
||||
class ChatlistFlag(IntEnum):
|
||||
"""Bit flags for get_chatlist() method."""
|
||||
|
||||
ARCHIVED_ONLY = 0x01
|
||||
NO_SPECIALS = 0x02
|
||||
ADD_ALLDONE_HINT = 0x04
|
||||
@@ -16,6 +22,8 @@ class ChatlistFlag(IntEnum):
|
||||
|
||||
|
||||
class SpecialContactId(IntEnum):
|
||||
"""Special contact IDs."""
|
||||
|
||||
SELF = 1
|
||||
INFO = 2 # centered messages as "member added", used in all chats
|
||||
DEVICE = 5 # messages "update info" in the device-chat
|
||||
@@ -23,7 +31,7 @@ class SpecialContactId(IntEnum):
|
||||
|
||||
|
||||
class EventType(str, Enum):
|
||||
"""Core event types"""
|
||||
"""Core event types."""
|
||||
|
||||
INFO = "Info"
|
||||
SMTP_CONNECTED = "SmtpConnected"
|
||||
@@ -71,7 +79,7 @@ class EventType(str, Enum):
|
||||
|
||||
|
||||
class ChatId(IntEnum):
|
||||
"""Special chat ids"""
|
||||
"""Special chat IDs."""
|
||||
|
||||
TRASH = 3
|
||||
ARCHIVED_LINK = 6
|
||||
@@ -80,17 +88,47 @@ class ChatId(IntEnum):
|
||||
|
||||
|
||||
class ChatType(IntEnum):
|
||||
"""Chat types"""
|
||||
"""Chat type."""
|
||||
|
||||
UNDEFINED = 0
|
||||
|
||||
SINGLE = 100
|
||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
||||
|
||||
GROUP = 120
|
||||
|
||||
MAILINGLIST = 140
|
||||
BROADCAST = 160
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
class ChatVisibility(str, Enum):
|
||||
"""Chat visibility types"""
|
||||
"""Chat visibility types."""
|
||||
|
||||
NORMAL = "Normal"
|
||||
ARCHIVED = "Archived"
|
||||
@@ -98,7 +136,7 @@ class ChatVisibility(str, Enum):
|
||||
|
||||
|
||||
class DownloadState(str, Enum):
|
||||
"""Message download state"""
|
||||
"""Message download state."""
|
||||
|
||||
DONE = "Done"
|
||||
AVAILABLE = "Available"
|
||||
@@ -159,14 +197,14 @@ class MessageState(IntEnum):
|
||||
|
||||
|
||||
class MessageId(IntEnum):
|
||||
"""Special message ids"""
|
||||
"""Special message IDs."""
|
||||
|
||||
DAYMARKER = 9
|
||||
LAST_SPECIAL = 9
|
||||
|
||||
|
||||
class CertificateChecks(IntEnum):
|
||||
"""Certificate checks mode"""
|
||||
"""Certificate checks mode."""
|
||||
|
||||
AUTOMATIC = 0
|
||||
STRICT = 1
|
||||
@@ -174,7 +212,7 @@ class CertificateChecks(IntEnum):
|
||||
|
||||
|
||||
class Connectivity(IntEnum):
|
||||
"""Connectivity states"""
|
||||
"""Connectivity states."""
|
||||
|
||||
NOT_CONNECTED = 1000
|
||||
CONNECTING = 2000
|
||||
@@ -183,7 +221,7 @@ class Connectivity(IntEnum):
|
||||
|
||||
|
||||
class KeyGenType(IntEnum):
|
||||
"""Type of the key to generate"""
|
||||
"""Type of the key to generate."""
|
||||
|
||||
DEFAULT = 0
|
||||
RSA2048 = 1
|
||||
@@ -193,21 +231,21 @@ class KeyGenType(IntEnum):
|
||||
|
||||
# "Lp" means "login parameters"
|
||||
class LpAuthFlag(IntEnum):
|
||||
"""Authorization flags"""
|
||||
"""Authorization flags."""
|
||||
|
||||
OAUTH2 = 0x2
|
||||
NORMAL = 0x4
|
||||
|
||||
|
||||
class MediaQuality(IntEnum):
|
||||
"""Media quality setting"""
|
||||
"""Media quality setting."""
|
||||
|
||||
BALANCED = 0
|
||||
WORSE = 1
|
||||
|
||||
|
||||
class ProviderStatus(IntEnum):
|
||||
"""Provider status according to manual testing"""
|
||||
"""Provider status according to manual testing."""
|
||||
|
||||
OK = 1
|
||||
PREPARATION = 2
|
||||
@@ -215,7 +253,7 @@ class ProviderStatus(IntEnum):
|
||||
|
||||
|
||||
class PushNotifyState(IntEnum):
|
||||
"""Push notifications state"""
|
||||
"""Push notifications state."""
|
||||
|
||||
NOT_CONNECTED = 0
|
||||
HEARTBEAT = 1
|
||||
@@ -223,7 +261,7 @@ class PushNotifyState(IntEnum):
|
||||
|
||||
|
||||
class ShowEmails(IntEnum):
|
||||
"""Show emails mode"""
|
||||
"""Show emails mode."""
|
||||
|
||||
OFF = 0
|
||||
ACCEPTED_CONTACTS = 1
|
||||
@@ -231,7 +269,7 @@ class ShowEmails(IntEnum):
|
||||
|
||||
|
||||
class SocketSecurity(IntEnum):
|
||||
"""Socket security"""
|
||||
"""Socket security."""
|
||||
|
||||
AUTOMATIC = 0
|
||||
SSL = 1
|
||||
@@ -240,7 +278,7 @@ class SocketSecurity(IntEnum):
|
||||
|
||||
|
||||
class VideochatType(IntEnum):
|
||||
"""Video chat URL type"""
|
||||
"""Video chat URL type."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BASICWEBRTC = 1
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Contact module."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -11,8 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""
|
||||
Contact API.
|
||||
"""Contact API.
|
||||
|
||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||
"""
|
||||
@@ -36,17 +37,14 @@ 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, containing your fingerprint and
|
||||
the fingerprint of the contact.
|
||||
"""Get a multi-line encryption info.
|
||||
|
||||
Encryption info contains your fingerprint and the fingerprint of the contact.
|
||||
"""
|
||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
|
||||
@@ -66,4 +64,5 @@ class Contact:
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
"""Make a vCard for the contact."""
|
||||
return self.account.make_vcard([self])
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Account manager module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,12 +12,13 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class DeltaChat:
|
||||
"""
|
||||
Delta Chat accounts manager.
|
||||
"""Delta Chat accounts manager.
|
||||
|
||||
This is the root of the object oriented API.
|
||||
"""
|
||||
|
||||
def __init__(self, rpc: "Rpc") -> None:
|
||||
"""Initialize account manager."""
|
||||
self.rpc = rpc
|
||||
|
||||
def add_account(self) -> Account:
|
||||
@@ -37,9 +40,7 @@ class DeltaChat:
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network likely has come back or just that the network
|
||||
conditions might have changed.
|
||||
"""
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
def get_system_info(self) -> AttrDict:
|
||||
|
||||
@@ -36,7 +36,7 @@ class EventFilter(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def __hash__(self) -> int:
|
||||
"""Object's unique hash"""
|
||||
"""Object's unique hash."""
|
||||
|
||||
@abstractmethod
|
||||
def __eq__(self, other) -> bool:
|
||||
@@ -52,9 +52,7 @@ class EventFilter(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, event):
|
||||
"""Return True-like value if the event passed the filter and should be
|
||||
used, or False-like value otherwise.
|
||||
"""
|
||||
"""Return True-like value if the event passed the filter."""
|
||||
|
||||
|
||||
class RawEvent(EventFilter):
|
||||
@@ -82,31 +80,17 @@ class RawEvent(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Filter an event.
|
||||
|
||||
Return true if the event should be processed.
|
||||
"""
|
||||
if self.types and event.kind not in self.types:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class NewMessage(EventFilter):
|
||||
"""Matches whenever a new message arrives.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||
content.
|
||||
:param command: If set, only match messages with the given command (ex. /help).
|
||||
Setting this property implies `is_info==False`.
|
||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||
match messages from bots and users. If omitted or set to False
|
||||
only messages from users will be matched.
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
"""Matches whenever a new message arrives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -121,6 +105,25 @@ class NewMessage(EventFilter):
|
||||
is_info: Optional[bool] = None,
|
||||
func: Optional[Callable[["AttrDict"], bool]] = None,
|
||||
) -> None:
|
||||
"""Initialize a new message filter.
|
||||
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||
content.
|
||||
:param command: If set, only match messages with the given command (ex. /help).
|
||||
Setting this property implies `is_info==False`.
|
||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||
match messages from bots and users. If omitted or set to False
|
||||
only messages from users will be matched.
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
super().__init__(func=func)
|
||||
self.is_bot = is_bot
|
||||
self.is_info = is_info
|
||||
@@ -159,6 +162,7 @@ class NewMessage(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return true if if the event is a new message event."""
|
||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||
return False
|
||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||
@@ -199,6 +203,7 @@ class MemberListChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return true if if the event is a member addition event."""
|
||||
if self.added is not None and self.added != event.member_added:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
@@ -231,6 +236,7 @@ class GroupImageChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return True if event is matched."""
|
||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||
return False
|
||||
return self._call_func(event)
|
||||
@@ -256,13 +262,12 @@ class GroupNameChanged(EventFilter):
|
||||
return False
|
||||
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
"""Return True if event is matched."""
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
"""
|
||||
Helper class to collect event hooks that can later be added to a Delta Chat client.
|
||||
"""
|
||||
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Message module."""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from typing import TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .const import EventType
|
||||
@@ -37,6 +39,11 @@ 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)
|
||||
@@ -45,6 +52,7 @@ class Message:
|
||||
return None
|
||||
|
||||
def get_sender_contact(self) -> Contact:
|
||||
"""Return sender contact."""
|
||||
from_id = self.get_snapshot().from_id
|
||||
return self.account.get_contact_by_id(from_id)
|
||||
|
||||
@@ -53,6 +61,11 @@ class Message:
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
|
||||
"""Continue the Autocrypt Setup Message key transfer.
|
||||
|
||||
This function can be called on received Autocrypt Setup Message
|
||||
to import the key encrypted with the provided setup code.
|
||||
"""
|
||||
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
|
||||
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
@@ -62,6 +75,7 @@ class Message:
|
||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
"""Return a list of Webxdc status updates for Webxdc instance message."""
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
def get_info(self) -> str:
|
||||
@@ -69,6 +83,7 @@ class Message:
|
||||
return self._rpc.get_message_info(self.account.id, self.id)
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
"""Get info from a Webxdc message in JSON format."""
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
def wait_until_delivered(self) -> None:
|
||||
@@ -80,8 +95,10 @@ class Message:
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
"""Send an advertisement to join the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Pytest plugin module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -11,26 +13,38 @@ 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}
|
||||
@@ -40,6 +54,7 @@ class ACFactory:
|
||||
return account
|
||||
|
||||
def new_configured_bot(self) -> Bot:
|
||||
"""Create a new configured bot."""
|
||||
addr, password = self.get_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(addr, password)
|
||||
@@ -47,11 +62,13 @@ class ACFactory:
|
||||
|
||||
@futuremethod
|
||||
def get_online_account(self):
|
||||
"""Create a new account and start I/O."""
|
||||
account = yield self.new_configured_account.future()
|
||||
account.bring_online()
|
||||
return account
|
||||
|
||||
def get_online_accounts(self, num: int) -> list[Account]:
|
||||
"""Create multiple online accounts."""
|
||||
futures = [self.get_online_account.future() for _ in range(num)]
|
||||
return [f() for f in futures]
|
||||
|
||||
@@ -66,6 +83,10 @@ class ACFactory:
|
||||
return ac_clone
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
|
||||
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
|
||||
|
||||
Returned chat is a chat with ac2 from ac1 point of view.
|
||||
"""
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
@@ -77,6 +98,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
"""Send a message."""
|
||||
if not from_account:
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account)
|
||||
@@ -95,6 +117,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
"""Send a message and wait until recipient processes it."""
|
||||
self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
@@ -108,6 +131,7 @@ class ACFactory:
|
||||
|
||||
@pytest.fixture
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
"""RPC client fixture."""
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
@@ -115,6 +139,7 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
"""Return account factory fixture."""
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
|
||||
@@ -132,7 +157,7 @@ def data():
|
||||
raise Exception("Data path cannot be found")
|
||||
|
||||
def get_path(self, bn):
|
||||
"""return path of file or None if it doesn't exist."""
|
||||
"""Return path of file or None if it doesn't exist."""
|
||||
fn = os.path.join(self.path, *bn.split("/"))
|
||||
assert os.path.exists(fn)
|
||||
return fn
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""JSON-RPC client module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
@@ -12,16 +14,19 @@ from typing import Any, Iterator, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
pass
|
||||
"""JSON-RPC error."""
|
||||
|
||||
|
||||
class RpcFuture:
|
||||
"""RPC future waiting for RPC call result."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
|
||||
self.rpc = rpc
|
||||
self.request_id = request_id
|
||||
self.event = event
|
||||
|
||||
def __call__(self):
|
||||
"""Wait for the future to return the result."""
|
||||
self.event.wait()
|
||||
response = self.rpc.request_results.pop(self.request_id)
|
||||
if "error" in response:
|
||||
@@ -32,17 +37,19 @@ class RpcFuture:
|
||||
|
||||
|
||||
class RpcMethod:
|
||||
"""RPC method."""
|
||||
|
||||
def __init__(self, rpc: "Rpc", name: str):
|
||||
self.rpc = rpc
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args) -> Any:
|
||||
"""Synchronously calls JSON-RPC method."""
|
||||
"""Call JSON-RPC method synchronously."""
|
||||
future = self.future(*args)
|
||||
return future()
|
||||
|
||||
def future(self, *args) -> Any:
|
||||
"""Asynchronously calls JSON-RPC method."""
|
||||
"""Call JSON-RPC method asynchronously."""
|
||||
request_id = next(self.rpc.id_iterator)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -58,8 +65,13 @@ class RpcMethod:
|
||||
|
||||
|
||||
class Rpc:
|
||||
"""RPC client."""
|
||||
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
"""Initialize RPC client.
|
||||
|
||||
The given arguments will be passed to subprocess.Popen().
|
||||
"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
**kwargs.get("env", os.environ),
|
||||
@@ -81,6 +93,7 @@ class Rpc:
|
||||
self.events_thread: Thread
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start RPC server subprocess."""
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
@@ -130,6 +143,7 @@ class Rpc:
|
||||
self.close()
|
||||
|
||||
def reader_loop(self) -> None:
|
||||
"""Process JSON-RPC responses from the RPC server process output."""
|
||||
try:
|
||||
while line := self.process.stdout.readline():
|
||||
response = json.loads(line)
|
||||
@@ -157,12 +171,13 @@ class Rpc:
|
||||
logging.exception("Exception in the writer loop")
|
||||
|
||||
def get_queue(self, account_id: int) -> Queue:
|
||||
"""Get event queue corresponding to the given account ID."""
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
"""Request new events and distributes them between queues."""
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
@@ -178,12 +193,12 @@ class Rpc:
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
"""Wait for the next event from the given account and returns it."""
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def clear_all_events(self, account_id: int):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
"""Remove all queued-up events for a given account. Useful for tests."""
|
||||
queue = self.get_queue(account_id)
|
||||
try:
|
||||
while True:
|
||||
|
||||
@@ -36,6 +36,9 @@ 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
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -16,14 +15,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.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
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.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -84,7 +83,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
@@ -93,7 +92,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.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
@@ -101,7 +100,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.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice2_contact_bob = alice2.create_contact(bob)
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
@@ -213,72 +212,8 @@ def test_setup_contact_resetup(acfactory) -> None:
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
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")
|
||||
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!")
|
||||
|
||||
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."""
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
@@ -291,7 +226,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.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
@@ -299,6 +234,8 @@ 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)
|
||||
|
||||
@@ -311,21 +248,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
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.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
|
||||
ac3_contact_ac2 = ac3.create_contact(ac2)
|
||||
ac3_chat.remove_contact(ac3_contact_ac2_old)
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
@@ -354,19 +280,16 @@ 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.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
assert ac1_contact_ac2_snapshot.is_verified
|
||||
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
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
"""Regression test for
|
||||
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
|
||||
issue <https://github.com/chatmail/core/issues/4894>.
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
@@ -400,12 +323,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.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
|
||||
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||
assert ac2.create_contact(ac3).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.get_contact_by_addr(ac2.get_config("addr")))
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
@@ -443,7 +366,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.get_contact_by_addr(ac2.get_config("addr")))
|
||||
ac1_new_chat.add_contact(ac1.create_contact(ac2))
|
||||
# Receive "Member added" message.
|
||||
ac2.wait_for_incoming_msg_event()
|
||||
|
||||
@@ -577,30 +500,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 resetups the account.
|
||||
ac1 = acfactory.resetup_account(ac1)
|
||||
|
||||
# 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)
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
@@ -11,7 +11,8 @@ from unittest.mock import MagicMock
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
|
||||
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@@ -170,7 +171,7 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contact_by_addr(bob_addr) is None # There is no address-contact, only key-contact
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
@@ -287,7 +288,6 @@ 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()
|
||||
@@ -458,8 +458,12 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
|
||||
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 snapshot.text == "Hello!"
|
||||
|
||||
|
||||
@@ -727,6 +731,26 @@ 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")
|
||||
@@ -847,3 +871,36 @@ 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()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "2.8.0"
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn main_impl() -> Result<()> {
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let writable = true;
|
||||
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||
|
||||
@@ -97,7 +97,7 @@ async fn main_impl() -> Result<()> {
|
||||
Some(message) => serde_json::to_string(&message)?,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC send {}", message);
|
||||
log::trace!("RPC send {message}");
|
||||
println!("{message}");
|
||||
}
|
||||
Ok(())
|
||||
@@ -141,7 +141,7 @@ async fn main_impl() -> Result<()> {
|
||||
Some(message) => message,
|
||||
}
|
||||
};
|
||||
log::trace!("RPC recv {}", message);
|
||||
log::trace!("RPC recv {message}");
|
||||
let session = session.clone();
|
||||
tokio::spawn(async move {
|
||||
session.handle_incoming(&message).await;
|
||||
|
||||
19
deny.toml
19
deny.toml
@@ -10,9 +10,6 @@ ignore = [
|
||||
# Unmaintained instant
|
||||
"RUSTSEC-2024-0384",
|
||||
|
||||
# Unmaintained backoff
|
||||
"RUSTSEC-2025-0012",
|
||||
|
||||
# Unmaintained paste
|
||||
"RUSTSEC-2024-0436",
|
||||
]
|
||||
@@ -25,26 +22,27 @@ ignore = [
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "derive_more-impl", version = "1.0.0" },
|
||||
{ name = "derive_more", version = "1.0.0" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "generator", version = "0.7.5" },
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "hashbrown", version = "0.14.5" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "lru", version = "0.12.3" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "nom", version = "7.1.3" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.4.1" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "serdect", version = "0.2.0" },
|
||||
{ name = "spin", version = "0.9.8" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
@@ -84,6 +82,7 @@ allow = [
|
||||
"MPL-2.0",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"Unlicense",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
|
||||
24
flake.lock
generated
24
flake.lock
generated
@@ -47,11 +47,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1737527504,
|
||||
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
|
||||
"lastModified": 1747291057,
|
||||
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
|
||||
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -147,11 +147,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1737469691,
|
||||
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +175,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1731139594,
|
||||
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,11 +202,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1737453499,
|
||||
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
|
||||
"lastModified": 1746889290,
|
||||
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
|
||||
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -6,7 +6,7 @@ edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
bolero = "0.8"
|
||||
bolero = "0.13.3"
|
||||
|
||||
[dependencies]
|
||||
mailparse = { workspace = true }
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,10 +1,7 @@
|
||||
import echo_and_quit
|
||||
import group_tracking
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from deltachat.events import FFIEventLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def datadir():
|
||||
@@ -36,55 +33,3 @@ 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"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "2.8.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
@@ -47,6 +47,10 @@ line-length = 120
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
|
||||
lint.ignore = [
|
||||
"PLC0415", # `import` should be at the top-level of a file
|
||||
"PLW1641" # Object does not implement `__hash__` method
|
||||
]
|
||||
line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
|
||||
@@ -293,6 +293,8 @@ 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)
|
||||
@@ -355,20 +357,16 @@ class Account:
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
only_verified: bool = False,
|
||||
) -> List[Contact]:
|
||||
"""get a (filtered) list of contacts.
|
||||
|
||||
:param query: if a string is specified, only return contacts
|
||||
whose name or e-mail matches query.
|
||||
:param only_verified: if true only return verified contacts.
|
||||
:param with_self: if true the self-contact is also returned.
|
||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
||||
"""
|
||||
flags = 0
|
||||
query_c = as_dc_charpointer(query)
|
||||
if only_verified:
|
||||
flags |= const.DC_GCL_VERIFIED_ONLY
|
||||
if with_self:
|
||||
flags |= const.DC_GCL_ADD_SELF
|
||||
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
|
||||
|
||||
@@ -417,7 +417,13 @@ class Chat:
|
||||
:raises ValueError: if contact could not be added
|
||||
:returns: None
|
||||
"""
|
||||
contact = self.account.create_contact(obj)
|
||||
from .contact import Contact
|
||||
|
||||
if isinstance(obj, Contact):
|
||||
contact = obj
|
||||
else:
|
||||
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")
|
||||
|
||||
@@ -13,7 +13,6 @@ 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={}):
|
||||
@@ -304,21 +303,15 @@ 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 map_system_message(msg) or ("ac_incoming_message", {"message": msg})
|
||||
yield ("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 map_system_message(msg) or (
|
||||
"ac_incoming_message",
|
||||
{"message": msg},
|
||||
)
|
||||
yield "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)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union
|
||||
|
||||
@@ -504,56 +503,3 @@ 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
|
||||
|
||||
@@ -20,6 +20,12 @@ 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")
|
||||
@@ -606,7 +612,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 guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg.text == "Messages are end-to-end encrypted."
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg is not None
|
||||
assert "Member Me " in msg.text and " added by " in msg.text
|
||||
|
||||
@@ -133,8 +133,7 @@ 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 guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
@@ -187,83 +186,6 @@ 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)
|
||||
|
||||
@@ -415,7 +337,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 guaranteed to be end-to-end encrypted from now on."
|
||||
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
|
||||
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
@@ -434,9 +356,10 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
|
||||
- Second device comes online, receives these new messages.
|
||||
The result is an unverified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the seconds device has all members verified.
|
||||
- Now the second device has all members and group verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.remove_preconfigured_keys()
|
||||
@@ -474,12 +397,12 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
|
||||
assert msg_in.text == "hi"
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert chat2_offl.is_protected()
|
||||
assert not chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
@@ -488,7 +411,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 guaranteed to be end-to-end encrypted from now on."
|
||||
assert msg_in.text == "Messages are end-to-end encrypted."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
@@ -500,6 +423,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.chat.is_protected()
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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):
|
||||
@@ -408,6 +409,10 @@ 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)
|
||||
@@ -622,6 +627,11 @@ 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)
|
||||
|
||||
@@ -738,7 +748,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
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("disable ac1 MDNs")
|
||||
ac1.set_config("mdns_enabled", "0")
|
||||
@@ -746,7 +756,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
|
||||
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
lp.sec("ac2: mark incoming message as seen")
|
||||
ac2.mark_seen_messages([msg])
|
||||
@@ -755,7 +765,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
|
||||
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
|
||||
|
||||
# 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.")
|
||||
@@ -767,7 +777,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
def test_send_receive_encrypt(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
ac1.get_device_chat().mark_noticed()
|
||||
@@ -798,12 +808,11 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
msg3.mark_seen()
|
||||
assert not list(ac1.get_fresh_messages())
|
||||
|
||||
lp.sec("create group chat with two members, one of which has no encrypt state")
|
||||
lp.sec("create group chat with two members")
|
||||
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 not msg.is_encrypted()
|
||||
assert msg.is_encrypted()
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
|
||||
@@ -1124,6 +1133,11 @@ 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)
|
||||
@@ -1139,9 +1153,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_addr, name="some1").create_chat()
|
||||
chat1 = ac1.create_contact(some1).create_chat()
|
||||
chat1.send_text("msg1")
|
||||
assert len(ac1.get_contacts(query="some1")) == 1
|
||||
assert len(ac1.get_contacts()) == 1
|
||||
|
||||
original_image_path = data.get_path("d.png")
|
||||
chat1.send_image(original_image_path)
|
||||
@@ -1153,16 +1167,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(query="some1")
|
||||
contacts = ac.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == some1_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
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
|
||||
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
|
||||
ac.set_config("displayname", "new displayname")
|
||||
assert ac.get_config("displayname") == "new displayname"
|
||||
|
||||
@@ -1288,79 +1302,6 @@ 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.
|
||||
@@ -1488,8 +1429,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
|
||||
assert msgs[0].text == "Hi"
|
||||
assert len(msgs) == 1 + E2EE_INFO_MSGS
|
||||
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
|
||||
|
||||
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
|
||||
|
||||
@@ -1499,8 +1440,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
|
||||
assert msgs[1].text == "Hi 2"
|
||||
assert len(msgs) == 2 + E2EE_INFO_MSGS
|
||||
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
@@ -1617,7 +1558,7 @@ def test_immediate_autodelete(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: wait for close/expunge on autodelete")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
ac2._evtracker.get_info_contains("close/expunge succeeded")
|
||||
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
|
||||
|
||||
lp.sec("ac2: check that message was autodeleted on server")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 0
|
||||
@@ -1653,7 +1594,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
|
||||
@@ -1760,44 +1701,6 @@ 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)
|
||||
|
||||
@@ -1,51 +1,12 @@
|
||||
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, 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
|
||||
from deltachat import Account, Message
|
||||
from deltachat.testplugin import E2EE_INFO_MSGS
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
@@ -177,15 +138,15 @@ class TestOfflineContact:
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac2)
|
||||
contacts = ac1.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact1 in contacts
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert not ac1.get_contacts(only_verified=True)
|
||||
assert not ac1.get_contacts(query="some1")
|
||||
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()
|
||||
@@ -200,9 +161,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)
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
|
||||
assert chat1 == chat2
|
||||
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
|
||||
ac3 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.create_chat(ac3)
|
||||
@@ -260,17 +221,18 @@ 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, ac1):
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
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)
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
@@ -317,13 +279,14 @@ class TestOfflineChat:
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_removing_blocked_user_from_group(self, ac1, lp):
|
||||
def test_removing_blocked_user_from_group(self, ac1, acfactory, 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")
|
||||
contact = ac1.create_contact("some1@example.org")
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
contact = ac1.create_contact(ac2)
|
||||
group = ac1.create_group_chat("title", contacts=[contact])
|
||||
group.send_text("First group message")
|
||||
|
||||
@@ -335,10 +298,6 @@ 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")
|
||||
@@ -481,7 +440,8 @@ class TestOfflineChat:
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -496,15 +456,15 @@ class TestOfflineChat:
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
@@ -512,8 +472,9 @@ 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("some1 <some1@example.org>").create_chat()
|
||||
chat = ac1.create_contact(ac2).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -534,15 +495,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
contact2_addr = contact2.addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -551,15 +512,15 @@ class TestOfflineChat:
|
||||
ac2.open(passphrase2)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == contact2_addr
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
@@ -567,8 +528,9 @@ 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("some1 <some1@example.org>").create_chat()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -590,15 +552,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
@@ -612,7 +574,8 @@ class TestOfflineChat:
|
||||
backupdir.mkdir()
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
ac_contact = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact(ac_contact).create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
@@ -635,15 +598,15 @@ class TestOfflineChat:
|
||||
ac2.import_all(path, bak_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
ac2.shutdown()
|
||||
|
||||
@@ -652,15 +615,15 @@ class TestOfflineChat:
|
||||
ac2.open(acct_passphrase)
|
||||
|
||||
# check data integrity
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
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)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg1 = Message.new_empty(chat1.account, "text")
|
||||
@@ -682,78 +645,10 @@ class TestOfflineChat:
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
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()
|
||||
def test_audit_log_view_without_daymarker(self, acfactory, lp):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -762,7 +657,7 @@ class TestOfflineChat:
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("create test data")
|
||||
chat.add_contact(ac1.create_contact("some-1@example.org"))
|
||||
chat.add_contact(ac2)
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-04-24
|
||||
2025-07-28
|
||||
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.86.0
|
||||
RUST_VERSION=1.88.0
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -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 "$CORE_ROOT/src/provider/data.rs"
|
||||
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
|
||||
rm -fr "$TMP"
|
||||
|
||||
cd "$CORE_ROOT"
|
||||
|
||||
137
src/accounts.rs
137
src/accounts.rs
@@ -1,25 +1,24 @@
|
||||
//! # Account manager module.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::task::{JoinHandle, JoinSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::sync::oneshot;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio::time::{Duration, sleep};
|
||||
|
||||
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;
|
||||
|
||||
@@ -54,6 +53,14 @@ 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)
|
||||
@@ -263,9 +270,51 @@ impl Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
/// Gets a list of all account ids in the user-configured order.
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.keys().copied().collect()
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
@@ -304,24 +353,28 @@ impl Accounts {
|
||||
/// This is an auxiliary function and not part of public API.
|
||||
/// Use [Accounts::background_fetch] instead.
|
||||
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
|
||||
async fn background_fetch_and_log_error(account: Context) {
|
||||
if let Err(error) = account.background_fetch().await {
|
||||
warn!(account, "{error:#}");
|
||||
}
|
||||
}
|
||||
|
||||
let n_accounts = accounts.len();
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Starting background fetch for {} accounts.",
|
||||
accounts.len()
|
||||
"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:#}");
|
||||
}
|
||||
});
|
||||
}
|
||||
set.join_all().await;
|
||||
events.emit(Event {
|
||||
id: 0,
|
||||
typ: EventType::Info(format!(
|
||||
"Finished background fetch for {n_accounts} accounts."
|
||||
)),
|
||||
});
|
||||
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].
|
||||
@@ -355,7 +408,10 @@ 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 = ()> {
|
||||
pub fn background_fetch(
|
||||
&self,
|
||||
timeout: std::time::Duration,
|
||||
) -> impl Future<Output = ()> + use<> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
let events = self.events.clone();
|
||||
Self::background_fetch_with_timeout(accounts, events, timeout)
|
||||
@@ -407,6 +463,10 @@ 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 {
|
||||
@@ -459,7 +519,9 @@ 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))
|
||||
}
|
||||
@@ -471,6 +533,7 @@ impl Config {
|
||||
accounts: Vec::new(),
|
||||
selected_account: 0,
|
||||
next_id: 1,
|
||||
accounts_order: Vec::new(),
|
||||
};
|
||||
if !lock {
|
||||
let cfg = Self {
|
||||
@@ -503,11 +566,13 @@ 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)
|
||||
@@ -601,6 +666,10 @@ 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
|
||||
};
|
||||
|
||||
@@ -622,6 +691,10 @@ 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
|
||||
@@ -965,9 +1038,11 @@ 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);
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use anyhow::{Context as _, Error, Result, bail};
|
||||
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
|
||||
@@ -217,7 +217,9 @@ 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);
|
||||
|
||||
@@ -240,38 +242,44 @@ 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,7 +266,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
@@ -520,41 +519,6 @@ 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();
|
||||
@@ -584,12 +548,13 @@ 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(())
|
||||
}
|
||||
|
||||
81
src/blob.rs
81
src/blob.rs
@@ -6,11 +6,11 @@ use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, ensure, format_err};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageReader;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::{fs, task};
|
||||
@@ -20,7 +20,8 @@ use crate::config::Config;
|
||||
use crate::constants::{self, MediaQuality};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::log::{LogExt, error, info, warn};
|
||||
use crate::message::Viewtype;
|
||||
use crate::tools::sanitize_filename;
|
||||
|
||||
/// Represents a file in the blob directory.
|
||||
@@ -93,7 +94,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}")
|
||||
};
|
||||
@@ -205,11 +206,7 @@ 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.
|
||||
@@ -267,32 +264,30 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
};
|
||||
|
||||
let maybe_sticker = &mut false;
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let is_avatar = true;
|
||||
self.recode_to_size(
|
||||
context,
|
||||
None, // The name of an avatar doesn't matter
|
||||
maybe_sticker,
|
||||
img_wh,
|
||||
max_bytes,
|
||||
is_avatar,
|
||||
self.check_or_recode_to_size(
|
||||
context, None, // The name of an avatar doesn't matter
|
||||
viewtype, img_wh, max_bytes, is_avatar,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// 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.
|
||||
///
|
||||
/// 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(
|
||||
/// 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(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
maybe_sticker: &mut bool,
|
||||
viewtype: &mut Viewtype,
|
||||
) -> Result<String> {
|
||||
let (img_wh, max_bytes) =
|
||||
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
|
||||
@@ -305,13 +300,10 @@ impl<'a> BlobObject<'a> {
|
||||
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
|
||||
};
|
||||
let is_avatar = false;
|
||||
let new_name =
|
||||
self.recode_to_size(context, name, maybe_sticker, img_wh, max_bytes, is_avatar)?;
|
||||
|
||||
Ok(new_name)
|
||||
self.check_or_recode_to_size(context, name, viewtype, img_wh, max_bytes, is_avatar)
|
||||
}
|
||||
|
||||
/// Recodes the image so that it fits into limits on width/height and byte size.
|
||||
/// Checks or 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.
|
||||
@@ -322,11 +314,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 recode_to_size(
|
||||
fn check_or_recode_to_size(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
name: Option<String>,
|
||||
maybe_sticker: &mut bool,
|
||||
viewtype: &mut Viewtype,
|
||||
mut img_wh: u32,
|
||||
max_bytes: usize,
|
||||
is_avatar: bool,
|
||||
@@ -337,6 +329,7 @@ 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)?;
|
||||
@@ -355,21 +348,28 @@ impl<'a> BlobObject<'a> {
|
||||
)
|
||||
}
|
||||
};
|
||||
let fmt = imgreader.format().context("No format??")?;
|
||||
let fmt = imgreader.format().context("Unknown format")?;
|
||||
if *vt == Viewtype::File {
|
||||
*vt = Viewtype::Image;
|
||||
return Ok(name);
|
||||
}
|
||||
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 *maybe_sticker {
|
||||
if *vt == Viewtype::Sticker {
|
||||
let x_max = img.width().saturating_sub(1);
|
||||
let y_max = img.height().saturating_sub(1);
|
||||
*maybe_sticker = img.in_bounds(x_max, y_max)
|
||||
&& (img.get_pixel(0, 0).0[3] == 0
|
||||
if !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);
|
||||
|| img.get_pixel(x_max, y_max).0[3] == 0)
|
||||
{
|
||||
*vt = Viewtype::Image;
|
||||
}
|
||||
}
|
||||
if *maybe_sticker && exif.is_none() {
|
||||
if *vt == Viewtype::Sticker && exif.is_none() {
|
||||
return Ok(name);
|
||||
}
|
||||
|
||||
@@ -504,10 +504,11 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(_) => res,
|
||||
Err(err) => {
|
||||
if !is_avatar && no_exif {
|
||||
warn!(
|
||||
error!(
|
||||
context,
|
||||
"Cannot recode image, using original data: {err:#}.",
|
||||
"Cannot check/recode image, using original data: {err:#}.",
|
||||
);
|
||||
*viewtype = Viewtype::File;
|
||||
Ok(original_name)
|
||||
} else {
|
||||
Err(err)
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::*;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::sql;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, 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 maybe_sticker = &mut false;
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, img_wh, 20_000, strict_limits)
|
||||
blob.check_or_recode_to_size(&t, None, viewtype, 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 maybe_sticker = &mut false;
|
||||
let viewtype = &mut Viewtype::Image;
|
||||
let strict_limits = true;
|
||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
||||
blob.check_or_recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
|
||||
.unwrap();
|
||||
let new_file_size = file_size(&blob.to_abs_path()).await;
|
||||
assert!(new_file_size <= 3000);
|
||||
@@ -241,9 +241,8 @@ 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");
|
||||
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");
|
||||
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
|
||||
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
|
||||
assert!(!avatar_blob.exists());
|
||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||
.await
|
||||
@@ -251,7 +250,7 @@ async fn test_selfavatar_copy_without_recode() {
|
||||
assert!(avatar_blob.exists());
|
||||
assert_eq!(
|
||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||
avatar_bytes.len() as u64
|
||||
AVATAR_64x64_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()));
|
||||
@@ -400,7 +399,7 @@ async fn test_recode_image_balanced_png() {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
// This will be sent as Image, see [`BlobObject::check_or_recode_image()`] for explanation.
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
@@ -580,20 +579,23 @@ 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()
|
||||
|
||||
1091
src/chat.rs
1091
src/chat.rs
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::chat::{Chat, ChatId, ChatVisibility, update_special_chat_names};
|
||||
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;
|
||||
@@ -244,9 +245,6 @@ 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
|
||||
@@ -322,7 +320,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 })
|
||||
@@ -408,7 +406,8 @@ impl Chatlist {
|
||||
if lastmsg.from_id == ContactId::SELF {
|
||||
None
|
||||
} else if chat.typ == Chattype::Group
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
|| chat.typ == Chattype::OutBroadcast
|
||||
|| chat.typ == Chattype::InBroadcast
|
||||
|| chat.typ == Chattype::Mailinglist
|
||||
|| chat.is_self_talk()
|
||||
{
|
||||
@@ -482,8 +481,8 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::chat::save_msgs;
|
||||
use crate::chat::{
|
||||
add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
|
||||
send_text_msg, ProtectionStatus,
|
||||
ProtectionStatus, add_contact_to_chat, create_group_chat, get_chat_contacts,
|
||||
remove_contact_from_chat, send_text_msg,
|
||||
};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
@@ -571,7 +570,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sort_self_talk_up_on_forward() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
@@ -579,19 +578,23 @@ 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
|
||||
@@ -604,7 +607,7 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_special_chat_names() {
|
||||
let t = TestContext::new().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
t.update_device_chats().await.unwrap();
|
||||
|
||||
let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
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;
|
||||
use crate::log::{LogExt, info};
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::provider::{Provider, get_provider_by_id};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::get_abs_path;
|
||||
|
||||
@@ -369,6 +369,9 @@ 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"))]
|
||||
@@ -414,7 +417,7 @@ pub enum Config {
|
||||
#[strum(props(default = "172800"))]
|
||||
GossipPeriod,
|
||||
|
||||
/// Feature flag for verified 1:1 chats; the UI should set it
|
||||
/// Deprecated 2025-07. 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.
|
||||
@@ -813,7 +816,10 @@ 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}}"#
|
||||
))?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||
use crate::test_utils::{TestContext, TestContextManager, sync};
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
@@ -20,10 +20,11 @@ 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"
|
||||
@@ -263,11 +264,13 @@ 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());
|
||||
@@ -321,17 +324,21 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -13,21 +13,21 @@ mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
pub(crate) mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail, ensure, format_err};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::{addr_normalize, EmailAddress};
|
||||
use deltachat_contact_tools::{EmailAddress, addr_normalize};
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
use server_params::{expand_param_vector, ServerParams};
|
||||
use server_params::{ServerParams, expand_param_vector};
|
||||
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;
|
||||
use crate::log::{LogExt, info, warn};
|
||||
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,7 +215,9 @@ 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<()> {
|
||||
|
||||
@@ -9,6 +9,7 @@ use quick_xml::events::{BytesStart, Event};
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ use quick_xml::events::Event;
|
||||
|
||||
use super::{Error, ServerParams};
|
||||
use crate::context::Context;
|
||||
use crate::log::warn;
|
||||
use crate::net::read_url;
|
||||
use crate::provider::{Protocol, Socket};
|
||||
|
||||
|
||||
@@ -88,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,17 +127,46 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Chattype {
|
||||
/// 1:1 chat.
|
||||
/// A 1:1 chat, i.e. a normal chat with a single contact.
|
||||
///
|
||||
/// Created by [`ChatId::create_for_contact`].
|
||||
Single = 100,
|
||||
|
||||
/// Group chat.
|
||||
///
|
||||
/// Created by [`crate::chat::create_group_chat`].
|
||||
Group = 120,
|
||||
|
||||
/// Mailing list.
|
||||
/// An (unencrypted) mailing list,
|
||||
/// created by an incoming mailing list email.
|
||||
Mailinglist = 140,
|
||||
|
||||
/// Broadcast list.
|
||||
Broadcast = 160,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
@@ -194,6 +223,10 @@ 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
|
||||
@@ -211,11 +244,6 @@ 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 = "✏️";
|
||||
@@ -233,6 +261,9 @@ 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;
|
||||
@@ -245,7 +276,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::Broadcast, Chattype::from_i32(160).unwrap());
|
||||
assert_eq!(Chattype::OutBroadcast, Chattype::from_i32(160).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
602
src/contact.rs
602
src/contact.rs
@@ -1,42 +1,43 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::{min, Reverse};
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::{BinaryHeap, HashSet};
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
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, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
|
||||
ContactAddress, VcardContact,
|
||||
self as contact_tools, ContactAddress, VcardContact, addr_normalize, sanitize_name,
|
||||
sanitize_name_and_addr,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rusqlite::OptionalExtension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
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::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::constants::{self, Blocked, Chattype};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::key::{
|
||||
DcKey, Fingerprint, SignedPublicKey, load_self_public_key, self_fingerprint,
|
||||
self_fingerprint_opt,
|
||||
};
|
||||
use crate::log::{LogExt, info, warn};
|
||||
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::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, time};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
@@ -102,7 +103,16 @@ impl ContactId {
|
||||
/// for this contact will switch to the
|
||||
/// contact's authorized name.
|
||||
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
|
||||
let addr = context
|
||||
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
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let is_changed = transaction.execute(
|
||||
@@ -111,30 +121,45 @@ impl ContactId {
|
||||
)? > 0;
|
||||
if is_changed {
|
||||
update_chat_names(context, transaction, self)?;
|
||||
let addr = transaction.query_row(
|
||||
"SELECT addr FROM contacts WHERE id=?",
|
||||
let (addr, fingerprint) = transaction.query_row(
|
||||
"SELECT addr, fingerprint FROM contacts WHERE id=?",
|
||||
(self,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
let fingerprint: String = row.get(1)?;
|
||||
Ok((addr, fingerprint))
|
||||
},
|
||||
)?;
|
||||
Ok(Some(addr))
|
||||
context.emit_event(EventType::ContactsChanged(Some(self)));
|
||||
Ok(Some((addr, fingerprint)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -196,31 +221,6 @@ 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,14 +267,8 @@ 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 = 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? {
|
||||
let key = c.public_key(context).await?.map(|k| k.to_base64());
|
||||
let profile_image = match c.get_profile_image_ex(context, false).await? {
|
||||
None => None,
|
||||
Some(path) => tokio::fs::read(path)
|
||||
.await
|
||||
@@ -287,6 +281,7 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
authname: c.authname,
|
||||
key,
|
||||
profile_image,
|
||||
biography: Some(c.status).filter(|s| !s.is_empty()),
|
||||
// Use the current time to not reveal our or contact's online time.
|
||||
timestamp: Ok(now),
|
||||
});
|
||||
@@ -329,15 +324,6 @@ 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(|| {
|
||||
@@ -349,50 +335,35 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
.log_err(context)
|
||||
.ok()
|
||||
});
|
||||
|
||||
let fingerprint;
|
||||
if let Some(public_key) = key {
|
||||
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)
|
||||
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)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
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)));
|
||||
}
|
||||
if modified != Modifier::Created {
|
||||
return Ok(id);
|
||||
@@ -423,6 +394,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(biography) = &contact.biography {
|
||||
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
@@ -456,6 +435,11 @@ 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,
|
||||
|
||||
@@ -605,7 +589,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.authname, c.param, c.status, c.is_bot, c.fingerprint
|
||||
FROM contacts c
|
||||
WHERE c.id=?;",
|
||||
(contact_id,),
|
||||
@@ -619,11 +603,14 @@ 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,
|
||||
@@ -646,6 +633,9 @@ 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?
|
||||
@@ -796,16 +786,17 @@ impl Contact {
|
||||
|
||||
let addr_normalized = addr_normalize(addr);
|
||||
|
||||
if context.is_self_addr(&addr_normalized).await? {
|
||||
if context.is_configured().await? && context.is_self_addr(addr).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=?)",
|
||||
"SELECT id FROM contacts
|
||||
WHERE addr=?1 COLLATE NOCASE
|
||||
AND fingerprint='' -- Do not lookup key-contacts
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
@@ -818,8 +809,19 @@ 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.
|
||||
/// The contact is identified by the email-address, a name and an "origin" can be given.
|
||||
/// 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 "origin" is where the address comes from -
|
||||
/// from-header, cc-header, addressbook, qr, manual-edit etc.
|
||||
@@ -843,21 +845,34 @@ 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(
|
||||
pub(crate) async fn add_or_lookup_ex(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
addr: &ContactAddress,
|
||||
addr: &str,
|
||||
fingerprint: &str,
|
||||
mut origin: Origin,
|
||||
) -> Result<(ContactId, Modifier)> {
|
||||
let mut sth_modified = Modifier::None;
|
||||
|
||||
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
|
||||
ensure!(
|
||||
!addr.is_empty() || !fingerprint.is_empty(),
|
||||
"Can not add_or_lookup empty address"
|
||||
);
|
||||
ensure!(origin != Origin::Unknown, "Missing valid origin");
|
||||
|
||||
if context.is_self_addr(addr).await? {
|
||||
if context.is_configured().await? && 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:
|
||||
@@ -893,8 +908,10 @@ impl Contact {
|
||||
let row = transaction
|
||||
.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE",
|
||||
(addr,),
|
||||
FROM contacts
|
||||
WHERE fingerprint=?1 AND
|
||||
(?1<>'' OR addr=?2 COLLATE NOCASE)",
|
||||
(fingerprint, addr),
|
||||
|row| {
|
||||
let row_id: u32 = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
@@ -918,7 +935,7 @@ impl Contact {
|
||||
|| row_authname.is_empty());
|
||||
|
||||
row_id = id;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
if origin >= row_origin && addr != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
@@ -962,11 +979,12 @@ impl Contact {
|
||||
let update_authname = !manual;
|
||||
|
||||
transaction.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
"INSERT INTO contacts (name, addr, fingerprint, origin, authname)
|
||||
VALUES (?, ?, ?, ?, ?);",
|
||||
(
|
||||
if update_name { &name } else { "" },
|
||||
&addr,
|
||||
fingerprint,
|
||||
origin,
|
||||
if update_authname { &name } else { "" },
|
||||
),
|
||||
@@ -974,7 +992,14 @@ impl Contact {
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
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}."
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(row_id)
|
||||
})
|
||||
@@ -1038,12 +1063,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` 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.
|
||||
/// * `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.
|
||||
pub async fn get_all(
|
||||
context: &Context,
|
||||
listflags: u32,
|
||||
@@ -1056,32 +1081,31 @@ impl Contact {
|
||||
.collect::<HashSet<_>>();
|
||||
let mut add_self = false;
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||
let flag_add_self = (listflags & constants::DC_GCL_ADD_SELF) != 0;
|
||||
let flag_address = (listflags & constants::DC_GCL_ADDRESS) != 0;
|
||||
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
|
||||
Origin::Unknown
|
||||
} else {
|
||||
Origin::IncomingReplyTo
|
||||
};
|
||||
if flag_verified_only || query.is_some() {
|
||||
if query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
|
||||
context
|
||||
.sql
|
||||
.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)?;
|
||||
@@ -1128,10 +1152,11 @@ 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, minimal_origin),
|
||||
(ContactId::LAST_SPECIAL, flag_address, minimal_origin),
|
||||
|row| {
|
||||
let id: ContactId = row.get(0)?;
|
||||
let addr: String = row.get(1)?;
|
||||
@@ -1157,7 +1182,7 @@ impl Contact {
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Adds blocked mailinglists as contacts
|
||||
/// Adds blocked mailinglists and broadcast channels as pseudo-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,
|
||||
@@ -1166,15 +1191,20 @@ impl Contact {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
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 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 blocked_mailinglists = rows.collect::<std::result::Result<Vec<_>, _>>()?;
|
||||
for (name, grpid) in blocked_mailinglists {
|
||||
for (name, grpid, typ) in blocked_mailinglists {
|
||||
let count = transaction.query_row(
|
||||
"SELECT COUNT(id) FROM contacts WHERE addr=?",
|
||||
[&grpid],
|
||||
@@ -1187,10 +1217,17 @@ 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 WHERE addr=?",
|
||||
(&name, Origin::MailinglistAddress, &grpid),
|
||||
"UPDATE contacts SET name=?, origin=?, blocked=1, fingerprint=? WHERE addr=?",
|
||||
(&name, Origin::MailinglistAddress, fingerprint, &grpid),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -1248,17 +1285,16 @@ impl Contact {
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
else {
|
||||
let Some(fingerprint_other) = contact.fingerprint() else {
|
||||
return Ok(stock_str::encr_none(context).await);
|
||||
};
|
||||
let fingerprint_other = fingerprint_other.to_string();
|
||||
|
||||
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 stock_message = if contact.public_key(context).await?.is_some() {
|
||||
stock_str::e2e_available(context).await
|
||||
} else {
|
||||
stock_str::encr_none(context).await
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
@@ -1268,43 +1304,31 @@ impl Contact {
|
||||
.await?
|
||||
.dc_fingerprint()
|
||||
.to_string();
|
||||
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 {
|
||||
if addr < contact.addr {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
&contact.addr,
|
||||
&fingerprint_other,
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1377,6 +1401,59 @@ 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
|
||||
@@ -1442,11 +1519,28 @@ 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 let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
} 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) {
|
||||
if !image_rel.is_empty() {
|
||||
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
|
||||
}
|
||||
@@ -1472,25 +1566,21 @@ 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);
|
||||
}
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(peerstate.peek_key(false).is_some())
|
||||
Ok(self.public_key(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
/// and Autocrypt key matches the verified key.
|
||||
/// can be added to verified chats.
|
||||
///
|
||||
/// 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, us this function only if there is no chat with the contact,
|
||||
/// In contact profile view, use 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.
|
||||
@@ -1501,64 +1591,31 @@ impl Contact {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
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())
|
||||
Ok(self.get_verifier_id(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns the `ContactId` that verified the contact.
|
||||
///
|
||||
/// If the function returns non-zero result,
|
||||
/// If this returns Some(_),
|
||||
/// 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 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())
|
||||
/// 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,))
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
.with_context(|| format!("Contact {} does not exist", self.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)
|
||||
}
|
||||
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))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1730,14 +1787,16 @@ WHERE type=? AND id IN (
|
||||
true => chat::SyncAction::Block,
|
||||
false => chat::SyncAction::Unblock,
|
||||
};
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(contact.addr.clone()),
|
||||
action,
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1857,29 +1916,52 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
/// 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",
|
||||
);
|
||||
if !fingerprint_verified.is_empty()
|
||||
&& !fingerprint_unverified.is_empty()
|
||||
&& fingerprint_verified != fingerprint_unverified
|
||||
{
|
||||
*ret += &format!("\n\n{name} (alternative):\n{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}");
|
||||
}
|
||||
|
||||
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr};
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
|
||||
@@ -56,64 +56,83 @@ fn test_split_address_book() {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_contacts() -> Result<()> {
|
||||
let context = TestContext::new().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let context = tcm.bob().await;
|
||||
let alice = tcm.alice().await;
|
||||
alice
|
||||
.set_config(Config::Displayname, 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?;
|
||||
// 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_eq!(contacts.len(), 0);
|
||||
|
||||
let (id, _modified) = Contact::add_or_lookup(
|
||||
&context.ctx,
|
||||
"bob",
|
||||
&ContactAddress::new("user@example.org")?,
|
||||
Origin::IncomingReplyTo,
|
||||
)
|
||||
.await?;
|
||||
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;
|
||||
assert_ne!(id, ContactId::UNDEFINED);
|
||||
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
let contact = Contact::get_by_id(&context, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_display_name(), "bob");
|
||||
assert_eq!(contact.get_authname(), "MyName");
|
||||
assert_eq!(contact.get_display_name(), "MyName");
|
||||
|
||||
// Search by name.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("myname")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
|
||||
// Search by address.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("user")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("alice@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.first(), Some(&id));
|
||||
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("alice")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("Foobar")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// 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);
|
||||
// Set Alice name to "someone" manually.
|
||||
id.set_name(&context, "someone").await?;
|
||||
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
|
||||
assert_eq!(contact.get_name(), "someone");
|
||||
assert_eq!(contact.get_authname(), "bob");
|
||||
assert_eq!(contact.get_authname(), "MyName");
|
||||
assert_eq!(contact.get_display_name(), "someone");
|
||||
|
||||
// Not searchable by authname, because it is not displayed.
|
||||
let contacts = Contact::get_all(&context.ctx, 0, Some("bob")).await?;
|
||||
let contacts = Contact::get_all(&context, 0, Some("MyName")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// 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));
|
||||
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]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -133,7 +152,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().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
let book = concat!(
|
||||
" Name one \n one@eins.org \n",
|
||||
"Name two\ntwo@deux.net\n",
|
||||
@@ -247,7 +266,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(), ""); // we're not configured
|
||||
assert_eq!(contact.get_addr(), "alice@example.org");
|
||||
assert!(!contact.is_blocked());
|
||||
}
|
||||
|
||||
@@ -282,7 +301,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(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// second message inits the name
|
||||
receive_imf(
|
||||
@@ -308,9 +327,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(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// third message changes the name
|
||||
receive_imf(
|
||||
@@ -338,11 +357,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(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
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(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
// change name manually
|
||||
let test_id = Contact::create(&t, "Falk", "f@example.org").await?;
|
||||
@@ -356,9 +375,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(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -366,20 +385,13 @@ 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, _) = 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;
|
||||
let contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
@@ -416,30 +428,57 @@ async fn test_delete() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_and_recreate_contact() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// test recreation after physical deletion
|
||||
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);
|
||||
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
|
||||
);
|
||||
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("foo@bar.de")).await?.len(), 0);
|
||||
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
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_ne!(contact_id2, contact_id1);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// test recreation after hiding
|
||||
t.create_chat_with_contact("Foo", "foo@bar.de").await;
|
||||
t.create_chat(&bob).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("foo@bar.de")).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
|
||||
let contact_id3 = t.add_or_lookup_contact_id(&bob).await;
|
||||
let contact = Contact::get_by_id(&t, contact_id3).await?;
|
||||
assert_eq!(contact.origin, Origin::ManuallyCreated);
|
||||
assert_eq!(contact.origin, Origin::CreateChat);
|
||||
assert_eq!(contact_id3, contact_id2);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Contact::get_all(&t, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -666,20 +705,28 @@ 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)]
|
||||
@@ -728,51 +775,59 @@ async fn test_contact_get_color() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().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 (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?;
|
||||
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?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
let contact = Contact::get_by_id(&alice, 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 = Contact::get_by_id(alice, address_contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(alice).await?);
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
let contact_bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
|
||||
assert_eq!(
|
||||
encrinfo,
|
||||
"End-to-end encryption preferred.
|
||||
"End-to-end encryption available.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
Bob (bob@example.net):
|
||||
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?);
|
||||
|
||||
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?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -780,24 +835,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 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let default_status = alice1.get_config(Config::Selfstatus).await?;
|
||||
|
||||
alice1
|
||||
.set_config(Config::Selfstatus, Some("New status"))
|
||||
.await?;
|
||||
let chat = alice1
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
let chat = alice1.create_email_chat(bob).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.
|
||||
@@ -813,18 +868,9 @@ 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);
|
||||
|
||||
// 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?;
|
||||
// Alice sends encrypted message.
|
||||
let chat = alice1.create_chat(bob).await;
|
||||
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// Second message is encrypted.
|
||||
@@ -845,12 +891,14 @@ 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 = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
|
||||
// Bob has one device.
|
||||
let bob = TestContext::new_bob().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
|
||||
|
||||
@@ -866,20 +914,9 @@ 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.get_last_msg().await.chat_id;
|
||||
alice1_chat_id.accept(&alice1).await?;
|
||||
send_text_msg(&alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let alice1_chat_id = alice1.create_chat(bob).await.id;
|
||||
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
|
||||
let sent_msg = alice1.pop_sent_msg().await;
|
||||
|
||||
// The message is encrypted.
|
||||
@@ -1008,7 +1045,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 create a peerstate.
|
||||
// Receive a message from Bob to save the public key.
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
@@ -1054,6 +1091,8 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
bob.set_config(Config::Selfstatus, Some("It's me, bob"))
|
||||
.await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
let avatar_base64 = base64::engine::general_purpose::STANDARD.encode(avatar_bytes);
|
||||
@@ -1061,16 +1100,12 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
|
||||
let chat = bob.create_chat(alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
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 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();
|
||||
let fiona_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
assert_eq!(make_vcard(alice, &[]).await?, "".to_string());
|
||||
@@ -1086,12 +1121,14 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
|
||||
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
assert_eq!(contacts[1].authname, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert_eq!(contacts[1].biography, None);
|
||||
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
|
||||
@@ -1114,6 +1151,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert_eq!(*contacts[0].biography.as_ref().unwrap(), bob_biography);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
|
||||
@@ -1145,13 +1183,16 @@ 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_updates_only_key() -> Result<()> {
|
||||
async fn test_import_vcard_key_change() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
@@ -1169,28 +1210,34 @@ async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
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 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 avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
bob1.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
|
||||
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 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!(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_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.get_showpadlock());
|
||||
|
||||
// The old vCard is imported, but doesn't change Bob's key for Alice.
|
||||
@@ -1202,63 +1249,6 @@ async fn test_import_vcard_updates_only_key() -> 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();
|
||||
@@ -1268,9 +1258,87 @@ 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(())
|
||||
}
|
||||
|
||||
129
src/context.rs
129
src/context.rs
@@ -5,37 +5,36 @@ use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
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::aheader::EncryptPreference;
|
||||
use crate::chat::{get_chat_cnt, ChatId, ProtectionStatus};
|
||||
use crate::chat::{ChatId, ProtectionStatus, get_chat_cnt};
|
||||
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};
|
||||
use crate::contact::{Contact, ContactId, import_vcard, mark_contact_id_as_verified};
|
||||
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_public_key, load_self_secret_key, DcKey as _};
|
||||
use crate::key::{load_self_secret_key, self_fingerprint};
|
||||
use crate::log::{info, warn};
|
||||
use crate::logged_debug_assert;
|
||||
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::{convert_folder_meaning, SchedulerState};
|
||||
use crate::scheduler::{SchedulerState, convert_folder_meaning};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
@@ -278,6 +277,13 @@ 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
|
||||
@@ -293,6 +299,11 @@ 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.
|
||||
@@ -322,6 +333,15 @@ 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());
|
||||
@@ -447,10 +467,12 @@ 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 {
|
||||
@@ -648,8 +670,16 @@ 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) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
debug_assert!(!msg_id.is_unset());
|
||||
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."
|
||||
);
|
||||
|
||||
self.emit_event(EventType::MsgsChanged { chat_id, msg_id });
|
||||
chatlist_events::emit_chatlist_changed(self);
|
||||
@@ -658,7 +688,11 @@ 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) {
|
||||
debug_assert!(!chat_id.is_unset());
|
||||
logged_debug_assert!(
|
||||
self,
|
||||
!chat_id.is_unset(),
|
||||
"emit_msgs_changed_without_msg_id: chat_id is unset."
|
||||
);
|
||||
|
||||
self.emit_event(EventType::MsgsChanged {
|
||||
chat_id,
|
||||
@@ -773,13 +807,11 @@ impl Context {
|
||||
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let unset = "0";
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await?;
|
||||
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;
|
||||
let request_msgs = message::get_request_msg_cnt(self).await;
|
||||
@@ -806,10 +838,10 @@ impl Context {
|
||||
|
||||
let pub_key_cnt = self
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM acpeerstates;", ())
|
||||
.count("SELECT COUNT(*) FROM public_keys;", ())
|
||||
.await?;
|
||||
let fingerprint_str = match load_self_public_key(self).await {
|
||||
Ok(key) => key.dc_fingerprint().hex(),
|
||||
let fingerprint_str = match self_fingerprint(self).await {
|
||||
Ok(fp) => fp.to_string(),
|
||||
Err(err) => format!("<key failure: {err}>"),
|
||||
};
|
||||
|
||||
@@ -858,7 +890,6 @@ impl Context {
|
||||
);
|
||||
res.insert("journal_mode", journal_mode);
|
||||
res.insert("blobdir", self.get_blobdir().display().to_string());
|
||||
res.insert("displayname", displayname.unwrap_or_else(|| unset.into()));
|
||||
res.insert(
|
||||
"selfavatar",
|
||||
self.get_config(Config::Selfavatar)
|
||||
@@ -1021,7 +1052,7 @@ impl Context {
|
||||
self.get_config_int(Config::GossipPeriod).await?.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"verified_one_on_one_chats",
|
||||
"verified_one_on_one_chats", // deprecated 2025-07
|
||||
self.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
.to_string(),
|
||||
@@ -1032,6 +1063,19 @@ 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));
|
||||
@@ -1043,7 +1087,6 @@ impl Context {
|
||||
#[derive(Default)]
|
||||
struct ChatNumbers {
|
||||
protected: u32,
|
||||
protection_broken: u32,
|
||||
opportunistic_dc: u32,
|
||||
opportunistic_mua: u32,
|
||||
unencrypted_dc: u32,
|
||||
@@ -1061,25 +1104,24 @@ impl Context {
|
||||
)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_msgs {}\n", num_msgs);
|
||||
res += &format!("num_msgs {num_msgs}\n");
|
||||
|
||||
let num_chats: u32 = self
|
||||
.sql
|
||||
.query_get_value("SELECT COUNT(*) FROM chats WHERE id>9 AND blocked!=1", ())
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
res += &format!("num_chats {}\n", num_chats);
|
||||
res += &format!("num_chats {num_chats}\n");
|
||||
|
||||
let db_size = tokio::fs::metadata(&self.sql.dbfile).await?.len();
|
||||
res += &format!("db_size_bytes {}\n", db_size);
|
||||
res += &format!("db_size_bytes {db_size}\n");
|
||||
|
||||
let secret_key = &load_self_secret_key(self).await?.primary_key;
|
||||
let key_created = secret_key.created_at().timestamp();
|
||||
res += &format!("key_created {}\n", key_created);
|
||||
let key_created = secret_key.public_key().created_at().timestamp();
|
||||
res += &format!("key_created {key_created}\n");
|
||||
|
||||
// how many of the chats active in the last months are:
|
||||
// - protected
|
||||
// - 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
|
||||
@@ -1122,8 +1164,6 @@ 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;
|
||||
@@ -1141,7 +1181,6 @@ 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);
|
||||
@@ -1155,7 +1194,7 @@ impl Context {
|
||||
id
|
||||
}
|
||||
};
|
||||
res += &format!("self_reporting_id {}", self_reporting_id);
|
||||
res += &format!("self_reporting_id {self_reporting_id}");
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -1166,32 +1205,14 @@ 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: &str = "self_reporting@testrun.org";
|
||||
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?;
|
||||
|
||||
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
Reference in New Issue
Block a user