Compare commits

..

220 Commits

Author SHA1 Message Date
Hocuri
2abfe3621f Implement smarter authservid algorithm 2022-12-19 16:43:03 +01:00
dependabot[bot]
f37e50cc71 Merge pull request #3798 from deltachat/dependabot/cargo/chrono-0.4.23 2022-12-12 15:09:57 +00:00
link2xt
3023d3c358 Don't use deprecated chrono functions 2022-12-11 23:43:00 +00:00
dependabot[bot]
08562b645e cargo: bump chrono from 0.4.22 to 0.4.23
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.22 to 0.4.23.
- [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.22...v0.4.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-11 21:20:36 +00:00
Asiel Díaz Benítez
edb1d2fb9e Merge pull request #3813 from deltachat/adb/rpc-client-improvements
python deltachat-rpc-client improvements
2022-12-10 14:34:23 -05:00
adbenitez
9645101de2 add SystemMessageType and ViewType 2022-12-10 13:44:42 -05:00
adbenitez
c1bbd6e766 fix tests 2022-12-10 13:44:21 -05:00
link2xt
72ca4c2e1f Changelog 2022-12-10 15:29:02 +00:00
link2xt
bccd79b6be Set read/write timeouts for IMAP SOCKS5 streams 2022-12-10 15:29:02 +00:00
link2xt
109a27c9ef Move Socks5Config to a separate module 2022-12-10 15:29:02 +00:00
Hocuri
2fa2ade3ae use context.get_config_bool() for Config::Bot everywhere (#3834)
We were already using context.get_config_bool(Config::Bot) in some
places, this unifies this.

Followup for #3831
2022-12-10 15:10:45 +01:00
adbenitez
0a4c8a40ba solve review comments 2022-12-09 12:52:01 -05:00
adbenitez
c49743d38c add more tests 2022-12-09 12:52:01 -05:00
adbenitez
a9afc1e6ba add more high-level methods and event hooking 2022-12-09 12:52:01 -05:00
link2xt
aa6f5fd139 Release 1.103.0 2022-12-09 17:43:03 +00:00
link2xt
519f658c07 Make bots automatically accept mailing list chats 2022-12-09 17:24:21 +00:00
link2xt
f5cb56fd86 Fix deltachat-rpc-server tests for tox 4 2022-12-09 17:23:20 +00:00
link2xt
c830db07ad Add testenv:.pkg to tox.ini 2022-12-09 17:23:20 +00:00
link2xt
9093702692 ci: upgrade to tox 4.0 2022-12-09 17:23:20 +00:00
link2xt
edd58b4b7a imap: disable read timeout during IDLE
Otherwise IDLE restarts every 30 seconds.
2022-12-09 11:06:34 +00:00
link2xt
72432d65ba imap: add connect() timeouts 2022-12-08 21:50:03 +00:00
Hocuri
eb611a2855 Make the IMAP_TIMEOUT type-safe 2022-12-08 17:17:33 +00:00
link2xt
8aa73ed6ae Set read/write timeouts for IMAP sockets 2022-12-08 17:16:32 +00:00
link2xt
1224222984 ci: remove dependency on actions-rs/cargo
It is unmaintained and throws many warnings about using deprecated
Node and GitHub Actions commands.
2022-12-08 18:04:41 +01:00
link2xt
3360c6aa96 Downgrade tox to version 3
Our CI pipeline currently does not work with tox 4.0.
2022-12-08 15:38:28 +00:00
holger krekel
bfddd3fc69 fix typo 2022-12-07 09:44:54 +01:00
link2xt
35cd81a75f python: do not pass NULL to ffi.gc if the context can't be created 2022-12-07 09:44:54 +01:00
link2xt
f11fceb63a Move IMAP session state into imap::session::Session
IMAP capabilities and selected folder are IMAP session,
not IMAP client property.

Moving most operations into IMAP session structure
removes the need to constantly check whether IMAP session exists
and reduces number of invalid states, e.g. when a folder is selected but
there is no connection.

Capabilities are determined immediately after logging in,
so there is no need for `capabilities_determined` flag anymore.
Capabilities of the server are always known if there is a session.

`should_reconnect` flag and `disconnect()` function are removed: we
drop the session on error. Even though RFC 3501 says that a client
SHOULD NOT close the connection without a LOGOUT, it is more reliable
to always just drop the connection, especially after an error.
2022-12-06 19:38:41 +00:00
link2xt
f14a28db54 Remove autogenerated typescript files 2022-12-06 19:25:04 +00:00
link2xt
cacc01bac0 Add IMAP server ID to the context info only when it is known 2022-12-06 16:01:26 +00:00
Hocuri
fc386f4fa1 Completely disable Autocrypt & Authres-checking for mailing lists (#3765)
* Because both only make problems with mailing lists, it's easiest to just disable them. If we want, we can make them work properly with mailing lists one day and re-enable them, but this needs some further thoughts.

Part of #3701

* Use load_from_db() in more tests

* clippy

* Changelog

* Downgrade warning to info, improve message

* Use lifetimes instead of cloning
2022-12-05 19:00:56 +00:00
link2xt
3743aaa16e Refactor fetch_many_msgs and add more logging 2022-12-05 12:54:40 +00:00
dependabot[bot]
22b4640b31 Merge pull request #3627 from deltachat/dependabot/cargo/criterion-0.4.0 2022-12-04 20:15:18 +00:00
dependabot[bot]
1dcda4989d cargo: bump criterion from 0.3.6 to 0.4.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.3.6 to 0.4.0.
- [Release notes](https://github.com/bheisler/criterion.rs/releases)
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.3.6...0.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-04 18:53:49 +00:00
dependabot[bot]
e59768167a Merge pull request #3801 from deltachat/dependabot/cargo/axum-0.6.1 2022-12-04 18:52:07 +00:00
link2xt
7e5becb5e5 Log the reason when the message cannot be sent to the chat 2022-12-04 18:51:14 +00:00
link2xt
b4f348ccea Merge async JSON-RPC client
PR: https://github.com/deltachat/deltachat-core-rust/pull/3734
2022-12-04 21:02:50 +03:00
dependabot[bot]
7a0dd24681 cargo: bump axum from 0.5.17 to 0.6.1
Bumps [axum](https://github.com/tokio-rs/axum) from 0.5.17 to 0.6.1.
- [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.5.17...axum-v0.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-04 15:29:42 +00:00
dependabot[bot]
e5ae82252f Merge pull request #3797 from deltachat/dependabot/cargo/env_logger-0.10.0 2022-12-04 15:28:07 +00:00
dependabot[bot]
49c45d1007 Merge pull request #3799 from deltachat/dependabot/cargo/image-0.24.5 2022-12-04 15:27:44 +00:00
link2xt
5502bff986 Make _args and _kwargs private in Rpc 2022-12-04 14:03:59 +00:00
link2xt
ee19789cac Make _rpc private 2022-12-04 14:03:43 +00:00
link2xt
bad5a1de55 Ignore .tox everywhere, not only in python/ 2022-12-04 13:57:38 +00:00
link2xt
3cdbe213a3 python: rename Deltachat class into DeltaChat 2022-12-04 13:56:53 +00:00
dependabot[bot]
3b5b1bf877 cargo: bump env_logger from 0.9.1 to 0.10.0
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.9.1 to 0.10.0.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.9.1...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-04 13:51:18 +00:00
dependabot[bot]
bcd9229ffe cargo: bump image from 0.24.4 to 0.24.5
Bumps [image](https://github.com/image-rs/image) from 0.24.4 to 0.24.5.
- [Release notes](https://github.com/image-rs/image/releases)
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.4...v0.24.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-04 13:43:02 +00:00
link2xt
4df588668a Bump MSRV to 1.61.0
This is required by `image` crate.

Also update current Rust version to 1.65.0.
2022-12-04 12:24:17 +00:00
iequidoo
de96500c1a Add a test on reactions after a reordering MOVE to DeltaChat folder (#3756) 2022-12-04 04:40:11 +04:00
iequidoo
9b881cdd19 Fetch messages in the order of their INTERNALDATE (#3756)
When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command, their
UIDs may be reordered (e.g. Gmail is known for that) which leads to that messages are processed by
receive_imf in the wrong order. But the INTERNALDATE attribute is preserved during a MOVE according
to RFC3501. So, use it for sorting fetched messages.
2022-12-04 04:40:11 +04:00
link2xt
2ccf39800d Remove start_rpc_server() and make Rpc a context manager
Rpc now has a start() method.
This way it is possible to use Rpc from IPython without calling __aenter__()
2022-12-04 00:02:32 +00:00
link2xt
5a3065344e Properly terminate Rpc and remove sleep() hack 2022-12-03 23:30:50 +00:00
link2xt
98b6b5e3f6 Update instructions on using ipython 2022-12-03 18:37:02 +00:00
dependabot[bot]
4d81fa6df5 Merge pull request #3795 from deltachat/dependabot/cargo/num_cpus-1.14.0 2022-12-03 17:47:28 +00:00
dependabot[bot]
8e8582e953 Merge pull request #3791 from deltachat/dependabot/cargo/serde-1.0.148 2022-12-03 17:02:47 +00:00
link2xt
7f4c05e88f ci: do not use deprecated set-output
See https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
2022-12-03 13:54:11 +00:00
link2xt
20e63659a1 CI: update GitHub Actions to avoid deprecation warnings 2022-12-03 13:51:57 +00:00
dependabot[bot]
c5af69db2b cargo: bump serde from 1.0.147 to 1.0.148
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.147 to 1.0.148.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.147...v1.0.148)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-03 13:25:10 +00:00
dependabot[bot]
030241d1c3 Merge pull request #3804 from deltachat/dependabot/cargo/tagger-4.3.4 2022-12-03 13:23:20 +00:00
dependabot[bot]
4f01c43a93 Merge pull request #3796 from deltachat/dependabot/cargo/serde_json-1.0.89 2022-12-03 13:19:51 +00:00
dependabot[bot]
ab94471e91 Merge pull request #3794 from deltachat/dependabot/cargo/uuid-1.2.2 2022-12-03 00:36:55 +00:00
dependabot[bot]
140aa68811 cargo: bump serde_json from 1.0.87 to 1.0.89
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.87 to 1.0.89.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.87...v1.0.89)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-03 00:13:28 +00:00
dependabot[bot]
d9ef38e370 Merge pull request #3803 from deltachat/dependabot/cargo/tokio-1.22.0 2022-12-03 00:11:39 +00:00
dependabot[bot]
acf66116cd Merge pull request #3805 from deltachat/dependabot/cargo/syn-1.0.105 2022-12-03 00:10:17 +00:00
dependabot[bot]
8e69125128 Merge pull request #3806 from deltachat/dependabot/cargo/sha-1-0.10.1 2022-12-02 23:06:29 +00:00
dependabot[bot]
7402ca3568 cargo: bump tokio from 1.21.2 to 1.22.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.21.2 to 1.22.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.21.2...tokio-1.22.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 18:28:24 +00:00
dependabot[bot]
d7b240f25c cargo: bump syn from 1.0.103 to 1.0.105
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.103 to 1.0.105.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.103...1.0.105)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 17:31:27 +00:00
dependabot[bot]
e5e4d3bed4 cargo: bump tagger from 4.3.3 to 4.3.4
Bumps [tagger](https://github.com/tiby312/tagger) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/tiby312/tagger/releases)
- [Commits](https://github.com/tiby312/tagger/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 17:31:25 +00:00
dependabot[bot]
1a59302749 Merge pull request #3793 from deltachat/dependabot/cargo/async-channel-1.8.0 2022-12-02 17:29:30 +00:00
dependabot[bot]
f0a5bbedb4 cargo: bump async-channel from 1.7.1 to 1.8.0
Bumps [async-channel](https://github.com/smol-rs/async-channel) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v1.7.1...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 15:15:40 +00:00
dependabot[bot]
139665b3d6 Merge pull request #3792 from deltachat/dependabot/cargo/typescript-type-def-0.5.5 2022-12-02 15:13:42 +00:00
dependabot[bot]
ca23d59148 Merge pull request #3802 from deltachat/dependabot/cargo/regex-1.7.0 2022-12-02 15:13:09 +00:00
dependabot[bot]
1635f00a62 Merge pull request #3800 from deltachat/dependabot/cargo/reqwest-0.11.13 2022-12-02 15:11:17 +00:00
adbenitez
29a4404257 enably type-checking in tests 2022-12-01 20:33:05 -05:00
adbenitez
24db29f526 Merge remote-tracking branch 'upstream/link2xt/async-jsonrpc-client' into adb/async-jsonrpc-client 2022-12-01 20:32:36 -05:00
dependabot[bot]
e27b64274f cargo: bump sha-1 from 0.10.0 to 0.10.1
Bumps [sha-1](https://github.com/RustCrypto/hashes) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/RustCrypto/hashes/releases)
- [Commits](https://github.com/RustCrypto/hashes/compare/sha-1-v0.10.0...md2-v0.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-01 21:04:08 +00:00
dependabot[bot]
46721bcf46 cargo: bump regex from 1.6.0 to 1.7.0
Bumps [regex](https://github.com/rust-lang/regex) from 1.6.0 to 1.7.0.
- [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.6.0...1.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-01 21:03:25 +00:00
dependabot[bot]
80cefdd1d5 cargo: bump reqwest from 0.11.12 to 0.11.13
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.12 to 0.11.13.
- [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.12...v0.11.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-01 21:03:06 +00:00
dependabot[bot]
9972f4da48 cargo: bump num_cpus from 1.13.1 to 1.14.0
Bumps [num_cpus](https://github.com/seanmonstar/num_cpus) from 1.13.1 to 1.14.0.
- [Release notes](https://github.com/seanmonstar/num_cpus/releases)
- [Changelog](https://github.com/seanmonstar/num_cpus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/num_cpus/compare/v1.13.1...v1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-01 21:02:25 +00:00
dependabot[bot]
1ab5401501 cargo: bump uuid from 1.2.1 to 1.2.2
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.2.1...1.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-01 21:02:20 +00:00
dependabot[bot]
0b60cc8341 cargo: bump typescript-type-def from 0.5.4 to 0.5.5
Bumps [typescript-type-def](https://github.com/dbeckwith/rust-typescript-type-def) from 0.5.4 to 0.5.5.
- [Release notes](https://github.com/dbeckwith/rust-typescript-type-def/releases)
- [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.4...v0.5.5)

---
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>
2022-12-01 21:01:55 +00:00
link2xt
85b4746516 Turn start_rpc_server into a context manager 2022-12-01 16:36:36 +00:00
adbenitez
d17ac9c83c add start_rpc_server() doc string 2022-12-01 03:34:43 -05:00
adbenitez
e6ff513aac add support for PEP 561 2022-12-01 03:15:25 -05:00
adbenitez
ffbfeab977 add pytest plugin 2022-12-01 03:09:23 -05:00
adbenitez
09db062958 fix bug in Rpc.__getattr__() 2022-12-01 02:36:55 -05:00
adbenitez
ab7732d9ae fix type hint in rpc.py 2022-12-01 02:26:50 -05:00
adbenitez
46594ec707 improve typing hints 2022-12-01 00:37:45 -05:00
adbenitez
18426561e3 fix bug in chat.get_encryption_info() 2022-12-01 00:14:07 -05:00
adbenitez
aeb7e3a9e1 fix some linter warnings 2022-11-30 23:56:50 -05:00
adbenitez
a77c46be20 fix bug in account.py: arguments declared as optional but not default value was given 2022-11-30 23:42:08 -05:00
adbenitez
a4be2cddf2 remove unused code in rpc.py 2022-11-30 23:11:50 -05:00
adbenitez
240b335181 fix bugs in account.secure_join() and chat.get_fresh_message_count() 2022-11-30 22:36:49 -05:00
link2xt
53d6807e8d Simplify process_messages() in echo bot 2022-11-30 22:45:03 +00:00
link2xt
db38cc8891 Add get_fresh_messages_in_arrival_order() call 2022-11-30 22:43:05 +00:00
link2xt
4e2aeceeec Do not reverse the list of fresh messages
Both reversed and original order do not make much sense
for the bot. Ideally bots should have their own key
to get the list of fresh messages in the order of IDs.
2022-11-30 22:38:25 +00:00
link2xt
9b04a04568 Add async python client for Delta Chat core JSON-RPC 2022-11-30 20:21:59 +00:00
link2xt
f2c97bda66 jsonrpc: add message errors to MessageObject 2022-11-30 20:35:36 +01:00
Hocuri
6c4d919828 Add members to chats in a single sql transation (#3780)
This esp. speeds up receive_imf a bit when we recreate the member list (recreate_member_list == true).

It's a preparation for https://github.com/deltachat/deltachat-core-rust/issues/3768, which will be a one-line-fix, but recreate the member list more often, so that we want to optimize this case a bit.

It also adds a benchmark for this case. It's not that easy to make the benchmark non-flaky, but by closing all other programs and locking the CPU to 1.5GHz it worked. It is consistently a few percent faster than ./without-optim:

```
Receive messages/Receive 100 Chat-Group-Member-{Added|Removed} messages                                                                            
                        time:   [52.257 ms 52.569 ms 52.941 ms]
                        change: [-3.5301% -2.6181% -1.6697%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 7 outliers among 100 measurements (7.00%)
  4 (4.00%) high mild
  3 (3.00%) high severe
```
2022-11-29 19:37:56 +00:00
iequidoo
f6a502a8e3 Remove the remaining AsRef<str> (#3669)
Using &str instead of AsRef is better for compile times, binary size and code complexity.
2022-11-28 17:02:05 -03:00
missytake
e5be023e4b document provider pull 2022-11-28 18:03:32 +01:00
link2xt
47743bdcfa Update humansize to version 2 2022-11-28 15:50:45 +00:00
link2xt
b0962363c2 Merge branch 'link2xt/peerstate-to-save-simplify' 2022-11-27 17:11:19 +00:00
link2xt
8855ef72bc Clippy fix 2022-11-27 17:10:46 +00:00
link2xt
264727a414 Changelog 2022-11-27 17:10:46 +00:00
link2xt
98c16ddc4d Remove Peerstate.to_save 2022-11-27 17:10:46 +00:00
link2xt
c7691fbebe Use UPSERT when saving peerstates
This way there is no need to distinguish between creating
and updating peerstate.
2022-11-27 17:10:46 +00:00
link2xt
62f92d5b28 Add UNIQUE constraint to acpeerstates table 2022-11-27 17:10:46 +00:00
link2xt
2ae9165bfb Remove different states of ToSave in peerstate 2022-11-27 17:10:46 +00:00
link2xt
08de326930 Fix misplaced info! message 2022-11-27 10:03:47 +00:00
link2xt
b341cfd4d9 mimeparser: assume all Thunderbird users prefer encryption
Co-Authored-By: missytake <missytake@systemli.org>
2022-11-24 19:20:56 +00:00
link2xt
a76b018900 Move changelog entry to the correct section 2022-11-24 11:23:36 +00:00
iequidoo
9783da5d8e receive_imf: trim() "Chat-Group-Name{,-Changed}:" headers content (#3650)
It's a w/a for "Space added before long group names after MIME serialization/deserialization"
issue. DC itself never creates group names with leading/trailing whitespace, so it can be safely
removed. On the sender side there's no trim() because group names anyway go through
improve_single_line_input(). And I believe we should send the exact name we have in our db. Also
there's no check for leading/trailing whitespace because there may be existing user databases with
group names having such whitespaces.
2022-11-24 08:18:36 -03:00
link2xt
6f0985dcaa Changelog 2022-11-23 22:38:12 +00:00
iequidoo
0b44886b62 Add a test for bug "Partially downloaded messages are received out of order" (#3688)
+ add Message.download_state property.
2022-11-23 23:51:50 +04:00
iequidoo
36991b5c8a Add Python API to send reactions (#3762) 2022-11-23 23:51:50 +04:00
link2xt
afb7f89722 Do not try to redownload the message in case of any error
Since switch to async we don't have spurious "database is busy"
errors anymore. Since an error is irrecoverable in most cases,
we can skip the message. The cost of this is we may
accidentally skip a correct message if I/O fails, but
the advantage is that we are guaranteed to never confuse
irrecoverable error with recoverable one and get stuck in
infinite loop redownloading the same message over and over.
2022-11-23 17:56:56 +00:00
link2xt
0248a36561 Release 1.102.0 (#3773) 2022-11-23 18:47:49 +01:00
Sebastian Klähn
2e7470115c Fix link in webxdc-dev-reference.md (#3770)
Update webxdc-dev-reference.md
2022-11-23 14:46:25 +01:00
Hocuri
06b376d242 Bugfix: Don't let malformed From: headers block the receiving pipeline (#3769)
That's a bug which @Simon-Laux and probably also @hpk42 had, where one malformed incoming (Spam-) mail blocked the receiving of all emails coming after it.

The problem was that from_field_to_contact_id() returned ContactId::UNDEFINED, and then lookup_by_contact() returned Err.
2022-11-22 21:10:26 +01:00
Hocuri
4b17813b9f Improve handling of multiple / no From addresses (#3667)
* Treat multiple From addresses as if there was no From: addr

* changelog

* Don't send invalid emails through the whole receive_imf pipeline

Instead, directly create a trash entry for them.

* Don't create trash entries for randomly generated Message-Id's

* clippy

* fix typo

Co-authored-by: link2xt <link2xt@testrun.org>
2022-11-21 21:38:56 +01:00
Hocuri
960a7f82ef Small test fix (#3764)
Doesn't make a difference at this point, since the test is ignored
anyway.
2022-11-19 12:03:05 +01:00
iequidoo
25be8ccd05 fetch_new_messages(): fetch messages sequentially (#3688)
If we fetch messages out of order, then f.e. reactions don't work because if we process a reaction not yet having the corresponding message processed, the reaction is thrown away.
2022-11-17 15:51:59 -03:00
link2xt
da6c68629d jsonrpc: do not return a result from deleteContact()
It was always true anyway.
2022-11-17 18:36:38 +00:00
link2xt
b63baf939e Ignore two typescript errors
`as any` is not enough anymore.
2022-11-16 14:06:17 +00:00
link2xt
90d8e0cedc Fix detection of Trash, Junk, All etc. folders
imap_proto has been updated, so attributes like `\All`,
`\Junk` from RFC 3501 and RFC 6154 are no longer considered
extensions.
2022-11-15 23:40:07 +00:00
link2xt
1c2d4c518e Fix typos 2022-11-15 23:23:44 +00:00
bjoern
0c030e811f prepare 1.101.0 (#3757)
* update changelog for core101

* bump version to 1.101.0
2022-11-15 16:08:29 +01:00
link2xt
428ef11157 Add IMAP UIDs to message info (#3755) 2022-11-15 15:19:32 +01:00
link2xt
ee34b64f5d Pop recently seen loop event out of the queue when it's in the past 2022-11-15 11:29:13 +00:00
bjoern
c1f9d8f7a1 allow deleting referenced contacts in UI (#3751)
* allow deleting referenced contacts in UI

we are quite often getting requests of users
who want to get rid of some contact in the "new chat" list.
there is already a "delete" option,
but it does not work for referenced contacts -
however, it is not obvious for users that a contact is in use,
esp. of some mailing list or larger chat, old contacts, whatever.

this pr revives an old idea [^1] of "soft deleting" referenced contacts -
this way, the user can remove the annoying entry
without the need to understand complicated things
and finally saying that deletion is impossible :)

once the contact is reused, it will reappear,
however, this is already explained in the confirmation dialog of the UIs.

technically, this pr was simpler as expected as we already have
a Origin::Hidden, that is just reused here.

[^1]: https://github.com/deltachat/deltachat-core/pull/542

* update rust doccomment

* update changelog

* avoid races on contact deletion

chats may be created between checking for "no chats" and contact deletion.

this is prevented by putting the statement into an EXCLUSIVE transaction.

* fix failing python test
2022-11-15 11:02:47 +01:00
link2xt
996be5d247 Improve IMAP logging
https://github.com/deltachat/deltachat-core-rust/pull/3749
2022-11-14 11:41:52 +00:00
link2xt
7d45419724 Convert anyhow errors to select_folder errors with alternate fmt 2022-11-14 11:40:40 +00:00
link2xt
c0ae5c0fb7 Use anyhow for close_folder() errors 2022-11-13 23:49:34 +00:00
link2xt
09042d12d4 Changelog 2022-11-13 20:35:05 +00:00
link2xt
7db147da14 Log fake IDLE interruptions 2022-11-13 20:35:05 +00:00
link2xt
1324b5da13 Rename folder argument into folder_config 2022-11-13 20:35:05 +00:00
link2xt
4ee14e6e77 Add more contexts to IMAP errors 2022-11-13 20:35:05 +00:00
link2xt
e1d50757b3 Better format for scan_folders errors 2022-11-13 20:35:05 +00:00
link2xt
9a447e8554 Log IDLE errors with all contexts 2022-11-13 20:35:05 +00:00
link2xt
516a5e9c5f Add contexts to both the timeout and actual IDLE error
Note that `IMAP IDLE protocol timed out` was previously
added to the wrong error: not the timeout error (first `?`)
but actual error happened during IDLE, such as a network error.
2022-11-13 20:35:05 +00:00
link2xt
4744f5eecf Add folder name to IMAP IDLE interrupt logs 2022-11-13 22:11:47 +03:00
link2xt
33839b5667 imap: log disconnection attempts 2022-11-13 17:59:28 +00:00
link2xt
43f2d64a6f imap: flatten fetch_idle() 2022-11-13 17:59:28 +00:00
link2xt
13f30c3167 Add configured_inbox_folder to account info
This goes into the log on Android.
2022-11-13 16:21:44 +00:00
Hocuri
749f00766f Go back to standard async_zip, fix build failures (#3747)
If building DC failed with some long error message about "spurious
network error" and "async_io_utilities", then this PR fixes that.

Majored deleted the rs-async-io-utilities repo because he [doesn't need it anymore](618f700811), so that https://github.com/dignifiedquire/rs-async-zip doesn't have its dependency anymore. The latter was a hotfix-fork of https://github.com/Majored/rs-async-zip because https://github.com/Majored/rs-async-zip/pull/27, fixing https://github.com/deltachat/deltachat-core-rust/issues/3476, wasn't merged.

But apparently the problem of
https://github.com/deltachat/deltachat-core-rust/issues/3476 is fixed in
`async_zip = "0.0.9"`, at least all the webxdcs from https://webxdc.org/
worked fine for me. So, let's just go back to the official async_zip.
2022-11-12 22:01:04 +01:00
bjoern
d2cc343649 prepare 1.100.0 💯 (#3745)
* update changelog for core100

* bump version to 1.100.0
2022-11-09 16:19:17 +00:00
Simon Laux
f20c3e08d4 jsonrpc: show sticker image in quote (#3744)
* jsonrpc: show sticker image in quote

* add pr number to changelog
2022-11-09 16:43:57 +01:00
Simon Laux
ae4c7b635d jsonrpc: add miscSaveSticker method (#3743)
* jsonrpc: add `miscSaveSticker` method

* apply suggestions from link2xt
2022-11-09 16:43:34 +01:00
link2xt
b9f1f9c41e jsonrpc: add invalid key to {get,set}_config error 2022-11-08 21:55:25 +00:00
Simon Laux
b46d40aa07 use dc fork of tiny emitter (#3741)
* use dc fork of tiny emitter

* add pr number to changelog
2022-11-08 21:22:58 +01:00
Simon Laux
11a6991b5c jsonrpc: ts-client use object instead of array for contextEmitters (#3740)
reason:
the array solution has many empty elements in between accounts in practice,
which is annoying when debugging desktop
2022-11-08 21:22:51 +01:00
link2xt
fcf0cb5d69 MSRV is now 1.57 2022-11-07 18:24:54 +00:00
link2xt
a271baa1ae Update rpgp to 0.9 and bump MSRV to 1.57.0 2022-11-07 18:23:26 +00:00
Hocuri
0194c7fcbc Typo fix (#3738) 2022-11-07 18:07:22 +01:00
link2xt
5ee6cba557 Update Cargo.lock 2022-11-06 21:27:21 +00:00
link2xt
475d18bd37 Update deltachat-rpc-server version 2022-11-06 21:26:05 +00:00
link2xt
75ed4fe398 JSON-RPC stdio server
It speaks JSON-RPC serialized into JSON Lines over stdio.
2022-11-06 20:33:24 +00:00
link2xt
d29b0baa25 Prepare core release 1.99.0 2022-11-06 17:41:47 +00:00
Hocuri
ffd57772e9 Add DC_EVENT_INCOMING_MSG_BUNCH event (#3643)
* Add DC_EVENT_INCOMING_MSG event

* Fix lots of compile errors

* Docs

* Changelog

* Fix python tests

Adding DC_EVENT_INCOMING_MSG_BUNCH made the python tests fail because they use `get_matching("DC_EVENT_INCOMING_MSG")`, which also matches DC_EVENT_INCOMING_MSG_BUNCH, so the tests got confused.

This fixes `get_matching()` to only match whole event names.

* Also fix test_ac_setup_message_twice()

The built regex was ^EVENT_NAME1|EVENT_NAME2$, which becomes parsed as
"^EVENT_NAME1" OR "EVENT_NAME2$". Introduce a group (parentheses) to fix
this.

* desktop will use DC_EVENT_INCOMING_MSG_BUNCH,
so I would not call it experimental anymore

* add generated node constants

* msg_ids in the event as Vec<u32>
number[] in js land

this is way more convinient than a json encoded string.

* Apply suggestions from code review

Co-authored-by: bjoern <r10s@b44t.com>

Co-authored-by: Simon Laux <mobile.info@simonlaux.de>
Co-authored-by: Simon Laux <Simon-Laux@users.noreply.github.com>
Co-authored-by: bjoern <r10s@b44t.com>
2022-11-06 17:17:48 +00:00
link2xt
e648e4fb29 DKIM-Checking: Don't disallow keychanges for now (#3728) 2022-11-06 16:18:21 +00:00
Hocuri
ecab62a56b Fix flaky test_block_mailing_list() (#3733)
Seems like consume_events() didn't work properly, i.e. in some cases it
didn't see the latest events and failed to consume them. So, the
IncomingMsg event from receiving DC_MAILINGLIST stayed in the events
channel, which made this fail:

```rust
        // Check that no notification is displayed for blocked mailing list message.
        while let Ok(event) = t.evtracker.try_recv() {
            assert!(!matches!(event.typ, EventType::IncomingMsg { .. }));
        }
```

Fix it by explicitly waiting for the first IncomingMsg event.
2022-11-05 14:27:39 +00:00
Hocuri
e21ea739d9 Ignore now-failing test 2022-11-05 10:10:50 +01:00
Hocuri
659bb08389 Also don't disallow going back to cleartext 2022-11-05 10:10:35 +01:00
Hocuri
f8da264e2b changelog 2022-11-05 01:47:27 +00:00
Hocuri
db84317be0 DKIM-Checking: Don't disallowing keychanges for now
To get back to a releaseable state - the info stays accessible in the
Message-info.

We can re-enable it as soon as it has been tested thoroughly.
2022-11-05 01:47:03 +00:00
link2xt
e93dc33ef8 Do not allow peerstate reset if DKIM check failed
The problem was that a message without Autocrypt key or with a wrong
signature resets peerstate regardless of what DKIM check says. I
inserted sleep(1.1) to make sure reset always happens and make the bug
reproducible, then fixed it by forbidding reset if DKIM check fails.

https://github.com/deltachat/deltachat-core-rust/pull/3731
2022-11-05 01:44:37 +00:00
link2xt
cb1a4291d0 Accept ToString instead of AsRef<str> in Params.set() (#3732) 2022-11-05 02:17:29 +01:00
link2xt
1a745b24d7 Merge branch 'unwrap-mailinglistaddr-in-cffi'
https://github.com/deltachat/deltachat-core-rust/pull/3706
2022-11-05 01:14:11 +00:00
link2xt
7b7ce30fe3 Changelog 2022-11-05 01:13:35 +00:00
link2xt
9bc525f579 Fix mailinglist test 2022-11-04 23:30:56 +00:00
Simon Laux
3fcbc03759 fix formatting and most of the test 2022-11-04 23:27:01 +00:00
Simon Laux
1bd53de1f7 unwrap mailinglist addr option in cffi
so rust api and jsonrpc return the option
2022-11-04 23:27:01 +00:00
link2xt
037739c634 mimeparser: do not allow key reset if DKIM check failed 2022-11-04 21:04:24 +00:00
Floris Bruynooghe
3150d2b94b Introduce a ContextBuilder struct (#3698)
The way to create a Context is now rather burdensome, users have to
create and import a bunch of things just to get a Context.  So let's
introduce a builder.

Notice that the builder can only produce an open context, if the
context can not be opened it is dropped.  This is on purpose, the
Context itself can become RAII again at some point by doing this.
Only the FFI needs to have the concept of an open and a closed
Context.
2022-11-04 20:23:06 +00:00
link2xt
91ab10084a Make error reproducible with sleep() 2022-11-04 19:45:29 +00:00
link2xt
96d2a7f0bf Assert that encryption preference is still mutual 2022-11-04 18:57:18 +00:00
link2xt
053c9372cb peerstate: use named columns for SELECT statements
This ensures wrong column is not accidentally used.
2022-11-04 18:55:32 +00:00
Simon Laux
0bb231ad00 jsonrpc: add SystemMessageType to Message and cffi: add missing DC_INFO_ constants (#3707)
* jsonrpc: add `SystemMessageType` to `Message`
and cffi: add missing `DC_INFO_` constants

* Update deltachat-ffi/deltachat.h

Co-authored-by: bjoern <r10s@b44t.com>

* regenerate js constants

Co-authored-by: bjoern <r10s@b44t.com>
2022-11-04 19:48:53 +01:00
link2xt
15db5adc7e Merge branch 'flub/unoptimised-debug'
https://github.com/deltachat/deltachat-core-rust/pull/3699
2022-11-04 13:14:35 +00:00
link2xt
146478e450 Optimize debug builds, but not tests 2022-11-04 12:33:08 +00:00
link2xt
97192a8055 Set RUST_MIN_STACK for Python tests 2022-11-04 12:32:22 +00:00
link2xt
772514940c Set RUST_MIN_STACK in JSON-RPC tests 2022-11-04 12:32:22 +00:00
Floris Bruynooghe
38efde6c98 Seems the npm stuff manages to avoid the cargo config file 2022-11-04 12:32:22 +00:00
Floris Bruynooghe
056b8ba1e8 link tokio issue, remove opt-level line
opt-level relies on the default instead
2022-11-04 12:32:22 +00:00
Floris Bruynooghe
ccd4d46391 Do not use optimised debug builds
Optimised debug builds result in an extremely slow code-build-test
cycle.  The reason we do this is because we have a few tests which end
up overflowing the stack.  This increases the stack instead.
2022-11-04 12:32:22 +00:00
link2xt
e3bf8265c4 Recently seen loop 2022-11-03 20:18:14 +00:00
Hocuri
f4ee86282e Fix clippy warnings (#3726) 2022-11-03 15:44:35 +00:00
dependabot[bot]
7b66eb8b9c Merge pull request #3719 from deltachat/dependabot/cargo/uuid-1.2.1 2022-11-03 08:47:32 +00:00
Simon Laux
a7861c2ea5 fix readme (#3725)
Update README.md
2022-11-02 19:48:37 +01:00
dependabot[bot]
163678dfe3 Merge pull request #3715 from deltachat/dependabot/cargo/axum-0.5.17 2022-11-02 14:18:04 +00:00
dependabot[bot]
e32b1341f8 Merge pull request #3717 from deltachat/dependabot/cargo/anyhow-1.0.66 2022-11-02 13:18:16 +00:00
dependabot[bot]
f579ad79a2 Merge pull request #3710 from deltachat/dependabot/cargo/syn-1.0.103 2022-11-02 13:06:06 +00:00
dependabot[bot]
a6d1b8b975 cargo: bump anyhow from 1.0.65 to 1.0.66
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.65 to 1.0.66.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.65...1.0.66)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 12:21:14 +00:00
dependabot[bot]
890c5596a9 cargo: bump syn from 1.0.102 to 1.0.103
Bumps [syn](https://github.com/dtolnay/syn) from 1.0.102 to 1.0.103.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/1.0.102...1.0.103)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 12:21:13 +00:00
dependabot[bot]
7797580c7f Merge pull request #3712 from deltachat/dependabot/cargo/futures-0.3.25 2022-11-02 12:19:35 +00:00
dependabot[bot]
7f4abc5285 cargo: bump axum from 0.5.16 to 0.5.17
Bumps [axum](https://github.com/tokio-rs/axum) from 0.5.16 to 0.5.17.
- [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.5.16...axum-v0.5.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 07:43:44 +00:00
dependabot[bot]
e559c467c6 cargo: bump futures from 0.3.24 to 0.3.25
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.24 to 0.3.25.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.24...0.3.25)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 07:43:44 +00:00
dependabot[bot]
916915d430 cargo: bump tokio-stream from 0.1.10 to 0.1.11
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.10 to 0.1.11.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.10...tokio-stream-0.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:41:33 +01:00
dependabot[bot]
46d3889b63 cargo: bump base64 from 0.13.0 to 0.13.1
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.13.0 to 0.13.1.
- [Release notes](https://github.com/marshallpierce/rust-base64/releases)
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.13.0...v0.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:40:50 +01:00
dependabot[bot]
e39dc25e2a cargo: bump libc from 0.2.134 to 0.2.137
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.134 to 0.2.137.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.134...0.2.137)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:40:16 +01:00
dependabot[bot]
1a022d8905 cargo: bump serde from 1.0.145 to 1.0.147
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.145 to 1.0.147.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.145...v1.0.147)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:38:28 +01:00
dependabot[bot]
3dc8888c6a cargo: bump kamadak-exif from 0.5.4 to 0.5.5
Bumps [kamadak-exif](https://github.com/kamadak/exif-rs) from 0.5.4 to 0.5.5.
- [Release notes](https://github.com/kamadak/exif-rs/releases)
- [Changelog](https://github.com/kamadak/exif-rs/blob/master/NEWS)
- [Commits](https://github.com/kamadak/exif-rs/compare/0.5.4...0.5.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-02 08:38:10 +01:00
dependabot[bot]
3ff3046b68 Merge pull request #3718 from deltachat/dependabot/cargo/native-tls-0.2.11 2022-11-02 02:13:47 +00:00
dependabot[bot]
910a5dd96a Merge pull request #3720 from deltachat/dependabot/cargo/smallvec-1.10.0 2022-11-02 02:12:59 +00:00
dependabot[bot]
6b521c4c43 Merge pull request #3724 from deltachat/dependabot/cargo/url-2.3.1 2022-11-02 02:12:15 +00:00
dependabot[bot]
fe909fbe92 cargo: bump native-tls from 0.2.10 to 0.2.11
Bumps [native-tls](https://github.com/sfackler/rust-native-tls) from 0.2.10 to 0.2.11.
- [Release notes](https://github.com/sfackler/rust-native-tls/releases)
- [Changelog](https://github.com/sfackler/rust-native-tls/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sfackler/rust-native-tls/compare/v0.2.10...v0.2.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 23:51:39 +00:00
dependabot[bot]
98c9139f3a Merge pull request #3713 from deltachat/dependabot/cargo/serde_json-1.0.87 2022-11-01 23:50:19 +00:00
dependabot[bot]
03d9ee31ec Merge pull request #3711 from deltachat/dependabot/cargo/once_cell-1.16.0 2022-11-01 23:50:02 +00:00
dependabot[bot]
ef1cc56439 cargo: bump smallvec from 1.9.0 to 1.10.0
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.9.0...v1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 23:15:10 +00:00
link2xt
47fcdef88c cargo update -p mio
mio 0.8.5 does not use libc epoll_create1() function on Android anymore,
so it will be possible to revert e29b6f9974
in the next Android release with the new core.
2022-11-01 23:12:45 +00:00
dependabot[bot]
a1c2260f9b cargo: bump url from 2.3.0 to 2.3.1
Bumps [url](https://github.com/servo/rust-url) from 2.3.0 to 2.3.1.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.3.0...v2.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:05:01 +00:00
dependabot[bot]
a4b01b83e7 cargo: bump uuid from 1.1.2 to 1.2.1
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.1.2 to 1.2.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.1.2...1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:04:32 +00:00
dependabot[bot]
b9e8edb3ef cargo: bump serde_json from 1.0.85 to 1.0.87
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.85 to 1.0.87.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.85...v1.0.87)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:03:43 +00:00
dependabot[bot]
b95861c378 cargo: bump once_cell from 1.15.0 to 1.16.0
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.15.0...v1.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-01 21:03:25 +00:00
Simon Laux
758ae185ba jsonrpc: change method naming (#3678)
* jsonrpc: change method naming

* re-gen types

* cargo fmt
2022-10-31 15:41:40 +00:00
Simon Laux
f60d9a51d4 update bindings section in readme (#3696)
* update bindings section in readme

* Update README.md

Co-authored-by: Floris Bruynooghe <flub@devork.be>

* Update README.md

Co-authored-by: bjoern <r10s@b44t.com>

Co-authored-by: Floris Bruynooghe <flub@devork.be>
Co-authored-by: bjoern <r10s@b44t.com>
2022-10-31 15:12:55 +00:00
link2xt
d81579730e Allow sender timestamp to be in the future
This can happen due to unsynchronized clocks or
when "smeared" timestamp is used as the sender sends
multiple messages without delay.
2022-10-30 10:26:22 +00:00
Hocuri
b1c6c40fa7 Check DKIM Authentication-Results (#3583)
Fix #3507

Note that this is not intended for a release at this point! We first have to test whether it runs stable enough. If we want to make a release while we are not confident enough in authres-checking, then we have to disable it.

BTW, most of the 3000 new lines are in `test_data/messages/dkimchecks...`, not the actual code

da3a4b94 adds the results to the Message info. It currently does this by adding them to `hop_info`. Maybe we should rename `hop_info` to `extra_info` or something; this has the disadvantage that we can't rename the sql column name though.

Follow-ups for this could be:
- In `update_authservid_candidates()`: Implement the rest of the algorithm @hpk42 and me thought about. What's missing is remembering how sure we are that these are the right authserv-ids. Esp., when receiving a message sent from another account at the same domain, we can be quite sure that the authserv-ids in there are the ones of our email server. This will make authres-checking work with buzon.uy, disroot.org, yandex.ru, mailo.com, and riseup.net.
- Think about how we present this to the user - e.g. currently the only change is that we don't accept key changes, which will mean that the small lock on the message is not shown.
- And it will mean that we can fully enable AEAP, after revisiting the security implications of this, and assuming everyone (esp. @link2xt who pointed out the problems in the first place) feels comfortable with it.
2022-10-28 12:15:37 +02:00
bjoern
d8bc3769a5 allow searching for unaccepted requests (#3694)
* let search_msgs() return unaccepted requests

unaccepted chat requests are shown in the chatlist,
it should be returned by search_msgs() an by the other search functions as well.

form the view of the user, the search acts like a filter,
so there is no reason to hide things additionally.

also, the user may remember a word in a chat request,
maybe even an archived one (there is no need to accept a request before archiving)
that one wants to search later on.

* test searching for unaccepted requests

* simplyfy expression; `c.blocked!=1` is also what is used in similar statements
2022-10-25 22:22:31 +02:00
Floris Bruynooghe
a73fbf7232 Update textwrap dependency
The current version is unsatisfyable if you use deltachat as a
dependency itself.
2022-10-24 22:09:41 +02:00
448 changed files with 9274 additions and 3704 deletions

11
.cargo/config.toml Normal file
View File

@@ -0,0 +1,11 @@
[env]
# In unoptimised builds tokio tends to use a lot of stack space when
# creating some complicated futures, tokio has an open issue for this:
# https://github.com/tokio-rs/tokio/issues/2055. Some of our tests
# manage to not fit in the default 2MiB stack anymore due to this, so
# while the issue is not resolved we want to work around this.
# Because compiling optimised builds takes a very long time we prefer
# to avoid that. Setting this environment variable ensures that when
# invoking `cargo test` threads are allowed to have a large enough
# stack size without needing to use an optimised build.
RUST_MIN_STACK = "8388608"

View File

@@ -17,7 +17,7 @@ jobs:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
@@ -26,15 +26,12 @@ jobs:
- run: rustup component add rustfmt
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- run: cargo fmt --all -- --check
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
@@ -54,7 +51,7 @@ jobs:
RUSTDOCFLAGS: -Dwarnings
steps:
- name: Checkout sources
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Install rust stable toolchain
uses: actions-rs/toolchain@v1
with:
@@ -65,10 +62,7 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v1
- name: Rustdoc
uses: actions-rs/cargo@v1
with:
command: doc
args: --document-private-items --no-deps
run: cargo doc --document-private-items --no-deps
build_and_test:
name: Build and test
@@ -77,19 +71,19 @@ jobs:
include:
# Currently used Rust version, same as in `rust-toolchain` file.
- os: ubuntu-latest
rust: 1.61.0
rust: 1.65.0
python: 3.9
- os: windows-latest
rust: 1.61.0
rust: 1.65.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.56.1
# Minimum Supported Rust Version = 1.61.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.56.1
rust: 1.61.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
@@ -105,21 +99,13 @@ jobs:
uses: swatinem/rust-cache@v1
- name: check
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests --features repl --benches
run: cargo check --all --bins --examples --tests --features repl --benches
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
run: cargo test --all
- name: test cargo vendor
uses: actions-rs/cargo@v1
with:
command: vendor
run: cargo vendor
- name: install python
if: ${{ matrix.python }}
@@ -133,10 +119,7 @@ jobs:
- name: build C library
if: ${{ matrix.python }}
uses: actions-rs/cargo@v1
with:
command: build
args: -p deltachat_ffi --features jsonrpc
run: cargo build -p deltachat_ffi --features jsonrpc
- name: run python tests
if: ${{ matrix.python }}
@@ -147,6 +130,21 @@ jobs:
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
- name: add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3
- name: install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4

View File

@@ -15,8 +15,8 @@ jobs:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- name: get tag
@@ -46,12 +46,12 @@ jobs:
shell: bash
run: |
cd deltachat-jsonrpc/typescript
npm run build:tsc
npm run build
npm pack .
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}

View File

@@ -8,14 +8,15 @@ on:
env:
CARGO_TERM_COLOR: always
RUST_MIN_STACK: "8388608"
jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 16.x
- uses: actions-rs/toolchain@v1

View File

@@ -15,7 +15,7 @@ jobs:
id: getid
run: |
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
echo ::set-output name=prid::$PULLREQUEST_ID
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
- name: Renaming
run: |
# create empty file to copy it over the outdated deliverable on download.delta.chat

View File

@@ -9,10 +9,10 @@ jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: 16.x

View File

@@ -16,8 +16,8 @@ jobs:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- name: System info
@@ -29,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/
@@ -58,7 +58,7 @@ jobs:
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
@@ -71,7 +71,7 @@ jobs:
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- uses: actions/setup-node@v2
with:
node-version: '16'
@@ -134,7 +134,7 @@ jobs:
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v3
with:
name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}

View File

@@ -16,8 +16,8 @@ jobs:
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v2
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- name: System info
@@ -29,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -37,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/

View File

@@ -11,7 +11,7 @@ jobs:
name: Build REPL example
runs-on: windows-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
@@ -20,13 +20,10 @@ jobs:
override: true
- name: build
uses: actions-rs/cargo@v1
with:
command: build
args: --example repl --features repl,vendored
run: cargo build --example repl --features repl,vendored
- name: Upload binary
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: repl.exe
path: 'target/debug/examples/repl.exe'

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
- name: Build the documentation with cargo
run: |

2
.gitignore vendored
View File

@@ -12,8 +12,8 @@ include
*.db
*.db-blobs
.tox
python/.eggs
python/.tox
*.egg-info
__pycache__
python/src/deltachat/capi*.so

View File

@@ -2,11 +2,139 @@
## Unreleased
### Changes
- Don't use deprecated `chrono` functions #3798
### API-Changes
### Fixes
- Set read/write timeouts for IMAP over SOCKS5 #3833
## 1.103.0
### Changes
- Disable Autocrypt & Authres-checking for mailing lists,
because they don't work well with mailing lists #3765
- Refactor: Remove the remaining AsRef<str> #3669
- Add more logging to `fetch_many_msgs` and refactor it #3811
- Small speedup #3780
- Log the reason when the message cannot be sent to the chat #3810
- Add IMAP server ID line to the context info only when it is known #3814
- Remove autogenerated typescript files #3815
- Move functions that require an IMAP session from `Imap` to `Session`
to reduce the number of code paths where IMAP session may not exist.
Drop connection on error instead of trying to disconnect,
potentially preventing IMAP task from getting stuck. #3812
### API-Changes
- Add Python API to send reactions #3762
- jsonrpc: add message errors to MessageObject #3788
- jsonrpc: Add async Python client #3734
### Fixes
- Make sure malformed messsages will never block receiving further messages anymore #3771
- strip leading/trailing whitespace from "Chat-Group-Name{,-Changed}:" headers content #3650
- Assume all Thunderbird users prefer encryption #3774
- refactor peerstate handling to ensure no duplicate peerstates #3776
- Fetch messages in order of their INTERNALDATE (fixes reactions for Gmail f.e.) #3789
- python: do not pass NULL to ffi.gc if the context can't be created #3818
- Add read/write timeouts to IMAP sockets #3820
- Add connection timeout to IMAP sockets #3828
- Disable read timeout during IMAP IDLE #3826
- Bots automatically accept mailing lists #3831
## 1.102.0
### Changes
- If an email has multiple From addresses, handle this as if there was
no From address, to prevent from forgery attacks. Also, improve
handling of emails with invalid From addresses in general #3667
### API-Changes
### Fixes
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
- fetch messages sequentially to fix reactions on partially downloaded messages #3688
- Fix a bug where one malformed message blocked receiving any further messages #3769
## 1.101.0
### Changes
- add `configured_inbox_folder` to account info #3748
- `dc_delete_contact()` hides contacts if referenced #3751
- add IMAP UIDs to message info #3755
### Fixes
- improve IMAP logging, in particular fix incorrect "IMAP IDLE protocol
timed out" message on network error during IDLE #3749
- pop Recently Seen Loop event out of the queue when it is in the past
to avoid busy looping #3753
- fix build failures by going back to standard `async_zip` #3747
## 1.100.0
### API-Changes
- jsonrpc: add `miscSaveSticker` method
### Changes
- add JSON-RPC stdio server `deltachat-rpc-server` and use it for JSON-RPC tests #3695
- update rPGP from 0.8 to 0.9 #3737
- jsonrpc: typescript client: use npm released deltachat fork of the tiny emitter package #3741
- jsonrpc: show sticker image in quote #3744
## 1.99.0
### API-Changes
- breaking jsonrpc: changed function naming
- `autocryptInitiateKeyTransfer` -> `initiateAutocryptKeyTransfer`
- `autocryptContinueKeyTransfer` -> `continueAutocryptKeyTransfer`
- `chatlistGetFullChatById` -> `getFullChatById`
- `messageGetMessage` -> `getMessage`
- `messageGetMessages` -> `getMessages`
- `messageGetNotificationInfo` -> `getMessageNotificationInfo`
- `contactsGetContact` -> `getContact`
- `contactsCreateContact` -> `createContact`
- `contactsCreateChatByContactId` -> `createChatByContactId`
- `contactsBlock` -> `blockContact`
- `contactsUnblock` -> `unblockContact`
- `contactsGetBlocked` -> `getBlockedContacts`
- `contactsGetContactIds` -> `getContactIds`
- `contactsGetContacts` -> `getContacts`
- `contactsGetContactsByIds` -> `getContactsByIds`
- `chatGetMedia` -> `getChatMedia`
- `chatGetNeighboringMedia` -> `getNeighboringChatMedia`
- `webxdcSendStatusUpdate` -> `sendWebxdcStatusUpdate`
- `webxdcGetStatusUpdates` -> `getWebxdcStatusUpdates`
- `messageGetWebxdcInfo` -> `getWebxdcInfo`
- jsonrpc: changed method signature
- `miscSendTextMessage(accountId, text, chatId)` -> `miscSendTextMessage(accountId, chatId, text)`
- jsonrpc: add `SystemMessageType` to `Message`
- cffi: add missing `DC_INFO_` constants
- Add DC_EVENT_INCOMING_MSG_BUNCH event #3643
- Python bindings: Make get_matching() only match the
whole event name, e.g. events.get_matching("DC_EVENT_INCOMING_MSG")
won't match DC_EVENT_INCOMING_MSG_BUNCH anymore #3643
- Rust: Introduce a ContextBuilder #3698
### Changes
- allow sender timestamp to be in the future, but not too much
- Disable the new "Authentication-Results/DKIM checking" security feature
until we have tested it a bit #3728
- refactorings #3706
### Fixes
- `dc_search_msgs()` returns unaccepted requests #3694
- emit "contacts changed" event when the contact is no longer "seen recently" #3703
- do not allow peerstate reset if DKIM check failed #3731
## 1.98.0
@@ -65,6 +193,9 @@
### Changes
- Look at Authentication-Results. Don't accept Autocrypt key changes
if they come with negative authentiation results while this contact
sent emails with positive authentication results in the past. #3583
- jsonrpc in cffi also sends events now #3662
- jsonrpc: new format for events and better typescript autocompletion
- Join all "[migration] vXX" log messages into one

941
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,19 @@
[package]
name = "deltachat"
version = "1.98.0"
version = "1.103.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
rust-version = "1.56"
rust-version = "1.61"
[profile.dev]
debug = 0
panic = 'abort'
opt-level = 1
[profile.test]
opt-level = 0
[profile.release]
lto = true
panic = 'abort'
@@ -36,25 +39,25 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch =
escaper = "0.1"
futures = "0.3"
hex = "0.4.0"
image = { version = "0.24.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.16", optional = true }
mailparse = "0.13"
native-tls = "0.2"
num_cpus = "1.13"
num_cpus = "1.14"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.15.0"
once_cell = "1.16.0"
percent-encoding = "2.2"
pgp = { version = "0.8", default-features = false }
pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
quick-xml = "0.23"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
regex = "1.6"
regex = "1.7"
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "10", optional = true }
@@ -71,19 +74,20 @@ toml = "0.5"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.8"
humansize = "1"
humansize = "2"
qrcodegen = "1.7.0"
tagger = "4.3.3"
textwrap = "0.15.1"
async-channel = "1.6.1"
tagger = "4.3.4"
textwrap = "0.16.0"
async-channel = "1.8.0"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.10", features = ["fs"] }
reqwest = { version = "0.11.12", features = ["json"] }
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-io-timeout = "1.2.0"
reqwest = { version = "0.11.13", features = ["json"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.3.6", features = ["async_tokio"] }
criterion = { version = "0.4.0", features = ["async_tokio"] }
futures-lite = "1.12"
log = "0.4"
pretty_env_logger = "0.4"
@@ -95,7 +99,8 @@ tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"]
members = [
"deltachat-ffi",
"deltachat_derive",
"deltachat-jsonrpc"
"deltachat-jsonrpc",
"deltachat-rpc-server"
]
[[example]]

View File

@@ -120,15 +120,26 @@ $ cargo test -- --ignored
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
- `nightly`: Enable nightly only performance and security related features.
## Update Provider Data
To add the updates from the
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
```
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
```
## Language bindings and frontend projects
Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- **Node.js**
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Go**[^1] \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)
The following "frontend" projects make use of the Rust-library
@@ -140,3 +151,5 @@ or its language bindings:
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -38,11 +38,64 @@ Hello {i}",
context
}
/// Receive 100 emails that remove charlie@example.com and add
/// him back
async fn recv_groupmembership_emails(context: Context) -> Context {
for i in 0..50 {
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Added: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
let imf_raw = format!(
"Subject: Benchmark
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
Date: Sat, 07 Dec 2019 19:00:27 +0000
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
From: sender@testrun.org
Chat-Version: 1.0
Chat-Disposition-Notification-To: sender@testrun.org
Chat-User-Avatar: 0
Chat-Group-Member-Removed: charlie@example.com
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
Hello {i}",
i = i,
i_dec = i - 1,
);
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
.await
.unwrap();
}
context
}
async fn create_context() -> Context {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let id = 100;
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
let context = Context::new(dbfile.as_path(), id, Events::new(), StockStrings::new())
.await
.unwrap();
@@ -52,7 +105,7 @@ async fn create_context() -> Context {
if backup.exists() {
println!("Importing backup");
imex(&context, ImexMode::ImportBackup, &backup, None)
imex(&context, ImexMode::ImportBackup, backup.as_path(), None)
.await
.unwrap();
}
@@ -83,6 +136,20 @@ fn criterion_benchmark(c: &mut Criterion) {
}
});
});
group.bench_function(
"Receive 100 Chat-Group-Member-{Added|Removed} messages",
|b| {
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(create_context());
b.to_async(&rt).iter(|| {
let ctx = context.clone();
async move {
recv_groupmembership_emails(black_box(ctx)).await;
}
});
},
);
group.finish();
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.98.0"
version = "1.103.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
@@ -25,7 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.7"
once_cell = "1.15.0"
once_cell = "1.16.0"
[features]
default = ["vendored"]

View File

@@ -2056,8 +2056,9 @@ char* dc_get_contact_encrinfo (dc_context_t* context, uint32_t co
/**
* Delete a contact. The contact is deleted from the local device. It may happen that this is not
* possible as the contact is in use. In this case, the contact can be blocked.
* Delete a contact so that it disappears from the corresponding lists.
* Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
* The contact is deleted from the local device.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
@@ -4097,9 +4098,19 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
// DC_INFO* uses the same values as SystemMessage in rust-land
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_UNKNOWN 0
#define DC_INFO_GROUP_NAME_CHANGED 2
#define DC_INFO_GROUP_IMAGE_CHANGED 3
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
#define DC_INFO_SECURE_JOIN_MESSAGE 7
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
* Check if a message is still in creation. A message is in creation between
@@ -5624,6 +5635,17 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_INCOMING_MSG 2005
/**
* Downloading a bunch of messages just finished. This is an experimental
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
*
* @param data1 0
* @param data2 (char*) msg_ids, a json object with the message ids.
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
/**
* Messages were marked noticed or seen.

View File

@@ -503,6 +503,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::MsgsChanged { .. } => 2000,
EventType::ReactionsChanged { .. } => 2001,
EventType::IncomingMsg { .. } => 2005,
EventType::IncomingMsgBunch { .. } => 2006,
EventType::MsgsNoticed { .. } => 2008,
EventType::MsgDelivered { .. } => 2010,
EventType::MsgFailed { .. } => 2012,
@@ -544,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_) => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
@@ -600,6 +602,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::MsgsNoticed(_)
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
@@ -671,6 +674,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
}
}
@@ -1651,7 +1659,7 @@ pub unsafe extern "C" fn dc_set_chat_profile_image(
let ctx = &*context;
block_on(async move {
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), to_string_lossy(image))
chat::set_chat_profile_image(ctx, ChatId::new(chat_id), &to_string_lossy(image))
.await
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set profile image")
@@ -2136,7 +2144,10 @@ pub unsafe extern "C" fn dc_delete_contact(
block_on(async move {
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(_) => 0,
Err(err) => {
error!(ctx, "cannot delete contact: {}", err);
0
}
}
})
}
@@ -2852,7 +2863,11 @@ pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *
return "".strdup();
}
let ffi_chat = &*chat;
ffi_chat.chat.get_mailinglist_addr().strdup()
ffi_chat
.chat
.get_mailinglist_addr()
.unwrap_or_default()
.strdup()
}
#[no_mangle]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.98.0"
version = "1.103.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -19,20 +19,21 @@ num-traits = "0.2"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.6.1" }
futures = { version = "0.3.24" }
serde_json = "1.0.85"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.25" }
serde_json = "1.0.89"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.21.2" }
# optional dependencies
axum = { version = "0.5.16", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.22.0" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
# optional dependencies
axum = { version = "0.6.1", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -120,6 +120,16 @@ pub enum JSONRPCEventType {
msg_id: u32,
},
/// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch {
msg_ids: Vec<u32>,
},
/// Messages were seen or noticed.
/// chat id is always set.
#[serde(rename_all = "camelCase")]
@@ -305,6 +315,9 @@ impl From<EventType> for JSONRPCEventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{anyhow, bail, ensure, Context, Result};
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
@@ -22,6 +22,7 @@ use deltachat::{
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
};
use sanitize_filename::is_sanitized;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::{collections::HashMap, str::FromStr};
@@ -400,12 +401,12 @@ impl CommandApi {
// autocrypt
// ---------------------------------------------
async fn autocrypt_initiate_key_transfer(&self, account_id: u32) -> Result<String> {
async fn initiate_autocrypt_key_transfer(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
deltachat::imex::initiate_key_transfer(&ctx).await
}
async fn autocrypt_continue_key_transfer(
async fn continue_autocrypt_key_transfer(
&self,
account_id: u32,
message_id: u32,
@@ -472,11 +473,7 @@ impl CommandApi {
// chat
// ---------------------------------------------
async fn chatlist_get_full_chat_by_id(
&self,
account_id: u32,
chat_id: u32,
) -> Result<FullChat> {
async fn get_full_chat_by_id(&self, account_id: u32, chat_id: u32) -> Result<FullChat> {
let ctx = self.get_context(account_id).await?;
FullChat::try_from_dc_chat_id(&ctx, chat_id).await
}
@@ -736,7 +733,7 @@ impl CommandApi {
image_path: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), image_path.unwrap_or_default())
chat::set_chat_profile_image(&ctx, ChatId::new(chat_id), &image_path.unwrap_or_default())
.await
}
@@ -911,7 +908,7 @@ impl CommandApi {
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn message_get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
}
@@ -921,7 +918,7 @@ impl CommandApi {
MsgId::new(message_id).get_html(&ctx).await
}
async fn message_get_messages(
async fn get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
@@ -938,7 +935,7 @@ impl CommandApi {
}
/// Fetch info desktop needs for creating a notification for a message
async fn message_get_notification_info(
async fn get_message_notification_info(
&self,
account_id: u32,
message_id: u32,
@@ -1029,11 +1026,7 @@ impl CommandApi {
// ---------------------------------------------
/// Get a single contact options by ID.
async fn contacts_get_contact(
&self,
account_id: u32,
contact_id: u32,
) -> Result<ContactObject> {
async fn get_contact(&self, account_id: u32, contact_id: u32) -> Result<ContactObject> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
@@ -1047,7 +1040,7 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
async fn contacts_create_contact(
async fn create_contact(
&self,
account_id: u32,
email: String,
@@ -1064,11 +1057,7 @@ impl CommandApi {
}
/// Returns contact id of the created or existing DM chat with that contact
async fn contacts_create_chat_by_contact_id(
&self,
account_id: u32,
contact_id: u32,
) -> Result<u32> {
async fn create_chat_by_contact_id(&self, account_id: u32, contact_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let contact = Contact::get_by_id(&ctx, ContactId::new(contact_id)).await?;
ChatId::create_for_contact(&ctx, contact.id)
@@ -1076,17 +1065,17 @@ impl CommandApi {
.map(|id| id.to_u32())
}
async fn contacts_block(&self, account_id: u32, contact_id: u32) -> Result<()> {
async fn block_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::block(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_unblock(&self, account_id: u32, contact_id: u32) -> Result<()> {
async fn unblock_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
Contact::unblock(&ctx, ContactId::new(contact_id)).await
}
async fn contacts_get_blocked(&self, account_id: u32) -> Result<Vec<ContactObject>> {
async fn get_blocked_contacts(&self, account_id: u32) -> Result<Vec<ContactObject>> {
let ctx = self.get_context(account_id).await?;
let blocked_ids = Contact::get_all_blocked(&ctx).await?;
let mut contacts: Vec<ContactObject> = Vec::with_capacity(blocked_ids.len());
@@ -1102,7 +1091,7 @@ impl CommandApi {
Ok(contacts)
}
async fn contacts_get_contact_ids(
async fn get_contact_ids(
&self,
account_id: u32,
list_flags: u32,
@@ -1115,7 +1104,7 @@ impl CommandApi {
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
async fn contacts_get_contacts(
async fn get_contacts(
&self,
account_id: u32,
list_flags: u32,
@@ -1136,7 +1125,7 @@ impl CommandApi {
Ok(contacts)
}
async fn contacts_get_contacts_by_ids(
async fn get_contacts_by_ids(
&self,
account_id: u32,
ids: Vec<u32>,
@@ -1157,12 +1146,12 @@ impl CommandApi {
Ok(contacts)
}
async fn delete_contact(&self, account_id: u32, contact_id: u32) -> Result<bool> {
async fn delete_contact(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
Contact::delete(&ctx, contact_id).await?;
Ok(true)
Ok(())
}
async fn change_contact_name(
@@ -1219,7 +1208,7 @@ impl CommandApi {
///
/// Setting `chat_id` to `None` (`null` in typescript) means get messages with media
/// from any chat of the currently used account.
async fn chat_get_media(
async fn get_chat_media(
&self,
account_id: u32,
chat_id: Option<u32>,
@@ -1247,7 +1236,7 @@ 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
async fn chat_get_neighboring_media(
async fn get_neighboring_chat_media(
&self,
account_id: u32,
msg_id: u32,
@@ -1399,7 +1388,7 @@ impl CommandApi {
// webxdc
// ---------------------------------------------
async fn webxdc_send_status_update(
async fn send_webxdc_status_update(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1411,7 +1400,7 @@ impl CommandApi {
.await
}
async fn webxdc_get_status_updates(
async fn get_webxdc_status_updates(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1426,7 +1415,7 @@ impl CommandApi {
}
/// Get info from a webxdc message
async fn message_get_webxdc_info(
async fn get_webxdc_info(
&self,
account_id: u32,
instance_msg_id: u32,
@@ -1524,7 +1513,7 @@ impl CommandApi {
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("./stickers");
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
sticker_folder_path
.to_str()
@@ -1532,15 +1521,55 @@ impl CommandApi {
.context("path conversion to string failed")
}
/// save a sticker to a collection/folder in the account's sticker folder
async fn misc_save_sticker(
&self,
account_id: u32,
msg_id: u32,
collection: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
ensure!(
message.get_viewtype() == Viewtype::Sticker,
"message {} is not a sticker",
msg_id
);
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
ensure!(
is_sanitized(&collection),
"illegal characters in collection name"
);
let destination_path = account_folder.join("stickers").join(collection);
fs::create_dir_all(&destination_path).await?;
let file = message.get_file(&ctx).context("no file")?;
fs::copy(
&file,
destination_path.join(format!(
"{}.{}",
msg_id,
file.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
Ok(())
}
/// for desktop, get stickers from stickers folder,
/// grouped by the folder they are in.
/// grouped by the collection/folder they are in.
async fn misc_get_stickers(&self, account_id: u32) -> Result<HashMap<String, Vec<String>>> {
let ctx = self.get_context(account_id).await?;
let account_folder = ctx
.get_dbfile()
.parent()
.context("account folder not found")?;
let sticker_folder_path = account_folder.join("./stickers");
let sticker_folder_path = account_folder.join("stickers");
fs::create_dir_all(&sticker_folder_path).await?;
let mut result = HashMap::new();
@@ -1579,8 +1608,8 @@ impl CommandApi {
async fn misc_send_text_message(
&self,
account_id: u32,
text: String,
chat_id: u32,
text: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
@@ -1686,8 +1715,11 @@ async fn set_config(
if key.starts_with("ui.") {
ctx.set_ui_config(key, value).await?;
} else {
ctx.set_config(Config::from_str(key).context("unknown key")?, value)
.await?;
ctx.set_config(
Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?,
value,
)
.await?;
match key {
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
@@ -1706,7 +1738,7 @@ async fn get_config(
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).context("unknown key")?)
ctx.get_config(Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?)
.await
}
}

View File

@@ -37,7 +37,7 @@ pub struct FullChat {
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
can_send: bool,
was_seen_recently: bool,
mailing_list_address: String,
mailing_list_address: Option<String>,
}
impl FullChat {
@@ -81,7 +81,7 @@ impl FullChat {
false
};
let mailing_list_address = chat.get_mailinglist_addr().to_string();
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
Ok(FullChat {
id: chat_id,

View File

@@ -34,6 +34,9 @@ pub struct MessageObject {
view_type: MessageViewtype,
state: u32,
/// An error text, if there is one.
error: Option<String>,
timestamp: i64,
sort_timestamp: i64,
received_timestamp: i64,
@@ -45,6 +48,8 @@ pub struct MessageObject {
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
duration: i32,
dimensions_height: i32,
@@ -125,6 +130,7 @@ impl MessageObject {
override_sender_name: quote.get_override_sender_name(),
image: if quote.get_viewtype() == Viewtype::Image
|| quote.get_viewtype() == Viewtype::Gif
|| quote.get_viewtype() == Viewtype::Sticker
{
match quote.get_file(context) {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
@@ -164,6 +170,7 @@ impl MessageObject {
.get_state()
.to_u32()
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(),
timestamp: message.get_timestamp(),
sort_timestamp: message.get_sort_timestamp(),
@@ -175,6 +182,7 @@ impl MessageObject {
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
system_message_type: message.get_info_type().into(),
duration: message.get_duration(),
dimensions_height: message.get_height(),
@@ -305,6 +313,61 @@ impl From<download::DownloadState> for DownloadState {
}
}
#[derive(Serialize, TypeDef)]
pub enum SystemMessageType {
Unknown,
GroupNameChanged,
GroupImageChanged,
MemberAddedToGroup,
MemberRemovedFromGroup,
AutocryptSetupMessage,
SecurejoinMessage,
LocationStreamingEnabled,
LocationOnly,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
/// Self-sent-message that contains only json used for multi-device-sync;
/// if possible, we attach that to other messages as for locations.
MultiDeviceSync,
// Sync message that contains a json payload
// sent to the other webxdc instances
// These messages are not shown in the chat.
WebxdcStatusUpdate,
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
fn from(system_message_type: deltachat::mimeparser::SystemMessage) -> Self {
use deltachat::mimeparser::SystemMessage;
match system_message_type {
SystemMessage::Unknown => SystemMessageType::Unknown,
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
SystemMessage::AutocryptSetupMessage => SystemMessageType::AutocryptSetupMessage,
SystemMessage::SecurejoinMessage => SystemMessageType::SecurejoinMessage,
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
}
}
}
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo {

View File

@@ -6,3 +6,4 @@ yarn.lock
package-lock.json
docs
accounts
generated

View File

@@ -68,7 +68,7 @@ async function run() {
null
);
for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.chatlistGetFullChatById(
const chat = await client.rpc.getFullChatById(
selectedAccount,
chatId
);
@@ -78,7 +78,7 @@ async function run() {
chatId,
0
);
const messages = await client.rpc.messageGetMessages(
const messages = await client.rpc.getMessages(
selectedAccount,
messageIds
);

View File

@@ -1,941 +0,0 @@
// AUTO-GENERATED by yerpc-derive
import * as T from "./types.js"
import * as RPC from "./jsonrpc.js"
type RequestMethod = (method: string, params?: RPC.Params) => Promise<unknown>;
type NotificationMethod = (method: string, params?: RPC.Params) => void;
interface Transport {
request: RequestMethod,
notification: NotificationMethod
}
export class RawClient {
constructor(private _transport: Transport) {}
/**
* Check if an email address is valid.
*/
public checkEmailValidity(email: string): Promise<boolean> {
return (this._transport.request('check_email_validity', [email] as RPC.Params)) as Promise<boolean>;
}
/**
* Get general system info.
*/
public getSystemInfo(): Promise<Record<string,string>> {
return (this._transport.request('get_system_info', [] as RPC.Params)) as Promise<Record<string,string>>;
}
public addAccount(): Promise<T.U32> {
return (this._transport.request('add_account', [] as RPC.Params)) as Promise<T.U32>;
}
public removeAccount(accountId: T.U32): Promise<null> {
return (this._transport.request('remove_account', [accountId] as RPC.Params)) as Promise<null>;
}
public getAllAccountIds(): Promise<(T.U32)[]> {
return (this._transport.request('get_all_account_ids', [] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Select account id for internally selected state.
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public selectAccount(id: T.U32): Promise<null> {
return (this._transport.request('select_account', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get the selected account id of the internal state..
* TODO: Likely this is deprecated as all methods take an account id now.
*/
public getSelectedAccountId(): Promise<(T.U32|null)> {
return (this._transport.request('get_selected_account_id', [] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Get a list of all configured accounts.
*/
public getAllAccounts(): Promise<(T.Account)[]> {
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
}
public startIoForAllAccounts(): Promise<null> {
return (this._transport.request('start_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public stopIoForAllAccounts(): Promise<null> {
return (this._transport.request('stop_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
}
public startIo(id: T.U32): Promise<null> {
return (this._transport.request('start_io', [id] as RPC.Params)) as Promise<null>;
}
public stopIo(id: T.U32): Promise<null> {
return (this._transport.request('stop_io', [id] as RPC.Params)) as Promise<null>;
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(accountId: T.U32): Promise<T.Account> {
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
}
/**
* Get the combined filesize of an account in bytes
*/
public getAccountFileSize(accountId: T.U32): Promise<T.U64> {
return (this._transport.request('get_account_file_size', [accountId] as RPC.Params)) as Promise<T.U64>;
}
/**
* Returns provider for the given domain.
*
* This function looks up domain in offline database.
*
* For compatibility, email address can be passed to this function
* instead of the domain.
*/
public getProviderInfo(accountId: T.U32, email: string): Promise<(T.ProviderInfo|null)> {
return (this._transport.request('get_provider_info', [accountId, email] as RPC.Params)) as Promise<(T.ProviderInfo|null)>;
}
/**
* Checks if the context is already configured.
*/
public isConfigured(accountId: T.U32): Promise<boolean> {
return (this._transport.request('is_configured', [accountId] as RPC.Params)) as Promise<boolean>;
}
/**
* Get system info for an account.
*/
public getInfo(accountId: T.U32): Promise<Record<string,string>> {
return (this._transport.request('get_info', [accountId] as RPC.Params)) as Promise<Record<string,string>>;
}
public setConfig(accountId: T.U32, key: string, value: (string|null)): Promise<null> {
return (this._transport.request('set_config', [accountId, key, value] as RPC.Params)) as Promise<null>;
}
public batchSetConfig(accountId: T.U32, config: Record<string,(string|null)>): Promise<null> {
return (this._transport.request('batch_set_config', [accountId, config] as RPC.Params)) as Promise<null>;
}
/**
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
* Before this function is called, `checkQr()` should confirm the type of the
* QR code is `account` or `webrtcInstance`.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
*/
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
}
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
}
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
}
public batchGetConfig(accountId: T.U32, keys: (string)[]): Promise<Record<string,(string|null)>> {
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
}
public setStockStrings(strings: Record<T.U32,string>): Promise<null> {
return (this._transport.request('set_stock_strings', [strings] as RPC.Params)) as Promise<null>;
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(accountId: T.U32): Promise<null> {
return (this._transport.request('configure', [accountId] as RPC.Params)) as Promise<null>;
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(accountId: T.U32): Promise<null> {
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
}
public exportSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
public importSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
/**
* Returns the message IDs of all _fresh_ messages of any chat.
* Typically used for implementing notification summaries
* or badge counters e.g. on the app icon.
* The list is already sorted and starts with the most recent fresh message.
*
* Messages belonging to muted chats or to the contact requests are not returned;
* these messages should not be notified
* and also badge counters should not include these messages.
*
* To get the number of fresh messages for a single chat, muted or not,
* use `get_fresh_msg_cnt()`.
*/
public getFreshMsgs(accountId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_fresh_msgs', [accountId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get the number of _fresh_ messages in a chat.
* Typically used to implement a badge with a number in the chatlist.
*
* If the specified chat is muted,
* the UI should show the badge counter "less obtrusive",
* e.g. using "gray" instead of "red" color.
*/
public getFreshMsgCnt(accountId: T.U32, chatId: T.U32): Promise<T.Usize> {
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
}
/**
* Estimate the number of messages that will be deleted
* by the set_config()-options `delete_device_after` or `delete_server_after`.
* This is typically used to show the estimated impact to the user
* before actually enabling deletion of old messages.
*/
public estimateAutoDeletionCount(accountId: T.U32, fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
return (this._transport.request('estimate_auto_deletion_count', [accountId, fromServer, seconds] as RPC.Params)) as Promise<T.Usize>;
}
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
}
public autocryptContinueKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('autocrypt_continue_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
public getChatlistEntries(accountId: T.U32, listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return (this._transport.request('get_chatlist_entries', [accountId, listFlags, queryString, queryContactId] as RPC.Params)) as Promise<(T.ChatListEntry)[]>;
}
public getChatlistItemsByEntries(accountId: T.U32, entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return (this._transport.request('get_chatlist_items_by_entries', [accountId, entries] as RPC.Params)) as Promise<Record<T.U32,T.ChatListItemFetchResult>>;
}
public chatlistGetFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
}
public acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public blockChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Delete a chat.
*
* Messages are deleted from the device and the chat database entry is deleted.
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
*
* Things that are _not done_ implicitly:
*
* - Messages are **not deleted from the server**.
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
* and the user may create the chat again.
* - **Groups are not left** - this would
* be unexpected as (1) deleting a normal chat also does not prevent new mails
* from arriving, (2) leaving a group requires sending a message to
* all group members - especially for groups not used for a longer time, this is
* really unexpected when deletion results in contacting all members again,
* (3) only leaving groups is also a valid usecase.
*
* To leave a chat explicitly, use leave_group()
*/
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a chat.
* Get a multi-line encryption info, containing encryption preferences of all members.
* Can be used to find out why messages sent to group are not encrypted.
*
* returns Multi-line text
*/
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
}
/**
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
* The QR code is compatible to the OPENPGP4FPR format
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
*
* The scanning device will pass the scanned content to `checkQr()` then;
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
* an out-of-band-verification can be joined using `secure_join()`
*
* chat_id: If set to a group-chat-id,
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If not set, the Setup-Contact protocol is offered in the QR code.
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* return format: `[code, svg]`
*/
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
}
/**
* Continue a Setup-Contact or Verified-Group-Invite protocol
* started on another device with `get_chat_securejoin_qr_code_svg()`.
* This function is typically called when `check_qr()` returns
* type=AskVerifyContact or type=AskVerifyGroup.
*
* The function returns immediately and the handshake runs in background,
* sending and receiving several messages.
* During the handshake, info messages are added to the chat,
* showing progress, success or errors.
*
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
*
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* **qr**: The text of the scanned QR code. Typically, the same string as given
* to `check_qr()`.
*
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
*
*/
public secureJoin(accountId: T.U32, qr: string): Promise<T.U32> {
return (this._transport.request('secure_join', [accountId, qr] as RPC.Params)) as Promise<T.U32>;
}
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Remove a member from a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Add a member to a group.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* If the group has group protection enabled, only verified contacts can be added to the group.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
}
/**
* Get the contact IDs belonging to a chat.
*
* - for normal chats, the function always returns exactly one contact,
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
*
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
* explicitly as it may happen that oneself gets removed from a still existing
* group
*
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
*
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
* for now, the UI should not show the list for mailing lists.
* (we do not know all members and there is not always a global mailing list address,
* so we could return only SELF or the known members; this is not decided yet)
*/
public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Create a new group chat.
*
* After creation,
* the group has one member with the ID DC_CONTACT_ID_SELF
* and is in _unpromoted_ state.
* This means, you can add or remove members, change the name,
* the group image and so on without messages being sent to all group members.
*
* This changes as soon as the first message is sent to the group members
* and the group becomes _promoted_.
* After that, all changes are synced with all group members
* by sending status message.
*
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
* This may be useful if you want to show some help for just created groups.
*
* @param protect If set to 1 the function creates group with protection initially enabled.
* Only verified members are allowed in these groups
* and end-to-end-encryption is always enabled.
*/
public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise<T.U32> {
return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise<T.U32>;
}
/**
* Create a new broadcast list.
*
* Broadcast lists are similar to groups on the sending device,
* however, recipients get the messages in normal one-to-one chats
* and will not be aware of other members.
*
* Replies to broadcasts go only to the sender
* and not to all broadcast recipients.
* Moreover, replies will not appear in the broadcast list
* but in the one-to-one chat with the person answering.
*
* The name and the image of the broadcast list is set automatically
* and is visible to the sender only.
* Not asking for these data allows more focused creation
* and we bypass the question who will get which data.
* Also, many users will have at most one broadcast list
* so, a generic name and image is sufficient at the first place.
*
* Later on, however, the name can be changed using dc_set_chat_name().
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
* All in all, this is also what other messengers are doing here.
*/
public createBroadcastList(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Set group name.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*/
public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise<null> {
return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise<null>;
}
/**
* Set group profile image.
*
* If the group is already _promoted_ (any message was sent to the group),
* all group members are informed by a special status message that is sent automatically by this function.
*
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
*
* To find out the profile image of a chat, use dc_chat_get_profile_image()
*
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
* `blobdir`; the original image will not be needed anymore.
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
* this change anyway).
*/
public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise<null> {
return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise<null>;
}
public setChatVisibility(accountId: T.U32, chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
return (this._transport.request('set_chat_visibility', [accountId, chatId, visibility] as RPC.Params)) as Promise<null>;
}
public setChatEphemeralTimer(accountId: T.U32, chatId: T.U32, timer: T.U32): Promise<null> {
return (this._transport.request('set_chat_ephemeral_timer', [accountId, chatId, timer] as RPC.Params)) as Promise<null>;
}
public getChatEphemeralTimer(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('get_chat_ephemeral_timer', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
}
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also markseen_msgs().
*/
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Set mute duration of a chat.
*
* The UI can then call is_chat_muted() when receiving a new message
* to decide whether it should trigger an notification.
*
* Muted chats should not sound or vibrate
* and should not show a visual notification in the system area.
* Moreover, muted chats should be excluded from global badge counter
* (get_fresh_msgs() skips muted chats therefore)
* and the in-app, per-chat badge counter should use a less obtrusive color.
*
* Sends out #DC_EVENT_CHAT_MODIFIED.
*/
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
}
/**
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
*
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
*/
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
}
/**
* Mark messages as presented to the user.
* Typically, UIs call this function on scrolling through the message list,
* when the messages are presented at least for a little moment.
* The concrete action depends on the type of the chat and on the users settings
* (dc_msgs_presented() may be a better name therefore, but well. :)
*
* - For normal chats, the IMAP state is updated, MDN is sent
* (if set_config()-options `mdns_enabled` is set)
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
*
* - For contact requests, no IMAP or MDNs is done
* and the internal state is not changed therefore.
* See also marknoticed_chat().
*
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*/
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
}
public getMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return (this._transport.request('get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
}
public getMessageListItems(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
return (this._transport.request('get_message_list_items', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.MessageListItem)[]>;
}
public messageGetMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('message_get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
}
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public messageGetNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('message_get_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
}
/**
* Get an informational text for a single message. The text is multiline and may
* contain e.g. the raw text of the message.
*
* The max. text returned is typically longer (about 100000 characters) than the
* max. text returned by dc_msg_get_text() (about 30000 characters).
*/
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
}
/**
* Asks the core to start downloading a message fully.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
*
* On success, the @ref DC_MSG "view type of the message" may change
* or the message may be replaced completely by one or more messages with other message IDs.
* That may happen e.g. in cases where the message was encrypted
* and the type could not be determined without fully downloading.
* Downloaded content can be accessed as usual after download.
*
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
*/
public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise<null> {
return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise<null>;
}
/**
* Search messages containing the given query string.
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
*
* Global chat results are typically displayed using dc_msg_get_summary(), chat
* search results may just hilite the corresponding messages and present a
* prev/next button.
*
* For global search, result is limited to 1000 messages,
* this allows incremental search done fast.
* So, when getting exactly 1000 results, the result may be truncated;
* the UIs may display sth. as "1000+ messages found" in this case.
* Chat search (if a chat_id is set) is not limited.
*/
public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
}
public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.MessageSearchResult>>;
}
/**
* Get a single contact options by ID.
*/
public contactsGetContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('contacts_get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public contactsCreateContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('contacts_create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public contactsCreateChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('contacts_create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
}
public contactsBlock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_block', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsUnblock(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('contacts_unblock', [accountId, contactId] as RPC.Params)) as Promise<null>;
}
public contactsGetBlocked(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_blocked', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('contacts_get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public contactsGetContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('contacts_get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
}
public contactsGetContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
}
public changeContactName(accountId: T.U32, contactId: T.U32, name: string): Promise<null> {
return (this._transport.request('change_contact_name', [accountId, contactId, name] as RPC.Params)) as Promise<null>;
}
/**
* Get encryption info for a contact.
* Get a multi-line encryption info, containing your fingerprint and the
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
*/
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
}
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
*
* To validate an e-mail address independently of the contact database
* use check_email_validity().
*/
public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> {
return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>;
}
/**
* Returns all message IDs of the given types in a chat.
* Typically used to show a gallery.
*
* The list is already sorted and starts with the oldest message.
* Clients should not try to re-sort the list as this would be an expensive action
* and would result in inconsistencies between clients.
*
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
* from any chat of the currently used account.
*/
public chatGetMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
* Search next/previous message based on a given message and a list of types.
* Typically used to implement the "next" and "previous" buttons
* in a gallery or in a media player.
*
* 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
*/
public chatGetNeighboringMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return (this._transport.request('chat_get_neighboring_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
public exportBackup(accountId: T.U32, destination: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('export_backup', [accountId, destination, passphrase] as RPC.Params)) as Promise<null>;
}
public importBackup(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
return (this._transport.request('import_backup', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
}
/**
* Indicate that the network likely has come back.
* or just that the network conditions might have changed
*/
public maybeNetwork(): Promise<null> {
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
}
/**
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
* One of:
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
*
* We don't use exact values but ranges here so that we can split up
* states into multiple states in the future.
*
* Meant as a rough overview that can be shown
* e.g. in the title of the main screen.
*
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*/
public getConnectivity(accountId: T.U32): Promise<T.U32> {
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
}
/**
* Get an overview of the current connectivity, and possibly more statistics.
* Meant to give the user more insight about the current status than
* the basic connectivity info returned by get_connectivity(); show this
* e.g., if the user taps on said basic connectivity info.
*
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
*
* This comes as an HTML from the core so that we can easily improve it
* and the improvement instantly reaches all UIs.
*/
public getConnectivityHtml(accountId: T.U32): Promise<string> {
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
}
public getLocations(accountId: T.U32, chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
return (this._transport.request('get_locations', [accountId, chatId, contactId, timestampBegin, timestampEnd] as RPC.Params)) as Promise<(T.Location)[]>;
}
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
}
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
}
/**
* Get info from a webxdc message
*/
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
* Forward messages to another chat.
*
* All types of messages can be forwarded,
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
*
* Original sender, info-state and webxdc updates are not forwarded on purpose.
*/
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
}
public sendSticker(accountId: T.U32, chatId: T.U32, stickerPath: string): Promise<T.U32> {
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
}
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*/
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
}
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
}
/**
* Get draft for a chat, if any.
*/
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
}
public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
*/
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
}
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
}
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
}
}

View File

@@ -1,201 +0,0 @@
// Generated!
export enum C {
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
DC_CERTCK_AUTO = 0,
DC_CERTCK_STRICT = 1,
DC_CHAT_ID_ALLDONE_HINT = 7,
DC_CHAT_ID_ARCHIVED_LINK = 6,
DC_CHAT_ID_LAST_SPECIAL = 9,
DC_CHAT_ID_TRASH = 3,
DC_CHAT_TYPE_BROADCAST = 160,
DC_CHAT_TYPE_GROUP = 120,
DC_CHAT_TYPE_MAILINGLIST = 140,
DC_CHAT_TYPE_SINGLE = 100,
DC_CHAT_TYPE_UNDEFINED = 0,
DC_CONNECTIVITY_CONNECTED = 4000,
DC_CONNECTIVITY_CONNECTING = 2000,
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
DC_CONNECTIVITY_WORKING = 3000,
DC_CONTACT_ID_DEVICE = 5,
DC_CONTACT_ID_INFO = 2,
DC_CONTACT_ID_LAST_SPECIAL = 9,
DC_CONTACT_ID_SELF = 1,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
DC_GCL_ARCHIVED_ONLY = 1,
DC_GCL_FOR_FORWARDING = 8,
DC_GCL_NO_SPECIALS = 2,
DC_GCL_VERIFIED_ONLY = 1,
DC_GCM_ADDDAYMARKER = 1,
DC_GCM_INFO_ONLY = 2,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,
DC_MEDIA_QUALITY_WORSE = 1,
DC_MSG_ID_DAYMARKER = 9,
DC_MSG_ID_LAST_SPECIAL = 9,
DC_MSG_ID_MARKER1 = 1,
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
DC_SHOW_EMAILS_ALL = 2,
DC_SHOW_EMAILS_OFF = 0,
DC_SOCKET_AUTO = 0,
DC_SOCKET_PLAIN = 3,
DC_SOCKET_SSL = 1,
DC_SOCKET_STARTTLS = 2,
DC_STATE_IN_FRESH = 10,
DC_STATE_IN_NOTICED = 13,
DC_STATE_IN_SEEN = 16,
DC_STATE_OUT_DELIVERED = 26,
DC_STATE_OUT_DRAFT = 19,
DC_STATE_OUT_FAILED = 24,
DC_STATE_OUT_MDN_RCVD = 28,
DC_STATE_OUT_PENDING = 20,
DC_STATE_OUT_PREPARING = 18,
DC_STATE_UNDEFINED = 0,
DC_STR_AC_SETUP_MSG_BODY = 43,
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
DC_STR_ADD_MEMBER_BY_OTHER = 129,
DC_STR_ADD_MEMBER_BY_YOU = 128,
DC_STR_AEAP_ADDR_CHANGED = 122,
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,
DC_STR_CANNOT_LOGIN = 60,
DC_STR_CANTDECRYPT_MSG_BODY = 29,
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
DC_STR_DEVICE_MESSAGES = 68,
DC_STR_DEVICE_MESSAGES_HINT = 70,
DC_STR_DOWNLOAD_AVAILABILITY = 100,
DC_STR_DRAFT = 3,
DC_STR_E2E_AVAILABLE = 25,
DC_STR_E2E_PREFERRED = 34,
DC_STR_ENCRYPTEDMSG = 24,
DC_STR_ENCR_NONE = 28,
DC_STR_ENCR_TRANSP = 27,
DC_STR_EPHEMERAL_DAY = 79,
DC_STR_EPHEMERAL_DAYS = 95,
DC_STR_EPHEMERAL_DISABLED = 75,
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
DC_STR_EPHEMERAL_HOUR = 78,
DC_STR_EPHEMERAL_HOURS = 94,
DC_STR_EPHEMERAL_MINUTE = 77,
DC_STR_EPHEMERAL_MINUTES = 93,
DC_STR_EPHEMERAL_SECONDS = 76,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
DC_STR_EPHEMERAL_WEEK = 80,
DC_STR_EPHEMERAL_WEEKS = 96,
DC_STR_ERROR = 112,
DC_STR_ERROR_NO_NETWORK = 87,
DC_STR_FAILED_SENDING_TO = 74,
DC_STR_FILE = 12,
DC_STR_FINGERPRINTS = 30,
DC_STR_FORWARDED = 97,
DC_STR_GIF = 23,
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
DC_STR_GROUP_LEFT_BY_OTHER = 133,
DC_STR_GROUP_LEFT_BY_YOU = 132,
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
DC_STR_MSGDELMEMBER = 18,
DC_STR_MSGGROUPLEFT = 19,
DC_STR_MSGGRPIMGCHANGED = 16,
DC_STR_MSGGRPIMGDELETED = 33,
DC_STR_MSGGRPNAME = 15,
DC_STR_MSGLOCATIONDISABLED = 65,
DC_STR_MSGLOCATIONENABLED = 64,
DC_STR_NOMESSAGES = 1,
DC_STR_NOT_CONNECTED = 121,
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
DC_STR_ONE_MOMENT = 106,
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,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
DC_STR_SELF = 2,
DC_STR_SELF_DELETED_MSG_BODY = 91,
DC_STR_SENDING = 110,
DC_STR_SERVER_TURNED_OFF = 92,
DC_STR_SETUP_CONTACT_QR_DESC = 119,
DC_STR_STICKER = 67,
DC_STR_STORAGE_ON_DOMAIN = 105,
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
DC_STR_SYNC_MSG_BODY = 102,
DC_STR_SYNC_MSG_SUBJECT = 101,
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
DC_STR_UPDATING = 109,
DC_STR_VIDEO = 10,
DC_STR_VIDEOCHAT_INVITATION = 82,
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
DC_VIDEOCHATTYPE_JITSI = 2,
DC_VIDEOCHATTYPE_UNKNOWN = 0,
}

View File

@@ -1,206 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Usize=number;
export type Event=(({
/**
* The library-user may write an informational string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Info";}&{"msg":string;})|({
/**
* Emitted when SMTP connection is established and login was successful.
*/
"type":"SmtpConnected";}&{"msg":string;})|({
/**
* Emitted when IMAP connection is established and login was successful.
*/
"type":"ImapConnected";}&{"msg":string;})|({
/**
* Emitted when a message was successfully sent to the SMTP server.
*/
"type":"SmtpMessageSent";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been marked as deleted
*/
"type":"ImapMessageDeleted";}&{"msg":string;})|({
/**
* Emitted when an IMAP message has been moved
*/
"type":"ImapMessageMoved";}&{"msg":string;})|({
/**
* Emitted when an new file in the $BLOBDIR was created
*/
"type":"NewBlobFile";}&{"file":string;})|({
/**
* Emitted when an file in the $BLOBDIR was deleted
*/
"type":"DeletedBlobFile";}&{"file":string;})|({
/**
* The library-user should write a warning string to the log.
*
* This event should *not* be reported to the end-user using a popup or something like
* that.
*/
"type":"Warning";}&{"msg":string;})|({
/**
* The library-user should report an error to the end-user.
*
* As most things are asynchronous, things may go wrong at any time and the user
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
*
* However, for ongoing processes (eg. configure())
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
* it might be better to delay showing these events until the function has really
* failed (returned false). It should be sufficient to report only the *last* error
* in a messasge box then.
*/
"type":"Error";}&{"msg":string;})|({
/**
* An action cannot be performed because the user is not in the group.
* Reported eg. after a call to
* setChatName(), setChatProfileImage(),
* addContactToChat(), removeContactFromChat(),
* and messages sending functions.
*/
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
/**
* Messages or chats changed. One or more messages or chats changed for various
* reasons in the database:
* - Messages sent, received or removed
* - Chats created, deleted or archived
* - A draft has been set
*
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
*/
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
/**
* Reactions for the message changed.
*/
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
/**
* There is a fresh message. Typically, the user will show an notification
* when receiving this message.
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*/
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
/**
* Messages were seen or noticed.
* chat id is always set.
*/
"type":"MsgsNoticed";}&{"chatId":U32;})|({
/**
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
* DC_STATE_OUT_DELIVERED, see `Message.state`.
*/
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_FAILED, see `Message.state`.
*/
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
/**
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
*/
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See setChatName(), setChatProfileImage(), addContactToChat()
* and removeContactFromChat().
*
* This event does not include ephemeral timer modification, which
* is a separate event.
*/
"type":"ChatModified";}&{"chatId":U32;})|({
/**
* Chat ephemeral timer changed.
*/
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
/**
* Contact(s) created, renamed, blocked or deleted.
*
* @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
*/
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
/**
* Location of one or more contact has changed.
*
* @param data1 (u32) contact_id of the contact for which the location has changed.
* If the locations of several contacts have been changed,
* this parameter is set to `None`.
*/
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
/**
* Inform about the configuration progress started by configure().
*/
"type":"ConfigureProgress";}&{
/**
* Progress.
*
* 0=error, 1-999=progress in permille, 1000=success and done
*/
"progress":Usize;
/**
* Progress comment or error, something to display to the user.
*/
"comment":(string|null);})|({
/**
* Inform about the import/export progress started by imex().
*
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
* @param data2 0
*/
"type":"ImexProgress";}&{"progress":Usize;})|({
/**
* A file has been exported. A file has been written by imex().
* This event may be sent multiple times by a single call to imex().
*
* A typical purpose for a handler of this event may be to make the file public to some system
* services.
*
* @param data2 0
*/
"type":"ImexFileWritten";}&{"path":string;})|({
/**
* Progress information of a secure-join handshake from the view of the inviter
* (Alice, the person who shows the QR code).
*
* These events are typically sent after a joiner has scanned the QR code
* generated by getChatSecurejoinQrCodeSvg().
*
* @param data1 (int) ID of the contact that wants to join.
* @param data2 (int) Progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
* 1000=Protocol finished for this contact.
*/
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
/**
* Progress information of a secure-join handshake from the view of the joiner
* (Bob, the person who scans the QR code).
* The events are typically sent while secureJoin(), which
* may take some time, is executed.
* @param data1 (int) ID of the inviting contact.
* @param data2 (int) Progress as:
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
* (Bob has verified alice and waits until Alice does the same for him)
*/
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
/**
* The connectivity to the server changed.
* This means that you should refresh the connectivity view
* and possibly the connectivtiy HTML; see getConnectivity() and
* getConnectivityHtml() for details.
*/
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
/**
* Inform that a message containing a webxdc instance has been deleted
*/
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));

View File

@@ -1,10 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type JSONValue=(null|boolean|number|string|(JSONValue)[]|{[key:string]:JSONValue;});
export type Params=((JSONValue)[]|Record<string,JSONValue>);
export type U32=number;
export type Request={"jsonrpc":"2.0";"method":string;"params"?:Params;"id"?:U32;};
export type I32=number;
export type Error={"code":I32;"message":string;"data"?:JSONValue;};
export type Response={"jsonrpc":"2.0";"id":(U32|null);"result"?:JSONValue;"error"?:Error;};
export type Message=(Request|Response);

View File

@@ -1,180 +0,0 @@
// AUTO-GENERATED by typescript-type-def
export type U32=number;
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
export type U64=number;
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
export type Usize=number;
export type I64=number;
export type ChatListEntry=[U32,U32];
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
/**
* true when chat is a broadcastlist
*/
"isBroadcast":boolean;
/**
* contact id if this is a dm chat (for view profile entry in context menu)
*/
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
/**
* the contact's last seen timestamp
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":string;};
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
export type BasicChat=
/**
* cheaper version of fullchat, omits:
* - contacts
* - contact_ids
* - fresh_message_counter
* - ephemeral_timer
* - self_in_group
* - was_seen_recently
* - can_send
*
* used when you only need the basic metadata of a chat like type, name, profile picture
*/
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
/**
* Day marker, separating messages that correspond to different
* days according to local time.
*/
"kind":"dayMarker";}&{
/**
* Marker timestamp, for day markers, in unix milliseconds
*/
"timestamp":I64;}));
export type Viewtype=("Unknown"|
/**
* Text message.
*/
"Text"|
/**
* Image message.
* If the image is an animated GIF, the type `Viewtype.Gif` should be used.
*/
"Image"|
/**
* Animated GIF message.
*/
"Gif"|
/**
* Message containing a sticker, similar to image.
* If possible, the ui should display the image without borders in a transparent way.
* A click on a sticker will offer to install the sticker set in some future.
*/
"Sticker"|
/**
* Message containing an Audio file.
*/
"Audio"|
/**
* A voice message that was directly recorded by the user.
* For all other audio messages, the type `Viewtype.Audio` should be used.
*/
"Voice"|
/**
* Video messages.
*/
"Video"|
/**
* Message containing any file, eg. a PDF.
*/
"File"|
/**
* Message is an invitation to a videochat.
*/
"VideochatInvitation"|
/**
* Message is an webxdc instance.
*/
"Webxdc");
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
export type I32=number;
export type WebxdcMessageInfo={
/**
* The name of the app.
*
* Defaults to the filename if not set in the manifest.
*/
"name":string;
/**
* App icon file name.
* Defaults to an standard icon if nothing is set in the manifest.
*
* To get the file, use dc_msg_get_webxdc_blob(). (not yet in jsonrpc, use rust api or cffi for it)
*
* App icons should should be square,
* the implementations will add round corners etc. as needed.
*/
"icon":string;
/**
* if the Webxdc represents a document, then this is the name of the document
*/
"document":(string|null);
/**
* short string describing the state of the app,
* sth. as "2 votes", "Highscore: 123",
* can be changed by the apps
*/
"summary":(string|null);
/**
* URL where the source code of the Webxdc and other information can be found;
* defaults to an empty string.
* Implementations may offer an menu or a button to open this URL.
*/
"sourceCodeUrl":(string|null);
/**
* True if full internet access should be granted to the app.
*/
"internetAccess":boolean;};
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
/**
* Structure representing all reactions to a particular message.
*/
export type Reactions=
/**
* Structure representing all reactions to a particular message.
*/
{
/**
* Map from a contact to it's reaction to message.
*/
"reactionsByContact":Record<U32,(string)[]>;
/**
* Unique reactions and their count
*/
"reactions":Record<string,U32>;};
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
/**
* also known as summary_text1
*/
"summaryPrefix":(string|null);
/**
* also known as summary_text2
*/
"summaryText":string;};
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type F64=number;
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];

View File

@@ -1,8 +1,8 @@
{
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
"yerpc": "^0.3.3"
},
"devDependencies": {
@@ -41,12 +41,12 @@
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
"test:prepare": "cargo build --package deltachat-rpc-server --bin deltachat-rpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
"test:run": "mocha dist/test",
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test"
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.98.0"
"version": "1.103.0"
}

View File

@@ -38,6 +38,7 @@ const constants = data
key.startsWith("DC_IMEX_") ||
key.startsWith("DC_CHAT_VISIBILITY") ||
key.startsWith("DC_DOWNLOAD") ||
key.startsWith("DC_INFO_") ||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
key.startsWith("DC_QR_")
);

View File

@@ -3,7 +3,7 @@ import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "tiny-emitter";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type DCWireEvent<T extends Event> = {
event: T;
@@ -28,14 +28,14 @@ type ContextEvents = { ALL: (event: Event) => void } & {
};
export type DcEvent = Event;
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: TinyEmitter<ContextEvents>[] = [];
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
@@ -43,12 +43,14 @@ export class BaseDeltaChat<
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event);
@@ -92,3 +94,34 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
close() {}
constructor(input: any, output: any) {
const transport = new StdioTransport(input, output);
super(transport);
}
}
export class StdioTransport extends BaseTransport {
constructor(public input: any, public output: any) {
super();
var buffer = "";
this.output.on("data", (data: any) => {
buffer += data.toString();
while (buffer.includes("\n")) {
const n = buffer.indexOf("\n");
const line = buffer.substring(0, n);
const message = JSON.parse(line);
this._onmessage(message);
buffer = buffer.substring(n + 1);
}
});
}
_send(message: RPC.Message): void {
const serialized = JSON.stringify(message);
this.input.write(serialized + "\n");
}
}

View File

@@ -2,7 +2,7 @@ import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { DeltaChat } from "../deltachat.js";
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import {
RpcServerHandle,
@@ -15,9 +15,7 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
// make sure server is up by the time we continue
await new Promise((res) => setTimeout(res, 100));
dc = new DeltaChat(serverHandle.url)
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout)
// dc.on("ALL", (event) => {
//console.log("event", event);
// });
@@ -84,21 +82,21 @@ describe("basic tests", () => {
accountId = await dc.rpc.addAccount();
});
it("should block and unblock contact", async function () {
const contactId = await dc.rpc.contactsCreateContact(
const contactId = await dc.rpc.createContact(
accountId,
"example@delta.chat",
null
);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
await dc.rpc.contactsBlock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
await dc.rpc.blockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.true;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
await dc.rpc.contactsUnblock(accountId, contactId);
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(1);
await dc.rpc.unblockContact(accountId, contactId);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(0);
});
});

View File

@@ -1,5 +1,5 @@
import { assert, expect } from "chai";
import { DeltaChat, DcEvent } from "../deltachat.js";
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000;
@@ -27,7 +27,7 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.url);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);
@@ -80,21 +80,18 @@ describe("online tests", function () {
}
this.timeout(15000);
const contactId = await dc.rpc.contactsCreateContact(
const contactId = await dc.rpc.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(
accountId1,
contactId
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
const { chatId: chatIdOnAccountB } = await eventPromise;
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
const messageList = await dc.rpc.getMessageIds(
@@ -104,7 +101,7 @@ describe("online tests", function () {
);
expect(messageList).have.length(1);
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
expect(message.text).equal("Hello");
});
@@ -115,20 +112,17 @@ describe("online tests", function () {
this.timeout(10000);
// send message from A to B
const contactId = await dc.rpc.contactsCreateContact(
const contactId = await dc.rpc.createContact(
accountId1,
account2.email,
null
);
const chatId = await dc.rpc.contactsCreateChatByContactId(
accountId1,
contactId
);
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
const eventPromise = Promise.race([
waitForEvent(dc, "MsgsChanged", accountId2),
waitForEvent(dc, "IncomingMsg", accountId2),
]);
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
// wait for message from A
console.log("wait for message from A");
@@ -141,7 +135,7 @@ describe("online tests", function () {
chatIdOnAccountB,
0
);
const message = await dc.rpc.messageGetMessage(
const message = await dc.rpc.getMessage(
accountId2,
messageList.reverse()[0]
);
@@ -151,14 +145,14 @@ describe("online tests", function () {
waitForEvent(dc, "MsgsChanged", accountId1),
waitForEvent(dc, "IncomingMsg", accountId1),
]);
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arives at A and if it is encrypted
await eventPromise2;
const messageId = (
await dc.rpc.getMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
const message2 = await dc.rpc.getMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");
expect(message2.showPadlock).equal(true);
});

View File

@@ -1,38 +1,38 @@
import { tmpdir } from "os";
import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises";
import { existsSync } from "fs";
import { spawn, exec } from "child_process";
import fetch from "node-fetch";
export const RPC_SERVER_PORT = 20808;
import { Readable, Writable } from "node:stream";
export type RpcServerHandle = {
url: string,
close: () => Promise<void>
}
stdin: Writable;
stdout: Readable;
close: () => Promise<void>;
};
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
export async function startServer(): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
console.log('using server binary: ' + pathToServerBinary);
if (!existsSync(pathToServerBinary)) {
throw new Error(
"server executable does not exist, you need to build it first" +
"\nserver executable not found at " +
pathToServerBinary
);
}
const pathToServerBinary = resolve(
join(await getTargetDir(), "debug/deltachat-rpc-server")
);
const server = spawn(pathToServerBinary, {
cwd: tmpDir,
env: {
RUST_LOG: process.env.RUST_LOG || "info",
DC_PORT: '' + port
RUST_MIN_STACK: "8388608",
},
});
server.on("error", (err) => {
throw new Error(
"Failed to start server executable " +
pathToServerBinary +
", make sure you built it first."
);
});
let shouldClose = false;
server.on("exit", () => {
@@ -43,12 +43,10 @@ export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcSe
});
server.stderr.pipe(process.stderr);
server.stdout.pipe(process.stdout)
const url = `ws://localhost:${port}/ws`
return {
url,
stdin: server.stdin,
stdout: server.stdout,
close: async () => {
shouldClose = true;
if (!server.kill()) {

View File

@@ -0,0 +1,41 @@
# Delta Chat RPC python client
RPC client connects to standalone Delta Chat RPC server `deltachat-rpc-server`
and provides asynchronous interface to it.
## Getting started
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
Install it anywhere in your `PATH`.
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
## Using in REPL
Setup a development environment:
```
$ 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()
```

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""Minimal echo bot example.
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):
print(event)
@hooks.on(events.NewMessage)
async def echo(msg):
await msg.chat.send_text(msg.text)
if __name__ == "__main__":
asyncio.run(run_bot_cli(hooks))

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Advanced echo bot example.
it will echo back any message that has non-empty text and also supports the /help command.
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
hooks = events.HookCollection()
@hooks.on(events.RawEvent)
async def log_event(event):
if event.type == EventType.INFO:
logging.info(event.msg)
elif event.type == EventType.WARNING:
logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR))
async def log_error(event):
logging.error(event.msg)
@hooks.on(events.NewMessage(r".+", func=lambda msg: not msg.text.startswith("/")))
async def echo(msg):
await msg.chat.send_text(msg.text)
@hooks.on(events.NewMessage(r"/help"))
async def help_command(msg):
await msg.chat.send_text("Send me any text message and I will echo it back")
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await 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()
bot = Bot(account, hooks)
if not await bot.is_configured():
asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
await bot.run_forever()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Example echo bot without using hooks
"""
import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = await 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()
await account.set_config("bot", "1")
if not await 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()
logging.info("Configured")
else:
logging.info("Account is already configured")
await deltachat.start_io()
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()
# Process old messages.
await process_messages()
while True:
event = await account.wait_for_event()
if event["type"] == EventType.INFO:
logging.info("%s", event["msg"])
elif event["type"] == EventType.WARNING:
logging.warning("%s", event["msg"])
elif event["type"] == EventType.ERROR:
logging.error("%s", event["msg"])
elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message")
await process_messages()
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())

View File

@@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
dependencies = [
"aiohttp",
"aiodns"
]
dynamic = [
"version"
]
[tool.setuptools]
# We declare the package not-zip-safe so that our type hints are also available
# when checking client code that uses our (installed) package.
# Ref:
# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561
zip-safe = false
[tool.setuptools.package-data]
deltachat_rpc_client = [
"py.typed"
]
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"

View File

@@ -0,0 +1,10 @@
"""Delta Chat asynchronous high-level API"""
from .account import Account
from .chat import Chat
from .client import Bot, Client
from .const import EventType
from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
from .rpc import Rpc
from .utils import AttrDict, run_bot_cli, run_client_cli

View File

@@ -0,0 +1,265 @@
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from .chat import Chat
from .const import ChatlistFlag, ContactFlag, SpecialContactId
from .contact import Contact
from .message import Message
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .deltachat import DeltaChat
class Account:
"""Delta Chat account."""
def __init__(self, manager: "DeltaChat", account_id: int) -> None:
self.manager = manager
self.id = account_id
@property
def _rpc(self) -> Rpc:
return self.manager.rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Account):
return False
return self.id == other.id and self.manager == other.manager
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Account id={self.id}>"
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(await self._rpc.wait_for_event(self.id))
async def remove(self) -> None:
"""Remove the account."""
await self._rpc.remove_account(self.id)
async def start_io(self) -> None:
"""Start the account I/O."""
await self._rpc.start_io(self.id)
async def stop_io(self) -> None:
"""Stop the account I/O."""
await self._rpc.stop_io(self.id)
async def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
return AttrDict(await self._rpc.get_info(self.id))
async def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
return await self._rpc.get_account_file_size(self.id)
async def is_configured(self) -> bool:
"""Return True if this account is configured."""
return await self._rpc.is_configured(self.id)
async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value."""
await self._rpc.set_config(self.id, key, value)
async def get_config(self, key: str) -> Optional[str]:
"""Get configuration value."""
return await self._rpc.get_config(self.id, key)
async def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
await self.set_config(key, value)
async 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)
async def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
return await self.get_config("selfavatar")
async def configure(self) -> None:
"""Configure an account."""
await self._rpc.configure(self.id)
async 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
underlying contact id. If there already is a Contact
with that e-mail address, it is unblocked and its display
name is updated if specified.
:param obj: email-address or contact id.
:param name: (optional) display name for this contact.
"""
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))
async 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]:
"""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)
return contact_id and Contact(self, contact_id)
async 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)
return [
AttrDict(contact=Contact(self, contact["id"]), **contact)
for contact in contacts
]
async def get_contacts(
self,
query: Optional[str] = None,
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[List[Contact], List[AttrDict]]:
"""Get a filtered list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param with_self: if True the self-contact is also included if it matches the query.
:param only_verified: if True only return verified contacts.
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
"""
flags = 0
if verified_only:
flags |= ContactFlag.VERIFIED_ONLY
if with_self:
flags |= ContactFlag.ADD_SELF
if snapshot:
contacts = await 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)
return [Contact(self, contact_id) for contact_id in contacts]
@property
def self_contact(self) -> Contact:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
async def get_chatlist(
self,
query: Optional[str] = None,
contact: Optional[Contact] = None,
archived_only: bool = False,
for_forwarding: bool = False,
no_specials: bool = False,
alldone_hint: bool = False,
snapshot: bool = False,
) -> Union[List[Chat], List[AttrDict]]:
"""Return list of chats.
:param query: if a string is specified only chats matching this query are returned.
:param contact: if a contact is specified only chats including this contact are returned.
:param archived_only: if True only archived chats are returned.
:param for_forwarding: if True the chat list is sorted with "Saved messages" at the top
and withot "Device chat" and contact requests.
:param no_specials: if True archive link is not added to the list.
:param alldone_hint: if True the "all done hint" special chat will be added to the list
as needed.
:param snapshot: If True return a list of chat snapshots instead of Chat instances.
"""
flags = 0
if archived_only:
flags |= ChatlistFlag.ARCHIVED_ONLY
if for_forwarding:
flags |= ChatlistFlag.FOR_FORWARDING
if no_specials:
flags |= ChatlistFlag.NO_SPECIALS
if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = await self._rpc.get_chatlist_entries(
self.id, flags, query, contact and contact.id
)
if not snapshot:
return [Chat(self, entry[0]) for entry in entries]
items = await 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:
"""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))
async 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:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
async 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)
async 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:
"""Mark the given set of messages as seen."""
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
async 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])
async 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)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]

View File

@@ -0,0 +1,263 @@
import calendar
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from .const import ChatVisibility
from .contact import Contact
from .message import Message
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .account import Account
class Chat:
"""Chat object which manages members and through which you can send and retrieve messages."""
def __init__(self, account: "Account", chat_id: int) -> None:
self.account = account
self.id = chat_id
@property
def _rpc(self) -> Rpc:
return self.account._rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Chat):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Chat id={self.id} account={self.account.id}>"
async def delete(self) -> None:
"""Delete this chat and all its messages.
Note:
- 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)
async def block(self) -> None:
"""Block this chat."""
await self._rpc.block_chat(self.account.id, self.id)
async def accept(self) -> None:
"""Accept this contact request chat."""
await self._rpc.accept_chat(self.account.id, self.id)
async def leave(self) -> None:
"""Leave this chat."""
await self._rpc.leave_group(self.account.id, self.id)
async 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] = dict(Until=duration)
else:
dur = "Forever"
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
async def unmute(self) -> None:
"""Unmute this chat."""
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
async def pin(self) -> None:
"""Pin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.PINNED
)
async def unpin(self) -> None:
"""Unpin this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
async def archive(self) -> None:
"""Archive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.ARCHIVED
)
async def unarchive(self) -> None:
"""Unarchive this chat."""
await self._rpc.set_chat_visibility(
self.account.id, self.id, ChatVisibility.NORMAL
)
async def set_name(self, name: str) -> None:
"""Set name of this chat."""
await self._rpc.set_chat_name(self.account.id, self.id, name)
async 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)
async 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)
async 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)
async 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)
return AttrDict(chat=self, **info)
async 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)
return AttrDict(chat=self, **info)
async def send_message(
self,
text: Optional[str] = None,
file: Optional[str] = None,
location: Optional[Tuple[float, float]] = None,
quoted_msg: Optional[Union[int, Message]] = None,
) -> Message:
"""Send a message and return the resulting Message instance."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
msg_id, _ = await self._rpc.misc_send_msg(
self.account.id, self.id, text, file, location, quoted_msg
)
return Message(self.account, msg_id)
async 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)
return Message(self.account, msg_id)
async 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)
return Message(self.account, msg_id)
async 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)
return Message(self.account, msg_id)
async 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)
async def set_draft(
self,
text: Optional[str] = None,
file: Optional[str] = None,
quoted_msg: Optional[int] = None,
) -> None:
"""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)
async def remove_draft(self) -> None:
"""Remove draft message."""
await self._rpc.remove_draft(self.account.id, self.id)
async def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
snapshot = await self._rpc.get_draft(self.account.id, self.id)
if not snapshot:
return None
snapshot = AttrDict(snapshot)
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
async def get_messages(self, flags: int = 0) -> List[Message]:
"""get the list of messages in this chat."""
msgs = await self._rpc.get_message_ids(self.account.id, self.id, flags)
return [Message(self.account, msg_id) for msg_id in msgs]
async 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)
async def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
await self._rpc.marknoticed_chat(self.account.id, self.id)
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
cnt = cnt.id
await self._rpc.add_contact_to_chat(self.account.id, self.id, cnt)
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
cnt = cnt.id
await self._rpc.remove_contact_from_chat(self.account.id, self.id, cnt)
async 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)
return [Contact(self.account, contact_id) for contact_id in contacts]
async 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)
async def remove_image(self) -> None:
"""Remove profile image of this chat."""
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
async def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional[datetime] = None,
timestamp_to: Optional[datetime] = None,
) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan."""
time_from = (
calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
)
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
)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:
loc = AttrDict(loc)
loc["chat"] = self
loc["contact"] = contacts.setdefault(
loc.contact_id, Contact(self.account, loc.contact_id)
)
loc["message"] = Message(self.account, loc.msg_id)
locations.append(loc)
return locations

View File

@@ -0,0 +1,100 @@
"""Event loop implementations offering high level event handling/hooking."""
import logging
from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union
from deltachat_rpc_client.account import Account
from .const import EventType
from .events import EventFilter, NewInfoMessage, NewMessage, RawEvent
from .utils import AttrDict
class Client:
"""Simple Delta Chat client that listen to events of a single account."""
def __init__(
self,
account: Account,
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self.account = account
self.logger = logger or logging
self._hooks: Dict[type, Set[tuple]] = {}
self.add_hooks(hooks or [])
def add_hooks(
self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]
) -> None:
for hook, event in hooks:
self.add_hook(hook, event)
def add_hook(
self, hook: Callable, event: Union[type, EventFilter] = RawEvent
) -> None:
"""Register hook for the given event filter."""
if isinstance(event, type):
event = event()
assert isinstance(event, EventFilter)
self._hooks.setdefault(type(event), set()).add((hook, event))
def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None:
"""Unregister hook from the given event filter."""
if isinstance(event, type):
event = event()
self._hooks.get(type(event), set()).remove((hook, event))
async def is_configured(self) -> bool:
return await 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)
for key, value in kwargs.items():
await self.account.set_config(key, value)
await self.account.configure()
self.logger.debug("Account configured")
async def run_forever(self) -> None:
self.logger.debug("Listening to incoming events...")
if await self.is_configured():
await self.account.start_io()
await self._process_messages() # Process old messages.
while True:
event = await self.account.wait_for_event()
event["type"] = EventType(event.type)
event["account"] = self.account
await self._on_event(event)
if event.type == EventType.INCOMING_MSG:
await self._process_messages()
async 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):
try:
await hook(event)
except Exception as ex:
self.logger.exception(ex)
def _should_process_messages(self) -> bool:
return any(issubclass(filter_type, NewMessage) for filter_type in self._hooks)
async def _process_messages(self) -> None:
if self._should_process_messages():
for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if snapshot.is_info:
await self._on_event(snapshot, NewInfoMessage)
else:
await self._on_event(snapshot, NewMessage)
await 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:
kwargs.setdefault("bot", "1")
await super().configure(email, password, **kwargs)

View File

@@ -0,0 +1,120 @@
from enum import Enum, IntEnum
class ContactFlag(IntEnum):
VERIFIED_ONLY = 0x01
ADD_SELF = 0x02
class ChatlistFlag(IntEnum):
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
FOR_FORWARDING = 0x08
class SpecialContactId(IntEnum):
SELF = 1
INFO = 2 # centered messages as "member added", used in all chats
DEVICE = 5 # messages "update info" in the device-chat
LAST_SPECIAL = 9
class EventType(str, Enum):
"""Core event types"""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
IMAP_CONNECTED = "ImapConnected"
SMTP_MESSAGE_SENT = "SmtpMessageSent"
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
NEW_BLOB_FILE = "NewBlobFile"
DELETED_BLOB_FILE = "DeletedBlobFile"
WARNING = "Warning"
ERROR = "Error"
ERROR_SELF_NOT_IN_GROUP = "ErrorSelfNotInGroup"
MSGS_CHANGED = "MsgsChanged"
REACTIONS_CHANGED = "ReactionsChanged"
INCOMING_MSG = "IncomingMsg"
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
MSGS_NOTICED = "MsgsNoticed"
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"
LOCATION_CHANGED = "LocationChanged"
CONFIGURE_PROGRESS = "ConfigureProgress"
IMEX_PROGRESS = "ImexProgress"
IMEX_FILE_WRITTEN = "ImexFileWritten"
SECUREJOIN_INVITER_PROGRESS = "SecurejoinInviterProgress"
SECUREJOIN_JOINER_PROGRESS = "SecurejoinJoinerProgress"
CONNECTIVITY_CHANGED = "ConnectivityChanged"
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
class ChatType(IntEnum):
"""Chat types"""
UNDEFINED = 0
SINGLE = 100
GROUP = 120
MAILINGLIST = 140
BROADCAST = 160
class ChatVisibility(str, Enum):
"""Chat visibility types"""
NORMAL = "Normal"
ARCHIVED = "Archived"
PINNED = "Pinned"
class DownloadState(str, Enum):
"""Message download state"""
DONE = "Done"
AVAILABLE = "Available"
FAILURE = "Failure"
IN_PROGRESS = "InProgress"
class ViewType(str, Enum):
"""Message view type."""
UNKNOWN = "Unknown"
TEXT = "Text"
IMAGE = "Image"
GIF = "Gif"
STICKER = "Sticker"
AUDIO = "Audio"
VOICE = "Voice"
VIDEO = "Video"
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
class SystemMessageType(str, Enum):
"""System message type."""
UNKNOWN = "Unknown"
GROUP_NAME_CHANGED = "GroupNameChanged"
GROUP_IMAGE_CHANGED = "GroupImageChanged"
MEMBER_ADDED_TO_GROUP = "MemberAddedToGroup"
MEMBER_REMOVED_FROM_GROUP = "MemberRemovedFromGroup"
AUTOCRYPT_SETUP_MESSAGE = "AutocryptSetupMessage"
SECUREJOIN_MESSAGE = "SecurejoinMessage"
LOCATION_STREAMING_ENABLED = "LocationStreamingEnabled"
LOCATION_ONLY = "LocationOnly"
CHAT_PROTECTION_ENABLED = "ChatProtectionEnabled"
CHAT_PROTECTION_DISABLED = "ChatProtectionDisabled"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"

View File

@@ -0,0 +1,72 @@
from typing import TYPE_CHECKING
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .account import Account
from .chat import Chat
class Contact:
"""
Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
def __init__(self, account: "Account", contact_id: int) -> None:
self.account = account
self.id = contact_id
def __eq__(self, other) -> bool:
if not isinstance(other, Contact):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Contact id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:
return self.account._rpc
async def block(self) -> None:
"""Block contact."""
await self._rpc.block_contact(self.account.id, self.id)
async def unblock(self) -> None:
"""Unblock contact."""
await self._rpc.unblock_contact(self.account.id, self.id)
async def delete(self) -> None:
"""Delete contact."""
await self._rpc.delete_contact(self.account.id, self.id)
async 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)
async 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)
async 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["contact"] = self
return snapshot
async 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),
)

View File

@@ -0,0 +1,47 @@
from typing import Dict, List
from .account import Account
from .rpc import Rpc
from .utils import AttrDict
class DeltaChat:
"""
Delta Chat accounts manager.
This is the root of the object oriented API.
"""
def __init__(self, rpc: Rpc) -> None:
self.rpc = rpc
async def add_account(self) -> Account:
"""Create a new account database."""
account_id = await self.rpc.add_account()
return Account(self, account_id)
async def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts."""
account_ids = await self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
async def start_io(self) -> None:
"""Start the I/O of all accounts."""
await self.rpc.start_io_for_all_accounts()
async def stop_io(self) -> None:
"""Stop the I/O of all accounts."""
await self.rpc.stop_io_for_all_accounts()
async 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()
async def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system."""
return AttrDict(await self.rpc.get_system_info())
async def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings."""
await self.rpc.set_stock_strings(translations)

View File

@@ -0,0 +1,161 @@
"""High-level classes for event processing and filtering."""
import inspect
import re
from abc import ABC, abstractmethod
from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from .const import EventType
from .utils import AttrDict
def _tuple_of(obj, type_: type) -> tuple:
if not obj:
return tuple()
if isinstance(obj, type_):
obj = (obj,)
if not all(isinstance(elem, type_) for elem in obj):
raise TypeError()
return tuple(obj)
class EventFilter(ABC):
"""The base event filter.
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
def __init__(self, func: Optional[Callable] = None):
self.func = func
@abstractmethod
def __hash__(self) -> int:
"""Object's unique hash"""
@abstractmethod
def __eq__(self, other) -> bool:
"""Return True if two event filters are equal."""
def __ne__(self, other):
return not self.__eq__(other)
async 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
@abstractmethod
async def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
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
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
def __init__(
self, types: Union[None, EventType, Iterable[EventType]] = None, **kwargs
):
super().__init__(**kwargs)
try:
self.types = _tuple_of(types, EventType)
except TypeError as err:
raise TypeError(f"Invalid event type given: {types}") from err
def __hash__(self) -> int:
return hash((self.types, self.func))
def __eq__(self, other) -> bool:
if isinstance(other, RawEvent):
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:
return False
return await self._call_func(event)
class NewMessage(EventFilter):
"""Matches whenever a new message arrives.
Warning: registering a handler for this event or any subclass will cause the messages
to be marked as read. Its usage is mainly intended for bots.
"""
def __init__(
self,
pattern: Union[
None,
str,
Callable[[str], bool],
re.Pattern,
] = None,
func: Optional[Callable[[AttrDict], bool]] = None,
) -> None:
super().__init__(func=func)
if isinstance(pattern, str):
pattern = re.compile(pattern)
if isinstance(pattern, re.Pattern):
self.pattern: Optional[Callable] = pattern.match
elif not pattern or callable(pattern):
self.pattern = pattern
else:
raise TypeError("Invalid pattern type")
def __hash__(self) -> int:
return hash((self.pattern, self.func))
def __eq__(self, other) -> bool:
if type(other) is self.__class__: # noqa
return (self.pattern, self.func) == (other.pattern, other.func)
return False
async def filter(self, event: AttrDict) -> bool:
if self.pattern:
match = self.pattern(event.text)
if inspect.isawaitable(match):
match = await match
if not match:
return False
return await super()._call_func(event)
class NewInfoMessage(NewMessage):
"""Matches whenever a new info/system message arrives."""
class HookCollection:
"""
Helper class to collect event hooks that can later be added to a Delta Chat client.
"""
def __init__(self) -> None:
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
return iter(self._hooks)
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
"""Register decorated function as listener for the given event."""
if isinstance(event, type):
event = event()
assert isinstance(event, EventFilter), "Invalid event filter"
def _decorator(func) -> Callable:
self._hooks.add((func, event))
return func
return _decorator

View File

@@ -0,0 +1,49 @@
from typing import TYPE_CHECKING
from .contact import Contact
from .rpc import Rpc
from .utils import AttrDict
if TYPE_CHECKING:
from .account import Account
class Message:
"""Delta Chat Message object."""
def __init__(self, account: "Account", msg_id: int) -> None:
self.account = account
self.id = msg_id
def __eq__(self, other) -> bool:
if not isinstance(other, Message):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Message id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> Rpc:
return self.account._rpc
async def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
await self._rpc.send_reaction(self.account.id, self.id, reaction)
async 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["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
async def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -0,0 +1 @@
# PEP 561 marker file. See https://peps.python.org/pep-0561/

View File

@@ -0,0 +1,64 @@
import json
import os
from typing import AsyncGenerator, List
import aiohttp
import pytest_asyncio
from .account import Account
from .client import Bot
from .deltachat import DeltaChat
from .rpc import Rpc
async 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"
async with aiohttp.ClientSession() as session:
async with session.post(url) as response:
return json.loads(await response.text())
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
async def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account()
async def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account())
async def new_configured_account(self) -> Account:
credentials = await get_temp_credentials()
account = await self.get_unconfigured_account()
assert not await account.is_configured()
await account.set_config("addr", credentials["email"])
await account.set_config("mail_pw", credentials["password"])
await account.configure()
assert await 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"])
return bot
async def get_online_accounts(self, num: int) -> List[Account]:
accounts = [await self.new_configured_account() for _ in range(num)]
for account in accounts:
await account.start_io()
return accounts
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
async with rpc_server:
yield rpc_server
@pytest_asyncio.fixture
async def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc))

View File

@@ -0,0 +1,103 @@
import asyncio
import json
import os
from typing import Any, Dict, Optional
class JsonRpcError(Exception):
pass
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
"DC_ACCOUNTS_PATH": os.path.abspath(
os.path.expanduser(str(accounts_dir))
),
}
self._kwargs = kwargs
self.process: asyncio.subprocess.Process
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.reader_task: asyncio.Task
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
)
self.id = 0
self.event_queues = {}
self.request_events = {}
self.reader_task = asyncio.create_task(self.reader_loop())
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.process.terminate()
await self.reader_task
async def __aenter__(self):
await self.start()
return self
async def __aexit__(self, exc_type, exc, tb):
await 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)
elif response["method"] == "event":
# An event notification.
params = response["params"]
account_id = params["contextId"]
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
await self.event_queues[account_id].put(params["event"])
else:
print(response)
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
if account_id in self.event_queues:
return await self.event_queues[account_id].get()
return None
def __getattr__(self, attr: str):
async def method(*args, **kwargs) -> Any:
self.id += 1
request_id = self.id
assert not (args and kwargs), "Mixing positional and keyword arguments"
request = {
"jsonrpc": "2.0",
"method": attr,
"params": kwargs or 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
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return method

View File

@@ -0,0 +1,114 @@
import argparse
import asyncio
import re
import sys
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING:
from .client import Client
from .events import EventFilter
def _camel_to_snake(name: str) -> str:
name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
name = re.sub("__([A-Z])", r"_\1", name)
name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", name)
return name.lower()
def _to_attrdict(obj):
if isinstance(obj, dict):
return AttrDict(obj)
if isinstance(obj, list):
return [_to_attrdict(elem) for elem in obj]
return obj
class AttrDict(dict):
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
def __init__(self, *args, **kwargs) -> None:
super().__init__(
{
_camel_to_snake(key): _to_attrdict(value)
for key, value in dict(*args, **kwargs).items()
}
)
def __getattr__(self, attr):
if attr in self:
return self[attr]
raise AttributeError("Attribute not found: " + str(attr))
def __setattr__(self, attr, val):
if attr in self:
raise AttributeError("Attribute-style access is read only")
super().__setattr__(attr, val)
async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
"""Run a simple command line app, using the given hooks.
Extra keyword arguments are passed to the internal Rpc object.
"""
from .client import Client
await _run_cli(Client, hooks, argv, **kwargs)
async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
"""Run a simple bot command line using the given hooks.
Extra keyword arguments are passed to the internal Rpc object.
"""
from .client import Bot
await _run_cli(Bot, hooks, argv, **kwargs)
async def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs
) -> None:
from .deltachat import DeltaChat
from .rpc import Rpc
if argv is None:
argv = sys.argv
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
parser.add_argument(
"accounts_dir",
help="accounts folder (default: current working directory)",
nargs="?",
)
parser.add_argument("--email", action="store", help="email address")
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:
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()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not await client.is_configured():
assert (
args.email and args.password
), "Account is not configured and email and password must be provided"
asyncio.create_task(
client.configure(email=args.email, password=args.password)
)
await client.run_forever()

View File

@@ -0,0 +1,246 @@
import pytest
from deltachat_rpc_client import AttrDict, 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()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
]
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
assert await rpc.check_email_validity(addr)
for addr in invalid_addresses:
assert not await rpc.check_email_validity(addr)
@pytest.mark.asyncio
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = await account.wait_for_event()
if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
break
else:
print(event)
print("Successful configuration")
@pytest.mark.asyncio
async def test_account(acfactory) -> None:
alice, bob = await 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!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
await 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.self_contact
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
await alice.get_fresh_messages()
await alice.get_fresh_messages_in_arrival_order()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await group.send_message(text="hello")
assert group_msg == await alice.get_message_by_id(group_msg.id)
assert group == await alice.get_chat_by_id(group.id)
await 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"
assert not await alice.get_blocked_contacts()
await alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
await bob.remove()
await alice.stop_io()
@pytest.mark.asyncio
async def test_chat(acfactory) -> None:
alice, bob = await 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!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob_chat_alice = await bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
await alice_chat_bob.delete()
await bob_chat_alice.accept()
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()
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()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
await group.get_qr_code()
snapshot = await group.get_basic_snapshot()
assert snapshot.name == "test group"
await group.set_name("new name")
snapshot = await 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])
await group.set_draft(text="test draft")
draft = await group.get_draft()
assert draft.text == "test draft"
await group.remove_draft()
assert not await 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()
@pytest.mark.asyncio
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == await 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()
assert snapshot.address == bob_addr
await alice_contact_bob.create_chat()
@pytest.mark.asyncio
async def test_message(acfactory) -> None:
alice, bob = await 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!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = await bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
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")
await message.mark_seen()
await message.send_reaction("😎")
@pytest.mark.asyncio
async def test_bot(acfactory) -> None:
async def callback(e):
res.append(e)
res = []
bot = await acfactory.new_configured_bot()
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
bot.add_hook(callback, events.RawEvent(EventType.INFO))
info_event = AttrDict(account=bot.account, type=EventType.INFO, msg="info")
warn_event = AttrDict(account=bot.account, type=EventType.WARNING, msg="warning")
await bot._on_event(info_event)
await bot._on_event(warn_event)
assert info_event in res
assert warn_event not in res
assert len(res) == 1
res = []
bot.add_hook(callback, events.NewMessage(r"hello"))
snapshot1 = AttrDict(text="hello")
snapshot2 = AttrDict(text="hello, world")
snapshot3 = AttrDict(text="hey!")
for snapshot in [snapshot1, snapshot2, snapshot3]:
await bot._on_event(snapshot, events.NewMessage)
assert len(res) == 2
assert snapshot1 in res
assert snapshot2 in res
assert snapshot3 not in res

View File

@@ -0,0 +1,19 @@
[tox]
isolated_build = true
envlist =
py3
[testenv]
commands =
pytest {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
pytest-async
pytest-asyncio
aiohttp
aiodns

View File

@@ -0,0 +1,26 @@
[package]
name = "deltachat-rpc-server"
version = "1.103.0"
description = "DeltaChat JSON-RPC server"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
readme = "README.md"
license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[[bin]]
name = "deltachat-rpc-server"
[dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.89"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.22.0", features = ["io-std"] }
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }

View File

@@ -0,0 +1,68 @@
///! Delta Chat core RPC server.
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use anyhow::Result;
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tokio::task::JoinHandle;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let events = accounts.get_event_emitter();
log::info!("Creating JSON-RPC API.");
let state = CommandApi::new(accounts);
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state);
// Events task converts core events to JSON-RPC notifications.
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await?;
}
Ok(())
});
// Send task prints JSON responses to stdout.
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
while let Some(message) = out_receiver.next().await {
let message = serde_json::to_string(&message)?;
log::trace!("RPC send {}", message);
println!("{}", message);
}
Ok(())
});
// Receiver task reads JSON requests from stdin.
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
session.handle_incoming(&message).await;
}
log::info!("EOF reached on stdin");
Ok(())
});
// Wait for the end of stdin.
recv_task.await??;
// Shutdown the server.
send_task.abort();
events_task.abort();
Ok(())
}

View File

@@ -4,7 +4,7 @@ This document gives a quick overview about the Webxdc specification,
It is meant for both, developing Webxdc apps
and developing Webxdc implementations.
The [Webxdc guidebook](https://deltachat.github.io/webxdc_docs/) shows more detailed information
The [Webxdc guidebook](https://docs.webxdc.org/) shows more detailed information
when developing Webxdc apps.

View File

@@ -554,7 +554,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sql::housekeeping(&context).await.ok_or_log(&context);
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
let listflags = if arg0 == "listarchived" {
DC_GCL_ARCHIVED_ONLY
} else {
0
};
let time_start = std::time::SystemTime::now();
let chatlist = Chatlist::try_load(
&context,

View File

@@ -446,7 +446,7 @@ async fn handle_cmd(
}
println!("{}", qr);
let output = Command::new("qrencode")
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
.expect("failed to execute process");
io::stdout().write_all(&output.stdout).unwrap();

View File

@@ -42,6 +42,7 @@ module.exports = {
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
DC_EVENT_IMEX_PROGRESS: 2051,
DC_EVENT_INCOMING_MSG: 2005,
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
DC_EVENT_INFO: 100,
DC_EVENT_LOCATION_CHANGED: 2035,
DC_EVENT_MSGS_CHANGED: 2000,
@@ -71,8 +72,19 @@ module.exports = {
DC_IMEX_EXPORT_SELF_KEYS: 1,
DC_IMEX_IMPORT_BACKUP: 12,
DC_IMEX_IMPORT_SELF_KEYS: 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE: 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
DC_INFO_GROUP_IMAGE_CHANGED: 3,
DC_INFO_GROUP_NAME_CHANGED: 2,
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
DC_INFO_LOCATION_ONLY: 9,
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP: 5,
DC_INFO_PROTECTION_DISABLED: 12,
DC_INFO_PROTECTION_ENABLED: 11,
DC_INFO_SECURE_JOIN_MESSAGE: 7,
DC_INFO_UNKNOWN: 0,
DC_INFO_WEBXDC_INFO_MESSAGE: 32,
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,

View File

@@ -16,6 +16,7 @@ module.exports = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',

View File

@@ -42,6 +42,7 @@ export enum C {
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
DC_EVENT_IMEX_PROGRESS = 2051,
DC_EVENT_INCOMING_MSG = 2005,
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
DC_EVENT_INFO = 100,
DC_EVENT_LOCATION_CHANGED = 2035,
DC_EVENT_MSGS_CHANGED = 2000,
@@ -71,8 +72,19 @@ export enum C {
DC_IMEX_EXPORT_SELF_KEYS = 1,
DC_IMEX_IMPORT_BACKUP = 12,
DC_IMEX_IMPORT_SELF_KEYS = 2,
DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6,
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
DC_INFO_GROUP_IMAGE_CHANGED = 3,
DC_INFO_GROUP_NAME_CHANGED = 2,
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
DC_INFO_LOCATION_ONLY = 9,
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5,
DC_INFO_PROTECTION_DISABLED = 12,
DC_INFO_PROTECTION_ENABLED = 11,
DC_INFO_SECURE_JOIN_MESSAGE = 7,
DC_INFO_UNKNOWN = 0,
DC_INFO_WEBXDC_INFO_MESSAGE = 32,
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
@@ -285,6 +297,7 @@ export const EventId2EventName: { [key: number]: string } = {
2000: 'DC_EVENT_MSGS_CHANGED',
2001: 'DC_EVENT_REACTIONS_CHANGED',
2005: 'DC_EVENT_INCOMING_MSG',
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
2008: 'DC_EVENT_MSGS_NOTICED',
2010: 'DC_EVENT_MSG_DELIVERED',
2012: 'DC_EVENT_MSG_FAILED',

View File

@@ -616,7 +616,7 @@ describe('Offline Tests with unconfigured account', function () {
const id = context.createContact('someuser', 'someuser@site.com')
const contact = context.getContact(id)
strictEqual(contact.getId(), id, 'contact id matches')
strictEqual(context.deleteContact(id), true, 'delete call succesful')
context.deleteContact(id)
strictEqual(context.getContact(id), null, 'contact is gone')
})

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.98.0"
"version": "1.103.0"
}

View File

@@ -156,6 +156,7 @@ def extract_defines(flags):
| DC_KEY_GEN
| DC_IMEX
| DC_CONNECTIVITY
| DC_DOWNLOAD
) # End of prefix matching
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
) # Close the capturing group, this contains

View File

@@ -82,12 +82,13 @@ class Account(object):
if hasattr(db_path, "encode"):
db_path = db_path.encode("utf8")
ptr = lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL)
if ptr == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._dc_context = ffi.gc(
lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL),
ptr,
lib.dc_context_unref,
)
if self._dc_context == ffi.NULL:
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._shutdown_event = Event()
self._event_thread = EventThread(self)

View File

@@ -105,7 +105,7 @@ class FFIEventTracker:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile("(?:{}).*".format(event_name_regex))
rex = re.compile("^(?:{})$".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
return ev
@@ -192,6 +192,12 @@ class FFIEventTracker:
return self.account.get_message_by_id(ev.data2)
return None
def wait_next_reactions_changed(self):
"""wait for and return next reactions-changed message"""
ev = self.get_matching("DC_EVENT_REACTIONS_CHANGED")
assert ev.data1 > 0
return self.account.get_message_by_id(ev.data2)
def wait_msg_delivered(self, msg):
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
assert ev.data1 == msg.chat.id
@@ -296,6 +302,10 @@ class EventThread(threading.Thread):
"ac_incoming_message",
dict(message=msg),
)
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", dict(message=msg)
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg)

View File

@@ -49,6 +49,10 @@ class PerAccount:
def ac_outgoing_message(self, message):
"""Called on each outgoing message (both system and "normal")."""
@account_hookspec
def ac_reactions_changed(self, message):
"""Called when message reactions changed."""
@account_hookspec
def ac_message_delivered(self, message):
"""Called when an outgoing message has been delivered to SMTP.

View File

@@ -9,6 +9,7 @@ from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message(object):
@@ -161,6 +162,17 @@ class Message(object):
)
)
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self):
"""return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg))
@@ -449,6 +461,17 @@ class Message(object):
"""mark this message as seen."""
self.account.mark_seen_messages([self.id])
#
# Message download state
#
@property
def download_state(self):
assert self.id > 0
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
# some code for handling DC_MSG_* view types

View File

@@ -0,0 +1,43 @@
""" The Reactions object. """
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions(object):
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions):
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self):
return "<Reactions dc_reactions={}>".format(self._dc_reactions)
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -1,6 +1,7 @@
import os
import queue
import sys
import time
from datetime import datetime, timezone
import pytest
@@ -405,6 +406,29 @@ def test_forward_own_message(acfactory, lp):
assert msg_in.is_forwarded()
def test_long_group_name(acfactory, lp):
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
group names after MIME serialization/deserialization".
When the mailadm bot creates a group with botadmin, the bot creates is as
"pytest-supportuser-282@x.testrun.org support group" (for example). But in the botadmin's
account object, the group chat is called " pytest-supportuser-282@x.testrun.org support group"
(with an additional space character in the beginning).
"""
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: creating group chat and sending a message")
group_name = "pytest-supportuser-282@x.testrun.org support group"
group = ac1.create_group_chat(group_name)
group.add_contact(ac2)
group.send_text("message")
# wait for other account to receive
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.chat.get_name() == group_name
def test_send_self_message(acfactory, lp):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
@@ -901,6 +925,34 @@ def test_dont_show_emails(acfactory, lp):
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org, unkwnown.add@junk.org
Subject: subj
To: {}
Message-ID: <spam.message2@junk.org>
Content-Type: text/plain; charset=utf-8
Unknown & malformed message in Spam
""".format(
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: alice@example.org
Subject: subj
To: {}
Message-ID: <spam.message3@junk.org>
Content-Type: text/plain; charset=utf-8
Actually interesting message in Spam
""".format(
ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Junk",
"""
@@ -926,7 +978,9 @@ def test_dont_show_emails(acfactory, lp):
ac1._evtracker.wait_idle_inbox_ready()
assert msg.text == "subj message in Sent"
assert len(msg.chat.get_messages()) == 1
chat_msgs = msg.chat.get_messages()
assert len(chat_msgs) == 2
assert any(msg.text == "subj Actually interesting message in Spam" for msg in chat_msgs)
assert not any("unknown.address" in c.get_name() for c in ac1.get_chats())
ac1.direct_imap.select_folder("Spam")
@@ -942,7 +996,7 @@ def test_dont_show_emails(acfactory, lp):
msg2 = ac1._evtracker.wait_next_messages_changed()
assert msg2.text == "subj message in Drafts that is moved to Sent later"
assert len(msg.chat.get_messages()) == 2
assert len(msg.chat.get_messages()) == 3
def test_no_old_msg_is_fresh(acfactory, lp):
@@ -1237,6 +1291,103 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 32768
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
reactions_queue = queue.Queue()
class InPlugin:
@account_hookimpl
def ac_reactions_changed(self, message):
reactions_queue.put(message)
ac2.add_account_plugin(InPlugin())
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmpdir.join("large")
with open(path, "wb") as fout:
fout.write(os.urandom(download_limit + 1))
msgs.append(chat.send_file(path.strpath))
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)
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1_addr
assert reactions.get_by_contact(contacts[0]) == react_str
def test_reactions_for_a_reordering_move(acfactory, lp):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
lp.sec("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
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)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
ac2.direct_imap.connect()
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
ac2.direct_imap.conn.move(uid, "DeltaChat")
lp.sec("receiving messages by ac2")
ac2.start_io()
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.text == msg1.text
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1.get_config("addr")
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmpdir, data, lp):
(ac1,) = acfactory.get_online_accounts(1)

View File

@@ -200,11 +200,11 @@ class TestOfflineContact:
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
def test_get_contacts_and_delete_fails(self, acfactory):
def test_delete_referenced_contact_hides_contact(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
msg = contact1.create_chat().send_text("one message")
assert not ac1.delete_contact(contact1)
assert ac1.delete_contact(contact1)
assert not msg.filemime
def test_create_chat_flexibility(self, acfactory):

View File

@@ -10,6 +10,9 @@ envlist =
commands =
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
passenv =
DCC_RS_DEV
DCC_RS_TARGET
@@ -24,6 +27,13 @@ deps =
pdbpp
requests
[testenv:.pkg]
passenv =
DCC_RS_DEV
DCC_RS_TARGET
CARGO_TARGET_DIR
RUSTC_WRAPPER
[testenv:auditwheels]
skipsdist = True
deps = auditwheel

View File

@@ -1 +1 @@
1.61.0
1.65.0

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.61.0
RUST_VERSION=1.65.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -63,8 +63,13 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
]
try:
opts = parser.parse_args()
except SystemExit:

959
src/authres.rs Normal file
View File

@@ -0,0 +1,959 @@
//! Parsing and handling of the Authentication-Results header.
//! See the comment on [`handle_authres`] for more.
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fmt;
use anyhow::Context as _;
use anyhow::Result;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use num_traits::FromPrimitive;
use num_traits::ToPrimitive;
use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::tools;
use crate::tools::time;
use crate::tools::EmailAddress;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
/// about whether DKIM and SPF passed.
///
/// To mitigate From forgery, we remember for each sending domain whether it is known
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
/// we don't allow changing the autocrypt key.
///
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DkimResults> {
let from = match EmailAddress::new(from) {
Ok(email) => email,
Err(e) => {
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from.domain);
update_authservid_candidates(context, &authres, &from).await?;
compute_dkim_results(context, authres, &from.domain, message_time).await
}
#[derive(Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
/// i.e. whether we expect DKIM to work.
pub dkim_should_work: bool,
/// Whether changing the public Autocrypt key should be allowed.
/// This is false if we expected DKIM to work (dkim_works=true),
/// but it failed now (dkim_passed=false).
pub allow_keychange: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
self.dkim_passed, self.dkim_should_work, self.allow_keychange
)?;
if !self.allow_keychange {
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
}
Ok(())
}
}
type AuthservId = String;
#[derive(Debug, PartialEq)]
enum DkimResult {
/// The header explicitly said that DKIM passed
Passed,
/// The header explicitly said that DKIM failed
Failed,
/// The header didn't say anything about DKIM; this might mean that it wasn't
/// checked, but it might also mean that it failed. This is because some providers
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
/// Authentication-Results if there was no DKIM.
Nothing,
}
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
fn parse_authres_headers(
headers: &mailparse::headers::Headers<'_>,
from_domain: &str,
) -> ParsedAuthresHeaders {
let mut res = Vec::new();
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
let header_value = remove_comments(&header_value);
if let Some(mut authserv_id) = header_value.split(';').next() {
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
// because there is whitespace in the first identifier before the ';'.
// Authentication-Results-parsing still works securely because they remove incoming
// Authentication-Results headers.
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
// with providers not implementing the RFC correctly, someone can trick us
// into thinking that an incoming email is DKIM-correct, anyway.
// The most important thing here is that we have some valid `authserv_id`.
authserv_id = "invalidAuthservId";
}
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
res.push((authserv_id.to_string(), dkim_passed));
}
}
res
}
/// The headers can contain comments that look like this:
/// ```text
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
/// ```
fn remove_comments(header: &str) -> Cow<'_, str> {
// In Pomsky, this is:
// "(" Codepoint* lazy ")"
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
RE.replace_all(header, " ")
}
/// Parses a single Authentication-Results header, like:
///
/// ```text
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
/// ```
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
// Check that the character right before `dkim=` is a space or a tab
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
if let Some(&"pass") = dkim_parts.first() {
// DKIM headers contain a header.d or header.i field
// that says which domain signed. We have to check ourselves
// that this is the same domain as in the From header.
let header_d: &str = &format!("header.d={}", &from_domain);
let header_i: &str = &format!("header.i=@{}", &from_domain);
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
// We have found a `dkim=pass` header!
return DkimResult::Passed;
}
} else {
// dkim=fail, dkim=none, ...
return DkimResult::Failed;
}
}
}
DkimResult::Nothing
}
#[derive(Debug, FromPrimitive, ToPrimitive, PartialEq, PartialOrd)]
#[repr(i32)]
pub(crate) enum AuthservIdCandidatesOrigin {
None = 0,
/// We just used the authserv-ids from an incoming email.
/// They might TODO docs
UnknownSender = 10,
SameDomainSender = 20,
}
/// ## About authserv-ids
///
/// After having checked DKIM, our email server adds an Authentication-Results header.
///
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
/// in order to make us think that DKIM was correct in their From-forged email.
///
/// In order to prevent this, each email server adds its authserv-id to the
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
///
/// We need to somehow find out the authserv-id(s) of our email server, so that
/// we can use the Authentication-Results with the right authserv-id.
///
/// ## What this function does
///
/// When receiving an email, this function is called and updates the candidates for
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
///
/// Usually, every incoming email has Authentication-Results with our server's
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
/// authserv-ids for our server's authserv-id is a good guess for our server's
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
/// changed and start over with the new authserv-ids.
///
/// See [`handle_authres`].
async fn update_authservid_candidates(
context: &Context,
authres: &ParsedAuthresHeaders,
from: &EmailAddress,
) -> Result<()> {
let mut new_ids: BTreeSet<&str> = authres
.iter()
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
.collect();
if new_ids.is_empty() {
// The incoming message doesn't contain any Authentication-Results, maybe it's a
// self-sent or a mailer-daemon message
return Ok(());
}
let old_origin = get_authservid_condidates_origin(context).await?;
let self_addr = tools::EmailAddress::new(&context.get_primary_self_addr().await?)?;
// If we get an email from another account on the same domain as ours, then we can
// assume that the Authentication-Results in there were added by our server.
// So, the authserv-id in there most probably is the one of our server, i.e. it's
// the one we want to remember.
// We have the `&& from.local != self_addr.local` because many servers, e.g. GMail,
// don't add Authentication-Results for self-sent emails.
let new_origin = if from.domain == self_addr.domain && from.local != self_addr.local {
AuthservIdCandidatesOrigin::SameDomainSender
} else {
AuthservIdCandidatesOrigin::UnknownSender
};
if old_origin > new_origin {
return Ok(());
}
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
let old_ids = parse_authservid_candidates_config(&old_config);
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
if !intersection.is_empty() {
new_ids = intersection;
}
// If there were no AuthservIdCandidates previously, just start with
// the ones from the incoming email
if old_ids != new_ids {
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
context
.set_config(Config::AuthservIdCandidates, Some(&new_config))
.await?;
// Updating the authservid candidates may mean that we now consider
// emails as "failed" which "passed" previously, so we need to
// reset our expectation which DKIMs work.
clear_dkim_works(context).await?
}
if old_origin != new_origin {
let new_origin_i32 = new_origin.to_i32().context("not a valid i32??")?;
context
.set_config(
Config::AuthservIdCandidatesOrigin,
Some(&new_origin_i32.to_string()),
)
.await?;
}
Ok(())
}
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
/// and whether a keychange should be allowed.
///
/// We track in the `sending_domains` table whether we get positive Authentication-Results
/// for mails from a contact (meaning that their provider properly authenticates against
/// our provider).
///
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
from_domain: &str,
message_time: i64,
) -> Result<DkimResults> {
let mut dkim_passed = false;
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
let ids = parse_authservid_candidates_config(&ids_config);
let origin = get_authservid_condidates_origin(context).await?;
// Remove all foreign Authentication-Results
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
if authres.is_empty() {
match origin {
AuthservIdCandidatesOrigin::None | AuthservIdCandidatesOrigin::UnknownSender => {
// If the Authentication-results are empty, then our provider doesn't add them
// and an attacker could just add their own Authentication-Results, making us
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
dkim_passed = true;
}
AuthservIdCandidatesOrigin::SameDomainSender => {
// We know that usually our provider adds Authentication-Results,
// and we are quite certain that we know the correct authserv-id.
//
// Some providers like buzon.uy, disroot.org, yandex.ru, mailo.com, and
// riseup.net only add Authentication-Results if DKIM/SPF passed.
// So, probably DKIM failed, and therefore no Authentication-Results
// were added. And since we probably know the correct authserv-id,
// we can acutally assume that an attacher couldn't just add their own
// Authentication-Results.
dkim_passed = false;
}
};
} else {
for (_authserv_id, current_dkim_passed) in authres {
match current_dkim_passed {
DkimResult::Passed => {
dkim_passed = true;
break;
}
DkimResult::Failed => {
dkim_passed = false;
break;
}
DkimResult::Nothing => {
// Continue looking for an Authentication-Results header
}
}
}
}
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
if message_time > last_working_timestamp && dkim_passed {
set_dkim_works_timestamp(context, from_domain, message_time).await?;
dkim_should_work = true;
}
Ok(DkimResults {
dkim_passed,
dkim_should_work,
allow_keychange: dkim_passed || !dkim_should_work,
})
}
/// Whether DKIM in emails from this domain should be considered to work.
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
// When we get an email with valid DKIM-Authentication-Results,
// then we assume that DKIM works for 30 days from this time on.
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
let dkim_ever_worked = last_working_timestamp > 0;
// We're using time() here and not the time when the message
// claims to have been sent (passed around as `message_time`)
// because otherwise an attacker could just put a time way
// in the future into the `Date` header and then we would
// assume that DKIM doesn't have to be valid anymore.
let dkim_should_work_now = should_work_until > time();
Ok(dkim_ever_worked && dkim_should_work_now)
}
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
let last_working_timestamp: i64 = context
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
paramsv![from_domain],
)
.await?
.unwrap_or(0);
Ok(last_working_timestamp)
}
async fn set_dkim_works_timestamp(
context: &Context,
from_domain: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
paramsv![from_domain, timestamp],
)
.await?;
Ok(())
}
async fn clear_dkim_works(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM sending_domains", paramsv![])
.await?;
Ok(())
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
config
.as_deref()
.map(|c| c.split_whitespace().collect())
.unwrap_or_default()
}
async fn get_authservid_condidates_origin(context: &Context) -> Result<AuthservIdCandidatesOrigin> {
AuthservIdCandidatesOrigin::from_i32(
context
.get_config_int(Config::AuthservIdCandidatesOrigin)
.await?,
)
.context("Invalid AuthservIdCandidatesOrigin config")
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::join_securejoin;
use crate::test_utils;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
#[test]
fn test_remove_comments() {
let header = "Authentication-Results: mx3.messagingengine.com;
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
.to_string();
assert_eq!(
remove_comments(&header),
"Authentication-Results: mx3.messagingengine.com;
dkim=pass header.d=riseup.net;"
);
let header = ") aaa (".to_string();
assert_eq!(remove_comments(&header), ") aaa (");
let header = "((something weird) no comment".to_string();
assert_eq!(remove_comments(&header), " no comment");
let header = "🎉(🎉(🎉))🎉(".to_string();
assert_eq!(remove_comments(&header), "🎉 )🎉(");
// Comments are allowed to include whitespace
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
assert_eq!(remove_comments(&header), " no comment ");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_authentication_results() -> Result<()> {
let t = TestContext::new().await;
t.configure_addr("alice@gmx.net").await;
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Passed),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Nothing),
("gmx.net".to_string(), DkimResult::Nothing)
]
);
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
// Weird Authentication-Results from Outlook without an authserv-id
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
header.d=hotmail.com;dmarc=pass action=none
header.from=hotmail.com;compauth=pass reason=100";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
// At this point, the most important thing to test is that there are no
// authserv-ids with whitespace in them.
assert_eq!(
actual,
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
);
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
assert_eq!(
actual,
vec![
("gmx.net".to_string(), DkimResult::Failed),
("gmx.net".to_string(), DkimResult::Passed)
]
);
// ';' in comments
let bytes = b"Authentication-Results: mx1.riseup.net;
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
dkim-atps=neutral";
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
assert_eq!(
actual,
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
);
let bytes = br#"Authentication-Results: box.hispanilandia.net;
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
dkim-atps=neutral
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
let mail = mailparse::parse_mail(bytes)?;
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
assert_eq!(
actual,
vec![
("box.hispanilandia.net".to_string(), DkimResult::Failed),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
]
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_update_authservid_candidates() -> Result<()> {
let t = TestContext::new_alice().await;
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"], "a@b.c").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx3.messagingengine.com");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::UnknownSender);
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"], "a@b.c").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::UnknownSender);
// A message without any Authentication-Results headers shouldn't remove all
// candidates since it could be a mailer-daemon message or so
update_authservid_candidates_test(&t, &[], "a@b.c").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::UnknownSender);
update_authservid_candidates_test(
&t,
&["mx4.messagingengine.com", "someotherdomain.com"],
"a@b.c",
)
.await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::UnknownSender);
// We get an email from another example.org user
update_authservid_candidates_test(&t, &["mx.example.org"], "friend@example.org").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx.example.org");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::SameDomainSender);
// "mx4.messagingengine.com" comes from another domain while "mx.example.org" came
// from an example.org address -> the latter is more trustworthy
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"], "a@b.c").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx.example.org");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::SameDomainSender);
// Another email from an example.org user, seems like example.org changed their
// authserv-id
update_authservid_candidates_test(&t, &["mx2.example.org"], "friend@example.org").await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx2.example.org");
let origin = get_authservid_condidates_origin(&t).await?;
assert_eq!(origin, AuthservIdCandidatesOrigin::SameDomainSender);
Ok(())
}
/// Calls update_authservid_candidates(), meant for using in a test.
///
/// update_authservid_candidates() only looks at the keys of its
/// `authentication_results` parameter. So, this function takes `incoming_ids`
/// and adds some AuthenticationResults to get the HashMap we need.
async fn update_authservid_candidates_test(
context: &Context,
incoming_ids: &[&str],
from_addr: &str,
) {
let v = incoming_ids
.iter()
.map(|id| (id.to_string(), DkimResult::Passed))
.collect();
update_authservid_candidates(context, &v, &EmailAddress::new(from_addr).unwrap())
.await
.unwrap()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_realworld_authentication_results() -> Result<()> {
let mut test_failed = false;
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
.await
.unwrap();
let mut bytes = Vec::new();
for entry in dir {
if !entry.file_type().await.unwrap().is_dir() {
continue;
}
let self_addr = entry.file_name().into_string().unwrap();
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
let authres_parsing_works = [
"ik.me",
"web.de",
"posteo.de",
"gmail.com",
"hotmail.com",
"mail.ru",
"aol.com",
"yahoo.com",
"icloud.com",
"fastmail.com",
"mail.de",
"outlook.com",
"gmx.de",
"testrun.org",
"buzon.uy",
]
.contains(&self_domain.as_str());
let t = TestContext::new().await;
t.configure_addr(&self_addr).await;
if !authres_parsing_works {
println!("========= Receiving as {} =========", &self_addr);
}
// Simulate receiving all emails once, so that we have the correct authserv-ids
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
// The ordering in which the emails are received can matter;
// the test _should_ pass for every ordering.
dir.sort_by_key(|d| d.file_name());
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
}
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
println!(
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
entry.path()
);
test_failed = true;
}
let from_domain = EmailAddress::new(from).unwrap().domain;
assert_eq!(
res.dkim_should_work,
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
);
assert_eq!(res.dkim_passed, res.dkim_should_work);
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
// These are (fictional) forged emails where the attacker added a fake
// Authentication-Results before sending the email
&& from != "forged-authres-added@example.com"
// Other forged emails
&& !from.starts_with("forged");
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!();
println!(
"!!!!!! FAILURE Receiving {:?}, wrong result: !!!!!!",
entry.path(),
);
println!(
"order: {:#?}",
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
println!("Info: {:#?}", t.get_info().await?);
println!();
test_failed = true;
}
println!("From {}: {}", from_domain, res.dkim_passed);
}
}
}
assert!(!test_failed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres() {
let t = TestContext::new_alice().await;
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
// return an Err because this would prevent the message from being added
// to the database and downloaded again and again
let bytes = b"From: invalid@from.com
Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalid@rom.com", time())
.await
.unwrap();
}
#[ignore = "Disallowing keychanges is disabled for now"]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob sends Alice a message, so she gets his key
tcm.send_recv_accept(&bob, &alice, "Hi").await;
// We don't need bob anymore, let's make sure it's not accidentally used
drop(bob);
// Assume Alice receives an email from bob@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
// And Alice knows her server's authserv-id
alice
.set_config(Config::AuthservIdCandidates, Some("example.org"))
.await?;
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
// Sleep to make sure key reset is ignored because of DKIM failure
// and not because reordering is suspected.
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob2 = tcm.unconfigured().await;
bob2.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob2).await?;
let chat = bob2.create_chat(&alice).await;
let mut sent = bob2
.send_text(chat.id, "Please send me lots of money")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
let received = alice.recv_msg(&sent).await;
// Assert that the error tells the user about the problem
assert!(received.error.unwrap().contains("DKIM failed"));
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
.await?
.unwrap();
// Encryption preference is still mutual.
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
// Also check that the keypair was not changed
assert_eq!(
bob_state.public_key.unwrap(),
test_utils::bob_keypair().public
);
// Since Alice didn't change the key, Bob can't read her message
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.as_ref().unwrap().contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob2.ctx, &qr).await.unwrap();
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
} else {
break;
}
}
// Unfortunately, securejoin currently doesn't work with authres-checking,
// so these checks would fail:
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
// assert_eq!(
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
// assert_eq!(
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// // Bob can read Alice's messages again
// let received = tcm
// .try_send_recv(&alice, &bob2, "Can you read this again?")
// .await;
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
// assert!(received.error.is_none());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_chat = alice.create_chat(&bob).await;
let bob_alice_chat = bob.create_chat(&alice).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_none());
// Do the same without the mailing list header, this time the peerstate should be accepted
let sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// This also means that Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
// But if Bob writes to a mailing list, Alice doesn't show a padlock
// since she can't verify the signature without accepting Bob's key:
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Assume Bob received an email from something@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
// And Bob knows his server's authserv-id
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
.await?;
let alice_bob_chat = alice.create_chat(&bob).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
assert!(rcvd.error.is_none());
// Do the same without the mailing list header, this time the failed
// authres isn't ignored
let mut sent = alice
.send_text(alice_bob_chat.id, "hellooo without mailing list")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(message::get_msg_info(&bob, rcvd.id)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));
Ok(())
}
}

View File

@@ -2,6 +2,7 @@
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
@@ -81,6 +82,44 @@ impl Default for ProtectionStatus {
}
}
/// The reason why messages cannot be sent to the chat.
///
/// The reason is mainly for logging and displaying in debug REPL, thus not translated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CantSendReason {
/// Special chat.
SpecialChat,
/// The chat is a device chat.
DeviceChat,
/// The chat is a contact request, it needs to be accepted before sending a message.
ContactRequest,
/// Mailing list without known List-Post header.
ReadOnlyMailingList,
/// Not a member of the chat.
NotAMember,
}
impl fmt::Display for CantSendReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SpecialChat => write!(f, "the chat is a special chat"),
Self::DeviceChat => write!(f, "the chat is a device chat"),
Self::ContactRequest => write!(
f,
"contact request chat should be accepted before sending messages"
),
Self::ReadOnlyMailingList => {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
}
}
}
/// Chat ID, including reserved IDs.
///
/// Some chat IDs are reserved to identify special chat types. This
@@ -645,8 +684,11 @@ impl ChatId {
}
let chat = Chat::load_from_db(context, self).await?;
if !chat.can_send(context).await? {
bail!("Can't set a draft: Can't send");
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 -
@@ -1082,14 +1124,33 @@ impl Chat {
self.typ == Chattype::Mailinglist
}
/// Returns true if user can send messages to this chat.
/// Returns None if user can send messages to this chat.
///
/// Otherwise returns a reason useful for logging.
pub(crate) async fn why_cant_send(&self, context: &Context) -> Result<Option<CantSendReason>> {
use CantSendReason::*;
let reason = if self.id.is_special() {
Some(SpecialChat)
} else if self.is_device_talk() {
Some(DeviceChat)
} else if self.is_contact_request() {
Some(ContactRequest)
} else if self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty() {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
Some(NotAMember)
} else {
None
};
Ok(reason)
}
/// Returns true if can send to the chat.
///
/// This function can be used by the UI to decide whether to display the input box.
pub async fn can_send(&self, context: &Context) -> Result<bool> {
let cannot_send = self.id.is_special()
|| self.is_device_talk()
|| self.is_contact_request()
|| (self.is_mailing_list() && self.param.get(Param::ListPost).is_none_or_empty())
|| !self.is_self_in_chat(context).await?;
Ok(!cannot_send)
Ok(self.why_cant_send(context).await?.is_none())
}
/// Checks if the user is part of a chat
@@ -1130,8 +1191,8 @@ impl Chat {
}
/// Returns mailing list address where messages are sent to.
pub fn get_mailinglist_addr(&self) -> &str {
self.param.get(Param::ListPost).unwrap_or_default()
pub fn get_mailinglist_addr(&self) -> Option<&str> {
self.param.get(Param::ListPost)
}
/// Returns profile image path for the chat.
@@ -1255,18 +1316,13 @@ impl Chat {
let mut to_id = 0;
let mut location_id = 0;
if !self.can_send(context).await? {
if self.typ == Chattype::Group
&& !is_contact_in_chat(context, self.id, ContactId::SELF).await?
{
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 set message; self not in group.");
} else {
error!(context, "Cannot send to chat type #{}.", self.typ,);
bail!("Cannot send to chat type #{}", self.typ);
}
bail!("Cannot send message to {}: {}", self.id, reason);
}
let from = context.get_primary_self_addr().await?;
@@ -1881,7 +1937,9 @@ async fn prepare_msg_common(
change_state_to: MessageState,
) -> Result<MsgId> {
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
}
// check current MessageState for drafts (to keep msg_id) ...
let update_msg_id = if msg.state == MessageState::OutDraft {
@@ -2188,7 +2246,7 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some(
stock_str::videochat_invite_msg_body(context, Message::parse_webrtc_instance(&instance).1)
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
.await,
);
send_msg(context, chat_id, &mut msg).await
@@ -2439,7 +2497,7 @@ pub async fn get_chat_media(
AND hidden=0
ORDER BY timestamp, id;",
paramsv![
if chat_id.is_none() { 1i32 } else { 0i32 },
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
msg_type,
if msg_type2 != Viewtype::Unknown {
@@ -2563,7 +2621,7 @@ pub async fn create_group_chat(
let chat_id = ChatId::new(u32::try_from(row_id)?);
if !is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
add_to_chat_contacts_table(context, chat_id, ContactId::SELF).await?;
add_to_chat_contacts_table(context, chat_id, &[ContactId::SELF]).await?;
}
context.emit_msgs_changed_without_ids();
@@ -2624,19 +2682,25 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
Ok(chat_id)
}
/// Adds a contact to the `chats_contacts` table.
/// Adds contacts to the `chats_contacts` table.
pub(crate) async fn add_to_chat_contacts_table(
context: &Context,
chat_id: ChatId,
contact_id: ContactId,
contact_ids: &[ContactId],
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
)
.transaction(move |transaction| {
for contact_id in contact_ids {
transaction.execute(
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
)?;
}
Ok(())
})
.await?;
Ok(())
}
@@ -2738,7 +2802,7 @@ pub(crate) async fn add_contact_to_chat_ex(
if is_contact_in_chat(context, chat_id, contact_id).await? {
return Ok(false);
}
add_to_chat_contacts_table(context, chat_id, contact_id).await?;
add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
}
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
@@ -3005,7 +3069,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
pub async fn set_chat_profile_image(
context: &Context,
chat_id: ChatId,
new_image: impl AsRef<str>, // XXX use PathBuf
new_image: &str, // XXX use PathBuf
) -> Result<()> {
ensure!(!chat_id.is_special(), "Invalid chat ID");
let mut chat = Chat::load_from_db(context, chat_id).await?;
@@ -3023,13 +3087,12 @@ pub async fn set_chat_profile_image(
let mut msg = Message::new(Viewtype::Text);
msg.param
.set_int(Param::Cmd, SystemMessage::GroupImageChanged as i32);
if new_image.as_ref().is_empty() {
if new_image.is_empty() {
chat.param.remove(Param::ProfileImage);
msg.param.remove(Param::Arg);
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
} else {
let mut image_blob =
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
let mut image_blob = BlobObject::new_from_path(context, Path::new(new_image)).await?;
image_blob.recode_to_avatar_size(context).await?;
chat.param.set(Param::ProfileImage, image_blob.as_name());
msg.param.set(Param::Arg, image_blob.as_name());
@@ -3054,7 +3117,9 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
chat_id.unarchive_if_not_muted(context).await?;
if let Ok(mut chat) = Chat::load_from_db(context, chat_id).await {
ensure!(chat.can_send(context).await?, "cannot send to {}", chat_id);
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
}
curr_timestamp = create_smeared_timestamps(context, msg_ids.len()).await;
let ids = context
.sql
@@ -3702,11 +3767,11 @@ mod tests {
// create group and sync it to the second device
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
a1.send_text(a1_chat_id, "ho!").await;
let sent = a1.send_text(a1_chat_id, "ho!").await;
let a1_msg = a1.get_last_msg().await;
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
let a2_msg = a2.recv_msg(&sent).await;
let a2_chat_id = a2_msg.chat_id;
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
@@ -4028,6 +4093,7 @@ mod tests {
assert!(chat.is_device_talk());
assert!(!chat.is_self_talk());
assert!(!chat.can_send(&t).await?);
assert!(chat.why_cant_send(&t).await? == Some(CantSendReason::DeviceChat));
assert_eq!(chat.name, stock_str::device_messages(&t).await);
assert!(chat.get_profile_image(&t).await?.is_some());

View File

@@ -184,24 +184,34 @@ pub enum Config {
/// In a future versions, this switch may be removed.
#[strum(props(default = "0"))]
SendSyncMsgs,
/// Space-separated list of all the authserv-ids which we believe
/// may be the one of our email server.
///
/// See `crate::authres::update_authservid_candidates`.
AuthservIdCandidates,
/// How sure we are that AuthservIdCandidates are the correct authserv-ids of our server.
/// This contains a value of [`crate::authres::AuthservIdCandidatesOrigin`].
AuthservIdCandidatesOrigin,
}
impl Context {
pub async fn config_exists(&self, key: Config) -> Result<bool> {
Ok(self.sql.get_raw_config(key).await?.is_some())
Ok(self.sql.get_raw_config(key.as_ref()).await?.is_some())
}
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub async fn get_config(&self, key: Config) -> Result<Option<String>> {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key).await?;
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key).await?,
_ => self.sql.get_raw_config(key.as_ref()).await?,
};
if value.is_some() {
@@ -291,26 +301,30 @@ impl Context {
Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql.set_raw_config(key, Some(blob.as_name())).await?;
self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
}
None => {
self.sql.set_raw_config(key, None).await?;
self.sql.set_raw_config(key.as_ref(), None).await?;
}
}
self.emit_event(EventType::SelfavatarChanged);
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key, value).await;
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
// Interrupt ephemeral loop to delete old messages immediately.
self.interrupt_ephemeral_task().await;
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql.set_raw_config(key, value.as_deref()).await?;
self.sql
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
}
_ => {
self.sql.set_raw_config(key, value).await?;
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
Ok(())

View File

@@ -17,12 +17,13 @@ use crate::context::Context;
use crate::imap::Imap;
use crate::job;
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
use crate::oauth2::get_oauth2_addr;
use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::scheduler::InterruptInfo;
use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
use crate::tools::{time, EmailAddress};
use crate::{chat, e2ee, provider};
@@ -87,7 +88,7 @@ impl Context {
self,
// We are using Anyhow's .context() and to show the
// inner error, too, we need the {:#}:
format!("{:#}", err),
&format!("{:#}", err),
)
.await
)
@@ -153,7 +154,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.ok_or_log_msg(context, "Cannot add AEAP explanation");
@@ -442,9 +443,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
// Send client ID as soon as possible before doing anything else.
imap.determine_capabilities(ctx).await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap.select_with_uidvalidity(ctx, "INBOX")

View File

@@ -1,14 +1,20 @@
//! Contacts module
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
@@ -24,7 +30,7 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -223,7 +229,7 @@ pub enum Origin {
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
SecurejoinJoined = 0x0200_0000,
/// contact added mannually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
ManuallyCreated = 0x0400_0000,
}
@@ -449,7 +455,7 @@ impl Contact {
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
@@ -938,43 +944,36 @@ impl Contact {
Ok(ret)
}
/// Delete a contact. The contact is deleted from the local device. It may happen that this is not
/// possible as the contact is in use. In this case, the contact can be blocked.
/// Delete a contact so that it disappears from the corresponding lists.
/// Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
/// The contact is deleted from the local device.
///
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
ensure!(!contact_id.is_special(), "Can not delete special contact");
let count_chats = context
context
.sql
.count(
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
paramsv![contact_id],
)
.transaction(move |transaction| {
// make sure, the transaction starts with a write command and becomes EXCLUSIVE by that -
// upgrading later may be impossible by races.
let deleted_contacts = transaction.execute(
"DELETE FROM contacts WHERE id=?
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
paramsv![contact_id, contact_id],
)?;
if deleted_contacts == 0 {
transaction.execute(
"UPDATE contacts SET origin=? WHERE id=?;",
paramsv![Origin::Hidden, contact_id],
)?;
}
Ok(())
})
.await?;
if count_chats == 0 {
match context
.sql
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
.await
{
Ok(_) => {
context.emit_event(EventType::ContactsChanged(None));
return Ok(());
}
Err(err) => {
error!(context, "delete_contact {} failed ({})", contact_id, err);
return Err(err);
}
}
}
info!(
context,
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
);
bail!("Could not delete contact with ongoing chats");
context.emit_event(EventType::ContactsChanged(None));
Ok(())
}
/// Get a single contact object. For a list, see eg. get_contacts().
@@ -1370,13 +1369,17 @@ pub(crate) async fn update_last_seen(
"Can not update special contact last seen timestamp"
);
context
if context
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
paramsv![timestamp, contact_id],
)
.await?;
.await?
> 0
{
context.interrupt_recently_seen(contact_id, timestamp).await;
}
Ok(())
}
@@ -1443,13 +1446,138 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
.collect()
}
#[derive(Debug)]
pub(crate) struct RecentlySeenInterrupt {
contact_id: ContactId,
timestamp: i64,
}
#[derive(Debug)]
pub(crate) struct RecentlySeenLoop {
/// Task running "recently seen" loop.
handle: task::JoinHandle<()>,
interrupt_send: Sender<RecentlySeenInterrupt>,
}
impl RecentlySeenLoop {
pub(crate) fn new(context: Context) -> Self {
let (interrupt_send, interrupt_recv) = channel::bounded(1);
let handle = task::spawn(async move { Self::run(context, interrupt_recv).await });
Self {
handle,
interrupt_send,
}
}
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
type MyHeapElem = (Reverse<i64>, ContactId);
// Priority contains all recently seen sorted by the timestamp
// when they become not recently seen.
//
// Initialize with contacts which are currently seen, but will
// become unseen in the future.
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
.sql
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
paramsv![time() - SEEN_RECENTLY_SECONDS],
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
},
|rows| {
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
.map_err(Into::into)
},
)
.await
.unwrap_or_default();
loop {
let now = SystemTime::now();
let (until, contact_id) =
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
(
UNIX_EPOCH
+ Duration::from_secs((*timestamp).try_into().unwrap_or(u64::MAX))
+ Duration::from_secs(1),
Some(contact_id),
)
} else {
// Sleep for 24 hours.
(now + Duration::from_secs(86400), None)
};
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Recently seen loop waiting for {} or interrupt",
duration_to_str(duration)
);
match timeout(duration, interrupt.recv()).await {
Err(_) => {
// Timeout, notify about contact.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
unseen_queue.pop();
}
}
Ok(Err(err)) => {
warn!(
context,
"Error receiving an interruption in recently seen loop: {}", err
);
}
Ok(Ok(RecentlySeenInterrupt {
contact_id,
timestamp,
})) => {
// Received an interrupt.
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
}
}
} else {
info!(
context,
"Recently seen loop is not waiting, event is already due."
);
// Event is already in the past.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
}
unseen_queue.pop();
}
}
}
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.try_send(RecentlySeenInterrupt {
contact_id,
timestamp,
})
.ok();
}
pub(crate) fn abort(self) {
self.handle.abort();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
use crate::message::Message;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager};
@@ -1602,7 +1730,7 @@ mod tests {
);
assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4);
// check first added contact, this modifies authname beacuse it is empty
// check first added contact, this modifies authname because it is empty
let (contact_id, sth_modified) =
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
.await
@@ -1810,21 +1938,70 @@ mod tests {
// Create Bob contact
let (contact_id, _) =
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
.await
.unwrap();
.await?;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.net")
.await;
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
1
);
// Can't delete a contact with ongoing chats.
assert!(Contact::delete(&alice, contact_id).await.is_err());
// If a contact has ongoing chats, contact is only hidden on deletion
Contact::delete(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
0
);
// Delete chat.
chat.get_id().delete(&alice).await?;
// Can delete contact now.
// Can delete contact physically now
Contact::delete(&alice, contact_id).await?;
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
.len(),
0
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_delete_and_recreate_contact() -> Result<()> {
let t = TestContext::new_alice().await;
// test recreation after physical deletion
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
// test recreation after hiding
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::load_from_db(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
let contact = Contact::load_from_db(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Ok(())
}
@@ -2150,7 +2327,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
let message = sent_msg.load_from_db().await;
assert!(!message.get_showpadlock());
// Alice's second devices receives a copy of outgoing message.
@@ -2177,7 +2354,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second devices receives a copy of second outgoing message.
@@ -2232,7 +2409,7 @@ CCCB 5AA9 F6E1 141C 9431
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
let message = Message::load_from_db(&alice1, sent_msg.sender_msg_id).await?;
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second device receives a copy of the outgoing message.

View File

@@ -26,6 +26,156 @@ use crate::sql::Sql;
use crate::stock_str::StockStrings;
use crate::tools::{duration_to_str, time};
/// Builder for the [`Context`].
///
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
/// multiple contexts, for which the [account manager](crate::accounts::Accounts) should be
/// used. This builder makes creating a new context simpler, especially for the
/// standalone-context case.
///
/// # Examples
///
/// Creating a new unecrypted database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
///
/// To use an encrypted database provide a password. If the database does not yet exist it
/// will be created:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .with_password("secret".into())
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ContextBuilder {
dbfile: PathBuf,
id: u32,
events: Events,
stock_strings: StockStrings,
password: Option<String>,
}
impl ContextBuilder {
/// Create the builder using the given database file.
///
/// The *dbfile* should be in a dedicated directory and this directory must exist. The
/// [`Context`] will create other files and folders in the same directory as the
/// database file used.
pub fn new(dbfile: PathBuf) -> Self {
ContextBuilder {
dbfile,
id: rand::random(),
events: Events::new(),
stock_strings: StockStrings::new(),
password: None,
}
}
/// Sets the context ID.
///
/// This identifier is used e.g. in [`Event`]s to identify which [`Context`] an event
/// belongs to. The only real limit on it is that it should not conflict with any other
/// [`Context`]s you currently have open. So if you handle multiple [`Context`]s you
/// may want to use this.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
pub fn with_id(mut self, id: u32) -> Self {
self.id = id;
self
}
/// Sets the event channel for this [`Context`].
///
/// Mostly useful when using multiple [`Context`]s, this allows creating one [`Events`]
/// channel and passing it to all [`Context`]s so all events are recieved on the same
/// channel.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
pub fn with_events(mut self, events: Events) -> Self {
self.events = events;
self
}
/// Sets the [`StockStrings`] map to use for this [`Context`].
///
/// This is useful in order to share the same translation strings in all [`Context`]s.
/// The mapping may be empty when set, it will be populated by
/// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
///
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
/// common case for using multiple [`Context`] instances.
///
/// [`Accounts::set_stock_translation`]: crate::accounts::Accounts::set_stock_translation
pub fn with_stock_strings(mut self, stock_strings: StockStrings) -> Self {
self.stock_strings = stock_strings;
self
}
/// Sets the password to unlock the database.
///
/// If an encrypted database is used it must be opened with a password. Setting a
/// password on a new database will enable encryption.
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
}
/// Opens the [`Context`].
pub async fn open(self) -> Result<Context, ContextError> {
let context =
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
let password = self.password.unwrap_or_default();
match context.open(password).await? {
true => Ok(context),
false => Err(ContextError::DatabaseEncrypted),
}
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum ContextError {
#[error("database could not be decrypted, incorrect or missing password")]
DatabaseEncrypted,
#[error("failed to open context")]
Other(#[from] anyhow::Error),
}
/// The context for a single DeltaChat account.
///
/// This contains all the state for a single DeltaChat account, including background tasks
/// running in Tokio to operate the account. The [`Context`] can be cheaply cloned.
///
/// Each context, and thus each account, must be associated with an directory where all the
/// state is kept. This state is also preserved between restarts.
///
/// To use multiple accounts it is best to look at the [accounts
/// manager][crate::accounts::Accounts] which handles storing multiple accounts in a single
/// directory structure and handles loading them all concurrently.
#[derive(Clone, Debug)]
pub struct Context {
pub(crate) inner: Arc<InnerContext>,
@@ -420,6 +570,10 @@ impl Context {
.await?
.unwrap_or_default();
let configured_inbox_folder = self
.get_config(Config::ConfiguredInboxFolder)
.await?
.unwrap_or_else(|| "<unset>".to_string());
let configured_sentbox_folder = self
.get_config(Config::ConfiguredSentboxFolder)
.await?
@@ -460,9 +614,9 @@ impl Context {
res.insert("entered_account_settings", l.to_string());
res.insert("used_account_settings", l2.to_string());
let server_id = self.server_id.read().await;
res.insert("imap_server_id", format!("{:?}", server_id));
drop(server_id);
if let Some(server_id) = &*self.server_id.read().await {
res.insert("imap_server_id", format!("{:?}", server_id));
}
res.insert("secondary_addrs", secondary_addrs);
res.insert(
@@ -491,6 +645,7 @@ impl Context {
res.insert("mvbox_move", mvbox_move.to_string());
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
res.insert("folders_configured", folders_configured.to_string());
res.insert("configured_inbox_folder", configured_inbox_folder);
res.insert("configured_sentbox_folder", configured_sentbox_folder);
res.insert("configured_mvbox_folder", configured_mvbox_folder);
res.insert("mdns_enabled", mdns_enabled.to_string());
@@ -544,6 +699,18 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"authserv_id_candidates",
self.get_config(Config::AuthservIdCandidates)
.await?
.unwrap_or_default(),
);
res.insert(
"authserv_id_candidates_origin",
self.get_config(Config::AuthservIdCandidatesOrigin)
.await?
.unwrap_or_default(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -650,7 +817,7 @@ impl Context {
ON m.chat_id=c.id
WHERE m.chat_id>9
AND m.hidden=0
AND c.blocked=0
AND c.blocked!=1
AND ct.blocked=0
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
@@ -703,6 +870,8 @@ mod tests {
use crate::chat::{
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
};
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::message::{Message, Viewtype};
use crate::receive_imf::receive_imf;
@@ -1038,6 +1207,59 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_search_unaccepted_requests() -> Result<()> {
let t = TestContext::new_alice().await;
receive_imf(
&t,
b"From: BobBar <bob@example.org>\n\
To: alice@example.org\n\
Subject: foo\n\
Message-ID: <msg1234@example.org>\n\
Chat-Version: 1.0\n\
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
\n\
hello bob, foobar test!\n",
false,
)
.await?;
let chat_id = t.get_last_msg().await.get_chat_id();
let chat = Chat::load_from_db(&t, chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Single);
assert!(chat.is_contact_request());
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
chat_id.block(&t).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
0
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
let contact_ids = get_chat_contacts(&t, chat_id).await?;
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
assert_eq!(
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
1
);
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_limit_search_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;

View File

@@ -5,11 +5,12 @@ use std::collections::HashSet;
use anyhow::{Context as _, Result};
use mailparse::ParsedMail;
use crate::aheader::Aheader;
use crate::aheader::{Aheader, EncryptPreference};
use crate::authres::handle_authres;
use crate::authres::{self, DkimResults};
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
@@ -55,31 +56,62 @@ pub async fn try_decrypt(
.await
}
pub async fn create_decryption_info(
pub(crate) async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
is_thunderbird: bool,
) -> Result<DecryptionInfo> {
let from = mail
.headers
.get_header(HeaderDef::From_)
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
.and_then(|from| from.extract_single_info())
.map(|from| from.addr)
.unwrap_or_default();
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
}
return Ok(DecryptionInfo {
from: from.to_string(),
autocrypt_header: None,
peerstate: None,
message_time,
dkim_results: DkimResults {
dkim_passed: false,
dkim_should_work: false,
allow_keychange: true,
},
});
}
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
let mut autocrypt_header = Aheader::from_headers(from, &mail.headers)
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
.flatten();
let peerstate =
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
if is_thunderbird {
if let Some(autocrypt_header) = &mut autocrypt_header {
autocrypt_header.prefer_encrypt = EncryptPreference::Mutual;
}
}
let dkim_results = handle_authres(context, mail, from, message_time).await?;
let peerstate = get_autocrypt_peerstate(
context,
from,
autocrypt_header.as_ref(),
message_time,
// Disallowing keychanges is disabled for now:
true, // dkim_results.allow_keychange,
)
.await?;
Ok(DecryptionInfo {
from,
from: from.to_string(),
autocrypt_header,
peerstate,
message_time,
dkim_results,
})
}
@@ -96,6 +128,7 @@ pub struct DecryptionInfo {
/// means out-of-order message arrival, We don't modify the
/// peerstate in this case.
pub message_time: i64,
pub(crate) dkim_results: authres::DkimResults,
}
/// Returns a reference to the encrypted payload of a ["Mixed
@@ -263,12 +296,16 @@ fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublic
/// If we already know this fingerprint from another contact's peerstate, return that
/// peerstate in order to make AEAP work, but don't save it into the db yet.
///
/// The param `allow_change` is used to prevent the autocrypt key from being changed
/// if we suspect that the message may be forged and have a spoofed sender identity.
///
/// Returns updated peerstate.
pub(crate) async fn get_autocrypt_peerstate(
context: &Context,
from: &str,
autocrypt_header: Option<&Aheader>,
message_time: i64,
allow_change: bool,
) -> Result<Option<Peerstate>> {
let mut peerstate;
@@ -289,8 +326,15 @@ pub(crate) async fn get_autocrypt_peerstate(
if let Some(ref mut peerstate) = peerstate {
if addr_cmp(&peerstate.addr, from) {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql, false).await?;
if allow_change {
peerstate.apply_header(header, message_time);
peerstate.save_to_db(&context.sql).await?;
} else {
info!(
context,
"Refusing to update existing peerstate of {}", &peerstate.addr
);
}
}
// If `peerstate.addr` and `from` differ, this means that
// someone is using the same key but a different addr, probably
@@ -300,7 +344,7 @@ pub(crate) async fn get_autocrypt_peerstate(
// to the database.
} else {
let p = Peerstate::from_header(header, message_time);
p.save_to_db(&context.sql, true).await?;
p.save_to_db(&context.sql).await?;
peerstate = Some(p);
}
} else {

View File

@@ -428,14 +428,12 @@ mod tests {
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let sent2_rfc742_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
.await?
.rfc724_mid;
let sent2_rfc724_mid = sent2.load_from_db().await.rfc724_mid;
// not downloading the status update results in an placeholder
receive_imf_inner(
&bob,
&sent2_rfc742_mid,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
false,
Some(sent2.payload().len() as u32),
@@ -451,7 +449,7 @@ mod tests {
// (usually status updates are too small for not being downloaded directly)
receive_imf_inner(
&bob,
&sent2_rfc742_mid,
&sent2_rfc724_mid,
sent2.payload().as_bytes(),
false,
None,

View File

@@ -147,7 +147,6 @@ mod tests {
use crate::chat;
use crate::message::{Message, Viewtype};
use crate::param::Param;
use crate::peerstate::ToSave;
use crate::test_utils::{bob_keypair, TestContext};
use super::*;
@@ -297,7 +296,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
gossip_key_fingerprint: Some(pub_key.fingerprint()),
verified_key: Some(pub_key.clone()),
verified_key_fingerprint: Some(pub_key.fingerprint()),
to_save: Some(ToSave::All),
fingerprint_changed: false,
};
vec![(Some(peerstate), addr)]

View File

@@ -226,13 +226,13 @@ pub(crate) async fn stock_ephemeral_timer_changed(
Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await,
Timer::Enabled { duration } => match duration {
0..=59 => {
stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await
stock_str::msg_ephemeral_timer_enabled(context, &timer.to_string(), from_id).await
}
60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await,
61..=3599 => {
stock_str::msg_ephemeral_timer_minutes(
context,
format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
&format!("{}", (f64::from(duration) / 6.0).round() / 10.0),
from_id,
)
.await
@@ -241,7 +241,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
3601..=86399 => {
stock_str::msg_ephemeral_timer_hours(
context,
format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
&format!("{}", (f64::from(duration) / 360.0).round() / 10.0),
from_id,
)
.await
@@ -250,7 +250,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
86401..=604_799 => {
stock_str::msg_ephemeral_timer_days(
context,
format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
&format!("{}", (f64::from(duration) / 8640.0).round() / 10.0),
from_id,
)
.await
@@ -259,7 +259,7 @@ pub(crate) async fn stock_ephemeral_timer_changed(
_ => {
stock_str::msg_ephemeral_timer_weeks(
context,
format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
&format!("{}", (f64::from(duration) / 60480.0).round() / 10.0),
from_id,
)
.await

View File

@@ -189,6 +189,10 @@ pub enum EventType {
msg_id: MsgId,
},
IncomingMsgBunch {
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),

View File

@@ -63,6 +63,11 @@ pub enum HeaderDef {
Sender,
EphemeralTimer,
Received,
/// A header that includes the results of the DKIM, SPF and DMARC checks.
/// See <https://datatracker.ietf.org/doc/html/rfc8601>
AuthenticationResults,
_TestHeader,
}

View File

@@ -94,7 +94,7 @@ impl HtmlMsgParser {
let parsedmail = mailparse::parse_mail(rawmime)?;
parser.collect_texts_recursive(context, &parsedmail).await?;
parser.collect_texts_recursive(&parsedmail).await?;
if parser.html.is_empty() {
if let Some(plain) = &parser.plain {
@@ -117,7 +117,6 @@ impl HtmlMsgParser {
/// therefore we use the first one.
fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,
mail: &'a mailparse::ParsedMail<'a>,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a + Send>> {
// Boxed future to deal with recursion
@@ -125,7 +124,7 @@ impl HtmlMsgParser {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in mail.subparts.iter() {
self.collect_texts_recursive(context, cur_data).await?
self.collect_texts_recursive(cur_data).await?
}
Ok(())
}
@@ -135,7 +134,7 @@ impl HtmlMsgParser {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.collect_texts_recursive(context, &mail).await
self.collect_texts_recursive(&mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -207,7 +206,7 @@ impl HtmlMsgParser {
Ok(re) => {
self.html = re
.replace_all(
&*self.html,
&self.html,
format!("${{1}}{}${{3}}", replacement).as_str(),
)
.as_ref()

File diff suppressed because it is too large Load Diff

26
src/imap/capabilities.rs Normal file
View File

@@ -0,0 +1,26 @@
//! # IMAP capabilities
//!
//! IMAP server capabilities are determined with a `CAPABILITY` command.
use std::collections::HashMap;
#[derive(Debug)]
pub(crate) struct Capabilities {
/// True if the server has IDLE capability as defined in
/// <https://tools.ietf.org/html/rfc2177>
pub can_idle: bool,
/// True if the server has MOVE capability as defined in
/// <https://tools.ietf.org/html/rfc6851>
pub can_move: bool,
/// True if the server has QUOTA capability as defined in
/// <https://tools.ietf.org/html/rfc2087>
pub can_check_quota: bool,
/// True if the server has CONDSTORE capability as defined in
/// <https://tools.ietf.org/html/rfc7162>
pub can_condstore: bool,
/// Server ID if the server supports ID capability.
pub server_id: Option<HashMap<String, String>>,
}

View File

@@ -6,17 +6,21 @@ use std::{
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use async_smtp::ServerAddress;
use tokio::net::{self, TcpStream};
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::login_param::{build_tls, Socks5Config};
use crate::login_param::build_tls;
use crate::socks::Socks5Config;
use super::session::SessionStream;
/// IMAP write and read timeout in seconds.
const IMAP_TIMEOUT: u64 = 30;
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub(crate) struct Client {
@@ -38,37 +42,66 @@ impl DerefMut for Client {
}
}
/// Determine server capabilities.
///
/// If server supports ID capability, send our client ID.
async fn determine_capabilities(
session: &mut ImapSession<Box<dyn SessionStream>>,
) -> Result<Capabilities> {
let caps = session
.capabilities()
.await
.context("CAPABILITY command error")?;
let server_id = if caps.has_str("ID") {
session.id([("name", Some("Delta Chat"))]).await?
} else {
None
};
let capabilities = Capabilities {
can_idle: caps.has_str("IDLE"),
can_move: caps.has_str("MOVE"),
can_check_quota: caps.has_str("QUOTA"),
can_condstore: caps.has_str("CONDSTORE"),
server_id,
};
Ok(capabilities)
}
impl Client {
pub async fn login(self, username: &str, password: &str) -> Result<Session> {
pub(crate) async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
let session = inner
let mut session = inner
.login(username, password)
.await
.map_err(|(err, _client)| err)?;
Ok(Session { inner: session })
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
}
pub async fn authenticate(
pub(crate) async fn authenticate(
self,
auth_type: &str,
authenticator: impl async_imap::Authenticator,
) -> Result<Session> {
let Client { inner, .. } = self;
let session = inner
let mut session = inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, _client)| err)?;
Ok(Session { inner: session })
let capabilities = determine_capabilities(&mut session).await?;
Ok(Session::new(session, capabilities))
}
pub async fn connect_secure(
addr: impl net::ToSocketAddrs,
domain: &str,
strict_tls: bool,
) -> Result<Self> {
let stream = TcpStream::connect(addr).await?;
pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result<Self> {
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect((hostname, port))).await??;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
let timeout_stream = Box::pin(timeout_stream);
let tls = build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(hostname, timeout_stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
@@ -83,7 +116,12 @@ impl Client {
}
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT));
timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT));
let timeout_stream = Box::pin(timeout_stream);
let stream: Box<dyn SessionStream> = Box::new(timeout_stream);
let mut client = ImapClient::new(stream);
let _greeting = client
@@ -98,19 +136,17 @@ impl Client {
}
pub async fn connect_secure_socks5(
target_addr: &ServerAddress,
target_addr: impl net::ToSocketAddrs,
domain: &str,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
);
let socks5_stream: Box<dyn SessionStream> =
Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?);
let tls = build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> =
Box::new(tls.connect(target_addr.host.clone(), socks5_stream).await?);
Box::new(tls.connect(domain, socks5_stream).await?);
let mut client = ImapClient::new(tls_stream);
let _greeting = client
@@ -125,14 +161,11 @@ impl Client {
}
pub async fn connect_insecure_socks5(
target_addr: &ServerAddress,
target_addr: impl net::ToSocketAddrs,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
);
let socks5_stream: Box<dyn SessionStream> =
Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
@@ -146,7 +179,7 @@ impl Client {
})
}
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Client> {
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Self> {
if self.is_secure {
Ok(self)
} else {

View File

@@ -1,103 +1,114 @@
use super::Imap;
use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use std::time::{Duration, SystemTime};
use super::session::Session;
use crate::imap::client::IMAP_TIMEOUT;
use crate::{context::Context, scheduler::InterruptInfo};
use super::session::Session;
impl Imap {
pub fn can_idle(&self) -> bool {
self.config.can_idle
}
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
impl Session {
pub async fn idle(
&mut self,
mut self,
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
watch_folder: Option<String>,
) -> Result<InterruptInfo> {
) -> Result<(Self, InterruptInfo)> {
use futures::future::FutureExt;
if !self.can_idle() {
bail!("IMAP server does not have IDLE capability");
}
self.prepare(context).await?;
let mut info = Default::default();
self.select_folder(context, watch_folder.as_deref()).await?;
let timeout = Duration::from_secs(23 * 60);
let mut info = Default::default();
if self.server_sent_unsolicited_exists(context)? {
return Ok(info);
return Ok((self, info));
}
if let Some(session) = self.session.take() {
if let Ok(info) = self.idle_interrupt.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
self.session = Some(session);
return Ok(info);
}
let mut handle = session.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
}
let (idle_wait, interrupt) = handle.wait_with_timeout(timeout);
enum Event {
IdleResponse(IdleResponse),
Interrupt(InterruptInfo),
}
info!(
context,
"{}: Idle entering wait-on-remote state",
watch_folder.as_deref().unwrap_or("None")
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = self.idle_interrupt.recv().await;
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
});
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
}
Ok(Event::Interrupt(i)) => {
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
}
}
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await?
.context("IMAP IDLE protocol timed out")?;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
if let Ok(info) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt {:?}", info);
return Ok((self, info));
}
Ok(info)
let mut handle = self.inner.idle();
if let Err(err) = handle.init().await {
bail!("IMAP IDLE protocol failed to init/complete: {}", err);
}
// At this point IDLE command was sent and we received a "+ idling" response. We will now
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
// disable read timeout, we would get a timeout after `IMAP_TIMEOUT`, which is a lot
// shorter than `IDLE_TIMEOUT`.
handle.as_mut().set_read_timeout(None);
let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
enum Event {
IdleResponse(IdleResponse),
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = idle_interrupt_receiver.recv().await;
// cancel imap idle connection properly
drop(interrupt);
Ok(Event::Interrupt(info.unwrap_or_default()))
});
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
}
Err(err) => {
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
}
}
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
self.inner = session;
Ok((self, info))
}
}
impl Imap {
pub(crate) async fn fake_idle(
&mut self,
context: &Context,
@@ -113,7 +124,11 @@ impl Imap {
watch_folder
} else {
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self.idle_interrupt.recv().await.unwrap_or_default();
return self
.idle_interrupt_receiver
.recv()
.await
.unwrap_or_default();
};
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
@@ -132,7 +147,7 @@ impl Imap {
.tick()
.map(|_| Event::Tick)
.race(
self.idle_interrupt
self.idle_interrupt_receiver
.recv()
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
)
@@ -146,9 +161,11 @@ impl Imap {
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if self.config.can_idle {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
if let Some(session) = &self.session {
if session.can_idle() {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}
}
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
@@ -167,12 +184,13 @@ impl Imap {
}
Err(err) => {
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context).await;
self.trigger_reconnect(context);
}
}
}
Event::Interrupt(info) => {
// Interrupt
info!(context, "Fake IDLE interrupted");
break info;
}
}

View File

@@ -63,16 +63,18 @@ impl Imap {
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string()) && !is_drafts {
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages
self.server_sent_unsolicited_exists(context)?;
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name(), is_spam_folder)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !self.server_sent_unsolicited_exists(context)? {
if !session.server_sent_unsolicited_exists(context)? {
break;
}
}

View File

@@ -1,4 +1,4 @@
use super::Imap;
use super::session::Session as ImapSession;
use crate::context::Context;
use anyhow::Context as _;
@@ -7,9 +7,6 @@ type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("IMAP Could not obtain imap-session object.")]
NoSession,
#[error("IMAP Connection Lost or no connection established")]
ConnectionLost,
@@ -19,69 +16,48 @@ pub enum Error {
#[error("Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}")]
NoFolder(String, String),
#[error("IMAP close/expunge failed")]
CloseExpungeFailed(#[from] async_imap::error::Error),
#[error("IMAP other error: {0}")]
Other(String),
}
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
impl From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Error {
Error::Other(format!("{:#}", err))
}
}
impl ImapSession {
/// Issues a CLOSE command if selected folder needs expunge,
/// i.e. if Delta Chat marked a message there as deleted previously.
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn close_folder(&mut self, context: &Context) -> Result<()> {
if let Some(ref folder) = self.config.selected_folder {
info!(context, "Expunge messages in \"{}\".", folder);
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in \"{}\".", folder);
if let Some(ref mut session) = self.session {
match session.close().await {
Ok(_) => {
info!(context, "close/expunge succeeded");
}
Err(err) => {
self.trigger_reconnect(context).await;
return Err(Error::CloseExpungeFailed(err));
}
}
} else {
return Err(Error::NoSession);
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "close/expunge succeeded");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
}
}
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
Ok(())
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}
Ok(())
}
/// select a folder, possibly update uid_validity and, if needed,
/// expunge the folder to remove delete-marked messages.
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
) -> Result<NewlySelected> {
if self.session.is_none() {
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
self.trigger_reconnect(context).await;
return Err(Error::NoSession);
}
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(folder) = folder {
if let Some(ref selected_folder) = self.config.selected_folder {
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
@@ -93,42 +69,30 @@ impl Imap {
// select new folder
if let Some(folder) = folder {
if let Some(ref mut session) = &mut self.session {
let res = if self.config.can_condstore {
session.select_condstore(folder).await
} else {
session.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.config.selected_folder = Some(folder.to_string());
self.config.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => {
self.trigger_reconnect(context).await;
self.config.selected_folder = None;
Err(Error::ConnectionLost)
}
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => {
self.config.selected_folder = None;
self.trigger_reconnect(context).await;
Err(Error::Other(err.to_string()))
}
}
let res = if self.can_condstore() {
self.select_condstore(folder).await
} else {
Err(Error::NoSession)
self.select(folder).await
};
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
// says that if the server reports select failure we are in
// authenticated (not-select) state.
match res {
Ok(mailbox) => {
self.selected_folder = Some(folder.to_string());
self.selected_mailbox = Some(mailbox);
Ok(NewlySelected::Yes)
}
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
Err(async_imap::error::Error::Validate(_)) => {
Err(Error::BadFolderName(folder.to_string()))
}
Err(async_imap::error::Error::No(response)) => {
Err(Error::NoFolder(folder.to_string(), response))
}
Err(err) => Err(Error::Other(err.to_string())),
}
} else {
Ok(NewlySelected::No)
@@ -145,8 +109,7 @@ impl Imap {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
let session = self.session.as_mut().context("no IMAP session")?;
session.create(folder).await.with_context(|| {
self.create(folder).await.with_context(|| {
format!("Couldn't select folder ('{}'), then create() failed", err)
})?;
@@ -157,6 +120,7 @@ impl Imap {
}
}
}
#[derive(PartialEq, Debug, Copy, Clone, Eq)]
pub(super) enum NewlySelected {
/// The folder was newly selected during this call to select_folder().

View File

@@ -1,24 +1,58 @@
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::time::Duration;
use async_imap::types::Mailbox;
use async_imap::Session as ImapSession;
use async_native_tls::TlsStream;
use fast_socks5::client::Socks5Stream;
use tokio::net::TcpStream;
use tokio_io_timeout::TimeoutStream;
use super::capabilities::Capabilities;
#[derive(Debug)]
pub(crate) struct Session {
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
pub capabilities: Capabilities,
/// Selected folder name.
pub selected_folder: Option<String>,
/// Mailbox structure returned by IMAP server.
pub selected_mailbox: Option<Mailbox>,
pub selected_folder_needs_expunge: bool,
}
pub(crate) trait SessionStream:
tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + std::fmt::Debug
{
/// Change the read timeout on the session stream.
fn set_read_timeout(&mut self, timeout: Option<Duration>);
}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {}
impl SessionStream for TlsStream<TcpStream> {}
impl SessionStream for TcpStream {}
impl SessionStream for Socks5Stream<TcpStream> {}
impl SessionStream for TlsStream<Box<dyn SessionStream>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
}
impl SessionStream for TlsStream<Pin<Box<TimeoutStream<TcpStream>>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_mut().set_read_timeout(timeout);
}
}
impl SessionStream for Pin<Box<TimeoutStream<TcpStream>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.as_mut().set_read_timeout_pinned(timeout);
}
}
impl SessionStream for Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>> {
fn set_read_timeout(&mut self, timeout: Option<Duration>) {
self.get_socket_mut().set_read_timeout(timeout)
}
}
impl Deref for Session {
type Target = ImapSession<Box<dyn SessionStream>>;
@@ -35,8 +69,32 @@ impl DerefMut for Session {
}
impl Session {
pub fn idle(self) -> async_imap::extensions::idle::Handle<Box<dyn SessionStream>> {
let Session { inner } = self;
inner.idle()
pub(crate) fn new(
inner: ImapSession<Box<dyn SessionStream>>,
capabilities: Capabilities,
) -> Self {
Self {
inner,
capabilities,
selected_folder: None,
selected_mailbox: None,
selected_folder_needs_expunge: false,
}
}
pub fn can_idle(&self) -> bool {
self.capabilities.can_idle
}
pub fn can_move(&self) -> bool {
self.capabilities.can_move
}
pub fn can_check_quota(&self) -> bool {
self.capabilities.can_check_quota
}
pub fn can_condstore(&self) -> bool {
self.capabilities.can_condstore
}
}

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