Compare commits

..

181 Commits

Author SHA1 Message Date
link2xt
60193dd527 Add script to benchmark the tests 2023-10-09 13:13:25 +00:00
link2xt
37383c10ac Merge 'stable' into 'master'
Resolved conflicts due to asyncio removal.
2023-10-08 01:30:46 +00:00
link2xt
09798df7a0 refactor(deltachat-rpc-client): remove print() calls 2023-10-07 23:50:38 +00:00
link2xt
b360225e08 refactor: fix Rust 1.73 clippy warnings 2023-10-06 21:21:16 +00:00
link2xt
09d5e44b13 ci: test with Rust 1.73 2023-10-06 21:21:16 +00:00
link2xt
8ba89c0fa1 ci: reduce required Python version for deltachat-rpc-client 2023-10-06 21:20:44 +00:00
link2xt
f984a27379 fix: use process_group Popen argument with Python 3.11 2023-10-06 19:40:26 +00:00
link2xt
425a2310fe refactor(deltachat-rpc-client): close stdin instead of sending SIGTERM 2023-10-06 18:44:19 +00:00
link2xt
95571be278 fix: run deltachat-rpc-server in its own process group
This ensures the server does not get SIGINT
when the bot is running in a terminal and user presses ^C
We want to send SIGTERM ourselves after clean shutdown,
e.g. stopping I/O for all accounts.
2023-10-06 18:30:59 +00:00
link2xt
7bf44a237e api(deltachat-rpc-client)!: replace asyncio with threads 2023-10-05 15:59:57 +00:00
link2xt
a119b24eeb Merge tag 'v1.124.1' 2023-10-05 05:02:18 +00:00
link2xt
47dbac9b50 chore(release): prepare for 1.124.1 2023-10-05 05:01:26 +00:00
link2xt
a49282727b ci: pin urllib3 version to <2
Otherwise it is impossible to build wheels.
2023-10-05 04:41:51 +00:00
iequidoo
0d22fc7ac1 fix: Remove footer from reactions on the receiver side (#4780)
Reactions do not have footer since 6d2ac30. However, mailing lists still add the footer to the
messages, and receiver interpreted words as a reaction.
2023-10-04 22:46:09 -03:00
link2xt
275791595c Merge tag 'v1.124.0' 2023-10-04 21:14:17 +00:00
link2xt
1040bc551f chore(release): prepare for 1.124.0 2023-10-04 21:12:38 +00:00
iequidoo
5aa0205c80 fix: Add protected-headers directive to Content-Type of encrypted/signed MIME (#2302)
Add protected-headers="v1" directive to Content-Type of an encrypted/signed MIME so that other MUAs
like Thunderbird display the true message Subject instead of "...".
2023-10-04 19:58:08 +00:00
link2xt
210a4ebcbe ci: test async python bindings with Python 3.11 2023-10-04 19:56:56 +00:00
link2xt
7fc2b06b3f ci(mypy): ignore distutils
Python 3.12 removed `distutils`.
`distutils` from `setuptools` work fine,
but have not typing stubs:
https://github.com/python/typeshed/issues/10255
2023-10-04 19:56:56 +00:00
link2xt
1177c19a43 bulid: build wheels for Python 3.12 and PyPy 3.10 2023-10-04 19:56:56 +00:00
link2xt
8a2417f32d ci: test with Python 3.12 and PyPy 3.10 2023-10-04 19:56:56 +00:00
link2xt
a154347834 fix(deltachat-rpc-client): increase stdio buffer to 64 MiB
Otherwise readline() gets stuck when JSON-RPC response
is longer that 64 KiB.
2023-10-04 16:08:15 +00:00
link2xt
d51adf2aa0 feat(deltachat-rpc-client): log exceptions when long-running tasks die
For example, reader_loop() may die
if readline() tries to read too large line
and thows an exception.
We want to at least log the exception in this case.
2023-10-04 08:22:50 +00:00
link2xt
a5f0c1613e fix: add Let's Encrypt root certificate to reqwest
This certificate does not exist on older Android phones.
It is already added manually for IMAP and SMTP,
this commit adds the same certificate for HTTP requests.
2023-10-03 19:35:39 +00:00
link2xt
7c4bcf9004 api!: deprecate get_next_media() 2023-10-03 16:08:25 +00:00
dependabot[bot]
f3fb0dc5fe chore(cargo): bump proptest from 1.2.0 to 1.3.1
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.2.0 to 1.3.1.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/v1.2.0...v1.3.1)

---
updated-dependencies:
- dependency-name: proptest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 01:34:58 -03:00
dependabot[bot]
24ea90bd68 Merge pull request #4770 from deltachat/dependabot/cargo/thiserror-1.0.49 2023-10-03 04:30:28 +00:00
dependabot[bot]
ef3e94c7e1 Merge pull request #4777 from deltachat/dependabot/cargo/fuzz/webpki-0.22.2 2023-10-03 04:14:01 +00:00
dependabot[bot]
883832f78d Merge pull request #4768 from deltachat/dependabot/cargo/brotli-3.4.0 2023-10-03 01:21:32 +00:00
dependabot[bot]
1f03267273 Merge pull request #4773 from deltachat/dependabot/cargo/sha2-0.10.8 2023-10-03 01:21:20 +00:00
dependabot[bot]
8bc2ce1c30 chore(deps): bump webpki from 0.22.0 to 0.22.2 in /fuzz
Bumps [webpki](https://github.com/briansmith/webpki) from 0.22.0 to 0.22.2.
- [Commits](https://github.com/briansmith/webpki/commits)

---
updated-dependencies:
- dependency-name: webpki
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-02 21:39:40 +00:00
dependabot[bot]
2ae92c06e3 Merge pull request #4771 from deltachat/dependabot/cargo/regex-1.9.6 2023-10-02 17:46:17 +00:00
dependabot[bot]
4a6a214f3c Merge pull request #4769 from deltachat/dependabot/cargo/tokio-util-0.7.9 2023-10-02 16:38:54 +00:00
dependabot[bot]
c2d7011aa7 Merge pull request #4772 from deltachat/dependabot/cargo/smallvec-1.11.1 2023-10-02 16:38:16 +00:00
dependabot[bot]
c0195ab23f chore(cargo): bump sha2 from 0.10.7 to 0.10.8
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.7 to 0.10.8.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.7...sha2-v0.10.8)

---
updated-dependencies:
- dependency-name: sha2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:44:09 +00:00
dependabot[bot]
e4e50d0e81 chore(cargo): bump smallvec from 1.11.0 to 1.11.1
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:43:50 +00:00
dependabot[bot]
573746ce54 chore(cargo): bump regex from 1.9.5 to 1.9.6
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.5 to 1.9.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.5...1.9.6)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:43:32 +00:00
dependabot[bot]
6b2df13cdb chore(cargo): bump thiserror from 1.0.47 to 1.0.49
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.47 to 1.0.49.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.47...1.0.49)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:43:13 +00:00
dependabot[bot]
3166b44580 chore(cargo): bump tokio-util from 0.7.8 to 0.7.9
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.8 to 0.7.9.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.8...tokio-util-0.7.9)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:42:54 +00:00
dependabot[bot]
e500485c21 chore(cargo): bump brotli from 3.3.4 to 3.4.0
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 3.3.4 to 3.4.0.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-01 21:42:27 +00:00
B. Petersen
2dd44d5f89 fix: cap percentage in connectivity layout to 100%
it may happen that percentages larger than 100% are reported by the provider,
eg. for some time a storage usage of 120% may be accepted.

while we should report the values "as is" to the user,
for the bar, percentages larger 100% will destroy the layout.
2023-10-01 14:54:37 +00:00
link2xt
f656cb29be fix: ignore special chats in get_similar_chat_ids()
For unknown reason trash chat contains members in some existing databases.
Workaround this by ignoring chats_contacts entries with special chat_id.
2023-10-01 07:38:13 +00:00
link2xt
59e5a63d5f Merge branch 'stable', resolving conflicts 2023-10-01 02:46:02 +00:00
link2xt
53230b6eb0 chore(cargo): update webpki to fix RUSTSEC-2023-0052 2023-10-01 00:04:45 +00:00
iequidoo
80ca59f152 feat: Remove extra members from the local list in sake of group membership consistency (#3782)
9bd7ab72 brings a possibility of group membership inconsistency to the original Hocuri's algo
described and implemented in e12e026b in sake of security so that nobody can add themselves to a
group by forging "InReplyTo" and other headers. This commit fixes the problem by removing group
members locally if we see a discrepancy with the "To" list in the received message as it is better
for privacy than adding absent members locally. But it shouldn't be a big problem if somebody missed
a member addition, because they will likely recreate the member list from the next received
message. The problem occurs only if that "somebody" managed to reply earlier. Really, it's a problem
for big groups with high message rate, but let it be for now.

Also:
- Query chat contacts from the db only once.
- Update chat contacts in the only transaction, otherwise we can just break the chat contact list
  halfway.
- Allow classic MUA messages to remove group members if a parent message is missing. Currently it
  doesn't matter because unrelated messages go to new ad-hoc groups, but let this logic be outside
  of apply_group_changes(). Just in case if there will be a MUA preserving "Chat-Group-ID" header
  f.e.
2023-09-30 19:14:22 -03:00
link2xt
eb624e43c0 refactor: remove incomplete protected header code skipping Legacy Display Part
The code removed is an incomplete implementation of skipping
the Legacy Display Part specified in
https://www.ietf.org/archive/id/draft-autocrypt-lamps-protected-headers-02.html#section-5.2

The code does not fully implement the specification, e.g.
it does not check that there are exactly two parts.

Delta Chat and Thunderbird are not adding this part anyway,
and it is defined as "transitional" in the draft.

This also removes misplaced warning "Ignoring nested protected headers"
that is printed for every incoming Delta Chat message
since commit 5690c48863
which is part of the PR <https://github.com/deltachat/deltachat-core-rust/pull/982>.
2023-09-30 21:54:08 +00:00
link2xt
532e9cb09a refactor: ignore public key argument in dc_preconfigure_keypair()
Public key can be extracted from the secret key file.
2023-09-30 19:16:23 +00:00
link2xt
ef4d2a7ed0 api!(python): use dc_contact_get_verifier_id()
get_verifier() returns a Contact rather than an address now

dc_contact_get_verifier_addr() is unused.
2023-09-30 15:49:22 +00:00
link2xt
5daa6274e8 Merge stable into master 2023-09-30 12:16:51 +00:00
link2xt
6d2ac30461 fix: do not put the status footer into reaction MIME parts 2023-09-29 16:38:55 +00:00
Hocuri
d109a1b27f Fix link to the template documentation of git-cliff
The old link gives me 404
2023-09-29 15:20:22 +00:00
link2xt
33a203d56e fix: initialise last_msg_id to the highest known row id
Otherwise existing bots migrating to get_next_msgs()
are trying to process all the messages they have in the database.
2023-09-29 13:28:58 +00:00
link2xt
a19811f379 chore(cargo): update tungstenite to fix RUSTSEC-2023-0065
Used `cargo update -p axum`.
2023-09-29 13:08:04 +00:00
link2xt
f23023961e api!: return DC_CONTACT_ID_SELF from dc_contact_get_verifier_id() for directly verified contacts 2023-09-28 19:10:15 +00:00
link2xt
b463a0566e refactor: flatten create_or_lookup_mailinglist() 2023-09-28 15:20:51 +00:00
link2xt
38d5743c06 refactor: do not ignore errors in get_kml()
This removes unnecessary warning
"mimefactory: could not send location: No locations processed"
when there are no locations to send.
2023-09-28 15:19:33 +00:00
link2xt
6990312051 fix: trash only empty *text* parts when location.kml is attached
If the message contains other attachment parts
such as images, they should not go into trash.
2023-09-27 18:51:40 +00:00
link2xt
a7cf51868b test: test send_location 2023-09-27 18:51:40 +00:00
link2xt
815c1b9c49 refactor: resultify location::set() 2023-09-27 18:51:40 +00:00
link2xt
88bba83383 refactor: flatten process_report() 2023-09-26 16:02:14 +00:00
WofWca
b1d517398d refactor: improve comment about Ratelimit
A few people got the impression that if you send 6 messages
in a burst you'll only be able to send the next one in 60 seconds.
Hopefully this can resolve it.
2023-09-26 15:58:24 +00:00
link2xt
4e5b41f150 fix: require valid email addresses in dc_provider_new_from_email[_with_dns]() 2023-09-25 15:51:10 +00:00
B. Petersen
56b2361f01 reset document.update on forwarding
this fixes the test test_forward_webxdc_instance()
2023-09-25 15:20:57 +00:00
B. Petersen
968cc65323 test that update.document is not forwarded
the test is failing currently
2023-09-25 15:20:57 +00:00
link2xt
d0ee21e6dc refactor: flatten GENERATED_PREFIX check in receive_imf_inner 2023-09-25 10:35:07 +00:00
link2xt
a1345f2542 refactor: flatten lookup_chat_by_reply 2023-09-25 10:34:20 +00:00
link2xt
f290fe0871 fix: wrap base64-encoded parts to 76 characters
This is an RFC 2045 requirement for base64-encoded MIME parts.
Previously referenced RFC 5322 requirement
is a general Internet Message Format requirement
and is more generous.
2023-09-25 10:33:46 +00:00
dependabot[bot]
c41687586c Merge pull request #4742 from deltachat/dependabot/cargo/fuzz/quinn-proto-0.9.5 2023-09-22 22:43:28 +00:00
link2xt
59a3bc0ff4 Merge tag 'v1.123.0' 2023-09-22 22:41:08 +00:00
link2xt
aa78e82fed chore(release): prepare for 1.123.0 2023-09-22 22:13:47 +00:00
link2xt
d4e670d5e9 chore(deps): update OpenSSL from 3.1.2 to 3.1.3 2023-09-22 21:57:36 +00:00
link2xt
4553c6521f api!: make dc_jsonrpc_blocking_call accept JSON-RPC request 2023-09-22 21:33:52 +00:00
dependabot[bot]
a42a6ca18c chore(deps): bump quinn-proto from 0.9.2 to 0.9.5 in /fuzz
Bumps [quinn-proto](https://github.com/quinn-rs/quinn) from 0.9.2 to 0.9.5.
- [Release notes](https://github.com/quinn-rs/quinn/releases)
- [Commits](https://github.com/quinn-rs/quinn/commits)

---
updated-dependencies:
- dependency-name: quinn-proto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-21 17:26:12 +00:00
link2xt
061d091c97 build(coredeps): only run yum if it is available
musllinux is based on Alpine and has no yum
2023-09-12 19:59:15 +00:00
link2xt
e7617f0abd build(coredeps): install perl-IPC-Cmd
It is required to configure OpenSSL 3.0.
2023-09-12 19:02:20 +00:00
link2xt
790e867af0 Merge tag 'v1.122.0' 2023-09-12 18:04:05 +00:00
link2xt
4a0585404a chore(cargo): bump webpki from 0.22.0 to 0.22.1 2023-09-08 07:00:55 +00:00
link2xt
dcbf5996c2 Merge tag 'v1.121.0' 2023-09-06 21:46:07 +00:00
dependabot[bot]
f7a47e60cd Merge pull request #4664 from deltachat/dependabot/cargo/strum_macros-0.25.2 2023-09-05 19:08:04 +00:00
Hocuri
2587ebbacd fix: Clear VerifiedOneOnOneChats config on backup (#4681)
If the user makes a backup from a UI that supports the experimental verified 1:1 chats (e.g. nightly Android) and imports it into a UI that doesn't, then this config should be cleared.

We already talked about this a long time ago after @Simon-Laux noticed this problem, but I think we didn't actually solve it back then?

Best to review with whitespace changes disabled.
2023-09-05 09:15:15 +02:00
dependabot[bot]
4815e9b990 Merge pull request #4680 from deltachat/dependabot/cargo/regex-1.9.5 2023-09-05 00:03:09 +00:00
dependabot[bot]
f05b0ddf04 chore(cargo): bump regex from 1.9.1 to 1.9.5
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.1 to 1.9.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.1...1.9.5)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 19:09:31 +00:00
link2xt
f94d34c94b chore: remove idna from deny.toml 2023-09-04 19:08:49 +00:00
link2xt
e9811fb6da AsyncResolver::tokio does not return a Result anymore 2023-09-04 19:08:49 +00:00
dependabot[bot]
178fc1736d chore(cargo): bump trust-dns-resolver from 0.22.0 to 0.23.0
Bumps [trust-dns-resolver](https://github.com/bluejekyll/trust-dns) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/bluejekyll/trust-dns/releases)
- [Changelog](https://github.com/bluejekyll/trust-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bluejekyll/trust-dns/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: trust-dns-resolver
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 19:08:49 +00:00
dependabot[bot]
54d632adaf chore(cargo): bump axum from 0.6.19 to 0.6.20
Bumps [axum](https://github.com/tokio-rs/axum) from 0.6.19 to 0.6.20.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.6.19...axum-v0.6.20)

---
updated-dependencies:
- dependency-name: axum
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 19:08:34 +00:00
Hocuri
ae939e79da Merge remote-tracking branch 'origin/stable' 2023-09-04 19:19:50 +02:00
dependabot[bot]
e12ef805a9 Merge pull request #4675 from deltachat/dependabot/cargo/backtrace-0.3.69 2023-09-03 19:50:35 +00:00
dependabot[bot]
b36acb2dc0 chore(cargo): bump log from 0.4.19 to 0.4.20
Bumps [log](https://github.com/rust-lang/log) from 0.4.19 to 0.4.20.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.19...0.4.20)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-03 02:25:33 +00:00
dependabot[bot]
1b883ae3fa chore(cargo): bump typescript-type-def from 0.5.7 to 0.5.8
Bumps [typescript-type-def](https://github.com/dbeckwith/rust-typescript-type-def) from 0.5.7 to 0.5.8.
- [Changelog](https://github.com/dbeckwith/rust-typescript-type-def/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dbeckwith/rust-typescript-type-def/compare/v0.5.7...v0.5.8)

---
updated-dependencies:
- dependency-name: typescript-type-def
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-03 02:25:00 +00:00
dependabot[bot]
72c94e1037 chore(cargo): bump serde from 1.0.180 to 1.0.188
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.180 to 1.0.188.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.180...v1.0.188)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-03 02:24:33 +00:00
dependabot[bot]
0a4c993bb8 chore(cargo): bump serde_json from 1.0.104 to 1.0.105
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.104 to 1.0.105.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.104...v1.0.105)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-03 00:01:58 +00:00
link2xt
ec56134583 Remove winreg entry from deny.toml 2023-09-02 23:01:00 +00:00
dependabot[bot]
d8bf1c1691 chore(cargo): bump reqwest from 0.11.18 to 0.11.20
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.18 to 0.11.20.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.18...v0.11.20)

---
updated-dependencies:
- dependency-name: reqwest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 23:01:00 +00:00
dependabot[bot]
8ac1754e18 chore(cargo): bump tempfile from 3.7.0 to 3.8.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.7.0 to 3.8.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 22:05:42 +00:00
dependabot[bot]
e1f1143919 chore(cargo): bump quote from 1.0.32 to 1.0.33
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.32 to 1.0.33.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.32...1.0.33)

---
updated-dependencies:
- dependency-name: quote
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 22:05:08 +00:00
dependabot[bot]
d9e38289c4 chore(cargo): bump image from 0.24.6 to 0.24.7
Bumps [image](https://github.com/image-rs/image) from 0.24.6 to 0.24.7.
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.6...v0.24.7)

---
updated-dependencies:
- dependency-name: image
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 21:57:51 +00:00
dependabot[bot]
486050d0b8 chore(cargo): bump backtrace from 0.3.68 to 0.3.69
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.68 to 0.3.69.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.68...0.3.69)

---
updated-dependencies:
- dependency-name: backtrace
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 19:16:16 +00:00
dependabot[bot]
828c90ac3d chore(cargo): bump tokio from 1.29.1 to 1.32.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.32.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.32.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 19:14:54 +00:00
dependabot[bot]
ab09ecce7e chore(cargo): bump schemars from 0.8.12 to 0.8.13
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.12 to 0.8.13.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.12...v0.8.13)

---
updated-dependencies:
- dependency-name: schemars
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 18:26:24 +00:00
dependabot[bot]
aebad2eb10 chore(cargo): bump url from 2.4.0 to 2.4.1
Bumps [url](https://github.com/servo/rust-url) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:45:25 +00:00
dependabot[bot]
2a39a85d9e chore(cargo): bump thiserror from 1.0.44 to 1.0.47
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.44 to 1.0.47.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.44...1.0.47)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:44:37 +00:00
dependabot[bot]
cb0270baa7 chore(cargo): bump anyhow from 1.0.72 to 1.0.75
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.72 to 1.0.75.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.72...1.0.75)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:44:11 +00:00
dependabot[bot]
99302c9598 chore(cargo): bump base64 from 0.21.2 to 0.21.3
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.21.2 to 0.21.3.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.21.2...v0.21.3)

---
updated-dependencies:
- dependency-name: base64
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:44:02 +00:00
dependabot[bot]
88ae653760 chore(cargo): bump chrono from 0.4.26 to 0.4.28
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.26 to 0.4.28.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.26...v0.4.28)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:43:13 +00:00
dependabot[bot]
6881f9d70f chore(cargo): bump syn from 2.0.28 to 2.0.29
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.28 to 2.0.29.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.28...2.0.29)

---
updated-dependencies:
- dependency-name: syn
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-02 17:42:09 +00:00
dependabot[bot]
60ddbe5729 chore(cargo): bump strum_macros from 0.25.1 to 0.25.2
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.25.1 to 0.25.2.
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

---
updated-dependencies:
- dependency-name: strum_macros
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-01 21:09:22 +00:00
iequidoo
5e5e557c8b Merge branch 'stable'
7c7cd9cc80 is a backport from `master`, so skip it.
2023-09-01 13:23:10 -03:00
iequidoo
bc8023644c Merge commit '7c7cd9cc8084f0e425b1919b84d5e79ed150d16b~' into HEAD
Merge `stable` up to the mentioned commit.
2023-09-01 13:14:44 -03:00
iequidoo
83ef25e7de fix: Return from dc_get_chatlist(DC_GCL_FOR_FORWARDING) only chats where we can send (#4616)
I.e. exclude from the list the following chats as well:
- Read-only mailing lists.
- Chats we're not a member of.

But as for ProtectionBroken chats, we return them, 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.
2023-09-01 10:26:22 -03:00
link2xt
9a7d1faf75 Merge tag 'v1.120.0' 2023-08-28 11:55:53 +00:00
link2xt
e59c4ee858 Merge branch 'stable' 2023-08-27 22:18:36 +00:00
link2xt
a520f0268f chore: rustfmt 2023-08-26 18:15:11 +00:00
link2xt
5e3b1fa540 Merge branch 'stable' 2023-08-26 18:12:13 +00:00
iequidoo
95f29f7b63 fix: receive_imf: Set protection only for Chattype::Single (#4597)
Also don't set protection to ProtectionBroken if it already is.

Co-authored-by: Hocuri <hocuri@gmx.de>
2023-08-26 13:19:48 -03:00
link2xt
20513475ef Merge branch 'stable' 2023-08-25 01:59:04 +00:00
link2xt
3b806320ec Merge branch 'stable' 2023-08-24 22:25:02 +00:00
dependabot[bot]
c857d6e1bd Merge pull request #4591 from deltachat/dependabot/cargo/sanitize-filename-0.5.0 2023-08-24 20:01:21 +00:00
dependabot[bot]
94cd9a713f chore(cargo): bump sanitize-filename from 0.4.0 to 0.5.0
Bumps [sanitize-filename](https://github.com/kardeiz/sanitize-filename) from 0.4.0 to 0.5.0.
- [Commits](https://github.com/kardeiz/sanitize-filename/commits)

---
updated-dependencies:
- dependency-name: sanitize-filename
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-24 18:42:54 +00:00
link2xt
7676473ebd fix: do not mark non-verified group chats as verified when using securejoin
Only mark the chat is verified if 1:1 verified chats are enabled
and securejoin targets a 1:1 chat.
2023-08-24 18:39:56 +00:00
link2xt
8c778b3f5c test: extend test_qr_join_chat to check that the group is not verified 2023-08-24 18:39:56 +00:00
iequidoo
a66f8bd9fc fix: Delete messages from SMTP queue only on user demand (#4579)
I.e. from delete_msgs(). Otherwise messages must not be deleted from there, e.g. if a message is
ephemeral, but a network outage lasts longer than the ephemeral message timer, the message still
must be sent upon a successful reconnection.
2023-08-24 13:39:22 -03:00
iequidoo
95b2a15930 fix: Sort old incoming messages below all outgoing ones (#4621)
If the Inbox is fetched before the Sentbox (as done currently), messages from the Sentbox will
correctly mingle with the Inbox messages in the end. So, this commit changes message ordering only
if we already have processed outgoing messages, e.g. if we just sent them in the chat as described
in #4621. Otherwise new incoming messages are displayed somewhere in the middle of the chat which
doesn't look usable.
2023-08-24 13:22:26 -03:00
iequidoo
0179ec2da9 fix: receive_imf: Update peerstate from db after handling Securejoin handshake (#4600)
Otherwise has_verified_encryption() would check a stale Peerstate object.
2023-08-24 01:29:25 +00:00
iequidoo
8f2313bb2a test: test_openrpc_command_line: Check that deltachat-rpc-server exists with 0 2023-08-23 19:30:14 +00:00
link2xt
488a3d1118 build(deny): ignore RUSTSEC-2023-0052 2023-08-23 19:30:14 +00:00
link2xt
9094df7bc7 build(python): pin sphinx to 7.1.2 2023-08-23 19:30:14 +00:00
link2xt
16aad3fa67 build(cargo-deny): ignore RUSTSEC-2022-0093
It is an API issue that can only be fixed in rPGP and iroh upstream.
2023-08-17 12:20:58 +00:00
iequidoo
3b47c3f21d ci: Run Rust tests with RUST_BACKTRACE set 2023-08-15 14:24:02 -03:00
link2xt
987ce58926 chore(python): fix lint errors 2023-08-11 17:06:15 +00:00
iequidoo
20c88743df test: W/a message reordering in test_reaction_to_partially_fetched_msg() 2023-08-08 21:02:41 -03:00
dependabot[bot]
03395b95cb chore(cargo): bump quick-xml from 0.29.0 to 0.30.0
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.29.0 to 0.30.0.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-08 21:02:41 -03:00
Hocuri
53f04a134a test: Don't accidentally accept that a chat protection is broken (#4550) 2023-08-08 11:35:38 +02:00
Hocuri
885f26ea8c test: Directly unwrap in TestContext::get_chat() (#4614)
Directly unwrap in TestContext::get_chat()

Turns out that all usages of get_chat() directly unwrapped, because in a
test it doesn't make sense to handle the error of there being no chat.
2023-08-08 11:34:52 +02:00
link2xt
3ab181fdf8 build: update to Zig 0.11.0 2023-08-06 19:44:48 +00:00
link2xt
e12044e6af api!(deltachat-jsonrpc): use kind as a tag for all union types 2023-08-03 23:46:24 +00:00
link2xt
954067eb6d Merge tag 'v1.119.0' 2023-08-03 17:05:00 +00:00
link2xt
aecbebd566 docs: improve JSON-RPC API documentation 2023-08-03 16:06:36 +00:00
link2xt
3111bcde5e refactor: flatten imports in deltachat-jsonrpc 2023-08-03 15:58:48 +00:00
link2xt
e6cffd537e refactor: remove Chattype::Undefined 2023-08-02 17:02:28 +00:00
link2xt
70000d9ebb build: increase MSRV to 1.67.0
This is required by time v0.3.24
2023-08-02 03:27:21 +00:00
link2xt
d95843b0bf chore(deps): update dependencies 2023-08-02 03:23:19 +00:00
link2xt
13e766bc37 feat(deltachat-rpc-server): add --openrpc option 2023-08-01 18:27:02 +00:00
link2xt
c34edc582e test: test that get_system_info() works over RPC backup import 2023-08-01 01:05:06 +00:00
link2xt
8eee389c09 refactor: use SQL transaction in MsgId.delete_from_db() 2023-07-31 20:01:35 +00:00
link2xt
8ed6d4d709 api!: make MsgId.delete_from_db() private
Use `delete_msgs()` if you are using the Delta Chat core
as a library and want to delete a message.
2023-07-31 20:01:35 +00:00
Hocuri
60bacbec47 feat: Don't show a contact as verified if their key changed since the verification (#4574)
Don't show a contact as verified if their key changed in the meantime

If a contact's key changed since the verification, then it's very
unlikely that they still have the old, verified key. So, don't show them
as verified anymore.

This also means that you can't add a contact like this to a verified
group, which is good.

The documentation actually already described this (new) behavior:

```rust
/// and if the key has not changed since this verification.
```

so, this adapts the code to the documentation.
2023-07-31 18:59:45 +02:00
link2xt
af013559de refactor: hide DcSecretKey trait from the API 2023-07-29 18:10:25 +00:00
link2xt
b784415c57 refactor: move dc_preconfigure_keypair() implementation into deltachat crate
This allows to hide `DcKey` trait from public API.
2023-07-29 18:05:05 +00:00
link2xt
85739ba6ad refactor: make last_added_location_id an Option 2023-07-29 17:45:15 +00:00
B. Petersen
a02a593f47 fix example; this was changed some time ago, see https://docs.webxdc.org/spec.html#sendupdate 2023-07-29 05:08:54 +02:00
link2xt
67f28f501a Merge branch 'stable' 2023-07-27 19:40:01 +00:00
link2xt
9b9703a48e refactor: replace DcKey.load_self trait method with functions 2023-07-27 18:23:56 +00:00
link2xt
c55a3d3873 refactor: flatten and simplify imports 2023-07-27 17:47:30 +00:00
iequidoo
f27d304f3b feat!: Add lockfile to account manager (#4310)
Opening the same account (context) from multiple processes is dangerous, can result in duplicate
downloads of the same message etc. Same for account manager, attempts to modify the same
accounts.toml even if done atomically with may result in corrupted files as atomic replacement
procedure does not expect that multiple processes may write to the same temporary file.

accounts.toml cannot be used as a lockfile because it is replaced during atomic update. Therefore, a
new file next to accounts.toml is needed to prevent starting second account manager in the same
directory.

But iOS needs to be able to open accounts from multiple processes at the same time. This is required
as the "share-to-DC extension" is a separate process by iOS design -- this process may or may not be
started while the main app is running. Accounts are not altered however by this extension, so let's
add to the `Accounts::new()` constructor an `rdwr` parameter which allows to read the accounts
config w/o locking the lockfile.
2023-07-26 16:02:25 -03:00
link2xt
6d51d19f01 refactor(e2ee): do not return anything from ensure_secret_key_exists()
The return value was never used.
2023-07-26 11:19:08 +00:00
Hocuri
170968dfc2 Update README.md 2023-07-25 16:50:10 +02:00
link2xt
f930576fd1 Merge branch 'stable' 2023-07-24 18:40:44 +00:00
link2xt
d797de7a8d refactor: use slices and vectors instead of Keyring wrapper
This change removes all traces of dc_keyring_t,
which was a C implementation of dynamically sized array.
2023-07-24 18:05:38 +00:00
link2xt
acc7bb00c5 chore(deps): update rPGP 2023-07-24 16:14:16 +00:00
link2xt
8fb8a877be chore(deps): update dependencies 2023-07-24 14:06:09 +00:00
Hocuri
b96028cd87 api!(Rust): Remove unused function is_verified_ex() (#4551)
No one used it anymore, and all occurences I could find (on
GitHub)[https://github.com/search?q=%22is_verified_ex%22&type=code&p=1]
are either forks of deltachat-core-rust or of deltachat-core (which is
the old C core).
2023-07-24 12:19:13 +02:00
Hocuri
682e241edb fix: Fix info-message orderings of verified 1:1 chats (#4545)
Correctly handle messages with old timestamps for verified chats:

 * They must not be sorted over a protection-changed info message

 * If they change the protection, then they must not be sorted over existing other messages, because then the protection-changed info message would also be above these existing messages.


This PR fixes this:

 1. Even seen messages can't be sorted into already-noticed messages anymore. **This also changes DC's behavior in the absence of verified 1:1 chats**. Before this PR, messages that are marked as seen when they are downloaded will always be sorted by their timestamp, even if it's very old.

 2. protection-changed info messages are always sorted to the bottom.

    **Edit:**

 3. There is an exception to rule 1: Outgoing messages are still allowed to be sorted purely by their timestamp, and don't influence old messages. This is to the problem described at [*].


Together, these rules also make sure that the protection-changed info message is always right above the message causing the change.

[*] If we receive messages from two different folders, e.g. `Sent` and `Inbox`, then this will lead to wrong message ordering in many cases. I need to think about this more, or maybe someone else has an idea. One new idea that came to my mind is:

 * Always sort noticed messages under the newest info message (this PR sorts them under the newest noticed message, master sorts them purely by their sent timestamp)

 * Always sort unnoticed messages under the newest noticed message (that's the same behavior as in this PR and on master)

 * Always sort protection-changed info messages to the bottom (as in this PR)


However, after a talk with @link2xt we instead decided to add rule 3. (see above) because it seemed a little bit easier.
2023-07-24 12:16:32 +02:00
Simon Laux
3a63628f1f update node constants
looks like this was fogotten when changing the chat protection stock strings
2023-07-23 09:26:01 +00:00
link2xt
3705616cd9 Merge branch 'stable' 2023-07-23 09:17:13 +00:00
Simon Laux
b8fcb660ad cargo fmt 2023-07-23 02:29:42 +02:00
Simon Laux
5673294623 api(jsonrpc): add resend_messages 2023-07-23 02:29:42 +02:00
B. Petersen
7062bb0502 clarify transitive behaviour of dc_contact_is_verfified() 2023-07-22 20:58:05 +02:00
link2xt
659cffe0cc ci: remove comment about python from rust tests 2023-07-19 13:43:20 +00:00
link2xt
a1663a98e0 build: use Rust 1.71.0 and increase MSRV to 1.66.0
Rust 1.66 is required by constant_time_eq 0.3.0.
2023-07-19 13:41:31 +00:00
link2xt
3de1dbc9e4 chore(deps): update dependencies 2023-07-19 13:26:47 +00:00
link2xt
6d37e8601e Merge branch 'stable' 2023-07-17 17:11:38 +00:00
Hocuri
d762753103 fix: Allow to save a draft if the verification is broken (#4542)
If the verification is broken, `can_send()` is false.

But if the user was typing a message right when a verification-breaking message came in, the UI still needs to be able to save it as a draft.

Steps to reproduce the bug:
  - Set a draft
  - Your chat partner breaks verification
  - Go back to the chats list
  - Go to the chat again
  - Accept the breakage
  - Expected: The draft is still there
  - Bug behavior: The draft is gone
2023-07-16 12:04:43 +02:00
link2xt
a020d5ccce Merge branch 'stable' 2023-07-14 11:23:43 +00:00
Hocuri
1e28ea9bb0 fix: Don't create 1:1 chat as protected for contact who doesn't prefer to encrypt (#4538) 2023-07-11 17:39:59 +00:00
Hocuri
17f2d33731 test: Remove unnecessary inner_set_protection() call (#4539)
1:1 chats are automatically created as protected if the contact is
verified, there is no need to explicitly do this.

Plus, by removing this call, the test also tests that automatically
creating 1:1 chats as protected works.
2023-07-11 19:15:23 +02:00
link2xt
976797d4cf build: remove examples/simple.rs
When `cargo test` is executed,
all examples are built by default
to ensure that they can be compiled.

This is a documented and expected behaviour,
even though it was previously reported as a bug:
<https://github.com/rust-lang/cargo/issues/6675>

In particular, `examples/simple.rs` is built into
a 67M binary `target/debug/examples/simple`.
This is unnecessary to do so every time
you change a line in the `deltachat` crate
and want to rerun the tests.

Workaround is to run `cargo test --tests`,
but it is easy to forget and is not discoverable
unless you read the "Target Selection" section of `cargo help test`.

We have a maintained example at https://github.com/deltachat-bot/echo,
so there is no need for an example in the core repository.
2023-07-10 21:49:31 +00:00
link2xt
31e3169433 chore: nightly clippy fixes 2023-07-10 11:38:46 +02:00
link2xt
d2b15cb629 docs: document how logs and error messages should be formatted 2023-07-09 16:18:18 +00:00
Hocuri
9cd000c4f2 feat: Verified 1:1 chats (#4315)
Implement #4188

BREAKING CHANGE: Remove unused DC_STR_PROTECTION_(EN)ABLED* strings
BREAKING CHANGE: Remove unused dc_set_chat_protection()
2023-07-09 14:06:45 +02:00
link2xt
243c035b03 chore: spellcheck 2023-07-07 21:56:59 +00:00
124 changed files with 3970 additions and 2842 deletions

View File

@@ -25,7 +25,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.72.0
RUSTUP_TOOLCHAIN: 1.73.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -81,19 +81,15 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.68.2
rust: 1.73.0
- os: windows-latest
rust: 1.68.2
rust: 1.73.0
- os: macos-latest
rust: 1.68.2
rust: 1.73.0
# Minimum Supported Rust Version = 1.65.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
# Minimum Supported Rust Version = 1.67.0
- os: ubuntu-latest
rust: 1.65.0
rust: 1.67.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
@@ -106,6 +102,8 @@ jobs:
uses: swatinem/rust-cache@v2
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace
- name: Test cargo vendor
@@ -182,15 +180,15 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
python: 3.12
- os: macos-latest
python: 3.11
python: 3.12
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
python: pypy3.10
- os: macos-latest
python: pypy3.9
python: pypy3.10
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
@@ -232,22 +230,19 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.11
python: 3.12
- os: macos-latest
python: 3.11
python: 3.12
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
python: pypy3.10
- os: macos-latest
python: pypy3.9
python: pypy3.10
# Minimum Supported Python Version = 3.8
#
# Python 3.7 has at least one known bug related to starting subprocesses
# in asyncio programs: <https://bugs.python.org/issue35621>
# Minimum Supported Python Version = 3.7
- os: ubuntu-latest
python: 3.8
python: 3.7
runs-on: ${{ matrix.os }}
steps:

View File

@@ -1,5 +1,65 @@
# Changelog
## [1.124.1] - 2023-10-05
### Fixes
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
### CI
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
## [1.124.0] - 2023-10-04
### API-Changes
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
- Deprecate `dc_contact_get_verifier_addr`.
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
- Deprecate `get_next_media()`.
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
### Fixes
- Wrap base64-encoded parts to 76 characters.
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
- Do not put the status footer into reaction MIME parts.
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
### Features / Changes
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
- deltachat-rpc-client: Log exceptions when long-running tasks die.
### Build
- Build wheels for Python 3.12 and PyPy 3.10.
## [1.123.0] - 2023-09-22
### API-Changes
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
### Fixes
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
- imex: Use "default" in the filename of the default key.
### Miscellaneous Tasks
- Update OpenSSL from 3.1.2 to 3.1.3.
## [1.122.0] - 2023-09-12
### API-Changes
@@ -2816,3 +2876,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1

View File

@@ -76,6 +76,29 @@ If you have multiple changes in one PR, create multiple conventional commits, an
[Conventional Commits]: https://www.conventionalcommits.org/
[git-cliff]: https://git-cliff.org/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.

679
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.122.0"
version = "1.124.1"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
rust-version = "1.67"
[profile.dev]
debug = 0
@@ -42,17 +42,18 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = { version = "3.3", default-features=false, features = ["std"] }
brotli = { version = "3.4", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
futures = "0.3"
futures-lite = "1.13.0"
hex = "0.4.0"
humansize = "2"
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
@@ -68,13 +69,13 @@ parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.29"
quick-xml = "0.30"
rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11.18", features = ["json"] }
regex = "1.9"
reqwest = { version = "0.11.20", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
sanitize-filename = "0.5"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
@@ -89,9 +90,9 @@ tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.8"
tokio-util = "0.7.9"
toml = "0.7"
trust-dns-resolver = "0.22"
trust-dns-resolver = "0.23"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
@@ -118,11 +119,6 @@ members = [
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]]
name = "create_account"
harness = false

View File

@@ -1,8 +1,16 @@
# Delta Chat Rust
<p align="center">
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
</p>
> Deltachat-core written in Rust
<p align="center">
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
</a>
</p>
[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
<p align="center">
The core library for Delta Chat, written in Rust
</p>
## Installing Rust and Cargo

View File

@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 2..n {
let id = accounts.add_account().await.unwrap();

View File

@@ -54,7 +54,7 @@ header = """
# Changelog\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
# https://keats.github.io/tera/docs/#templates
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}

View File

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

View File

@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
# exclude all test directories use the pattern */test/*
######################################################
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
EXCLUDE_SYMBOLS += _dc_* jsmn*
######################################################

View File

@@ -500,6 +500,13 @@ char* dc_get_blobdir (const dc_context_t* context);
* to not mess up with non-delivery-reports or read-receipts.
* 0=no limit (default).
* Changes affect future messages only.
* - `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.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
@@ -824,7 +831,7 @@ void dc_maybe_network (dc_context_t* context);
* @param context The context as created by dc_context_new().
* @param addr The e-mail address of the user. This must match the
* configured_addr setting of the context as well as the UID of the key.
* @param public_data ASCII armored public key.
* @param public_data Ignored, actual public key is extracted from secret_data.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -1118,7 +1125,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
*
* In JS land, that would be mapped to something as:
* ```
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
* ```
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
* See dc_get_webxdc_status_updates() for the receiving counterpart.
@@ -1175,24 +1182,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
/**
* Replaces webxdc app with a new version.
*
* On the JavaScript side this API could be used like this:
* ```
* window.webxdc.replaceWebxdc(blob);
* ```
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the WebXDC message to be replaced.
* @param blob New blob to replace WebXDC with.
* @param n Blob size.
*/
void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n);
/**
* Save a draft for a chat in the database.
*
@@ -1499,6 +1488,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param msg_id The ID of the current message from which the next or previous message should be searched.
@@ -1517,24 +1507,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
/**
* Enable or disable protection against active attacks.
* To enable protection, it is needed that all members are verified;
* if this condition is met, end-to-end-encryption is always enabled
* and only the verified keys are used.
*
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat to change the protection for.
* @param protect 1=protect chat, 0=unprotect chat
* @return 1=success, 0=error, e.g. some members may be unverified
*/
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
/**
* Set chat visibility to pinned, archived or normal.
*
@@ -2974,12 +2946,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
* @param dir The directory to create the context-databases in.
* If the directory does not exist,
* dc_accounts_new() will try to create it.
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
* @return An account manager object.
* The object must be passed to the other account manager functions
* and must be freed using dc_accounts_unref() after usage.
* On errors, NULL is returned.
*/
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
/**
@@ -3760,7 +3735,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
* Check if a chat is protected.
* Protected chats contain only verified members and encryption is always enabled.
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
* The status can be changed using dc_set_chat_protection().
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3769,6 +3743,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* 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().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
/**
* Check if locations are sent to the chat
* at the time the object was created using dc_get_chat().
@@ -4363,7 +4357,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
* Check if the message is an informational message, created by the
* device or by another users. Such messages are not "typed" by the user but
* created due to other actions,
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
* or dc_add_contact_to_chat().
*
* These messages are typically shown in the center of the chat view,
@@ -5057,7 +5051,12 @@ int dc_contact_is_verified (dc_contact_t* contact);
/**
* Return the address that verified a contact
*
* The UI may use this in addition to a checkmark showing the verification status
* The UI may use this in addition to a checkmark showing the verification status.
* In case of verification chains,
* the last contact in the chain is shown.
* This is because of privacy reasons, but also as it would not help the user
* to see a unknown name here - where one can mostly always ask the shown name
* as it is directly known.
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5065,6 +5064,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
* A string containing the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is an empty string, we don't have verifier
* information or the contact is not verified.
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
*/
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
@@ -5077,7 +5077,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5773,12 +5773,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
*
* @memberof dc_jsonrpc_instance_t
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @param input JSON-RPC request.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* On error, NULL is returned.
* If there is no response, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
/**
* @class dc_event_emitter_t
@@ -6798,15 +6797,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in error strings.
#define DC_STR_ERROR_NO_NETWORK 87
/// "Chat protection enabled."
///
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
#define DC_STR_PROTECTION_ENABLED 88
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
#define DC_STR_PROTECTION_DISABLED 89
/// "Reply"
///
/// Used in summaries.
@@ -7251,26 +7241,6 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You enabled chat protection."
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
/// "Chat protection enabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
/// "You disabled chat protection."
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
/// "Chat protection disabled by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7281,6 +7251,16 @@ 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."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%1$s sent a message from another device."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/**
* @}
*/

View File

@@ -29,14 +29,14 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::DcKey;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
use num_traits::{FromPrimitive, ToPrimitive};
@@ -805,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
pub unsafe extern "C" fn dc_preconfigure_keypair(
context: *mut dc_context_t,
addr: *const libc::c_char,
public_data: *const libc::c_char,
_public_data: *const libc::c_char,
secret_data: *const libc::c_char,
) -> i32 {
if context.is_null() {
@@ -813,21 +813,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
return 0;
}
let ctx = &*context;
block_on(async move {
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
let keypair = key::KeyPair {
addr,
public,
secret,
};
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
Ok::<_, anyhow::Error>(1)
})
.context("Failed to save keypair")
.log_err(ctx)
.unwrap_or(0)
let addr = to_string_lossy(addr);
let secret_data = to_string_lossy(secret_data);
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
.context("Failed to save keypair")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -1097,32 +1088,6 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_replace_webxdc(
context: *mut dc_context_t,
msg_id: u32,
blob: *const u8,
n: libc::size_t,
) {
if context.is_null() {
eprintln!("ignoring careless call to dc_replace_webxdc()");
return;
}
let msg_id = MsgId::new(msg_id);
let blob_slice = std::slice::from_raw_parts(blob, n);
let ctx = &*context;
block_on(async move {
replace_webxdc(ctx, msg_id, blob_slice)
.await
.context("Failed to replace WebXDC")
.log_err(ctx)
.ok();
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1457,6 +1422,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
}
#[no_mangle]
#[allow(deprecated)]
pub unsafe extern "C" fn dc_get_next_media(
context: *mut dc_context_t,
msg_id: u32,
@@ -1497,32 +1463,6 @@ pub unsafe extern "C" fn dc_get_next_media(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_protection(
context: *mut dc_context_t,
chat_id: u32,
protect: libc::c_int,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_set_chat_protection()");
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
return 0;
};
block_on(async move {
match ChatId::new(chat_id).set_protection(ctx, protect).await {
Ok(()) => 1,
Err(_) => 0,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_chat_visibility(
context: *mut dc_context_t,
@@ -1565,10 +1505,14 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
}
let ctx = &*context;
block_on(ChatId::new(chat_id).delete(ctx))
.context("Failed chat delete")
.log_err(ctx)
.ok();
block_on(async move {
ChatId::new(chat_id)
.delete(ctx)
.await
.context("Failed chat delete")
.log_err(ctx)
.ok();
})
}
#[no_mangle]
@@ -2589,7 +2533,12 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
}
#[no_mangle]
@@ -3148,6 +3097,16 @@ 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_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
@@ -4553,7 +4512,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4580,11 +4546,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
match socks5_enabled {
Ok(socks5_enabled) => {
match block_on(provider::get_provider_info(
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
socks5_enabled,
)) {
))
.log_err(ctx)
.unwrap_or_default()
{
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4756,17 +4725,17 @@ pub type dc_accounts_t = AccountsWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_new(
_os_name: *const libc::c_char,
dbfile: *const libc::c_char,
dir: *const libc::c_char,
writable: libc::c_int,
) -> *mut dc_accounts_t {
setup_panic!();
if dbfile.is_null() {
if dir.is_null() {
eprintln!("ignoring careless call to dc_accounts_new()");
return ptr::null_mut();
}
let accs = block_on(Accounts::new(as_path(dbfile).into()));
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
match accs {
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
@@ -5032,7 +5001,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
#[cfg(feature = "jsonrpc")]
mod jsonrpc {
use deltachat_jsonrpc::api::CommandApi;
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
use super::*;
@@ -5108,25 +5077,24 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
method: *const libc::c_char,
params: *const libc::c_char,
input: *const libc::c_char,
) -> *mut libc::c_char {
if jsonrpc_instance.is_null() {
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
return ptr::null_mut();
}
let api = &*jsonrpc_instance;
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
Ok(params) => Some(params),
Err(_) => None,
};
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
let res = block_on(api.handle.server().handle_request(method, params));
let input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
match res {
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.122.0"
version = "1.124.1"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -15,26 +15,26 @@ required-features = ["webserver"]
anyhow = "1"
deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.11"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.6.0"
tempfile = "3.8.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.99"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.29.1" }
sanitize-filename = "0.4"
serde_json = "1.0.105"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.32.0" }
sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.18", optional = true, features = ["ws"] }
axum = { version = "0.6.20", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -4,30 +4,30 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::message::get_msg_read_receipts;
use deltachat::qr::Qr;
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
},
chatlist::Chatlist,
config::Config,
constants::DC_MSG_ID_DAYMARKER,
contact::{may_be_valid_addr, Contact, ContactId, Origin},
context::get_info,
ephemeral::Timer,
imex, location,
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
reaction::{get_msg_reactions, send_reaction},
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::Config;
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::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -142,11 +142,7 @@ impl CommandApi {
}
}
#[rpc(
all_positional,
ts_outdir = "typescript/generated",
openrpc_outdir = "openrpc"
)]
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
@@ -157,12 +153,12 @@ impl CommandApi {
// Misc top level functions
// ---------------------------------------------
/// Check if an email address is valid.
/// Checks if an email address is valid.
async fn check_email_validity(&self, email: String) -> bool {
may_be_valid_addr(&email)
}
/// Get general system info.
/// Returns general system info.
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
get_info()
}
@@ -223,11 +219,13 @@ impl CommandApi {
Ok(accounts)
}
/// Starts background tasks for all accounts.
async fn start_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.start_io().await;
Ok(())
}
/// Stops background tasks for all accounts.
async fn stop_io_for_all_accounts(&self) -> Result<()> {
self.accounts.read().await.stop_io().await;
Ok(())
@@ -237,14 +235,16 @@ impl CommandApi {
// Methods that work on individual accounts
// ---------------------------------------------
async fn start_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
async fn stop_io(&self, id: u32) -> Result<()> {
let ctx = self.get_context(id).await?;
/// Stops background tasks for a single account.
async fn stop_io(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
Ok(())
}
@@ -311,11 +311,13 @@ impl CommandApi {
ctx.get_info().await
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
set_config(&ctx, &key, value.as_deref()).await
}
/// Updates a batch of configuration values.
async fn batch_set_config(
&self,
account_id: u32,
@@ -347,6 +349,7 @@ impl CommandApi {
Ok(qr_object)
}
/// Returns configuration value for the given key.
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
get_config(&ctx, &key).await
@@ -1427,6 +1430,10 @@ impl CommandApi {
///
/// one combined call for getting chat::get_next_media for both directions
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
///
/// Deprecated 2023-10-03, use `get_chat_media` method
/// and navigate the returned array instead.
#[allow(deprecated)]
async fn get_neighboring_chat_media(
&self,
account_id: u32,
@@ -1882,7 +1889,7 @@ impl CommandApi {
.context("path conversion to string failed")
}
/// save a sticker to a collection/folder in the account's sticker folder
/// Saves a sticker to a collection/folder in the account's sticker folder.
async fn misc_save_sticker(
&self,
account_id: u32,

View File

@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum Account {
#[serde(rename_all = "camelCase")]
Configured {

View File

@@ -167,10 +167,11 @@ impl BasicChat {
}
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
pub enum MuteDuration {
NotMuted,
Forever,
Until(i64),
Until { duration: i64 },
}
impl MuteDuration {
@@ -178,13 +179,13 @@ impl MuteDuration {
match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until(n) => {
if n <= 0 {
MuteDuration::Until { duration } => {
if duration <= 0 {
bail!("failed to read mute duration")
}
Ok(SystemTime::now()
.checked_add(Duration::from_secs(n as u64))
.checked_add(Duration::from_secs(duration as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
}
}

View File

@@ -15,7 +15,7 @@ use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum ChatListItemFetchResult {
#[serde(rename_all = "camelCase")]
ChatListItem {

View File

@@ -22,7 +22,7 @@ impl From<CoreEvent> for Event {
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum EventType {
/// The library-user may write an informational string to the log.
///

View File

@@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "variant")]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },

View File

@@ -4,7 +4,7 @@ use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "Qr", rename_all = "camelCase")]
#[serde(tag = "type")]
#[serde(tag = "kind")]
pub enum QrObject {
AskVerifyContact {
contact_id: u32,

View File

@@ -13,7 +13,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();
@@ -54,7 +55,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into();
let accounts = Accounts::new(tmp_dir).await?;
let writable = true;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts);
let (sender, mut receiver) = unbounded::<String>();

View File

@@ -19,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> {
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`.");
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let state = CommandApi::new(accounts);
let app = Router::new()

View File

@@ -35,7 +35,7 @@ async function run() {
const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts);
for (const account of accounts) {
if (account.type === "Configured") {
if (account.kind === "Configured") {
write(
$head,
`<a href="#" onclick="selectDeltaAccount(${account.id})">
@@ -57,7 +57,7 @@ async function run() {
clear($main);
const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.type !== "Configured") {
if (info.kind !== "Configured") {
return write($main, "Account is not configured");
}
write($main, `<h2>${info.addr!}</h2>`);
@@ -81,8 +81,7 @@ async function run() {
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
}
}
@@ -93,9 +92,9 @@ async function run() {
$side,
`
<p class="message">
[<strong>${event.type}</strong> on account ${accountId}]<br>
[<strong>${event.kind}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { type: undefined })
Object.assign({}, event, { kind: undefined })
)}
</p>`
);

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.122.0"
"version": "1.124.1"
}

View File

@@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["type"]]: (
[Property in EventType["kind"]]: (
accountId: number,
event: Extract<EventType, { type: Property }>
event: Extract<EventType, { kind: Property }>
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["type"]]: (
event: Extract<EventType, { type: Property }>
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
) => void;
};
export type DcEvent = EventType;
export type DcEventType<T extends EventType["type"]> = Extract<
export type DcEventType<T extends EventType["kind"]> = Extract<
EventType,
{ type: T }
{ kind: T }
>;
export class BaseDeltaChat<
@@ -46,12 +46,12 @@ export class BaseDeltaChat<
while (true) {
const event = await this.rpc.getNextEvent();
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event);
this.emit(event.event.kind, event.contextId, event.event);
this.emit("ALL", event.contextId, event.event);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
event.event.kind,
//@ts-ignore
event.event as any
);

View File

@@ -29,8 +29,8 @@ describe("online tests", function () {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
dc.on("ALL", (contextId, { kind }) => {
if (kind !== "Info") console.log(contextId, kind);
});
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
@@ -177,12 +177,12 @@ describe("online tests", function () {
});
});
async function waitForEvent<T extends DcEvent["type"]>(
async function waitForEvent<T extends DcEvent["kind"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { type: T }>> {
): Promise<Extract<DcEvent, { kind: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.122.0"
version = "1.124.1"
license = "MPL-2.0"
edition = "2021"
@@ -9,7 +9,7 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.19"
log = "0.4.20"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rustyline = "12"

View File

@@ -18,6 +18,7 @@ use deltachat::imex::*;
use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -138,11 +139,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
/* import a directory */
let dir_name = std::path::Path::new(&real_spec);
let dir = fs::read_dir(dir_name).await;
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let mut dir = dir.unwrap();
if let Ok(mut dir) = dir {
while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
@@ -154,6 +151,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
}
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
@@ -211,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.is_info() {
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -396,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
@@ -894,7 +902,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?;
let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
@@ -1072,20 +1080,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
};
chat::set_muted(&context, chat_id, duration).await?;
}
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);

View File

@@ -37,19 +37,14 @@ $ tox --devenv env
$ . env/bin/activate
```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
```
$ pip install ipython
$ PATH="../target/debug:$PATH" ipython
...
In [1]: from deltachat_rpc_client import *
In [2]: rpc = Rpc()
In [3]: await rpc.start()
In [4]: dc = DeltaChat(rpc)
In [5]: system_info = await dc.get_system_info()
In [6]: system_info["level"]
Out[6]: 'awesome'
In [7]: await rpc.close()
$ python
>>> from deltachat_rpc_client import *
>>> rpc = Rpc()
>>> rpc.start()
>>> dc = DeltaChat(rpc)
>>> system_info = dc.get_system_info()
>>> system_info["level"]
'awesome'
>>> rpc.close()
```

View File

@@ -4,23 +4,21 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
import asyncio
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()
@hooks.on(events.RawEvent)
async def log_event(event):
def log_event(event):
print(event)
@hooks.on(events.NewMessage)
async def echo(event):
def echo(event):
snapshot = event.message_snapshot
await snapshot.chat.send_text(snapshot.text)
snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__":
asyncio.run(run_bot_cli(hooks))
run_bot_cli(hooks)

View File

@@ -3,9 +3,9 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
import asyncio
import logging
import sys
from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -13,62 +13,62 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent)
async def log_event(event):
if event.type == EventType.INFO:
def log_event(event):
if event.kind == EventType.INFO:
logging.info(event.msg)
elif event.type == EventType.WARNING:
elif event.kind == EventType.WARNING:
logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR))
async def log_error(event):
def log_error(event):
logging.error(event.msg)
@hooks.on(events.MemberListChanged)
async def on_memberlist_changed(event):
def on_memberlist_changed(event):
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged)
async def on_group_image_changed(event):
def on_group_image_changed(event):
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged)
async def on_group_name_changed(event):
def on_group_name_changed(event):
logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command))
async def echo(event):
def echo(event):
snapshot = event.message_snapshot
if snapshot.text or snapshot.file:
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
@hooks.on(events.NewMessage(command="/help"))
async def help_command(event):
def help_command(event):
snapshot = event.message_snapshot
await snapshot.chat.send_text("Send me any message and I will echo it back")
snapshot.chat.send_text("Send me any message and I will echo it back")
async def main():
async with Rpc() as rpc:
def main():
with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await deltachat.get_system_info()
system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
bot = Bot(account, hooks)
if not await bot.is_configured():
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
await bot.run_forever()
if not bot.is_configured():
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
configure_thread.start()
bot.run_forever()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
main()

View File

@@ -2,45 +2,44 @@
"""
Example echo bot without using hooks
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
async def main():
async with Rpc() as rpc:
def main():
with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await deltachat.get_system_info()
system_info = deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
await account.set_config("bot", "1")
if not await account.is_configured():
account.set_config("bot", "1")
if not account.is_configured():
logging.info("Account is not configured, configuring")
await account.set_config("addr", sys.argv[1])
await account.set_config("mail_pw", sys.argv[2])
await account.configure()
account.set_config("addr", sys.argv[1])
account.set_config("mail_pw", sys.argv[2])
account.configure()
logging.info("Configured")
else:
logging.info("Account is already configured")
await deltachat.start_io()
deltachat.start_io()
async def process_messages():
for message in await account.get_next_messages():
snapshot = await message.get_snapshot()
def process_messages():
for message in account.get_next_messages():
snapshot = message.get_snapshot()
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
snapshot.chat.send_text(snapshot.text)
snapshot.message.mark_seen()
# Process old messages.
await process_messages()
process_messages()
while True:
event = await account.wait_for_event()
event = account.wait_for_event()
if event["type"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
@@ -49,9 +48,9 @@ async def main():
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
await process_messages()
process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
main()

View File

@@ -5,9 +5,6 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp"
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Framework :: AsyncIO",

View File

@@ -1,4 +1,4 @@
"""Delta Chat asynchronous high-level API"""
"""Delta Chat JSON-RPC high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat

View File

@@ -1,7 +1,7 @@
import argparse
import asyncio
import re
import sys
from threading import Thread
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING:
@@ -43,7 +43,7 @@ class AttrDict(dict):
super().__setattr__(attr, val)
async def run_client_cli(
def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -54,10 +54,10 @@ async def run_client_cli(
"""
from .client import Client
await _run_cli(Client, hooks, argv, **kwargs)
_run_cli(Client, hooks, argv, **kwargs)
async def run_bot_cli(
def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -68,10 +68,10 @@ async def run_bot_cli(
"""
from .client import Bot
await _run_cli(Bot, hooks, argv, **kwargs)
_run_cli(Bot, hooks, argv, **kwargs)
async def _run_cli(
def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
@@ -93,20 +93,20 @@ async def _run_cli(
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc)
core_version = (await deltachat.get_system_info()).deltachat_core_version
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
core_version = (deltachat.get_system_info()).deltachat_core_version
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not await client.is_configured():
if not client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
await client.run_forever()
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
configure_thread.start()
client.run_forever()
def extract_addr(text: str) -> str:

View File

@@ -24,63 +24,63 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
async def wait_for_event(self) -> AttrDict:
def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(await self._rpc.wait_for_event(self.id))
return AttrDict(self._rpc.wait_for_event(self.id))
async def remove(self) -> None:
def remove(self) -> None:
"""Remove the account."""
await self._rpc.remove_account(self.id)
self._rpc.remove_account(self.id)
async def start_io(self) -> None:
def start_io(self) -> None:
"""Start the account I/O."""
await self._rpc.start_io(self.id)
self._rpc.start_io(self.id)
async def stop_io(self) -> None:
def stop_io(self) -> None:
"""Stop the account I/O."""
await self._rpc.stop_io(self.id)
self._rpc.stop_io(self.id)
async def get_info(self) -> AttrDict:
def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
return AttrDict(await self._rpc.get_info(self.id))
return AttrDict(self._rpc.get_info(self.id))
async def get_size(self) -> int:
def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
return await self._rpc.get_account_file_size(self.id)
return self._rpc.get_account_file_size(self.id)
async def is_configured(self) -> bool:
def is_configured(self) -> bool:
"""Return True if this account is configured."""
return await self._rpc.is_configured(self.id)
return self._rpc.is_configured(self.id)
async def set_config(self, key: str, value: Optional[str] = None) -> None:
def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value."""
await self._rpc.set_config(self.id, key, value)
self._rpc.set_config(self.id, key, value)
async def get_config(self, key: str) -> Optional[str]:
def get_config(self, key: str) -> Optional[str]:
"""Get configuration value."""
return await self._rpc.get_config(self.id, key)
return self._rpc.get_config(self.id, key)
async def update_config(self, **kwargs) -> None:
def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
await self.set_config(key, value)
self.set_config(key, value)
async def set_avatar(self, img_path: Optional[str] = None) -> None:
def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar.
Passing None will discard the currently set avatar.
"""
await self.set_config("selfavatar", img_path)
self.set_config("selfavatar", img_path)
async def get_avatar(self) -> Optional[str]:
def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
return await self.get_config("selfavatar")
return self.get_config("selfavatar")
async def configure(self) -> None:
def configure(self) -> None:
"""Configure an account."""
await self._rpc.configure(self.id)
self._rpc.configure(self.id)
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
Calling this method will always result in the same
@@ -94,24 +94,24 @@ class Account:
if isinstance(obj, int):
obj = Contact(self, obj)
if isinstance(obj, Contact):
obj = (await obj.get_snapshot()).address
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
async def get_blocked_contacts(self) -> List[AttrDict]:
def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = await self._rpc.get_blocked_contacts(self.id)
contacts = self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
async def get_contacts(
def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
@@ -133,9 +133,9 @@ class Account:
flags |= ContactFlag.ADD_SELF
if snapshot:
contacts = await self._rpc.get_contacts(self.id, flags, query)
contacts = self._rpc.get_contacts(self.id, flags, query)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
contacts = self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@property
@@ -143,7 +143,7 @@ class Account:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
async def get_chatlist(
def get_chatlist(
self,
query: Optional[str] = None,
contact: Optional[Contact] = None,
@@ -175,29 +175,29 @@ class Account:
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry) for entry in entries]
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
for item in items.values():
item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item))
return chats
async def create_group(self, name: str, protect: bool = False) -> Chat:
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.
"""
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
async def secure_join(self, qrdata: str) -> Chat:
def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
@@ -208,62 +208,62 @@ class Account:
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
return Chat(self, self._rpc.secure_join(self.id, qrdata))
async def get_qr_code(self) -> Tuple[str, str]:
def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX.
"""
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
async def mark_seen_messages(self, messages: List[Message]) -> None:
def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
async def delete_messages(self, messages: List[Message]) -> None:
def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
async def get_fresh_messages(self) -> List[Message]:
def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first.
"""
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def get_next_messages(self) -> List[Message]:
def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = await self._rpc.get_next_msgs(self.id)
next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def wait_next_messages(self) -> List[Message]:
def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def export_backup(self, path, passphrase: str = "") -> None:
def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
await self._rpc.export_backup(self.id, str(path), passphrase)
self._rpc.export_backup(self.id, str(path), passphrase)
async def import_backup(self, path, passphrase: str = "") -> None:
def import_backup(self, path, passphrase: str = "") -> None:
"""Import backup."""
await self._rpc.import_backup(self.id, str(path), passphrase)
self._rpc.import_backup(self.id, str(path), passphrase)

View File

@@ -25,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc":
return self.account._rpc
async def delete(self) -> None:
def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
@@ -33,83 +33,83 @@ class Chat:
- does not delete messages on server
- the chat or contact is not blocked, new message will arrive
"""
await self._rpc.delete_chat(self.account.id, self.id)
self._rpc.delete_chat(self.account.id, self.id)
async def block(self) -> None:
def block(self) -> None:
"""Block this chat."""
await self._rpc.block_chat(self.account.id, self.id)
self._rpc.block_chat(self.account.id, self.id)
async def accept(self) -> None:
def accept(self) -> None:
"""Accept this contact request chat."""
await self._rpc.accept_chat(self.account.id, self.id)
self._rpc.accept_chat(self.account.id, self.id)
async def leave(self) -> None:
def leave(self) -> None:
"""Leave this chat."""
await self._rpc.leave_group(self.account.id, self.id)
self._rpc.leave_group(self.account.id, self.id)
async def mute(self, duration: Optional[int] = None) -> None:
def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever.
:param duration: mute duration from now in seconds. Must be greater than zero.
"""
if duration is not None:
assert duration > 0, "Invalid duration"
dur: Union[str, dict] = {"Until": duration}
dur: dict = {"kind": "Until", "duration": duration}
else:
dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
dur = {"kind": "Forever"}
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
async def unmute(self) -> None:
def unmute(self) -> None:
"""Unmute this chat."""
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
async def pin(self) -> None:
def pin(self) -> None:
"""Pin this chat."""
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
async def unpin(self) -> None:
def unpin(self) -> None:
"""Unpin this chat."""
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
async def archive(self) -> None:
def archive(self) -> None:
"""Archive this chat."""
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
async def unarchive(self) -> None:
def unarchive(self) -> None:
"""Unarchive this chat."""
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
async def set_name(self, name: str) -> None:
def set_name(self, name: str) -> None:
"""Set name of this chat."""
await self._rpc.set_chat_name(self.account.id, self.id, name)
self._rpc.set_chat_name(self.account.id, self.id, name)
async def set_ephemeral_timer(self, timer: int) -> None:
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
async def get_encryption_info(self) -> str:
def get_encryption_info(self) -> str:
"""Return encryption info for this chat."""
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
async def get_qr_code(self) -> Tuple[str, str]:
def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
async def get_basic_snapshot(self) -> AttrDict:
def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat."""
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def get_full_snapshot(self) -> AttrDict:
def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat."""
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def can_send(self) -> bool:
def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
return await self._rpc.can_send(self.account.id, self.id)
return self._rpc.can_send(self.account.id, self.id)
async def send_message(
def send_message(
self,
text: Optional[str] = None,
html: Optional[str] = None,
@@ -132,30 +132,30 @@ class Chat:
"overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg,
}
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id)
async def send_text(self, text: str) -> Message:
def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance."""
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
async def send_videochat_invitation(self) -> Message:
def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id)
async def send_sticker(self, path: str) -> Message:
def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
async def forward_messages(self, messages: List[Message]) -> None:
def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
async def set_draft(
def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
@@ -164,15 +164,15 @@ class Chat:
"""Set draft message."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
async def remove_draft(self) -> None:
def remove_draft(self) -> None:
"""Remove draft message."""
await self._rpc.remove_draft(self.account.id, self.id)
self._rpc.remove_draft(self.account.id, self.id)
async def get_draft(self) -> Optional[AttrDict]:
def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
snapshot = await self._rpc.get_draft(self.account.id, self.id)
snapshot = self._rpc.get_draft(self.account.id, self.id)
if not snapshot:
return None
snapshot = AttrDict(snapshot)
@@ -181,61 +181,61 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat."""
msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
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]
async def get_fresh_message_count(self) -> int:
def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
async def mark_noticed(self) -> None:
def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
await self._rpc.marknoticed_chat(self.account.id, self.id)
self._rpc.marknoticed_chat(self.account.id, self.id)
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = (await self.account.create_contact(cnt)).id
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = (await self.account.create_contact(cnt)).id
contact_id = self.account.create_contact(cnt).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
async def get_contacts(self) -> List[Contact]:
def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
"""
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
async def set_image(self, path: str) -> None:
def set_image(self, path: str) -> None:
"""Set profile image of this chat.
:param path: Full path of the image to use as the group image.
"""
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
async def remove_image(self) -> None:
def remove_image(self) -> None:
"""Remove profile image of this chat."""
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
async def get_locations(
def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
@@ -246,7 +246,7 @@ class Chat:
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:

View File

@@ -1,5 +1,4 @@
"""Event loop implementations offering high level event handling/hooking."""
import inspect
import logging
from typing import (
TYPE_CHECKING,
@@ -78,22 +77,22 @@ class Client:
)
self._hooks.get(type(event), set()).remove((hook, event))
async def is_configured(self) -> bool:
return await self.account.is_configured()
def is_configured(self) -> bool:
return self.account.is_configured()
async def configure(self, email: str, password: str, **kwargs) -> None:
await self.account.set_config("addr", email)
await self.account.set_config("mail_pw", password)
def configure(self, email: str, password: str, **kwargs) -> None:
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
await self.account.set_config(key, value)
await self.account.configure()
self.account.set_config(key, value)
self.account.configure()
self.logger.debug("Account configured")
async def run_forever(self) -> None:
def run_forever(self) -> None:
"""Process events forever."""
await self.run_until(lambda _: False)
self.run_until(lambda _: False)
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
"""Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the
@@ -101,39 +100,37 @@ class Client:
evaluates to True.
"""
self.logger.debug("Listening to incoming events...")
if await self.is_configured():
await self.account.start_io()
await self._process_messages() # Process old messages.
if self.is_configured():
self.account.start_io()
self._process_messages() # Process old messages.
while True:
event = await self.account.wait_for_event()
event["type"] = EventType(event.type)
event = self.account.wait_for_event()
event["kind"] = EventType(event.kind)
event["account"] = self.account
await self._on_event(event)
if event.type == EventType.INCOMING_MSG:
await self._process_messages()
self._on_event(event)
if event.kind == EventType.INCOMING_MSG:
self._process_messages()
stop = func(event)
if inspect.isawaitable(stop):
stop = await stop
if stop:
return event
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
if await evfilter.filter(event):
if evfilter.filter(event):
try:
await hook(event)
hook(event)
except Exception as ex:
self.logger.exception(ex)
async def _parse_command(self, event: AttrDict) -> None:
def _parse_command(self, event: AttrDict) -> None:
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0)
if "@" in cmd:
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
suffix = "@" + self.account.self_contact.get_snapshot().address
if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)]
else:
@@ -153,32 +150,32 @@ class Client:
event["command"], event["payload"] = cmd, payload
async def _on_new_msg(self, snapshot: AttrDict) -> None:
def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
await self._parse_command(event)
await self._on_event(event, NewMessage)
self._parse_command(event)
self._on_event(event, NewMessage)
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
def _handle_info_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text)
if img_changed:
_, event["image_deleted"] = img_changed
await self._on_event(event, GroupImageChanged)
self._on_event(event, GroupImageChanged)
return
title_changed = parse_system_title_changed(snapshot.text)
if title_changed:
_, event["old_name"] = title_changed
await self._on_event(event, GroupNameChanged)
self._on_event(event, GroupNameChanged)
return
members_changed = parse_system_add_remove(snapshot.text)
if members_changed:
action, event["member"], _ = members_changed
event["member_added"] = action == "added"
await self._on_event(event, MemberListChanged)
self._on_event(event, MemberListChanged)
return
self.logger.warning(
@@ -187,20 +184,20 @@ class Client:
snapshot.text,
)
async def _process_messages(self) -> None:
def _process_messages(self) -> None:
if self._should_process_messages:
for message in await self.account.get_next_messages():
snapshot = await message.get_snapshot()
for message in self.account.get_next_messages():
snapshot = message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
await self._on_new_msg(snapshot)
self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()
self._handle_info_msg(snapshot)
snapshot.message.mark_seen()
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
async def configure(self, email: str, password: str, **kwargs) -> None:
def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
await super().configure(email, password, **kwargs)
super().configure(email, password, **kwargs)

View File

@@ -24,39 +24,39 @@ class Contact:
def _rpc(self) -> "Rpc":
return self.account._rpc
async def block(self) -> None:
def block(self) -> None:
"""Block contact."""
await self._rpc.block_contact(self.account.id, self.id)
self._rpc.block_contact(self.account.id, self.id)
async def unblock(self) -> None:
def unblock(self) -> None:
"""Unblock contact."""
await self._rpc.unblock_contact(self.account.id, self.id)
self._rpc.unblock_contact(self.account.id, self.id)
async def delete(self) -> None:
def delete(self) -> None:
"""Delete contact."""
await self._rpc.delete_contact(self.account.id, self.id)
self._rpc.delete_contact(self.account.id, self.id)
async def set_name(self, name: str) -> None:
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
await self._rpc.change_contact_name(self.account.id, self.id, name)
self._rpc.change_contact_name(self.account.id, self.id, name)
async def get_encryption_info(self) -> str:
def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
async def get_snapshot(self) -> AttrDict:
def get_snapshot(self) -> AttrDict:
"""Return a dictionary with a snapshot of all contact properties."""
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
snapshot["contact"] = self
return snapshot
async def create_chat(self) -> "Chat":
def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact."""
from .chat import Chat
return Chat(
self.account,
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)

View File

@@ -16,34 +16,34 @@ class DeltaChat:
def __init__(self, rpc: "Rpc") -> None:
self.rpc = rpc
async def add_account(self) -> Account:
def add_account(self) -> Account:
"""Create a new account database."""
account_id = await self.rpc.add_account()
account_id = self.rpc.add_account()
return Account(self, account_id)
async def get_all_accounts(self) -> List[Account]:
def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
account_ids = await self.rpc.get_all_account_ids()
account_ids = self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
async def start_io(self) -> None:
def start_io(self) -> None:
"""Start the I/O of all accounts."""
await self.rpc.start_io_for_all_accounts()
self.rpc.start_io_for_all_accounts()
async def stop_io(self) -> None:
def stop_io(self) -> None:
"""Stop the I/O of all accounts."""
await self.rpc.stop_io_for_all_accounts()
self.rpc.stop_io_for_all_accounts()
async def maybe_network(self) -> None:
def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
await self.rpc.maybe_network()
self.rpc.maybe_network()
async def get_system_info(self) -> AttrDict:
def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system."""
return AttrDict(await self.rpc.get_system_info())
return AttrDict(self.rpc.get_system_info())
async def set_translations(self, translations: Dict[str, str]) -> None:
def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
await self.rpc.set_stock_strings(translations)
self.rpc.set_stock_strings(translations)

View File

@@ -1,5 +1,4 @@
"""High-level classes for event processing and filtering."""
import inspect
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
@@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple:
class EventFilter(ABC):
"""The base event filter.
:param func: A Callable (async or not) function that should accept the event as input
: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.
"""
@@ -43,16 +42,13 @@ class EventFilter(ABC):
def __ne__(self, other):
return not self == other
async def _call_func(self, event) -> bool:
def _call_func(self, event) -> bool:
if not self.func:
return True
res = self.func(event)
if inspect.isawaitable(res):
return await res
return res
return self.func(event)
@abstractmethod
async def filter(self, event):
def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
"""Matches raw core events.
:param types: The types of event to match.
:param func: A Callable (async or not) function that should accept the event as input
: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.
"""
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
if self.types and event.type not in self.types:
def filter(self, event: "AttrDict") -> bool:
if self.types and event.kind not in self.types:
return False
return await self._call_func(event)
return self._call_func(event)
class NewMessage(EventFilter):
@@ -104,7 +100,7 @@ class NewMessage(EventFilter):
: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 (async or not) function that should accept the event as input
: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.
"""
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
)
return False
async def filter(self, event: "AttrDict") -> bool:
def filter(self, event: "AttrDict") -> bool:
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:
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
return False
if self.pattern:
match = self.pattern(event.message_snapshot.text)
if inspect.isawaitable(match):
match = await match
if not match:
return False
return await super()._call_func(event)
return super()._call_func(event)
class MemberListChanged(EventFilter):
@@ -184,7 +178,7 @@ class MemberListChanged(EventFilter):
:param added: If set to True only match if a member was added, if set to False
only match if a member was removed. If omitted both, member additions
and removals, will be matched.
:param func: A Callable (async or not) function that should accept the event as input
: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.
"""
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
def filter(self, event: "AttrDict") -> bool:
if self.added is not None and self.added != event.member_added:
return False
return await self._call_func(event)
return self._call_func(event)
class GroupImageChanged(EventFilter):
@@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter):
:param deleted: If set to True only match if the image was deleted, if set to False
only match if a new image was set. If omitted both, image changes and
removals, will be matched.
:param func: A Callable (async or not) function that should accept the event as input
: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.
"""
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
def filter(self, event: "AttrDict") -> bool:
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return await self._call_func(event)
return self._call_func(event)
class GroupNameChanged(EventFilter):
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param func: A Callable (async or not) function that should accept the event as input
: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.
"""
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func
return False
async def filter(self, event: "AttrDict") -> bool:
return await self._call_func(event)
def filter(self, event: "AttrDict") -> bool:
return self._call_func(event)
class HookCollection:

View File

@@ -21,39 +21,39 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
async def send_reaction(self, *reaction: str):
def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
await self._rpc.send_reaction(self.account.id, self.id, reaction)
self._rpc.send_reaction(self.account.id, self.id, reaction)
async def get_snapshot(self) -> AttrDict:
def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
from .chat import Chat
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
async def get_reactions(self) -> Optional[AttrDict]:
def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
async def mark_seen(self) -> None:
def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id])
self._rpc.markseen_msgs(self.account.id, [self.id])
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str):
update = json.dumps(update)
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
async def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id)
def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -1,70 +1,67 @@
import asyncio
import json
import os
import urllib.request
from typing import AsyncGenerator, List, Optional
import aiohttp
import pytest_asyncio
import pytest
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from .rpc import Rpc
async def get_temp_credentials() -> dict:
def get_temp_credentials() -> dict:
url = os.getenv("DCC_NEW_TMP_EMAIL")
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
# Replace default 5 minute timeout with a 1 minute timeout.
timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
request = urllib.request.Request(url, method="POST")
with urllib.request.urlopen(request, timeout=60) as f:
return json.load(f)
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
async def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account()
def get_unconfigured_account(self) -> Account:
return self.deltachat.add_account()
async def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account())
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
async def new_preconfigured_account(self) -> Account:
def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started."""
credentials = await get_temp_credentials()
account = await self.get_unconfigured_account()
await account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"])
assert not await account.is_configured()
credentials = get_temp_credentials()
account = self.get_unconfigured_account()
account.set_config("addr", credentials["email"])
account.set_config("mail_pw", credentials["password"])
assert not account.is_configured()
return account
async def new_configured_account(self) -> Account:
account = await self.new_preconfigured_account()
await account.configure()
assert await account.is_configured()
def new_configured_account(self) -> Account:
account = self.new_preconfigured_account()
account.configure()
assert account.is_configured()
return account
async def new_configured_bot(self) -> Bot:
credentials = await get_temp_credentials()
bot = await self.get_unconfigured_bot()
await bot.configure(credentials["email"], credentials["password"])
def new_configured_bot(self) -> Bot:
credentials = get_temp_credentials()
bot = self.get_unconfigured_bot()
bot.configure(credentials["email"], credentials["password"])
return bot
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
def get_online_account(self) -> Account:
account = self.new_configured_account()
account.start_io()
while True:
event = await account.wait_for_event()
print(event)
if event.type == EventType.IMAP_INBOX_IDLE:
event = account.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
return account
async def get_online_accounts(self, num: int) -> List[Account]:
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
def get_online_accounts(self, num: int) -> List[Account]:
return [self.get_online_account() for _ in range(num)]
async def send_message(
def send_message(
self,
to_account: Account,
from_account: Optional[Account] = None,
@@ -73,16 +70,16 @@ class ACFactory:
group: Optional[str] = None,
) -> Message:
if not from_account:
from_account = (await self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account.get_config("addr"))
if group:
to_chat = await from_account.create_group(group)
await to_chat.add_contact(to_contact)
to_chat = from_account.create_group(group)
to_chat.add_contact(to_contact)
else:
to_chat = await to_contact.create_chat()
return await to_chat.send_message(text=text, file=file)
to_chat = to_contact.create_chat()
return to_chat.send_message(text=text, file=file)
async def process_message(
def process_message(
self,
to_client: Client,
from_account: Optional[Account] = None,
@@ -90,7 +87,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
await self.send_message(
self.send_message(
to_account=to_client.account,
from_account=from_account,
text=text,
@@ -98,16 +95,16 @@ class ACFactory:
group=group,
)
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture()
def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
async with rpc_server:
with rpc_server:
yield rpc_server
@pytest_asyncio.fixture
async def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc))
@pytest.fixture()
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))

View File

@@ -1,6 +1,10 @@
import asyncio
import json
import logging
import os
import subprocess
import sys
from queue import Queue
from threading import Event, Thread
from typing import Any, Dict, Optional
@@ -10,7 +14,7 @@ class JsonRpcError(Exception):
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
"""The given arguments will be passed to subprocess.Popen()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -18,78 +22,127 @@ class Rpc:
}
self._kwargs = kwargs
self.process: asyncio.subprocess.Process
self.process: subprocess.Popen
self.id: int
self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.event_queues: Dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: Dict[int, Event]
# Map from request ID to the result.
self.request_results: Dict[int, Any]
self.request_queue: Queue[Any]
self.closing: bool
self.reader_task: asyncio.Task
self.events_task: asyncio.Task
self.reader_thread: Thread
self.writer_thread: Thread
self.events_thread: Thread
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
"deltachat-rpc-server",
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
**self._kwargs,
)
def start(self) -> None:
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# Prevent subprocess from capturing SIGINT.
process_group=0,
**self._kwargs,
)
else:
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# `process_group` is not supported before Python 3.11.
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
self.id = 0
self.event_queues = {}
self.request_events = {}
self.request_results = {}
self.request_queue = Queue()
self.closing = False
self.reader_task = asyncio.create_task(self.reader_loop())
self.events_task = asyncio.create_task(self.events_loop())
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
self.writer_thread = Thread(target=self.writer_loop)
self.writer_thread.start()
self.events_thread = Thread(target=self.events_loop)
self.events_thread.start()
async def close(self) -> None:
def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
await self.stop_io_for_all_accounts()
await self.events_task
self.process.terminate()
await self.reader_task
self.stop_io_for_all_accounts()
self.events_thread.join()
self.process.stdin.close()
self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
async def __aenter__(self):
await self.start()
def __enter__(self):
self.start()
return self
async def __aexit__(self, _exc_type, _exc, _tb):
await self.close()
def __exit__(self, _exc_type, _exc, _tb):
self.close()
async def reader_loop(self) -> None:
while True:
line = await self.process.stdout.readline() # noqa
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
else:
print(response)
def reader_loop(self) -> None:
try:
while True:
line = self.process.stdout.readline()
if not line: # EOF
break
response = json.loads(line)
if "id" in response:
response_id = response["id"]
event = self.request_events.pop(response_id)
self.request_results[response_id] = response
event.set()
else:
logging.warning("Got a response without ID: %s", response)
except Exception:
# Log an exception if the reader loop dies.
logging.exception("Exception in the reader loop")
async def get_queue(self, account_id: int) -> asyncio.Queue:
def writer_loop(self) -> None:
"""Writer loop ensuring only a single thread writes requests."""
try:
while True:
request = self.request_queue.get()
if not request:
break
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
self.process.stdin.flush()
except Exception:
# Log an exception if the writer loop dies.
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
self.event_queues[account_id] = Queue()
return self.event_queues[account_id]
async def events_loop(self) -> None:
def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["contextId"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
try:
while True:
if self.closing:
return
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
queue.put(event["event"])
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
async def wait_for_event(self, account_id: int) -> Optional[dict]:
def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
queue = await self.get_queue(account_id)
return await queue.get()
queue = self.get_queue(account_id)
return queue.get()
def __getattr__(self, attr: str):
async def method(*args) -> Any:
def method(*args) -> Any:
self.id += 1
request_id = self.id
@@ -99,12 +152,12 @@ class Rpc:
"params": args,
"id": self.id,
}
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data) # noqa
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut
response = await fut
event = Event()
self.request_events[request_id] = event
self.request_queue.put(request)
event.wait()
response = self.request_results.pop(request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:

View File

@@ -1,4 +1,6 @@
import asyncio
import concurrent.futures
import json
import subprocess
from unittest.mock import MagicMock
import pytest
@@ -6,26 +8,26 @@ from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@pytest.mark.asyncio()
async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
def test_system_info(rpc) -> None:
system_info = rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
assert sleep_3_task in done
assert sleep_5_task in pending
sleep_5_task.cancel()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
sleep_5_future = executor.submit(rpc.sleep, 5.0)
sleep_3_future = executor.submit(rpc.sleep, 3.0)
done, pending = concurrent.futures.wait(
[sleep_5_future, sleep_3_future],
return_when=concurrent.futures.FIRST_COMPLETED,
)
assert sleep_3_future in done
assert sleep_5_future in pending
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -33,17 +35,16 @@ async def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
assert await rpc.check_email_validity(addr)
assert rpc.check_email_validity(addr)
for addr in invalid_addresses:
assert not await rpc.check_email_validity(addr)
assert not rpc.check_email_validity(addr)
@pytest.mark.asyncio()
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
while True:
event = await account.wait_for_event()
if event.type == EventType.CONFIGURE_PROGRESS:
event = account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
@@ -52,248 +53,241 @@ async def test_acfactory(acfactory) -> None:
print("Successful configuration")
@pytest.mark.asyncio()
async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account()
def test_configure_starttls(acfactory) -> None:
account = acfactory.new_preconfigured_account()
# Use STARTTLS
await account.set_config("mail_security", "2")
await account.set_config("send_security", "2")
await account.configure()
assert await account.is_configured()
account.set_config("mail_security", "2")
account.set_config("send_security", "2")
account.configure()
assert account.is_configured()
@pytest.mark.asyncio()
async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
await bob.mark_seen_messages([message])
bob.mark_seen_messages([message])
assert alice != bob
assert repr(alice)
assert (await alice.get_info()).level
assert await alice.get_size()
assert await alice.is_configured()
assert not await alice.get_avatar()
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert await alice.get_contacts()
assert await alice.get_contacts(snapshot=True)
assert alice.get_info().level
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_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
assert await alice.get_fresh_messages()
assert await alice.get_next_messages()
assert alice.get_chatlist()
assert alice.get_chatlist(snapshot=True)
assert alice.get_qr_code()
assert alice.get_fresh_messages()
assert alice.get_next_messages()
# Test sending empty message.
assert len(await bob.wait_next_messages()) == 0
await alice_chat_bob.send_text("")
messages = await bob.wait_next_messages()
assert len(bob.wait_next_messages()) == 0
alice_chat_bob.send_text("")
messages = bob.wait_next_messages()
assert len(messages) == 1
message = messages[0]
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert snapshot.text == ""
await bob.mark_seen_messages([message])
bob.mark_seen_messages([message])
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello")
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group_msg = group.send_message(text="hello")
assert group_msg == alice.get_message_by_id(group_msg.id)
assert group == alice.get_chat_by_id(group.id)
await alice.delete_messages([group_msg])
alice.delete_messages([group_msg])
await alice.set_config("selfstatus", "test")
assert await alice.get_config("selfstatus") == "test"
await alice.update_config(selfstatus="test2")
assert await alice.get_config("selfstatus") == "test2"
alice.set_config("selfstatus", "test")
assert alice.get_config("selfstatus") == "test"
alice.update_config(selfstatus="test2")
assert alice.get_config("selfstatus") == "test2"
assert not await alice.get_blocked_contacts()
await alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts()
assert not alice.get_blocked_contacts()
alice_contact_bob.block()
blocked_contacts = alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
await bob.remove()
await alice.stop_io()
bob.remove()
alice.stop_io()
@pytest.mark.asyncio()
async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob_chat_alice = bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
await alice_chat_bob.delete()
assert not await bob_chat_alice.can_send()
await bob_chat_alice.accept()
assert await bob_chat_alice.can_send()
await bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat()
await bob_chat_alice.mute()
await bob_chat_alice.unmute()
await bob_chat_alice.pin()
await bob_chat_alice.unpin()
await bob_chat_alice.archive()
await bob_chat_alice.unarchive()
alice_chat_bob.delete()
assert not bob_chat_alice.can_send()
bob_chat_alice.accept()
assert bob_chat_alice.can_send()
bob_chat_alice.block()
bob_chat_alice = snapshot.sender.create_chat()
bob_chat_alice.mute()
bob_chat_alice.unmute()
bob_chat_alice.pin()
bob_chat_alice.unpin()
bob_chat_alice.archive()
bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
await bob_chat_alice.set_name("test")
await bob_chat_alice.set_ephemeral_timer(300)
await bob_chat_alice.get_encryption_info()
bob_chat_alice.set_name("test")
bob_chat_alice.set_ephemeral_timer(300)
bob_chat_alice.get_encryption_info()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
await group.get_qr_code()
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group.get_qr_code()
snapshot = await group.get_basic_snapshot()
snapshot = group.get_basic_snapshot()
assert snapshot.name == "test group"
await group.set_name("new name")
snapshot = await group.get_full_snapshot()
group.set_name("new name")
snapshot = group.get_full_snapshot()
assert snapshot.name == "new name"
msg = await group.send_message(text="hi")
assert (await msg.get_snapshot()).text == "hi"
await group.forward_messages([msg])
msg = group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi"
group.forward_messages([msg])
await group.set_draft(text="test draft")
draft = await group.get_draft()
group.set_draft(text="test draft")
draft = group.get_draft()
assert draft.text == "test draft"
await group.remove_draft()
assert not await group.get_draft()
group.remove_draft()
assert not group.get_draft()
assert await group.get_messages()
await group.get_fresh_message_count()
await group.mark_noticed()
assert await group.get_contacts()
await group.remove_contact(alice_chat_bob)
await group.get_locations()
assert group.get_messages()
group.get_fresh_message_count()
group.mark_noticed()
assert group.get_contacts()
group.remove_contact(alice_chat_bob)
group.get_locations()
@pytest.mark.asyncio()
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
await alice_contact_bob.block()
await alice_contact_bob.unblock()
await alice_contact_bob.set_name("new name")
await alice_contact_bob.get_encryption_info()
snapshot = await alice_contact_bob.get_snapshot()
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr
await alice_contact_bob.create_chat()
alice_contact_bob.create_chat()
@pytest.mark.asyncio()
async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_text("Hello!")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert not snapshot.is_bot
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
await snapshot.chat.send_text("hi")
await snapshot.chat.accept()
await snapshot.chat.send_text("hi")
snapshot.chat.send_text("hi")
snapshot.chat.accept()
snapshot.chat.send_text("hi")
await message.mark_seen()
await message.send_reaction("😎")
reactions = await message.get_reactions()
message.mark_seen()
message.send_reaction("😎")
reactions = message.get_reactions()
assert reactions
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert reactions == snapshot.reactions
@pytest.mark.asyncio()
async def test_is_bot(acfactory) -> None:
def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = await acfactory.get_online_accounts(2)
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
# Alice becomes a bot.
await alice.set_config("bot", "1")
await alice_chat_bob.send_text("Hello!")
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
snapshot = message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
@pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
def test_bot(acfactory) -> None:
mock = MagicMock()
user = (await acfactory.get_online_accounts(1))[0]
bot = await acfactory.new_configured_bot()
bot2 = await acfactory.new_configured_bot()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
assert bot.is_configured()
assert bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
@@ -305,53 +299,62 @@ async def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
@pytest.mark.asyncio()
async def test_wait_next_messages(acfactory) -> None:
alice = await acfactory.new_configured_account()
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = await acfactory.new_preconfigured_account()
await bot.set_config("bot", "1")
await bot.configure()
bot = acfactory.new_preconfigured_account()
bot.set_config("bot", "1")
bot.configure()
# There are no old messages and the call returns immediately.
assert not await bot.wait_next_messages()
assert not bot.wait_next_messages()
# Bot starts waiting for messages.
next_messages_task = asyncio.create_task(bot.wait_next_messages())
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
bot_addr = await bot.get_config("addr")
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
alice_chat_bot = await alice_contact_bot.create_chat()
await alice_chat_bot.send_text("Hello!")
bot_addr = bot.get_config("addr")
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
next_messages = await next_messages_task
assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
next_messages = next_messages_task.result()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
@pytest.mark.asyncio()
async def test_import_export(acfactory, tmp_path) -> None:
alice = await acfactory.new_configured_account()
await alice.export_backup(tmp_path)
def test_import_export(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = await acfactory.get_unconfigured_account()
await alice2.import_backup(files[0])
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
assert alice2.manager.get_system_info()
def test_openrpc_command_line() -> None:
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc

View File

@@ -1,24 +1,22 @@
import pytest
from deltachat_rpc_client import EventType
@pytest.mark.asyncio()
async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
event = bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id)
break
webxdc_info = await message.get_webxdc_info()
webxdc_info = message.get_webxdc_info()
assert webxdc_info == {
"document": None,
"icon": "icon.png",
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
"summary": None,
}
status_updates = await message.get_webxdc_status_updates()
status_updates = message.get_webxdc_status_updates()
assert status_updates == []
await bob_chat_alice.accept()
await message.send_webxdc_status_update({"payload": 42}, "")
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
bob_chat_alice.accept()
message.send_webxdc_status_update({"payload": 42}, "")
message.send_webxdc_status_update({"payload": "Second update"}, "description")
status_updates = await message.get_webxdc_status_updates()
status_updates = message.get_webxdc_status_updates()
assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
status_updates = await message.get_webxdc_status_updates(1)
status_updates = message.get_webxdc_status_updates(1)
assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2},
]

View File

@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
pytest {posargs}
pytest -n6 {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -14,10 +14,8 @@ passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
pytest-asyncio
pytest-timeout
aiohttp
aiodns
pytest-xdist
[testenv:lint]
skipsdist = True

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.122.0"
version = "1.124.1"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -17,11 +17,11 @@ anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
log = "0.4"
serde_json = "1.0.99"
serde_json = "1.0.105"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.29.1", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
tokio = { version = "1.32.0", features = ["io-std"] }
tokio-util = "0.7.9"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]
default = ["vendored"]

View File

@@ -32,3 +32,6 @@ languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.

View File

@@ -10,6 +10,7 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
use tokio::signal::unix as signal_unix;
@@ -39,6 +40,12 @@ async fn main_impl() -> Result<()> {
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
println!("{}", CommandApi::openrpc_specification()?);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
}
@@ -56,7 +63,8 @@ async fn main_impl() -> Result<()> {
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));

View File

@@ -11,7 +11,6 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "ahash", version = "0.7.6" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
@@ -25,12 +24,10 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "idna", version = "<0.3" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "linux-raw-sys", version = "0.3.8" },
{ name = "num-derive", version = "0.3.3" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -38,8 +35,8 @@ skip = [
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
@@ -55,11 +52,10 @@ skip = [
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "winreg", version = "0.10.1" },
]

View File

@@ -1,100 +0,0 @@
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
use deltachat::contact::*;
use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress { progress, .. } => {
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
log::info!("{}", msg);
}
EventType::Warning(msg) => {
log::warn!("{}", msg);
}
EventType::Error(msg) => {
log::error!("{}", msg);
}
event => {
log::info!("{:?}", event);
}
}
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
#[tokio::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
while let Some(event) = events.recv().await {
cb(event.typ);
}
});
log::info!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
let pw = args[2].clone();
ctx.set_config(config::Config::Addr, Some(&email))
.await
.unwrap();
ctx.set_config(config::Config::MailPw, Some(&pw))
.await
.unwrap();
ctx.configure().await.unwrap();
log::info!("------ RUN ------");
ctx.start_io().await;
log::info!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
.unwrap();
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
.await
.unwrap();
}
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
log::info!("[{}] msg: {:?}", i, msg);
}
log::info!("stopping");
ctx.stop_io().await;
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
}

372
fuzz/Cargo.lock generated
View File

@@ -177,13 +177,13 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
dependencies = [
"async-channel",
"base64 0.21.0",
"byte-pool",
"bytes",
"chrono",
"futures",
"imap-proto",
@@ -337,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.0.2"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "blake3"
@@ -529,16 +529,6 @@ version = "3.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
[[package]]
name = "byte-pool"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
dependencies = [
"crossbeam-queue",
"stable_deref_trait",
]
[[package]]
name = "bytemuck"
version = "1.12.3"
@@ -741,16 +731,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.14"
@@ -926,7 +906,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.117.0"
version = "1.123.0"
dependencies = [
"anyhow",
"async-channel",
@@ -943,6 +923,7 @@ dependencies = [
"encoded-words",
"escaper",
"fast-socks5",
"fd-lock",
"format-flowed",
"futures",
"futures-lite",
@@ -955,7 +936,7 @@ dependencies = [
"libc",
"mailparse 0.14.0",
"mime",
"num-derive",
"num-derive 0.4.0",
"num-traits",
"num_cpus",
"once_cell",
@@ -1429,14 +1410,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
[[package]]
name = "enum-as-inner"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116"
checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 1.0.107",
"syn 2.0.15",
]
[[package]]
@@ -1464,6 +1445,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "errno"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd"
dependencies = [
"errno-dragonfly",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
@@ -1532,6 +1524,17 @@ dependencies = [
"instant",
]
[[package]]
name = "fd-lock"
version = "3.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
dependencies = [
"cfg-if",
"rustix 0.38.14",
"windows-sys 0.48.0",
]
[[package]]
name = "ff"
version = "0.12.1"
@@ -1616,9 +1619,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
dependencies = [
"percent-encoding",
]
@@ -1843,18 +1846,15 @@ dependencies = [
[[package]]
name = "heck"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.2.6"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
dependencies = [
"libc",
]
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
@@ -1951,7 +1951,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"socket2 0.4.7",
"tokio",
"tower-service",
"tracing",
@@ -2012,20 +2012,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.2.3"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -2033,9 +2022,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.6"
version = "0.24.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a"
checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
dependencies = [
"bytemuck",
"byteorder",
@@ -2100,10 +2089,10 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be"
dependencies = [
"socket2",
"socket2 0.4.7",
"widestring",
"winapi",
"winreg",
"winreg 0.10.1",
]
[[package]]
@@ -2230,9 +2219,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.139"
version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "libm"
@@ -2279,6 +2268,12 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]]
name = "linux-raw-sys"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128"
[[package]]
name = "lock_api"
version = "0.4.9"
@@ -2341,15 +2336,9 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata",
"regex-automata 0.1.10",
]
[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md-5"
version = "0.10.5"
@@ -2367,9 +2356,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1"
[[package]]
name = "memchr"
version = "2.5.0"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
[[package]]
name = "mime"
@@ -2394,14 +2383,13 @@ dependencies = [
[[package]]
name = "mio"
version = "0.8.5"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.42.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -2555,6 +2543,17 @@ dependencies = [
"syn 1.0.107",
]
[[package]]
name = "num-derive"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.15",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -2599,9 +2598,9 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.15.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
@@ -2854,7 +2853,7 @@ dependencies = [
"md-5",
"nom",
"num-bigint-dig",
"num-derive",
"num-derive 0.3.3",
"num-traits",
"p256 0.13.1",
"p384 0.13.0",
@@ -2894,9 +2893,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.9"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pin-utils"
@@ -3087,9 +3086,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.28.2"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"memchr",
]
@@ -3114,9 +3113,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
version = "0.9.2"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9"
checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989"
dependencies = [
"bytes",
"rand 0.8.5",
@@ -3139,7 +3138,7 @@ checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
dependencies = [
"libc",
"quinn-proto",
"socket2",
"socket2 0.4.7",
"tracing",
"windows-sys 0.42.0",
]
@@ -3268,13 +3267,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.8.4"
version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.2",
"regex-automata 0.3.8",
"regex-syntax 0.7.5",
]
[[package]]
@@ -3286,6 +3286,17 @@ dependencies = [
"regex-syntax 0.6.28",
]
[[package]]
name = "regex-automata"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.7.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.28"
@@ -3294,15 +3305,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
[[package]]
name = "regex-syntax"
version = "0.7.2"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "reqwest"
version = "0.11.18"
version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64 0.21.0",
"bytes",
@@ -3332,7 +3343,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
"winreg 0.50.0",
]
[[package]]
@@ -3438,7 +3449,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
dependencies = [
"bitflags 2.0.2",
"bitflags 2.4.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
@@ -3489,13 +3500,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags 1.3.2",
"errno",
"errno 0.2.8",
"io-lifetimes",
"libc",
"linux-raw-sys",
"linux-raw-sys 0.1.4",
"windows-sys 0.42.0",
]
[[package]]
name = "rustix"
version = "0.38.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f"
dependencies = [
"bitflags 2.4.0",
"errno 0.3.3",
"libc",
"linux-raw-sys 0.4.7",
"windows-sys 0.48.0",
]
[[package]]
name = "rustls"
version = "0.20.8"
@@ -3557,9 +3581,9 @@ dependencies = [
[[package]]
name = "sanitize-filename"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c"
checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603"
dependencies = [
"lazy_static",
"regex",
@@ -3855,6 +3879,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "socket2"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "spin"
version = "0.5.2"
@@ -3919,12 +3953,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stop-token"
version = "0.7.0"
@@ -3945,21 +3973,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.24.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.24.3"
version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.107",
"syn 2.0.15",
]
[[package]]
@@ -4038,7 +4066,7 @@ dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"rustix 0.36.7",
"windows-sys 0.42.0",
]
@@ -4147,22 +4175,21 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
dependencies = [
"autocfg",
"backtrace",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"socket2 0.5.4",
"tokio-macros",
"windows-sys 0.42.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -4177,13 +4204,13 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.8.2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.107",
"syn 2.0.15",
]
[[package]]
@@ -4239,9 +4266,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d"
dependencies = [
"bytes",
"futures-core",
@@ -4365,9 +4392,9 @@ dependencies = [
[[package]]
name = "trust-dns-proto"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26"
checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
dependencies = [
"async-trait",
"cfg-if",
@@ -4376,9 +4403,9 @@ dependencies = [
"futures-channel",
"futures-io",
"futures-util",
"idna 0.2.3",
"idna",
"ipnet",
"lazy_static",
"once_cell",
"rand 0.8.5",
"smallvec",
"thiserror",
@@ -4390,16 +4417,17 @@ dependencies = [
[[package]]
name = "trust-dns-resolver"
version = "0.22.0"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe"
checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lazy_static",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror",
@@ -4431,9 +4459,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
name = "unicode-bidi"
version = "0.3.8"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
@@ -4480,12 +4508,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.3.1"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
"idna 0.3.0",
"idna",
"percent-encoding",
]
@@ -4648,9 +4676,9 @@ dependencies = [
[[package]]
name = "webpki"
version = "0.22.0"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
dependencies = [
"ring",
"untrusted",
@@ -4731,21 +4759,51 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_gnullvm 0.42.0",
"windows_aarch64_msvc 0.42.0",
"windows_i686_gnu 0.42.0",
"windows_i686_msvc 0.42.0",
"windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm",
"windows_x86_64_gnullvm 0.42.0",
"windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -4764,6 +4822,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -4782,6 +4846,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -4800,6 +4870,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -4818,12 +4894,24 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -4842,6 +4930,12 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winreg"
version = "0.10.1"
@@ -4851,6 +4945,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "x25519-dalek"
version = "2.0.0-pre.1"

View File

@@ -159,6 +159,8 @@ module.exports = {
DC_STR_BROADCAST_LIST: 115,
DC_STR_CANNOT_LOGIN: 60,
DC_STR_CANTDECRYPT_MSG_BODY: 29,
DC_STR_CHAT_PROTECTION_DISABLED: 171,
DC_STR_CHAT_PROTECTION_ENABLED: 170,
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
@@ -244,12 +246,6 @@ module.exports = {
DC_STR_OUTGOING_MESSAGES: 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_PROTECTION_DISABLED: 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
DC_STR_PROTECTION_ENABLED: 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,

View File

@@ -159,6 +159,8 @@ export enum C {
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CHAT_PROTECTION_DISABLED = 171,
DC_STR_CHAT_PROTECTION_ENABLED = 170,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
@@ -244,12 +246,6 @@ export enum C {
DC_STR_OUTGOING_MESSAGES = 104,
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_PROTECTION_DISABLED = 89,
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
DC_STR_PROTECTION_ENABLED = 88,
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,

View File

@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
)
}
/**
*
* @param chatId
* @param protect
* @returns success boolean
*/
setChatProtection(chatId: number, protect: boolean) {
debug(`setChatProtection ${chatId} ${protect}`)
return Boolean(
binding.dcn_set_chat_protection(
this.dcn_context,
Number(chatId),
protect ? 1 : 0
)
)
}
getChatEphemeralTimer(chatId: number): number {
debug(`getChatEphemeralTimer ${chatId}`)
return binding.dcn_get_chat_ephemeral_timer(

View File

@@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter {
accountDir: string
jsonRpcStarted = false
constructor(cwd: string, os = 'deltachat-node') {
constructor(cwd: string, writable = true) {
super()
debug('DeltaChat constructor')
this.accountDir = cwd
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
this.dcn_accounts = binding.dcn_accounts_new(
this.accountDir,
writable ? 1 : 0
)
}
getAllAccountIds() {

View File

@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_set_chat_protection) {
NAPI_ARGV(3);
NAPI_DCN_CONTEXT();
NAPI_ARGV_UINT32(chat_id, 1);
NAPI_ARGV_INT32(protect, 1);
int result = dc_set_chat_protection(dcn_context->dc_context,
chat_id,
protect);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
NAPI_ARGV(2);
NAPI_DCN_CONTEXT();
@@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
NAPI_METHOD(dcn_accounts_new) {
NAPI_ARGV(2);
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
NAPI_ARGV_UTF8_MALLOC(dir, 1);
NAPI_ARGV_UTF8_MALLOC(dir, 0);
NAPI_ARGV_INT32(writable, 1);
TRACE("calling..");
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
@@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) {
}
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
napi_value result;
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_send_msg);
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);

View File

@@ -60,5 +60,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.122.0"
"version": "1.124.1"
}

View File

@@ -24,3 +24,5 @@ ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True
[mypy-distutils.*]
ignore_missing_imports = True

View File

@@ -195,7 +195,7 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
"""See dc_preconfigure_keypair() in deltachat.h.
In other words, you don't need this.
@@ -203,7 +203,7 @@ class Account:
res = lib.dc_preconfigure_keypair(
self._dc_context,
as_dc_charpointer(addr),
as_dc_charpointer(public),
ffi.NULL,
as_dc_charpointer(secret),
)
if res == 0:

View File

@@ -75,9 +75,12 @@ class Contact:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact):
def get_verifier(self, contact) -> Optional["Contact"]:
"""Return the address of the contact that verified the contact."""
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
if verifier_id == 0:
return None
return Contact(self.account, verifier_id)
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.

View File

@@ -478,10 +478,9 @@ class ACFactory:
except IndexError:
pass
else:
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
if fname_sec:
account._preconfigure_keypair(addr, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")

View File

@@ -1,6 +1,7 @@
import sys
import pytest
import deltachat as dc
class TestGroupStressTests:
@@ -136,6 +137,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: read member added message")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.is_encrypted()
assert msg.is_system_message()
assert "added" in msg.text.lower()
lp.sec("ac1: send message")
@@ -149,9 +151,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
lp.sec("ac2: Check that ac2 verified ac1")
# If we verified the contact ourselves then verifier addr == contact addr
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
lp.sec("ac2: send message and let ac1 read it")
chat2.send_text("world")
@@ -176,14 +177,15 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
ac2_ac1_contact = ac2.get_contacts()[0]
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")
# Skip system message about added member
ac3._evtracker.wait_next_incoming_message()
# System message about the added member.
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.is_system_message()
msg = ac3._evtracker.wait_next_incoming_message()
assert msg.text == "hi"
assert msg.is_encrypted()
@@ -524,7 +526,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
lp.sec("ac2: sending message")
# Message can be sent only after a receipt of "vg-member-added" message. Just wait for
# "Member Me (<addr>) added by <addr>." message.
ac2._evtracker.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")

View File

@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat import account_hookimpl, Message, Chat
from deltachat.tracker import ImexTracker
@@ -1467,13 +1467,18 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
lp.sec("sending a reaction to the large message from ac1 to ac2")
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
lp.sec("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
ac1._evtracker.wait_msg_delivered(msgs[-1])
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
@@ -1505,7 +1510,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(2)
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
@@ -1664,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_join_chat(acfactory, lp):
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
@@ -1678,6 +1687,17 @@ def test_qr_join_chat(acfactory, lp):
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
assert not ch.is_protected()
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.

View File

@@ -67,10 +67,9 @@ class TestOfflineAccountBasic:
def test_preconfigure_keypair(self, acfactory, data):
ac = acfactory.get_unconfigured_account()
alice_public = data.read_path("key/alice-public.asc")
alice_secret = data.read_path("key/alice-secret.asc")
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
assert alice_secret
ac._preconfigure_keypair("alice@example.org", alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -1,3 +1,4 @@
import json
from queue import Queue
import deltachat as dc
@@ -221,15 +222,32 @@ def test_logged_ac_process_ffi_failure(acfactory):
def test_jsonrpc_blocking_call(tmp_path):
accounts_fname = tmp_path / "accounts"
writable = True
accounts = ffi.gc(
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable),
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
).encode("utf-8"),
),
),
)
assert res == "true"
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"
res = json.loads(
from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(
jsonrpc,
json.dumps(
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
).encode("utf-8"),
),
),
)
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}

View File

@@ -25,6 +25,9 @@ deps =
pytest-xdist
pdbpp
requests
# urllib3 2.0 does not work in manylinux2014 containers.
# https://github.com/deltachat/deltachat-core-rust/issues/4788
urllib3<2
[testenv:.pkg]
passenv =
@@ -59,7 +62,8 @@ commands =
[testenv:doc]
changedir=doc
deps =
sphinx
# Pinned due to incompatibility of breathe with sphinx 7.2: <https://github.com/breathe-doc/breathe/issues/943>
sphinx<=7.1.2
breathe
commands =
sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html

View File

@@ -1 +1 @@
2023-09-12
2023-10-05

View File

@@ -6,10 +6,18 @@ and an own build machine.
## Description of scripts
Scripts should be ran from the root of the repository,
for example:
```
$ scripts/clippy.sh
```
- `clippy.sh` runs `cargo clippy` for all Rust code in the project.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
- `benchmark-tests.sh`
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs

8
scripts/benchmark-tests.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
# Benchmark the tests with `hyperfine`.
set -e
# `--tests` selects unit tests but not the documentation tests.
# `--skip` arguments skip online tests.
hyperfine --warmup 1 --min-runs 20 "cargo test --tests --release -- --skip imex::transfer::tests::test_drop_provider --skip imex::transfer::tests::test_send_receive --skip oauth2::tests::test_oauth_from_mx --skip oauth2::tests::test_get_oauth2_token --skip oauth2::tests::test_get_oauth2_addr --skip configure::tests::test_no_panic_on_bad_credentials --skip qr_code_generator::tests::test_generate_backup_qr"

View File

@@ -6,3 +6,4 @@ FROM $BASEIMAGE
RUN pipx install tox
COPY install-rust.sh /scripts/
RUN /scripts/install-rust.sh
RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi

View File

@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -7,6 +7,9 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
use uuid::Uuid;
use crate::context::Context;
@@ -33,16 +36,16 @@ pub struct Accounts {
impl Accounts {
/// Loads or creates an accounts folder at the given `dir`.
pub async fn new(dir: PathBuf) -> Result<Self> {
if !dir.exists() {
pub async fn new(dir: PathBuf, writable: bool) -> Result<Self> {
if writable && !dir.exists() {
Accounts::create(&dir).await?;
}
Accounts::open(dir).await
Accounts::open(dir, writable).await
}
/// Creates a new default structure.
pub async fn create(dir: &Path) -> Result<()> {
async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
.await
.context("failed to create folder")?;
@@ -54,13 +57,13 @@ impl Accounts {
/// Opens an existing accounts structure. Will error if the folder doesn't exist,
/// no account exists and no config exists.
pub async fn open(dir: PathBuf) -> Result<Self> {
async fn open(dir: PathBuf, writable: bool) -> Result<Self> {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
let config = Config::from_file(config_file)
let config = Config::from_file(config_file, writable)
.await
.context("failed to load accounts config")?;
let events = Events::new();
@@ -298,14 +301,20 @@ impl Accounts {
/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";
/// Lockfile name.
const LOCKFILE_NAME: &str = "accounts.lock";
/// Database file name.
const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug)]
struct Config {
file: PathBuf,
inner: InnerConfig,
// We lock the lockfile in the Config constructors to protect also from having multiple Config
// objects for the same config file.
lock_task: Option<JoinHandle<anyhow::Result<()>>>,
}
/// Account manager configuration file contents.
@@ -319,17 +328,74 @@ struct InnerConfig {
pub accounts: Vec<AccountConfig>,
}
impl Drop for Config {
fn drop(&mut self) {
if let Some(lock_task) = self.lock_task.take() {
lock_task.abort();
}
}
}
impl Config {
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
/// Creates a new Config for `file`, but doesn't open/sync it.
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
let dir = file.parent().context("Cannot get config file directory")?;
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
let file = dir.join(CONFIG_NAME);
let mut cfg = Self { file, inner };
if !lock {
let cfg = Self {
file,
inner,
lock_task: None,
};
return Ok(cfg);
}
let lockfile = dir.join(LOCKFILE_NAME);
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
let (locked_tx, locked_rx) = oneshot::channel();
let lock_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let mut timeout = Duration::from_millis(100);
let _guard = loop {
match lock.try_write() {
Ok(guard) => break Ok(guard),
Err(err) => {
if timeout.as_millis() > 1600 {
break Err(err);
}
// We need to wait for the previous lock_task to be aborted thus unlocking
// the lockfile. We don't open configs for writing often outside of the
// tests, so this adds delays to the tests, but otherwise ok.
sleep(timeout).await;
if err.kind() == std::io::ErrorKind::WouldBlock {
timeout *= 2;
}
}
}
}?;
locked_tx
.send(())
.ok()
.context("Cannot notify about lockfile locking")?;
let (_tx, rx) = oneshot::channel();
rx.await?;
Ok(())
});
let cfg = Self {
file,
inner,
lock_task: Some(lock_task),
};
locked_rx.await?;
Ok(cfg)
}
/// Creates a new configuration file in the given account manager directory.
pub async fn new(dir: &Path) -> Result<Self> {
let lock = true;
let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?;
cfg.sync().await?;
Ok(cfg)
@@ -339,6 +405,11 @@ impl Config {
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
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)
.await
@@ -357,24 +428,28 @@ impl Config {
}
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf) -> Result<Self> {
let dir = file.parent().context("can't get config file directory")?;
let bytes = fs::read(&file).await.context("failed to read file")?;
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
let dir = file
.parent()
.context("Cannot get config file directory")?
.to_path_buf();
let mut config = Self::new_nosync(file, writable).await?;
let bytes = fs::read(&config.file)
.await
.context("Failed to read file")?;
let s = std::str::from_utf8(&bytes)?;
let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?;
config.inner = toml::from_str(s).context("Failed to parse config")?;
// Previous versions of the core stored absolute paths in account config.
// Convert them to relative paths.
let mut modified = false;
for account in &mut inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(dir) {
for account in &mut config.inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
account.dir = new_dir.to_path_buf();
modified = true;
}
}
let mut config = Self { file, inner };
if modified {
if modified && writable {
config.sync().await?;
}
@@ -518,26 +593,44 @@ mod tests {
let p: PathBuf = dir.path().join("accounts1");
{
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
accounts.add_account().await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
{
let accounts = Accounts::open(p).await.unwrap();
for writable in [true, false] {
let accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_open_conflict() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let writable = true;
let _accounts = Accounts::new(p.clone(), writable).await.unwrap();
let writable = true;
assert!(Accounts::new(p.clone(), writable).await.is_err());
let writable = false;
let accounts = Accounts::new(p, writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_account_new_add_remove() {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -564,7 +657,8 @@ mod tests {
let dir = tempfile::tempdir()?;
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
assert!(accounts.get_selected_account().is_none());
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -585,7 +679,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
assert_eq!(accounts.accounts.len(), 0);
assert_eq!(accounts.config.get_selected_account(), 0);
@@ -622,7 +717,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await.unwrap();
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 1..10 {
let id = accounts.add_account().await.unwrap();
@@ -642,7 +738,8 @@ mod tests {
let dummy_accounts = 10;
let (id0, id1, id2) = {
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
let ids = accounts.get_all();
assert_eq!(ids.len(), 1);
@@ -677,7 +774,8 @@ mod tests {
assert!(id2 > id1 + dummy_accounts);
let (id0_reopened, id1_reopened, id2_reopened) = {
let accounts = Accounts::new(p.clone()).await?;
let writable = false;
let accounts = Accounts::new(p.clone(), writable).await?;
let ctx = accounts.get_selected_account().unwrap();
assert_eq!(
ctx.get_config(crate::config::Config::Addr).await?,
@@ -722,7 +820,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let accounts = Accounts::new(p.clone()).await?;
let writable = true;
let accounts = Accounts::new(p.clone(), writable).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.len(), 0);
@@ -748,7 +847,8 @@ mod tests {
let dir = tempfile::tempdir().context("failed to create tempdir")?;
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone())
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create accounts manager")?;
@@ -768,7 +868,8 @@ mod tests {
assert!(passphrase_set_success);
drop(accounts);
let accounts = Accounts::new(p.clone())
let writable = false;
let accounts = Accounts::new(p.clone(), writable)
.await
.context("failed to create second accounts manager")?;
let account = accounts
@@ -792,7 +893,8 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts");
let mut accounts = Accounts::new(p.clone()).await?;
let writable = true;
let mut accounts = Accounts::new(p.clone(), writable).await?;
accounts.add_account().await?;
accounts.add_account().await?;

View File

@@ -27,6 +27,7 @@ use crate::download::DownloadState;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::events::EventType;
use crate::html::new_html_mimepart;
use crate::location;
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -35,6 +36,7 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::receive_imf::ReceivedMsg;
use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
@@ -42,7 +44,6 @@ use crate::tools::{
strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -88,6 +89,14 @@ pub enum ProtectionStatus {
///
/// All members of the chat must be verified.
Protected = 1,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
/// The user has to confirm that this is OK.
///
/// We only do this in 1:1 chats; in group chats, the chat just
/// stays protected.
ProtectionBroken = 3, // `2` was never used as a value.
}
/// The reason why messages cannot be sent to the chat.
@@ -104,6 +113,10 @@ pub(crate) enum CantSendReason {
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// The chat was protected, but now a new message came in
/// which was not encrypted / signed correctly.
ProtectionBroken,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
@@ -120,6 +133,10 @@ impl fmt::Display for CantSendReason {
f,
"contact request chat should be accepted before sending messages"
),
Self::ProtectionBroken => write!(
f,
"accept that the encryption isn't verified anymore before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
@@ -272,6 +289,7 @@ impl ChatId {
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let smeared_time = create_smeared_timestamp(context);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -280,13 +298,20 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
create_smeared_timestamp(context),
smeared_time,
create_protected,
param.unwrap_or_default(),
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
.await?;
}
info!(
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
@@ -334,7 +359,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Undefined | Chattype::Broadcast => {
Chattype::Broadcast => {
bail!("Can't block chat of type {:?}", chat.typ)
}
Chattype::Single => {
@@ -375,7 +400,16 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
match chat.typ {
Chattype::Undefined => bail!("Can't accept chat of undefined chattype"),
Chattype::Single
if chat.blocked == Blocked::Not
&& chat.protected == ProtectionStatus::ProtectionBroken =>
{
// The protection was broken, then the user clicked 'Accept'/'OK',
// so, now we want to set the status to Unprotected again:
chat.id
.inner_set_protection(context, ProtectionStatus::Unprotected)
.await?;
}
Chattype::Single | Chattype::Group | Chattype::Broadcast => {
// User has "created a chat" with all these contacts.
//
@@ -402,20 +436,19 @@ impl ChatId {
/// Sets protection without sending a message.
///
/// Used when a message arrives indicating that someone else has
/// changed the protection value for a chat.
/// Returns whether the protection status was actually modified.
pub(crate) async fn inner_set_protection(
self,
context: &Context,
protect: ProtectionStatus,
) -> Result<()> {
ensure!(!self.is_special(), "Invalid chat-id.");
) -> Result<bool> {
ensure!(!self.is_special(), "Invalid chat-id {self}.");
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!(context, "Protection status unchanged for {}.", self);
return Ok(());
return Ok(false);
}
match protect {
@@ -430,9 +463,8 @@ impl ChatId {
}
}
Chattype::Mailinglist => bail!("Cannot protect mailing lists"),
Chattype::Undefined => bail!("Undefined group type"),
},
ProtectionStatus::Unprotected => {}
ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {}
};
context
@@ -445,68 +477,58 @@ impl ChatId {
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
Ok(())
Ok(true)
}
/// Send protected status message to the chat.
/// Adds an info message to the chat, telling the user that the protection status changed.
///
/// This sends the message with the protected status change to the chat,
/// notifying the user on this device as well as the other users in the chat.
/// Params:
///
/// If `promote` is false this means, the message must not be sent out
/// and only a local info message should be added to the chat.
/// This is used when protection is enabled implicitly or when a chat is not yet promoted.
/// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id.
/// * `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn add_protection_msg(
self,
context: &Context,
protect: ProtectionStatus,
promote: bool,
from_id: ContactId,
contact_id: Option<ContactId>,
timestamp_sort: i64,
) -> Result<()> {
let text = context.stock_protection_msg(protect, from_id).await;
let text = context.stock_protection_msg(protect, contact_id).await;
let cmd = match protect {
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
};
if promote {
let mut msg = Message {
viewtype: Viewtype::Text,
text,
..Default::default()
};
msg.param.set_cmd(cmd);
send_msg(context, self, &mut msg).await?;
} else {
add_info_msg_with_cmd(
context,
self,
&text,
cmd,
create_smeared_timestamp(context),
None,
None,
None,
)
.await?;
}
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
Ok(())
}
/// Sets protection and sends or adds a message.
pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> {
ensure!(!self.is_special(), "set protection: invalid chat-id.");
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
return Err(e);
///
/// `timestamp_sort` is used as the timestamp of the added message
/// and should be the timestamp of the change happening.
pub(crate) async fn set_protection(
self,
context: &Context,
protect: ProtectionStatus,
timestamp_sort: i64,
contact_id: Option<ContactId>,
) -> Result<()> {
match self.inner_set_protection(context, protect).await {
Ok(protection_status_modified) => {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
}
Ok(())
}
Err(e) => {
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
Err(e)
}
}
self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF)
.await
}
/// Archives or unarchives a chat.
@@ -743,14 +765,6 @@ impl ChatId {
}
}
let chat = Chat::load_from_db(context, self).await?;
if let Some(cant_send_reason) = chat.why_cant_send(context).await? {
bail!(
"Can't set a draft because chat is not writeable: {}",
cant_send_reason
);
}
// set back draft information to allow identifying the draft later on -
// no matter if message object is reused or reloaded from db
msg.state = MessageState::OutDraft;
@@ -918,9 +932,10 @@ impl ChatId {
.sql
.query_map(
"SELECT chat_id, count(*) AS n
FROM chats_contacts where contact_id > 9
FROM chats_contacts
WHERE contact_id > ? AND chat_id > ?
GROUP BY chat_id",
(),
(ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
|row| {
let chat_id: ChatId = row.get(0)?;
let size: f64 = row.get(1)?;
@@ -1259,7 +1274,7 @@ pub struct Chat {
pub grpid: String,
/// Whether the chat is blocked, unblocked or a contact request.
pub(crate) blocked: Blocked,
pub blocked: Blocked,
/// Additional chat parameters stored in the database.
pub param: Params,
@@ -1271,7 +1286,7 @@ pub struct Chat {
pub mute_duration: MuteDuration,
/// If the chat is protected (verified).
protected: ProtectionStatus,
pub(crate) protected: ProtectionStatus,
}
impl Chat {
@@ -1366,6 +1381,8 @@ impl Chat {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_protection_broken() {
Some(ProtectionBroken)
} else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
@@ -1390,7 +1407,6 @@ impl Chat {
match self.typ {
Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true),
Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await,
Chattype::Undefined => Ok(false),
}
}
@@ -1529,6 +1545,27 @@ impl Chat {
self.protected == ProtectionStatus::Protected
}
/// Returns true if the chat was protected, and then an incoming message broke this protection.
///
/// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
/// otherwise it will return false for all chats.
///
/// 1:1 chats are automatically set as protected when a contact is verified.
/// When a message comes in that is not encrypted / signed correctly,
/// the chat is automatically set as unprotected again.
/// `is_protection_broken()` will return true until `chat_id.accept()` is called.
///
/// 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 `chat_id.accept()`.
pub fn is_protection_broken(&self) -> bool {
match self.protected {
ProtectionStatus::Protected => false,
ProtectionStatus::Unprotected => false,
ProtectionStatus::ProtectionBroken => true,
}
}
/// Returns true if location streaming is enabled in the chat.
pub fn is_sending_locations(&self) -> bool {
self.is_sending_locations
@@ -1559,15 +1596,6 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if let Some(reason) = self.why_cant_send(context).await? {
if self.typ == Chattype::Group && reason == CantSendReason::NotAMember {
context.emit_event(EventType::ErrorSelfNotInGroup(
"Cannot send message; self not in group.".into(),
));
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
@@ -2088,19 +2116,30 @@ impl ChatIdBlocked {
_ => (),
}
let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?;
let protected = peerstate.map_or(false, |p| {
p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual
});
let smeared_time = create_smeared_timestamp(context);
let chat_id = context
.sql
.transaction(move |transaction| {
transaction.execute(
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
(type, name, param, blocked, created_timestamp, protected)
VALUES(?, ?, ?, ?, ?, ?)",
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
create_smeared_timestamp(context),
smeared_time,
if protected {
ProtectionStatus::Protected
} else {
ProtectionStatus::Unprotected
},
),
)?;
let chat_id = ChatId::new(
@@ -2121,6 +2160,17 @@ impl ChatIdBlocked {
})
.await?;
if protected {
chat_id
.add_protection_msg(
context,
ProtectionStatus::Protected,
Some(contact_id),
smeared_time,
)
.await?;
}
match contact_id {
ContactId::SELF => update_saved_messages_icon(context).await?,
ContactId::DEVICE => update_device_icon(context).await?,
@@ -2234,7 +2284,13 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification
} else {
bail!("cannot send to {chat_id}: {reason}");
}
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2336,7 +2392,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages againts RTLO attacks
// protect all system messages against RTLO attacks
if msg.is_system_message() {
msg.text = strip_rtlo_characters(&msg.text);
}
@@ -2458,14 +2514,13 @@ pub(crate) async fn create_send_msg_job(
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
}
if 0 != rendered_msg.last_added_location_id {
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
location::set_msg_location_id(context, msg.id, last_added_location_id).await
{
error!(context, "Failed to set msg_location_id: {err:#}.");
}
@@ -2897,6 +2952,9 @@ pub enum Direction {
}
/// Searches next/previous message based on the given message and list of types.
///
/// Deprecated since 2023-10-03.
#[deprecated(note = "use `get_chat_media` instead")]
pub async fn get_next_media(
context: &Context,
curr_msg_id: MsgId,
@@ -2975,18 +3033,14 @@ pub async fn create_group_chat(
let grpid = create_id();
let timestamp = create_smeared_timestamp(context);
let row_id = context
.sql
.insert(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
),
(Chattype::Group, chat_name, grpid, timestamp),
)
.await?;
@@ -2998,9 +3052,9 @@ pub async fn create_group_chat(
context.emit_msgs_changed_without_ids();
if protect == ProtectionStatus::Protected {
// this part is to stay compatible to verified groups,
// in some future, we will drop the "protect"-flag from create_group_chat()
chat_id.inner_set_protection(context, protect).await?;
chat_id
.set_protection(context, protect, timestamp, None)
.await?;
}
Ok(chat_id)
@@ -3528,6 +3582,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
msg.param.remove(Param::OverrideSenderDisplayname);
msg.param.remove(Param::WebxdcDocument);
msg.param.remove(Param::WebxdcDocumentTimestamp);
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.in_reply_to = None;
@@ -5254,72 +5310,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_protection() -> Result<()> {
let t = TestContext::new_alice().await;
t.set_config_bool(Config::BccSelf, false).await?;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 1);
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// disable protection again, still unpromoted
chat_id
.set_protection(&t, ProtectionStatus::Unprotected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled);
assert_eq!(msg.get_state(), MessageState::InNoticed);
// send a message, this switches to promoted state
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(!chat.is_protected());
assert!(!chat.is_unpromoted());
let msgs = get_chat_msgs(&t, chat_id).await?;
assert_eq!(msgs.len(), 3);
// enable protection on promoted chat, the info-message is sent via send_msg() this time
chat_id
.set_protection(&t, ProtectionStatus::Protected)
.await?;
let chat = Chat::load_from_db(&t, chat_id).await?;
assert!(chat.is_protected());
assert!(!chat.is_unpromoted());
let msg = t.get_last_msg_in(chat_id).await;
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_by_contact_id() {
let ctx = TestContext::new_alice().await;

View File

@@ -397,7 +397,7 @@ impl Chatlist {
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
Chattype::Single => (Some(lastmsg), None),
}
}
} else {

View File

@@ -312,6 +312,16 @@ pub enum Config {
/// Last message processed by the bot.
LastMsgId,
/// 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.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
}
impl Context {

View File

@@ -125,7 +125,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
/// Chat type.
#[derive(
Debug,
Default,
Display,
Clone,
Copy,
@@ -141,10 +140,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
)]
#[repr(u32)]
pub enum Chattype {
/// Undefined chat type.
#[default]
Undefined = 0,
/// 1:1 chat.
Single = 100,
@@ -223,8 +218,6 @@ mod tests {
#[test]
fn test_chattype_values() {
// values may be written to disk and must not change
assert_eq!(Chattype::Undefined, Chattype::default());
assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap());
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());

View File

@@ -25,7 +25,7 @@ use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
use crate::key::{load_self_public_key, DcKey};
use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
const LAST_SPECIAL: ContactId = ContactId::new(9);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -1009,7 +1009,7 @@ impl Contact {
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{stock_message}.\n{finger_prints}:");
let fingerprint_self = SignedPublicKey::load_self(context)
let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.to_string();
@@ -1211,31 +1211,15 @@ impl Contact {
/// and if the key has not changed since this verification.
///
/// The UI may draw a checkbox or something like that beside verified contacts.
///
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
self.is_verified_ex(context, None).await
}
/// Same as `Contact::is_verified` but allows speeding up things
/// by adding the peerstate belonging to the contact.
/// If you do not have the peerstate available, it is loaded automatically.
pub async fn is_verified_ex(
&self,
context: &Context,
peerstate: Option<&Peerstate>,
) -> Result<VerifiedStatus> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
return Ok(VerifiedStatus::BidirectVerified);
}
if let Some(peerstate) = peerstate {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.is_using_verified_key() {
return Ok(VerifiedStatus::BidirectVerified);
}
}
@@ -1252,11 +1236,22 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
let verifier_addr = self.get_verifier_addr(context).await?;
if let Some(addr) = verifier_addr {
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
} else {
Ok(None)
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
return Ok(None);
};
if 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::AddressBook).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)
}
}
}

View File

@@ -19,7 +19,7 @@ use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::key::{load_self_public_key, DcKey as _};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
use crate::quota::QuotaInfo;
@@ -382,7 +382,7 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
@@ -586,7 +586,7 @@ impl Context {
.sql
.count("SELECT COUNT(*) FROM acpeerstates;", ())
.await?;
let fingerprint_str = match SignedPublicKey::load_self(self).await {
let fingerprint_str = match load_self_public_key(self).await {
Ok(key) => key.fingerprint().hex(),
Err(err) => format!("<key failure: {err}>"),
};
@@ -761,6 +761,12 @@ impl Context {
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats",
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -814,7 +820,22 @@ impl Context {
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
None => MsgId::new_unset(),
None => {
// If `last_msg_id` is not set yet,
// subtract 1 from the last id,
// so a single message is returned and can
// be marked as seen.
self.sql
.query_row(
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
(),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
)
.await?
}
};
let list = self

View File

@@ -1,14 +1,12 @@
//! Forward log messages to logging webxdc
use crate::{
chat::ChatId,
config::Config,
context::Context,
message::{Message, MsgId, Viewtype},
param::Param,
tools::time,
webxdc::StatusUpdateItem,
EventType,
};
use crate::chat::ChatId;
use crate::config::Config;
use crate::context::Context;
use crate::events::EventType;
use crate::message::{Message, MsgId, Viewtype};
use crate::param::Param;
use crate::tools::time;
use crate::webxdc::StatusUpdateItem;
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;

View File

@@ -13,7 +13,6 @@ use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::peerstate::Peerstate;
use crate::pgp;
@@ -26,8 +25,8 @@ use crate::pgp;
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match {
let mime = get_autocrypt_mime(mail);
@@ -227,8 +226,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail
/// Returns Ok(None) if nothing encrypted was found.
fn decrypt_part(
mail: &ParsedMail<'_>,
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
private_keyring: &[SignedSecretKey],
public_keyring_for_validate: &[SignedPublicKey],
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let data = mail.get_body_raw()?;
@@ -263,7 +262,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts.
pub(crate) fn validate_detached_signature<'a, 'b>(
mail: &'a ParsedMail<'b>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
public_keyring_for_validate: &[SignedPublicKey],
) -> Option<(&'a ParsedMail<'b>, HashSet<Fingerprint>)> {
if mail.ctype.mimetype != "multipart/signed" {
return None;
@@ -283,13 +282,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>(
}
}
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring<SignedPublicKey> {
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec<SignedPublicKey> {
let mut public_keyring_for_validate = Vec::new();
if let Some(peerstate) = peerstate {
if let Some(key) = &peerstate.public_key {
public_keyring_for_validate.add(key.clone());
public_keyring_for_validate.push(key.clone());
} else if let Some(key) = &peerstate.gossip_key {
public_keyring_for_validate.add(key.clone());
public_keyring_for_validate.push(key.clone());
}
}
public_keyring_for_validate

View File

@@ -6,8 +6,7 @@ use num_traits::FromPrimitive;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::pgp;
@@ -24,7 +23,7 @@ impl EncryptHelper {
EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?)
.unwrap_or_default();
let addr = context.get_primary_self_addr().await?;
let public_key = SignedPublicKey::load_self(context).await?;
let public_key = load_self_public_key(context).await?;
Ok(EncryptHelper {
prefer_encrypt,
@@ -104,7 +103,7 @@ impl EncryptHelper {
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
let mut keyring: Keyring<SignedPublicKey> = Keyring::new();
let mut keyring: Vec<SignedPublicKey> = Vec::new();
for (peerstate, addr) in peerstates
.into_iter()
@@ -113,10 +112,10 @@ impl EncryptHelper {
let key = peerstate
.take_key(min_verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.add(key);
keyring.push(key);
}
keyring.add(self.public_key.clone());
let sign_key = SignedSecretKey::load_self(context).await?;
keyring.push(self.public_key.clone());
let sign_key = load_self_secret_key(context).await?;
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
@@ -132,7 +131,7 @@ impl EncryptHelper {
context: &Context,
mail: lettre_email::PartBuilder,
) -> Result<(lettre_email::MimeMessage, String)> {
let sign_key = SignedSecretKey::load_self(context).await?;
let sign_key = load_self_secret_key(context).await?;
let mime_message = mail.build();
let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?;
Ok((mime_message, signature))
@@ -145,20 +144,17 @@ impl EncryptHelper {
/// sent but in a few locations there are no such guarantees,
/// e.g. when exporting keys, and calling this function ensures a
/// private key will be present.
///
/// If this succeeds you are also guaranteed that the
/// [Config::ConfiguredAddr] is configured, this address is returned.
// TODO, remove this once deltachat::key::Key no longer exists.
pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
let self_addr = context.get_primary_self_addr().await?;
SignedPublicKey::load_self(context).await?;
Ok(self_addr)
pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> {
load_self_public_key(context).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat;
use crate::key::DcKey;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::test_utils::{bob_keypair, TestContext};
@@ -169,10 +165,7 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_prexisting() {
let t = TestContext::new_alice().await;
assert_eq!(
ensure_secret_key_exists(&t).await.unwrap(),
"alice@example.org"
);
assert!(ensure_secret_key_exists(&t).await.is_ok());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
ephemeral_timestamp
.into_iter()
.chain(delete_device_after_timestamp.into_iter())
.chain(delete_device_after_timestamp)
.min()
}
@@ -986,7 +986,7 @@ mod tests {
t.send_text(self_chat.id, "Saved message, which we delete manually")
.await;
let msg = t.get_last_msg_in(self_chat.id).await;
msg.id.delete_from_db(&t).await?;
msg.id.trash(&t).await?;
check_msg_is_deleted(&t, &self_chat, msg.id).await;
self_chat
@@ -1003,7 +1003,7 @@ mod tests {
.await
.unwrap();
// Set DeleteDeviceAfter to 1800s. Thend send a saved message which will
// Set DeleteDeviceAfter to 1800s. Then send a saved message which will
// still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages.
t.set_config(Config::DeleteDeviceAfter, Some("1800"))
.await?;
@@ -1260,8 +1260,8 @@ mod tests {
);
let msg = alice.get_last_msg().await;
// Message is deleted from the database when its timer expires.
msg.id.delete_from_db(&alice).await?;
// Message is deleted when its timer expires.
msg.id.trash(&alice).await?;
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
// <second@example.com>, is received. The message <second@example.come> is not in the

View File

@@ -17,12 +17,12 @@ use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use crate::context::Context;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
use crate::message::{self, Message, MsgId};
use crate::mimeparser::parse_message_id;
use crate::param::Param::SendHtml;
use crate::plaintext::PlainText;
use crate::{context::Context, message};
impl Message {
/// Check if the message can be retrieved as HTML.

View File

@@ -19,7 +19,9 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::key::{
self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey,
};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
@@ -186,7 +188,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
} else {
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = SignedSecretKey::load_self(context).await?;
let private_key = load_self_secret_key(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
@@ -781,6 +783,11 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");
conn.execute(
"UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';",
[],
)
.ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
@@ -911,55 +918,73 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_backup() -> Result<()> {
let backup_dir = tempfile::tempdir().unwrap();
for set_verified_oneonone_chats in [true, false] {
let backup_dir = tempfile::tempdir().unwrap();
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
if set_verified_oneonone_chats {
context1
.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await?;
}
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
.await
.is_err());
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
.await
.is_err());
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
assert_eq!(
context2
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
false
);
assert_eq!(
context1
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
set_verified_oneonone_chats
);
}
Ok(())
}

View File

@@ -3,11 +3,9 @@
use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use std::pin::Pin;
use anyhow::{ensure, Context as _, Result};
use base64::Engine as _;
use futures::Future;
use num_traits::FromPrimitive;
use pgp::composed::Deserializable;
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
@@ -18,8 +16,7 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
// Re-export key types
pub use crate::pgp::KeyPair;
use crate::pgp::KeyPair;
use crate::tools::{time, EmailAddress};
/// Convenience trait for working with keys.
@@ -27,7 +24,7 @@ use crate::tools::{time, EmailAddress};
/// This trait is implemented for rPGP's [SignedPublicKey] and
/// [SignedSecretKey] types and makes working with them a little
/// easier in the deltachat world.
pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// Create a key from some bytes.
fn from_slice(bytes: &[u8]) -> Result<Self> {
Ok(<Self as Deserializable>::from_bytes(Cursor::new(bytes))?)
@@ -50,11 +47,6 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
Self::from_armor_single(Cursor::new(bytes)).context("rPGP error")
}
/// Load the users' default key from the database.
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>>;
/// Serialise the key as bytes.
fn to_bytes(&self) -> Vec<u8> {
// Not using Serialize::to_bytes() to make clear *why* it is
@@ -85,38 +77,55 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
}
}
impl DcKey for SignedPublicKey {
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
let addr = context.get_primary_self_addr().await?;
match context
.sql
.query_row_optional(
r#"
SELECT public_key
FROM keypairs
WHERE addr=?
AND is_default=1;
"#,
(addr,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => Self::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
}
})
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
match context
.sql
.query_row_optional(
r#"SELECT public_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => SignedPublicKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.public)
}
}
}
pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecretKey> {
match context
.sql
.query_row_optional(
r#"SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => SignedSecretKey::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
}
}
impl DcKey for SignedPublicKey {
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to ignore this error.
@@ -135,36 +144,6 @@ impl DcKey for SignedPublicKey {
}
impl DcKey for SignedSecretKey {
fn load_self<'a>(
context: &'a Context,
) -> Pin<Box<dyn Future<Output = Result<Self>> + 'a + Send>> {
Box::pin(async move {
match context
.sql
.query_row_optional(
r#"
SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
AND is_default=1;
"#,
(),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
},
)
.await?
{
Some(bytes) => Self::from_slice(&bytes),
None => {
let keypair = generate_keypair(context).await?;
Ok(keypair.secret)
}
}
})
}
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is
// safe to do these unwraps.
@@ -185,7 +164,7 @@ impl DcKey for SignedSecretKey {
/// Deltachat extension trait for secret keys.
///
/// Provides some convenience wrappers only applicable to [SignedSecretKey].
pub trait DcSecretKey {
pub(crate) trait DcSecretKey {
/// Create a public key from a private one.
fn split_public_key(&self) -> Result<SignedPublicKey>;
}
@@ -328,6 +307,24 @@ pub async fn store_self_keypair(
Ok(())
}
/// Saves a keypair as the default keys.
///
/// This API is used for testing purposes
/// to avoid generating the key in tests.
/// Use import/export APIs instead.
pub async fn preconfigure_keypair(context: &Context, addr: &str, secret_data: &str) -> Result<()> {
let addr = EmailAddress::new(addr)?;
let secret = SignedSecretKey::from_asc(secret_data)?.0;
let public = secret.split_public_key()?;
let keypair = KeyPair {
addr,
public,
secret,
};
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
Ok(())
}
/// A key fingerprint
#[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Fingerprint(Vec<u8>);
@@ -522,9 +519,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
async fn test_load_self_existing() {
let alice = alice_keypair();
let t = TestContext::new_alice().await;
let pubkey = SignedPublicKey::load_self(&t).await.unwrap();
let pubkey = load_self_public_key(&t).await.unwrap();
assert_eq!(alice.public, pubkey);
let seckey = SignedSecretKey::load_self(&t).await.unwrap();
let seckey = load_self_secret_key(&t).await.unwrap();
assert_eq!(alice.secret, seckey);
}
@@ -534,7 +531,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = SignedPublicKey::load_self(&t).await;
let key = load_self_public_key(&t).await;
assert!(key.is_ok());
}
@@ -544,7 +541,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
.await
.unwrap();
let key = SignedSecretKey::load_self(&t).await;
let key = load_self_secret_key(&t).await;
assert!(key.is_ok());
}
@@ -561,7 +558,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(SignedPublicKey::load_self(&ctx))
.block_on(load_self_public_key(&ctx))
})
};
let thr1 = {
@@ -569,7 +566,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
thread::spawn(move || {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(SignedPublicKey::load_self(&ctx))
.block_on(load_self_public_key(&ctx))
})
};
let res0 = thr0.join().unwrap();

View File

@@ -1,87 +0,0 @@
//! Keyring to perform rpgp operations with.
use anyhow::Result;
use crate::context::Context;
use crate::key::DcKey;
/// An in-memory keyring.
///
/// Instances are usually constructed just for the rpgp operation and
/// short-lived.
#[derive(Clone, Debug, Default)]
pub struct Keyring<T>
where
T: DcKey,
{
keys: Vec<T>,
}
impl<T> Keyring<T>
where
T: DcKey,
{
/// New empty keyring.
pub fn new() -> Keyring<T> {
Keyring { keys: Vec::new() }
}
/// Create a new keyring with the the user's secret key loaded.
pub async fn new_self(context: &Context) -> Result<Keyring<T>> {
let mut keyring: Keyring<T> = Keyring::new();
keyring.load_self(context).await?;
Ok(keyring)
}
/// Load the user's key into the keyring.
pub async fn load_self(&mut self, context: &Context) -> Result<()> {
self.add(T::load_self(context).await?);
Ok(())
}
/// Add a key to the keyring.
pub fn add(&mut self, key: T) {
self.keys.push(key);
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
/// A vector with reference to all the keys in the keyring.
pub fn keys(&self) -> &[T] {
&self.keys
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::key::{SignedPublicKey, SignedSecretKey};
use crate::test_utils::{alice_keypair, TestContext};
#[test]
fn test_keyring_add_keys() {
let alice = alice_keypair();
let mut pub_ring: Keyring<SignedPublicKey> = Keyring::new();
pub_ring.add(alice.public.clone());
assert_eq!(pub_ring.keys(), [alice.public]);
let mut sec_ring: Keyring<SignedSecretKey> = Keyring::new();
sec_ring.add(alice.secret.clone());
assert_eq!(sec_ring.keys(), [alice.secret]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_keyring_load_self() {
// new_self() implies load_self()
let t = TestContext::new_alice().await;
let alice = alice_keypair();
let pub_ring: Keyring<SignedPublicKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(pub_ring.keys(), [alice.public]);
let sec_ring: Keyring<SignedSecretKey> = Keyring::new_self(&t).await.unwrap();
assert_eq!(sec_ring.keys(), [alice.secret]);
}
}

View File

@@ -8,7 +8,6 @@
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::indexing_slicing,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
@@ -17,6 +16,7 @@
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
@@ -70,7 +70,6 @@ mod scheduler;
#[macro_use]
mod job;
pub mod key;
mod keyring;
pub mod location;
mod login_param;
pub mod message;

View File

@@ -328,13 +328,13 @@ pub async fn is_sending_locations_to_chat(
}
/// Sets current location of the user device.
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
if latitude == 0.0 && longitude == 0.0 {
return true;
return Ok(true);
}
let mut continue_streaming = false;
if let Ok(chats) = context
let chats = context
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
@@ -346,33 +346,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
.map_err(Into::into)
},
)
.await
{
for chat_id in chats {
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
(
latitude,
longitude,
accuracy,
time(),
chat_id,
ContactId::SELF,
)
).await {
warn!(context, "failed to store location {:#}", err);
} else {
info!(context, "stored location for chat {}", chat_id);
continue_streaming = true;
}
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
};
}
.await?;
continue_streaming
for chat_id in chats {
context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
(
latitude,
longitude,
accuracy,
time(),
chat_id,
ContactId::SELF,
)).await.context("Failed to store location")?;
info!(context, "Stored location for chat {chat_id}.");
continue_streaming = true;
}
if continue_streaming {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
};
Ok(continue_streaming)
}
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
@@ -464,7 +460,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
}
/// Returns `location.kml` contents.
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
let mut last_added_location_id = 0;
let self_addr = context.get_primary_self_addr().await?;
@@ -534,9 +530,11 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
ret += "</Document>\n</kml>";
}
ensure!(location_count > 0, "No locations processed");
Ok((ret, last_added_location_id))
if location_count > 0 {
Ok(Some((ret, last_added_location_id)))
} else {
Ok(None)
}
}
fn get_kml_timestamp(utc: i64) -> String {
@@ -735,7 +733,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_send_until - now).into_iter())
.chain(u64::try_from(locations_send_until - now))
.min();
if has_locations {
@@ -759,7 +757,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
);
next_event = next_event
.into_iter()
.chain(u64::try_from(locations_last_sent + 61 - now).into_iter())
.chain(u64::try_from(locations_last_sent + 61 - now))
.min();
}
} else {
@@ -928,4 +926,38 @@ Content-Disposition: attachment; filename="location.kml"
assert_eq!(locations.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_locations_to_chat() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
let sent = alice.pop_sent_msg().await;
let msg = bob.recv_msg(&sent).await;
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
let bob_chat_id = msg.chat_id;
assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
// Send image without text.
let file_name = "image.png";
let bytes = include_bytes!("../test-data/image/logo.png");
let file = alice.get_blobdir().join(file_name);
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
let msg = bob.recv_msg_opt(&sent).await.unwrap();
assert!(msg.chat_id == bob_chat_id);
assert_eq!(msg.msg_ids.len(), 1);
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.get(0).unwrap()).await?;
assert_eq!(bob_msg.chat_id, bob_chat_id);
assert_eq!(bob_msg.viewtype, Viewtype::Image);
Ok(())
}
}

View File

@@ -5,9 +5,10 @@ use std::fmt;
use anyhow::{ensure, Result};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
use crate::provider::Socket;
use crate::provider::{get_provider_by_id, Provider};
use crate::socks::Socks5Config;
use crate::{context::Context, provider::Socket};
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[repr(u32)]

View File

@@ -114,24 +114,16 @@ WHERE id=?;
}
/// Deletes a message, corresponding MDNs and unsent SMTP messages from the database.
pub async fn delete_from_db(self, context: &Context) -> Result<()> {
// We don't use transactions yet, so remove MDNs first to make
// sure they are not left while the message is deleted.
pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM smtp WHERE msg_id=?", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs_mdns WHERE msg_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs_status_updates WHERE msg_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE id=?;", (self,))
.transaction(move |transaction| {
transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?;
transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?;
Ok(())
})
.await?;
Ok(())
}
@@ -772,7 +764,7 @@ impl Message {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
Some(Contact::get_by_id(context, self.from_id).await?)
}
Chattype::Single | Chattype::Undefined => None,
Chattype::Single => None,
}
} else {
None

View File

@@ -66,7 +66,7 @@ pub struct MimeFactory<'a> {
in_reply_to: String,
references: String,
req_mdn: bool,
last_added_location_id: u32,
last_added_location_id: Option<u32>,
/// If the created mime-structure contains sync-items,
/// the IDs of these items are listed here.
@@ -85,7 +85,7 @@ pub struct RenderedEmail {
// pub envelope: Envelope,
pub is_encrypted: bool,
pub is_gossiped: bool,
pub last_added_location_id: u32,
pub last_added_location_id: Option<u32>,
/// A comma-separated string of sync-IDs that are used by the rendered email
/// and must be deleted once the message is actually queued for sending
@@ -223,7 +223,7 @@ impl<'a> MimeFactory<'a> {
in_reply_to,
references,
req_mdn,
last_added_location_id: 0,
last_added_location_id: None,
sync_ids_to_delete: None,
attach_selfavatar,
};
@@ -264,7 +264,7 @@ impl<'a> MimeFactory<'a> {
in_reply_to: String::default(),
references: String::default(),
req_mdn: false,
last_added_location_id: 0,
last_added_location_id: None,
sync_ids_to_delete: None,
attach_selfavatar: false,
};
@@ -678,6 +678,12 @@ impl<'a> MimeFactory<'a> {
})
};
let get_content_type_directives_header = || {
(
"Content-Type-Deltachat-Directives".to_string(),
"protected-headers=\"v1\"".to_string(),
)
};
let outer_message = if is_encrypted {
headers.protected.push(from_header);
@@ -714,10 +720,7 @@ impl<'a> MimeFactory<'a> {
if !existing_ct.ends_with(';') {
existing_ct += ";";
}
let message = message.replace_header(Header::new(
"Content-Type".to_string(),
format!("{existing_ct} protected-headers=\"v1\";"),
));
let message = message.header(get_content_type_directives_header());
// Set the appropriate Content-Type for the outer message
let outer_message = PartBuilder::new().header((
@@ -786,11 +789,12 @@ impl<'a> MimeFactory<'a> {
{
message
} else {
let message = message.header(get_content_type_directives_header());
let (payload, signature) = encrypt_helper.sign(context, message).await?;
PartBuilder::new()
.header((
"Content-Type".to_string(),
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
"Content-Type",
"multipart/signed; protocol=\"application/pgp-signature\"",
))
.child(payload)
.child(
@@ -860,9 +864,13 @@ impl<'a> MimeFactory<'a> {
}
/// Returns MIME part with a `location.kml` attachment.
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
let (kml_content, last_added_location_id) =
location::get_kml(context, self.msg.chat_id).await?;
async fn get_location_kml_part(&mut self, context: &Context) -> Result<Option<PartBuilder>> {
let Some((kml_content, last_added_location_id)) =
location::get_kml(context, self.msg.chat_id).await?
else {
return Ok(None);
};
let part = PartBuilder::new()
.content_type(
&"application/vnd.google-earth.kml+xml"
@@ -876,9 +884,9 @@ impl<'a> MimeFactory<'a> {
.body(kml_content);
if !self.msg.param.exists(Param::SetLatitude) {
// otherwise, the independent location is already filed
self.last_added_location_id = last_added_location_id;
self.last_added_location_id = Some(last_added_location_id);
}
Ok(part)
Ok(Some(part))
}
#[allow(clippy::cognitive_complexity)]
@@ -896,7 +904,16 @@ impl<'a> MimeFactory<'a> {
let mut placeholdertext = None;
let mut meta_part = None;
if chat.is_protected() {
let send_verified_headers = match chat.typ {
// In single chats, the protection status isn't necessarily the same for both sides,
// so we don't send the Chat-Verified header:
Chattype::Single => false,
Chattype::Group => true,
// Mailinglists and broadcast lists can actually never be verified:
Chattype::Mailinglist => false,
Chattype::Broadcast => false,
};
if chat.is_protected() && send_verified_headers {
headers
.protected
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
@@ -1168,7 +1185,10 @@ impl<'a> MimeFactory<'a> {
}
let flowed_text = format_flowed(final_text);
let footer = &self.selfstatus;
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
let footer = if is_reaction { "" } else { &self.selfstatus };
let message_text = format!(
"{}{}{}{}{}{}",
fwdhint.unwrap_or_default(),
@@ -1191,7 +1211,7 @@ impl<'a> MimeFactory<'a> {
))
.body(message_text);
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
if is_reaction {
main_part = main_part.header(("Content-Disposition", "reaction"));
}
@@ -1230,11 +1250,8 @@ impl<'a> MimeFactory<'a> {
}
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
match self.get_location_kml_part(context).await {
Ok(part) => parts.push(part),
Err(err) => {
warn!(context, "mimefactory: could not send location: {}", err);
}
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
}
}
@@ -1363,15 +1380,16 @@ impl<'a> MimeFactory<'a> {
}
}
/// Returns base64-encoded buffer `buf` split into 78-bytes long
/// Returns base64-encoded buffer `buf` split into 76-bytes long
/// chunks separated by CRLF.
///
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8)
/// says that "The encoded output stream must be represented in lines of no more than 76 characters each."
/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin.
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
let mut chars = base64.chars();
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
std::iter::repeat_with(|| chars.by_ref().take(76).collect::<String>())
.take_while(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("\r\n")
@@ -1530,6 +1548,7 @@ fn maybe_encode_words(words: &str) -> String {
#[cfg(test)]
mod tests {
use mailparse::{addrparse_header, MailHeaderMap};
use std::str;
use super::*;
use crate::chat::ChatId;
@@ -1538,10 +1557,11 @@ mod tests {
ProtectionStatus,
};
use crate::chatlist::Chatlist;
use crate::constants;
use crate::contact::{ContactAddress, Origin};
use crate::mimeparser::MimeMessage;
use crate::receive_imf::receive_imf;
use crate::test_utils::{get_chat_msg, TestContext};
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
#[test]
fn test_render_email_address() {
let display_name = "ä space";
@@ -1611,8 +1631,8 @@ mod tests {
fn test_wrapped_base64_encode() {
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let output =
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
FBQUFBQUFBQQ==";
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
QUFBQUFBQUFBQQ==";
assert_eq!(wrapped_base64_encode(input), output);
}
@@ -1954,7 +1974,7 @@ mod tests {
let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await;
if delete_original_msg {
incoming_msg.id.delete_from_db(&t).await.unwrap();
incoming_msg.id.trash(&t).await.unwrap();
}
if message_arrives_inbetween {
@@ -2190,7 +2210,11 @@ mod tests {
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/mixed").count(), 1);
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
@@ -2302,4 +2326,37 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_headers_directive() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let chat = tcm
.send_recv_accept(&alice, &bob, "alice->bob")
.await
.chat_id;
// Now Bob can send an encrypted message to Alice.
let mut msg = Message::new(Viewtype::File);
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
// decoded_data to check presense of the necessary headers.
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
.await?;
let sent = bob.send_msg(chat, &mut msg).await;
assert!(msg.get_showpadlock());
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("Subject:").count(), 1);
Ok(())
}
}

View File

@@ -27,8 +27,7 @@ use crate::decrypt::{
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::key::{load_self_secret_key, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
};
@@ -278,9 +277,10 @@ impl MimeMessage {
headers.remove("chat-verified");
let from = from.context("No from in message")?;
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
let private_keyring = vec![load_self_secret_key(context)
.await
.context("failed to get own keyring")?;
.context("Failed to get own key")?];
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, message_time).await?;
@@ -804,18 +804,6 @@ impl MimeMessage {
// Boxed future to deal with recursion
async move {
if mail.ctype.params.get("protected-headers").is_some() {
if mail.ctype.mimetype == "text/rfc822-headers" {
warn!(
context,
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
);
return Ok(false);
}
warn!(context, "Ignoring nested protected headers");
}
enum MimeS {
Multiple,
Single,
@@ -852,7 +840,10 @@ impl MimeMessage {
self.parse_mime_recursive(context, &mail, is_related).await
}
MimeS::Single => self.add_single_part_if_known(context, mail, is_related).await,
MimeS::Single => {
self.add_single_part_if_known(context, mail, is_related)
.await
}
}
}
.boxed()
@@ -1442,33 +1433,36 @@ impl MimeMessage {
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
// must be present
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
.collect()
});
if report_fields
.get_header_value(HeaderDef::Disposition)
.is_none()
{
warn!(
context,
"Ignoring unknown disposition-notification, Message-Id: {:?}.",
report_fields.get_header_value(HeaderDef::MessageId)
);
return Ok(None);
};
return Ok(Some(Report {
original_message_id,
additional_message_ids,
}));
}
warn!(
context,
"ignoring unknown disposition-notification, Message-Id: {:?}",
report_fields.get_header_value(HeaderDef::MessageId)
);
let original_message_id = report_fields
.get_header_value(HeaderDef::OriginalMessageId)
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
// the original message id into the In-Reply-To header:
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
.and_then(|v| parse_message_id(&v).ok());
let additional_message_ids = report_fields
.get_header_value(HeaderDef::AdditionalMessageIds)
.map_or_else(Vec::new, |v| {
v.split(' ')
.filter_map(|s| parse_message_id(s).ok())
.collect()
});
Ok(None)
Ok(Some(Report {
original_message_id,
additional_message_ids,
}))
}
fn process_delivery_status(
@@ -1899,7 +1893,7 @@ fn get_mime_type(
} else {
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
// be handled separatedly.
// be handled separately.
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
// which are unwanted at all).
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
@@ -2198,7 +2192,7 @@ async fn ndn_maybe_add_info_msg(
// If we get an NDN for the mailing list, just issue a warning.
warn!(context, "ignoring NDN for mailing list.");
}
Chattype::Single | Chattype::Undefined => {}
Chattype::Single => {}
}
Ok(())
}

View File

@@ -4,12 +4,20 @@ use std::time::Duration;
use anyhow::{anyhow, Result};
use mime::Mime;
use once_cell::sync::Lazy;
use crate::context::Context;
use crate::socks::Socks5Config;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
reqwest::tls::Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
))
.unwrap()
});
/// HTTP(S) GET response.
#[derive(Debug)]
pub struct Response {
@@ -79,7 +87,10 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
}
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
let builder = reqwest::ClientBuilder::new()
.timeout(HTTP_TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)

View File

@@ -392,6 +392,31 @@ impl Peerstate {
}
}
/// Returns a reference to the contact's public key fingerprint.
///
/// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key_fingerprint
.as_ref()
.or(self.gossip_key_fingerprint.as_ref()),
}
}
/// Returns true if the key used for opportunistic encryption in the 1:1 chat
/// is the same as the verified key.
///
/// Note that verified groups always use the verified key no matter if the
/// opportunistic key matches or not.
pub(crate) fn is_using_verified_key(&self) -> bool {
let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified);
verified.is_some()
&& verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified)
}
/// Set this peerstate to verified
/// Make sure to call `self.save_to_db` to save these changes
/// Params:

View File

@@ -20,7 +20,6 @@ use tokio::runtime::Handle;
use crate::constants::KeyGenType;
use crate::key::{DcKey, Fingerprint};
use crate::keyring::Keyring;
use crate::tools::EmailAddress;
#[allow(missing_docs)]
@@ -237,7 +236,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSu
/// and signs it using `private_key_for_signing`.
pub async fn pk_encrypt(
plain: &[u8],
public_keys_for_encryption: Keyring<SignedPublicKey>,
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
) -> Result<String> {
let lit_msg = Message::new_literal_bytes("", plain);
@@ -245,7 +244,6 @@ pub async fn pk_encrypt(
Handle::current()
.spawn_blocking(move || {
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
.keys()
.iter()
.filter_map(select_pk_for_encryption)
.collect();
@@ -298,15 +296,15 @@ pub fn pk_calc_signature(
#[allow(clippy::implicit_hasher)]
pub fn pk_decrypt(
ctext: Vec<u8>,
private_keys_for_decryption: &Keyring<SignedSecretKey>,
public_keys_for_validation: &Keyring<SignedPublicKey>,
private_keys_for_decryption: &[SignedSecretKey],
public_keys_for_validation: &[SignedPublicKey],
) -> Result<(Vec<u8>, HashSet<Fingerprint>)> {
let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
let cursor = Cursor::new(ctext);
let (msg, _) = Message::from_armor_single(cursor)?;
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect();
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let (decryptor, _) = msg.decrypt(|| "".into(), &skeys[..])?;
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
@@ -321,20 +319,13 @@ pub fn pk_decrypt(
None => bail!("The decrypted message is empty"),
};
if !public_keys_for_validation.is_empty() {
let pkeys = public_keys_for_validation.keys();
let mut fingerprints: Vec<Fingerprint> = Vec::new();
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in pkeys {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
fingerprints.push(fp);
}
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
for pkey in public_keys_for_validation {
if signed_msg.verify(&pkey.primary_key).is_ok() {
let fp = DcKey::fingerprint(pkey);
ret_signature_fingerprints.insert(fp);
}
}
ret_signature_fingerprints.extend(fingerprints);
}
Ok((content, ret_signature_fingerprints))
} else {
@@ -346,12 +337,11 @@ pub fn pk_decrypt(
pub fn pk_validate(
content: &[u8],
signature: &[u8],
public_keys_for_validation: &Keyring<SignedPublicKey>,
public_keys_for_validation: &[SignedPublicKey],
) -> Result<HashSet<Fingerprint>> {
let mut ret: HashSet<Fingerprint> = Default::default();
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
let pkeys = public_keys_for_validation.keys();
// Remove trailing CRLF before the delimiter.
// According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of
@@ -360,7 +350,7 @@ pub fn pk_validate(
.get(..content.len().saturating_sub(2))
.context("index is out of range")?;
for pkey in pkeys {
for pkey in public_keys_for_validation {
if standalone_signature.verify(pkey, content).is_ok() {
let fp = DcKey::fingerprint(pkey);
ret.insert(fp);
@@ -495,9 +485,7 @@ mod tests {
async fn ctext_signed() -> &'static String {
CTEXT_SIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone()))
.await
@@ -510,9 +498,7 @@ mod tests {
async fn ctext_unsigned() -> &'static String {
CTEXT_UNSIGNED
.get_or_init(|| async {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_public.clone());
keyring.add(KEYS.bob_public.clone());
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
pk_encrypt(CLEARTEXT, keyring, None).await.unwrap()
})
.await
@@ -537,10 +523,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_singed() {
// Check decrypting as Alice
let mut decrypt_keyring: Keyring<SignedSecretKey> = Keyring::new();
decrypt_keyring.add(KEYS.alice_secret.clone());
let mut sig_check_keyring: Keyring<SignedPublicKey> = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let decrypt_keyring = vec![KEYS.alice_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -551,10 +535,8 @@ mod tests {
assert_eq!(valid_signatures.len(), 1);
// Check decrypting as Bob
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.alice_public.clone());
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.alice_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -567,15 +549,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_no_sig_check() {
let mut keyring = Keyring::new();
keyring.add(KEYS.alice_secret.clone());
let empty_keyring = Keyring::new();
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&keyring,
&empty_keyring,
)
.unwrap();
let keyring = vec![KEYS.alice_secret.clone()];
let (plain, valid_signatures) =
pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap();
assert_eq!(plain, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -583,10 +559,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_signed_no_key() {
// The validation does not have the public key of the signer.
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let mut sig_check_keyring = Keyring::new();
sig_check_keyring.add(KEYS.bob_public.clone());
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let sig_check_keyring = vec![KEYS.bob_public.clone()];
let (plain, valid_signatures) = pk_decrypt(
ctext_signed().await.as_bytes().to_vec(),
&decrypt_keyring,
@@ -599,13 +573,11 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let mut decrypt_keyring = Keyring::new();
decrypt_keyring.add(KEYS.bob_secret.clone());
let sig_check_keyring = Keyring::new();
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let (plain, valid_signatures) = pk_decrypt(
ctext_unsigned().await.as_bytes().to_vec(),
&decrypt_keyring,
&sig_check_keyring,
&[],
)
.unwrap();
assert_eq!(plain, CLEARTEXT);

View File

@@ -8,6 +8,7 @@ use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
use crate::config::Config;
use crate::context::Context;
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
use crate::tools::EmailAddress;
/// Provider status according to manual testing.
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
@@ -171,25 +172,34 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
let resolver = AsyncResolver::tokio(
config::ResolverConfig::default(),
config::ResolverOpts::default(),
)?;
);
Ok(resolver)
}
/// Returns provider for the given an e-mail address.
///
/// Returns an error if provided address is not valid.
pub async fn get_provider_info_by_addr(
context: &Context,
addr: &str,
skip_mx: bool,
) -> Result<Option<&'static Provider>> {
let addr = EmailAddress::new(addr)?;
let provider = get_provider_info(context, &addr.domain, skip_mx).await;
Ok(provider)
}
/// Returns provider for the given domain.
///
/// This function looks up domain in offline database first. If not
/// found, it queries MX record for the domain and looks up offline
/// database for MX domains.
///
/// For compatibility, email address can be passed to this function
/// instead of the domain.
pub async fn get_provider_info(
context: &Context,
domain: &str,
skip_mx: bool,
) -> Option<&'static Provider> {
let domain = domain.rsplit('@').next()?;
if let Some(provider) = get_provider_by_domain(domain) {
return Some(provider);
}
@@ -314,15 +324,25 @@ mod tests {
let t = TestContext::new().await;
assert!(get_provider_info(&t, "", false).await.is_none());
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
assert!(get_provider_info(&t, "example@google.com", false)
.await
.is_none());
}
// get_provider_info() accepts email addresses for backwards compatibility
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_provider_info_by_addr() -> Result<()> {
let t = TestContext::new().await;
assert!(get_provider_info_by_addr(&t, "google.com", false)
.await
.is_err());
assert!(
get_provider_info(&t, "example@google.com", false)
.await
get_provider_info_by_addr(&t, "example@google.com", false)
.await?
.unwrap()
.id
== "gmail"
);
Ok(())
}
#[test]

View File

@@ -17,11 +17,12 @@ use crate::contact::{
addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::socks::Socks5Config;
use crate::{token, EventType};
use crate::token;
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";

View File

@@ -4,17 +4,15 @@ use anyhow::Result;
use base64::Engine as _;
use qrcodegen::{QrCode, QrCodeEcc};
use crate::{
blob::BlobObject,
chat::{Chat, ChatId},
color::color_int_to_hex_string,
config::Config,
contact::{Contact, ContactId},
context::Context,
qr::{self, Qr},
securejoin,
stock_str::{self, backup_transfer_qr},
};
use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId};
use crate::color::color_int_to_hex_string;
use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::qr::{self, Qr};
use crate::securejoin;
use crate::stock_str::{self, backup_transfer_qr};
/// Returns SVG of the QR code to join the group or verify contact.
///

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