Compare commits

..

92 Commits

Author SHA1 Message Date
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
382 changed files with 4882 additions and 1717 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

@@ -83,13 +83,13 @@ jobs:
rust: 1.61.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.56.1
# Minimum Supported Rust Version = 1.57.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.57.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:

View File

@@ -8,6 +8,7 @@ on:
env:
CARGO_TERM_COLOR: always
RUST_MIN_STACK: "8388608"
jobs:
build_and_test:

View File

@@ -2,13 +2,87 @@
## Unreleased
### Changes
### API-Changes
- jsonrpc: add `Context` class you can get via `getContext` or `getContextEvents`, it is a convinience wrapper that has all account specific functions with the accountId pre-set.
### Fixes
## 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
@@ -67,6 +141,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

534
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.101.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
license = "MPL-2.0"
rust-version = "1.56"
rust-version = "1.57"
[profile.dev]
debug = 0
panic = 'abort'
opt-level = 1
[profile.test]
opt-level = 0
[profile.release]
lto = true
panic = 'abort'
@@ -46,9 +49,9 @@ native-tls = "0.2"
num_cpus = "1.13"
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"
@@ -77,9 +80,9 @@ tagger = "4.3.3"
textwrap = "0.16.0"
async-channel = "1.6.1"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.10", features = ["fs"] }
tokio-stream = { version = "0.1.11", 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"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
@@ -95,7 +98,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

@@ -125,10 +125,12 @@ $ cargo test -- --ignored
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 +142,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

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.98.0"
version = "1.101.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(),
}
}
@@ -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.101.0"
description = "DeltaChat JSON-RPC API"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2021"
@@ -20,16 +20,17 @@ 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"
futures = { version = "0.3.25" }
serde_json = "1.0.87"
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
tokio = { version = "1.21.2" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
# optional dependencies
axum = { version = "0.5.16", optional = true, features = ["ws"] }
axum = { version = "0.5.17", optional = true, features = ["ws"] }
env_logger = { version = "0.9.1", optional = true }
walkdir = "2.3.2"
[dev-dependencies]
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }

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
}
@@ -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>,
@@ -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,93 +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
}
}
#[cfg(test)]
#[test]
fn stuff() {
let root_namespace = Some("T");
let methods: Vec<yerpc::typescript::Method> = raw_ts_binding_methods();
let account_methods: Vec<String> = methods
.into_iter()
.filter_map(|m| {
// No idea how to check the type, to lazy to find out
// if let Some((name, _)) = m.args.get(0) {
// println!("{}", name);
// }
//println!("-> {}", m.to_string(root_namespace));
if m.to_string(root_namespace).contains("accountId: T.U32") {
//println!("old: {}", m.to_string(root_namespace));
let args2 = Vec::from_iter(m.args[1..].iter().map(|m| m.to_owned()));
let m2 = yerpc::typescript::Method::new(
&m.ts_name,
&m.rpc_name,
args2,
m.output,
m.is_notification,
m.is_positional,
m.docs.as_deref(),
);
let args_string = if m2.args.is_empty() {
"".to_string()
} else {
format!(
", {}",
m2.args
.iter()
.map(|(name, _)| name.as_ref())
.collect::<Vec<&str>>()
.join(", ")
)
};
let replacement_line = format!(
" return this.controller.rpc.{}(this.accountId{})",
m.ts_name, args_string
);
// println!("{}", m2.to_string(root_namespace));
let m_string = m2.to_string(root_namespace);
let new_string: String = m_string
.split('\n')
.map(|line| {
if line.contains("this._transport") {
&replacement_line
} else {
line
}
})
.collect::<Vec<&str>>()
.join("\n");
// println!("new: {}", new_string);
Some(new_string)
} else {
None
}
})
.collect();
let head = r#"import * as T from "./types.js"
import { RawClient } from "./client.js"
import { TinyEmitter } from "tiny-emitter";
export class Context<T> extends TinyEmitter<T> {
constructor(
private controller: {rpc: RawClient},
readonly accountId: T.U32
){
super()
}"#;
let context_methods = format!("{}\n{}\n}}\n", head, account_methods.join(""));
std::fs::write("typescript/generated/context_methods.ts", context_methods).unwrap();
}

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

@@ -45,6 +45,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 +127,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()),
@@ -175,6 +178,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 +309,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

@@ -18,7 +18,6 @@ async function run() {
const client = new DeltaChat("ws://localhost:20808/ws");
(window as any).dc = client;
(window as any).client = client.rpc;
client.on("ALL", (accountId, event) => {
@@ -69,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
);
@@ -79,7 +78,7 @@ async function run() {
chatId,
0
);
const messages = await client.rpc.messageGetMessages(
const messages = await client.rpc.getMessages(
selectedAccount,
messageIds
);

View File

@@ -232,13 +232,13 @@ export class RawClient {
}
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
return (this._transport.request('initiate_autocrypt_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 continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
return (this._transport.request('continue_autocrypt_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
}
@@ -252,8 +252,8 @@ export class RawClient {
}
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>;
public getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
}
/**
@@ -593,8 +593,8 @@ export class RawClient {
}
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 getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
}
@@ -603,15 +603,15 @@ export class RawClient {
}
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>>;
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return (this._transport.request('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>;
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
}
/**
@@ -676,8 +676,8 @@ export class RawClient {
/**
* 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>;
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
return (this._transport.request('get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
}
/**
@@ -685,48 +685,48 @@ export class RawClient {
*
* 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>;
public createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
return (this._transport.request('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 createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
return (this._transport.request('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 blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('block_contact', [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 unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
return (this._transport.request('unblock_contact', [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 getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
return (this._transport.request('get_blocked_contacts', [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)[]>;
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return (this._transport.request('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 getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return (this._transport.request('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 getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
}
@@ -770,8 +770,8 @@ export class RawClient {
* 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)[]>;
public getChatMedia(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('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
}
/**
@@ -782,8 +782,8 @@ export class RawClient {
* 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 getNeighboringChatMedia(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('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
}
@@ -845,20 +845,20 @@ export class RawClient {
}
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 sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return (this._transport.request('send_webxdc_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>;
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return (this._transport.request('get_webxdc_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>;
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
}
/**
@@ -912,9 +912,16 @@ export class RawClient {
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
}
/**
* save a sticker to a collection/folder in the account's sticker folder
*/
public miscSaveSticker(accountId: T.U32, msgId: T.U32, collection: string): Promise<null> {
return (this._transport.request('misc_save_sticker', [accountId, msgId, collection] as RPC.Params)) as Promise<null>;
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
* grouped by the collection/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)[]>>;
@@ -923,8 +930,8 @@ export class RawClient {
/**
* 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 miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
}

View File

@@ -29,8 +29,6 @@ export enum C {
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,

View File

@@ -1,855 +0,0 @@
import * as T from "./types.js"
import { RawClient } from "./client.js"
import { TinyEmitter } from "tiny-emitter";
export class Context<T> extends TinyEmitter<T> {
constructor(
private controller: {rpc: RawClient},
readonly accountId: T.U32
){
super()
}
public removeAccount(): Promise<null> {
return this.controller.rpc.removeAccount(this.accountId)
}
/**
* Get top-level info for an account.
*/
public getAccountInfo(): Promise<T.Account> {
return this.controller.rpc.getAccountInfo(this.accountId)
}
/**
* Get the combined filesize of an account in bytes
*/
public getAccountFileSize(): Promise<T.U64> {
return this.controller.rpc.getAccountFileSize(this.accountId)
}
/**
* 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(email: string): Promise<(T.ProviderInfo|null)> {
return this.controller.rpc.getProviderInfo(this.accountId, email)
}
/**
* Checks if the context is already configured.
*/
public isConfigured(): Promise<boolean> {
return this.controller.rpc.isConfigured(this.accountId)
}
/**
* Get system info for an account.
*/
public getInfo(): Promise<Record<string,string>> {
return this.controller.rpc.getInfo(this.accountId)
}
public setConfig(key: string, value: (string|null)): Promise<null> {
return this.controller.rpc.setConfig(this.accountId, key, value)
}
public batchSetConfig(config: Record<string,(string|null)>): Promise<null> {
return this.controller.rpc.batchSetConfig(this.accountId, config)
}
/**
* 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(qrContent: string): Promise<null> {
return this.controller.rpc.setConfigFromQr(this.accountId, qrContent)
}
public checkQr(qrContent: string): Promise<T.Qr> {
return this.controller.rpc.checkQr(this.accountId, qrContent)
}
public getConfig(key: string): Promise<(string|null)> {
return this.controller.rpc.getConfig(this.accountId, key)
}
public batchGetConfig(keys: (string)[]): Promise<Record<string,(string|null)>> {
return this.controller.rpc.batchGetConfig(this.accountId, keys)
}
/**
* Configures this account with the currently set parameters.
* Setup the credential config before calling this.
*/
public configure(): Promise<null> {
return this.controller.rpc.configure(this.accountId)
}
/**
* Signal an ongoing process to stop.
*/
public stopOngoingProcess(): Promise<null> {
return this.controller.rpc.stopOngoingProcess(this.accountId)
}
public exportSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.exportSelfKeys(this.accountId, path, passphrase)
}
public importSelfKeys(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.importSelfKeys(this.accountId, path, passphrase)
}
/**
* 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(): Promise<(T.U32)[]> {
return this.controller.rpc.getFreshMsgs(this.accountId)
}
/**
* 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(chatId: T.U32): Promise<T.Usize> {
return this.controller.rpc.getFreshMsgCnt(this.accountId, chatId)
}
/**
* 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(fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
return this.controller.rpc.estimateAutoDeletionCount(this.accountId, fromServer, seconds)
}
public autocryptInitiateKeyTransfer(): Promise<string> {
return this.controller.rpc.autocryptInitiateKeyTransfer(this.accountId)
}
public autocryptContinueKeyTransfer(messageId: T.U32, setupCode: string): Promise<null> {
return this.controller.rpc.autocryptContinueKeyTransfer(this.accountId, messageId, setupCode)
}
public getChatlistEntries(listFlags: (T.U32|null), queryString: (string|null), queryContactId: (T.U32|null)): Promise<(T.ChatListEntry)[]> {
return this.controller.rpc.getChatlistEntries(this.accountId, listFlags, queryString, queryContactId)
}
public getChatlistItemsByEntries(entries: (T.ChatListEntry)[]): Promise<Record<T.U32,T.ChatListItemFetchResult>> {
return this.controller.rpc.getChatlistItemsByEntries(this.accountId, entries)
}
public chatlistGetFullChatById(chatId: T.U32): Promise<T.FullChat> {
return this.controller.rpc.chatlistGetFullChatById(this.accountId, chatId)
}
/**
* get basic info about a chat,
* use chatlist_get_full_chat_by_id() instead if you need more information
*/
public getBasicChatInfo(chatId: T.U32): Promise<T.BasicChat> {
return this.controller.rpc.getBasicChatInfo(this.accountId, chatId)
}
public acceptChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.acceptChat(this.accountId, chatId)
}
public blockChat(chatId: T.U32): Promise<null> {
return this.controller.rpc.blockChat(this.accountId, chatId)
}
/**
* 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(chatId: T.U32): Promise<null> {
return this.controller.rpc.deleteChat(this.accountId, chatId)
}
/**
* 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(chatId: T.U32): Promise<string> {
return this.controller.rpc.getChatEncryptionInfo(this.accountId, chatId)
}
/**
* 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(chatId: (T.U32|null)): Promise<[string,string]> {
return this.controller.rpc.getChatSecurejoinQrCodeSvg(this.accountId, chatId)
}
/**
* 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(qr: string): Promise<T.U32> {
return this.controller.rpc.secureJoin(this.accountId, qr)
}
public leaveGroup(chatId: T.U32): Promise<null> {
return this.controller.rpc.leaveGroup(this.accountId, chatId)
}
/**
* 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(chatId: T.U32, contactId: T.U32): Promise<null> {
return this.controller.rpc.removeContactFromChat(this.accountId, chatId, contactId)
}
/**
* 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(chatId: T.U32, contactId: T.U32): Promise<null> {
return this.controller.rpc.addContactToChat(this.accountId, chatId, contactId)
}
/**
* 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(chatId: T.U32): Promise<(T.U32)[]> {
return this.controller.rpc.getChatContacts(this.accountId, chatId)
}
/**
* 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(name: string, protect: boolean): Promise<T.U32> {
return this.controller.rpc.createGroupChat(this.accountId, name, protect)
}
/**
* 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(): Promise<T.U32> {
return this.controller.rpc.createBroadcastList(this.accountId)
}
/**
* 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(chatId: T.U32, newName: string): Promise<null> {
return this.controller.rpc.setChatName(this.accountId, chatId, newName)
}
/**
* 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(chatId: T.U32, imagePath: (string|null)): Promise<null> {
return this.controller.rpc.setChatProfileImage(this.accountId, chatId, imagePath)
}
public setChatVisibility(chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
return this.controller.rpc.setChatVisibility(this.accountId, chatId, visibility)
}
public setChatEphemeralTimer(chatId: T.U32, timer: T.U32): Promise<null> {
return this.controller.rpc.setChatEphemeralTimer(this.accountId, chatId, timer)
}
public getChatEphemeralTimer(chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.getChatEphemeralTimer(this.accountId, chatId)
}
public addDeviceMessage(label: string, text: string): Promise<T.U32> {
return this.controller.rpc.addDeviceMessage(this.accountId, label, text)
}
/**
* 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(chatId: T.U32): Promise<null> {
return this.controller.rpc.marknoticedChat(this.accountId, chatId)
}
public getFirstUnreadMessageOfChat(chatId: T.U32): Promise<(T.U32|null)> {
return this.controller.rpc.getFirstUnreadMessageOfChat(this.accountId, chatId)
}
/**
* 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(chatId: T.U32, duration: T.MuteDuration): Promise<null> {
return this.controller.rpc.setChatMuteDuration(this.accountId, chatId, duration)
}
/**
* 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(chatId: T.U32): Promise<boolean> {
return this.controller.rpc.isChatMuted(this.accountId, chatId)
}
/**
* 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(msgIds: (T.U32)[]): Promise<null> {
return this.controller.rpc.markseenMsgs(this.accountId, msgIds)
}
public getMessageIds(chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
return this.controller.rpc.getMessageIds(this.accountId, chatId, flags)
}
public getMessageListItems(chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
return this.controller.rpc.getMessageListItems(this.accountId, chatId, flags)
}
public messageGetMessage(messageId: T.U32): Promise<T.Message> {
return this.controller.rpc.messageGetMessage(this.accountId, messageId)
}
public getMessageHtml(messageId: T.U32): Promise<(string|null)> {
return this.controller.rpc.getMessageHtml(this.accountId, messageId)
}
public messageGetMessages(messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
return this.controller.rpc.messageGetMessages(this.accountId, messageIds)
}
/**
* Fetch info desktop needs for creating a notification for a message
*/
public messageGetNotificationInfo(messageId: T.U32): Promise<T.MessageNotificationInfo> {
return this.controller.rpc.messageGetNotificationInfo(this.accountId, messageId)
}
/**
* Delete messages. The messages are deleted on the current device and
* on the IMAP server.
*/
public deleteMessages(messageIds: (T.U32)[]): Promise<null> {
return this.controller.rpc.deleteMessages(this.accountId, messageIds)
}
/**
* 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(messageId: T.U32): Promise<string> {
return this.controller.rpc.getMessageInfo(this.accountId, messageId)
}
/**
* 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(messageId: T.U32): Promise<null> {
return this.controller.rpc.downloadFullMessage(this.accountId, messageId)
}
/**
* 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(query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
return this.controller.rpc.searchMessages(this.accountId, query, chatId)
}
public messageIdsToSearchResults(messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
return this.controller.rpc.messageIdsToSearchResults(this.accountId, messageIds)
}
/**
* Get a single contact options by ID.
*/
public contactsGetContact(contactId: T.U32): Promise<T.Contact> {
return this.controller.rpc.contactsGetContact(this.accountId, contactId)
}
/**
* Add a single contact as a result of an explicit user action.
*
* Returns contact id of the created or existing contact
*/
public contactsCreateContact(email: string, name: (string|null)): Promise<T.U32> {
return this.controller.rpc.contactsCreateContact(this.accountId, email, name)
}
/**
* Returns contact id of the created or existing DM chat with that contact
*/
public contactsCreateChatByContactId(contactId: T.U32): Promise<T.U32> {
return this.controller.rpc.contactsCreateChatByContactId(this.accountId, contactId)
}
public contactsBlock(contactId: T.U32): Promise<null> {
return this.controller.rpc.contactsBlock(this.accountId, contactId)
}
public contactsUnblock(contactId: T.U32): Promise<null> {
return this.controller.rpc.contactsUnblock(this.accountId, contactId)
}
public contactsGetBlocked(): Promise<(T.Contact)[]> {
return this.controller.rpc.contactsGetBlocked(this.accountId)
}
public contactsGetContactIds(listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
return this.controller.rpc.contactsGetContactIds(this.accountId, listFlags, query)
}
/**
* Get a list of contacts.
* (formerly called getContacts2 in desktop)
*/
public contactsGetContacts(listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
return this.controller.rpc.contactsGetContacts(this.accountId, listFlags, query)
}
public contactsGetContactsByIds(ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
return this.controller.rpc.contactsGetContactsByIds(this.accountId, ids)
}
public deleteContact(contactId: T.U32): Promise<boolean> {
return this.controller.rpc.deleteContact(this.accountId, contactId)
}
public changeContactName(contactId: T.U32, name: string): Promise<null> {
return this.controller.rpc.changeContactName(this.accountId, contactId, name)
}
/**
* 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(contactId: T.U32): Promise<string> {
return this.controller.rpc.getContactEncryptionInfo(this.accountId, contactId)
}
/**
* 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(addr: string): Promise<(T.U32|null)> {
return this.controller.rpc.lookupContactIdByAddr(this.accountId, addr)
}
/**
* 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(chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
return this.controller.rpc.chatGetMedia(this.accountId, chatId, messageType, orMessageType2, orMessageType3)
}
/**
* 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(msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
return this.controller.rpc.chatGetNeighboringMedia(this.accountId, msgId, messageType, orMessageType2, orMessageType3)
}
public exportBackup(destination: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.exportBackup(this.accountId, destination, passphrase)
}
public importBackup(path: string, passphrase: (string|null)): Promise<null> {
return this.controller.rpc.importBackup(this.accountId, path, passphrase)
}
/**
* 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(): Promise<T.U32> {
return this.controller.rpc.getConnectivity(this.accountId)
}
/**
* 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(): Promise<string> {
return this.controller.rpc.getConnectivityHtml(this.accountId)
}
public getLocations(chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
return this.controller.rpc.getLocations(this.accountId, chatId, contactId, timestampBegin, timestampEnd)
}
public webxdcSendStatusUpdate(instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
return this.controller.rpc.webxdcSendStatusUpdate(this.accountId, instanceMsgId, updateStr, description)
}
public webxdcGetStatusUpdates(instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
return this.controller.rpc.webxdcGetStatusUpdates(this.accountId, instanceMsgId, lastKnownSerial)
}
/**
* Get info from a webxdc message
*/
public messageGetWebxdcInfo(instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
return this.controller.rpc.messageGetWebxdcInfo(this.accountId, instanceMsgId)
}
/**
* 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(messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
return this.controller.rpc.forwardMessages(this.accountId, messageIds, chatId)
}
public sendSticker(chatId: T.U32, stickerPath: string): Promise<T.U32> {
return this.controller.rpc.sendSticker(this.accountId, chatId, stickerPath)
}
/**
* 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(messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
return this.controller.rpc.sendReaction(this.accountId, messageId, reaction)
}
public removeDraft(chatId: T.U32): Promise<null> {
return this.controller.rpc.removeDraft(this.accountId, chatId)
}
/**
* Get draft for a chat, if any.
*/
public getDraft(chatId: T.U32): Promise<(T.Message|null)> {
return this.controller.rpc.getDraft(this.accountId, chatId)
}
public sendVideochatInvitation(chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.sendVideochatInvitation(this.accountId, chatId)
}
public miscGetStickerFolder(): Promise<string> {
return this.controller.rpc.miscGetStickerFolder(this.accountId)
}
/**
* for desktop, get stickers from stickers folder,
* grouped by the folder they are in.
*/
public miscGetStickers(): Promise<Record<string,(string)[]>> {
return this.controller.rpc.miscGetStickers(this.accountId)
}
/**
* Returns the messageid of the sent message
*/
public miscSendTextMessage(text: string, chatId: T.U32): Promise<T.U32> {
return this.controller.rpc.miscSendTextMessage(this.accountId, text, chatId)
}
public miscSendMsg(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.controller.rpc.miscSendMsg(this.accountId, chatId, text, file, location, quotedMessageId)
}
public miscSetDraft(chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
return this.controller.rpc.miscSetDraft(this.accountId, chatId, text, file, quotedMessageId)
}
}

View File

@@ -88,6 +88,14 @@ export type Event=(({
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*/
"type":"IncomingMsg";}&{"chatId":U32;"msgId":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.
*/
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
/**
* Messages were seen or noticed.
* chat id is always set.

View File

@@ -1,6 +1,67 @@
// 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|null);};
/**
* 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.
@@ -47,14 +108,21 @@ export type Viewtype=("Unknown"|
*/
"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 I64=number;
export type I32=number;
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;
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
/**
* the contact's last seen timestamp
* Chat ephemeral message timer is changed.
*/
"lastSeen":I64;"wasSeenRecently":boolean;};
export type U64=number;
"EphemeralTimerChanged"|"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"|"WebxdcStatusUpdate"|
/**
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
*/
"WebxdcInfoMessage");
export type I32=number;
export type WebxdcMessageInfo={
/**
* The name of the app.
@@ -110,48 +178,11 @@ export type Reactions=
* 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 Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":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;
/**
* 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
* when is_info is true this describes what type of system message it is
*/
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 Usize=number;
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;};
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 MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
"systemMessageType":SystemMessageType;"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
@@ -161,20 +192,7 @@ export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"imag
* also known as summary_text2
*/
"summaryText":string;};
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
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 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 ChatListEntry=[U32,U32];
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 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=[null,[(U32|null),(U32|null)],[string,string],[U32,Message],Account,Record<string,string>,BasicChat,Contact,FullChat,Record<string,(string|null)>,Record<string,(string)[]>,Record<U32,ChatListItemFetchResult>,Record<U32,Contact>,Record<U32,Message>,Record<U32,MessageSearchResult>,Record<U32,string>,ChatVisibility,MessageNotificationInfo,Message,Viewtype,MuteDuration,([F64,F64]|null),(Message|null),(Viewtype|null),(ProviderInfo|null),(string|null),(U32|null),Qr,string,(Account)[],(ChatListEntry)[],(Contact)[],(MessageListItem)[],(Location)[],(string)[],(U32)[],WebxdcMessageInfo,boolean,I64,U32,U64,Usize];
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,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,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.101.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

@@ -2,9 +2,8 @@ import * as T from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { Event } from "../generated/events.js";
import { Context } from "../generated/context_methods.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;
@@ -36,7 +35,7 @@ export class BaseDeltaChat<
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: Context<ContextEvents>[] = [];
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
constructor(public transport: Transport) {
super();
this.rpc = new RawClient(this.transport);
@@ -63,14 +62,10 @@ export class BaseDeltaChat<
}
getContextEvents(account_id: number) {
return this.getContext(account_id);
}
getContext(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];
} else {
this.contextEmitters[account_id] = new Context(this, account_id);
this.contextEmitters[account_id] = new TinyEmitter();
return this.contextEmitters[account_id];
}
}
@@ -97,3 +92,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,26 @@
[package]
name = "deltachat-rpc-server"
version = "1.101.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.9.1" }
futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.85"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.21.2", 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

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

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

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

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

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:

753
src/authres.rs Normal file
View File

@@ -0,0 +1,753 @@
//! 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::Result;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
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_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
warn!(context, "invalid email {:#}", 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 Ok(DkimResults::default());
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres, &from_domain, message_time).await
}
#[derive(Default, 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
}
/// ## 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,
) -> 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_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?
}
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);
// Remove all foreign authentication results
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
if authres.is_empty() {
// 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;
} 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()
}
#[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::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"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx3.messagingengine.com");
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
// 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, &[]).await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
.await;
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
assert_eq!(candidates, "mx4.messagingengine.com");
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]) {
let v = incoming_ids
.iter()
.map(|id| (id.to_string(), DkimResult::Passed))
.collect();
update_authservid_candidates(context, &v).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",
]
.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)[0].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)[0].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!(
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
entry.path(),
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
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().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"Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalidfrom.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");
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");
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(())
}
}

View File

@@ -1130,8 +1130,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.
@@ -2439,7 +2439,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 {
@@ -3702,11 +3702,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?;

View File

@@ -184,6 +184,12 @@ 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,
}
impl Context {

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.
@@ -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,6 +1446,132 @@ 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 interupt",
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::*;
@@ -1810,21 +1939,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(())
}

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?
@@ -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,12 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"authserv_id_candidates",
self.get_config(Config::AuthservIdCandidates)
.await?
.unwrap_or_default(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));

View File

@@ -4,12 +4,13 @@ use std::collections::HashSet;
use anyhow::{Context as _, Result};
use mailparse::ParsedMail;
use mailparse::SingleInfo;
use crate::aheader::Aheader;
use crate::authres;
use crate::authres::handle_authres;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::headerdef::HeaderDefMap;
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
use crate::keyring::Keyring;
use crate::log::LogExt;
@@ -55,35 +56,44 @@ pub async fn try_decrypt(
.await
}
pub async fn create_decryption_info(
pub async fn prepare_decryption(
context: &Context,
mail: &ParsedMail<'_>,
from: &[SingleInfo],
message_time: i64,
) -> 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();
let from = if let Some(f) = from.first() {
&f.addr
} else {
return Ok(DecryptionInfo::default());
};
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
let 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?;
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,
})
}
#[derive(Debug)]
#[derive(Default, Debug)]
pub struct DecryptionInfo {
/// The From address. This is the address from the unnencrypted, outer
/// From header.
@@ -96,6 +106,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 +274,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 +304,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, false).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

View File

@@ -428,14 +428,14 @@ 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)
let sent2_rfc724_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
.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 +451,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

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

View File

@@ -494,6 +494,8 @@ impl Imap {
}
async fn disconnect(&mut self, context: &Context) {
info!(context, "disconnecting");
// Close folder if messages should be expunged
if let Err(err) = self.close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
@@ -902,6 +904,12 @@ impl Imap {
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
let msg_ids = received_msgs
.iter()
.flat_map(|m| m.msg_ids.clone())
.collect();
context.emit_event(EventType::IncomingMsgBunch { msg_ids });
chat::mark_old_messages_as_noticed(context, received_msgs).await?;
Ok(read_cnt > 0)

View File

@@ -54,10 +54,10 @@ impl Imap {
Interrupt(InterruptInfo),
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!(
context,
"{}: Idle entering wait-on-remote state",
watch_folder.as_deref().unwrap_or("None")
"{}: Idle entering wait-on-remote state", folder_name
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = self.idle_interrupt.recv().await;
@@ -70,26 +70,36 @@ impl Imap {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!(context, "Idle has NewData {:?}", x);
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!(context, "Idle-wait timeout or interruption");
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!(context, "Idle wait was interrupted");
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
}
Ok(Event::Interrupt(i)) => {
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
info!(context, "Idle wait was interrupted");
}
Err(err) => {
warn!(context, "Idle wait errored: {:?}", err);
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
}
}
let session = tokio::time::timeout(Duration::from_secs(15), handle.done())
.await?
.context("IMAP IDLE protocol timed out")?;
.await
.with_context(|| format!("{}: IMAP IDLE protocol timed out", folder_name))?
.with_context(|| format!("{}: IMAP IDLE failed", folder_name))?;
self.session = Some(Session { inner: session });
} else {
warn!(context, "Attempted to idle without a session");
@@ -173,6 +183,7 @@ impl Imap {
}
Event::Interrupt(info) => {
// Interrupt
info!(context, "Fake IDLE interrupted");
break info;
}
}

View File

@@ -19,35 +19,31 @@ 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 From<anyhow::Error> for Error {
fn from(err: anyhow::Error) -> Error {
Error::Other(format!("{:#}", err))
}
}
impl Imap {
/// Issues a CLOSE command to expunge selected folder.
///
/// 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<()> {
pub(super) async fn close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(ref folder) = self.config.selected_folder {
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);
let session = self.session.as_mut().context("no session")?;
if let Err(err) = session.close().await.context("IMAP close/expunge failed") {
self.trigger_reconnect(context).await;
return Err(err);
}
info!(context, "close/expunge succeeded");
}
self.config.selected_folder = None;
self.config.selected_folder_needs_expunge = false;
@@ -56,7 +52,7 @@ impl Imap {
}
/// Issues a CLOSE command if selected folder needs expunge.
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> Result<()> {
pub(crate) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if self.config.selected_folder_needs_expunge {
self.close_folder(context).await?;
}

View File

@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use futures::{StreamExt, TryStreamExt};
use futures::StreamExt;
use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
@@ -17,7 +17,6 @@ use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
@@ -31,6 +30,7 @@ use crate::tools::{
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
EmailAddress,
};
use crate::{e2ee, tools};
// Name of the database file in the backup.
const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite";
@@ -576,10 +576,7 @@ async fn export_backup_inner(
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
let read_dir: Vec<_> =
tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(context.get_blobdir()).await?)
.try_collect()
.await?;
let read_dir = tools::read_dir(context.get_blobdir()).await?;
let count = read_dir.len();
let mut written_files = 0;

View File

@@ -18,7 +18,11 @@
clippy::mixed_read_write_in_expression,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string
clippy::format_push_string,
clippy::bool_to_int_with_if,
// This lint can be re-enabled once we don't target
// Rust 1.56 anymore:
clippy::collapsible_str_replace
)]
#[macro_use]
@@ -93,6 +97,7 @@ mod update_helper;
pub mod webxdc;
#[macro_use]
mod dehtml;
mod authres;
mod color;
pub mod html;
pub mod plaintext;

View File

@@ -726,7 +726,7 @@ impl Message {
self.text = text;
}
pub fn set_file(&mut self, file: impl AsRef<str>, filemime: Option<&str>) {
pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) {
self.param.set(Param::File, file);
if let Some(filemime) = filemime {
self.param.set(Param::MimeType, filemime);
@@ -1122,6 +1122,28 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result<String> {
}
if !msg.rfc724_mid.is_empty() {
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
let server_uids = context
.sql
.query_map(
"SELECT folder, uid FROM imap WHERE rfc724_mid=?",
paramsv![msg.rfc724_mid],
|row| {
let folder: String = row.get("folder")?;
let uid: u32 = row.get("uid")?;
Ok((folder, uid))
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?;
for (folder, uid) in server_uids {
// Format as RFC 5092 relative IMAP URL.
ret += &format!("\n</{}/;UID={}>", folder, uid);
}
}
let hop_info: Option<String> = context
.sql

View File

@@ -1311,7 +1311,7 @@ impl<'a> MimeFactory<'a> {
/// This line length limit is an
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
fn wrapped_base64_encode(buf: &[u8]) -> String {
let base64 = base64::encode(&buf);
let base64 = base64::encode(buf);
let mut chars = base64.chars();
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
.take_while(|s| !s.is_empty())

View File

@@ -15,7 +15,7 @@ use crate::blob::BlobObject;
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
use crate::contact::{addr_cmp, addr_normalize, ContactId};
use crate::context::Context;
use crate::decrypt::{create_decryption_info, try_decrypt};
use crate::decrypt::{prepare_decryption, try_decrypt};
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::format_flowed::unformat_flowed;
@@ -178,7 +178,7 @@ impl MimeMessage {
.get_header_value(HeaderDef::Date)
.and_then(|v| mailparse::dateparse(&v).ok())
.unwrap_or_default();
let hop_info = parse_receive_headers(&mail.get_headers());
let mut hop_info = parse_receive_headers(&mail.get_headers());
let mut headers = Default::default();
let mut recipients = Default::default();
@@ -220,7 +220,9 @@ impl MimeMessage {
let mut mail_raw = Vec::new();
let mut gossiped_addr = Default::default();
let mut from_is_signed = false;
let mut decryption_info = create_decryption_info(context, &mail, message_time).await?;
let mut decryption_info = prepare_decryption(context, &mail, &from, message_time).await?;
hop_info += "\n\n";
hop_info += &decryption_info.dkim_results.to_string();
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
let (mail, signatures, warn_empty_signature) =
@@ -296,6 +298,8 @@ impl MimeMessage {
if let Some(peerstate) = &mut decryption_info.peerstate {
if message_time > peerstate.last_seen_autocrypt
&& mail.ctype.mimetype != "multipart/report"
// Disallowing keychanges is disabled for now:
// && decryption_info.dkim_results.allow_keychange
{
peerstate.degrade_encryption(message_time);
peerstate.save_to_db(&context.sql, false).await?;
@@ -369,6 +373,12 @@ impl MimeMessage {
parser.heuristically_parse_ndn(context).await;
parser.parse_headers(context).await?;
// Disallowing keychanges is disabled for now
// if !decryption_info.dkim_results.allow_keychange {
// for part in parser.parts.iter_mut() {
// part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string());
// }
// }
if warn_empty_signature && parser.signatures.is_empty() {
for part in parser.parts.iter_mut() {
part.error = Some("No valid signature".to_string());

View File

@@ -266,8 +266,8 @@ impl Params {
}
/// Set the given key to the passed in value.
pub fn set(&mut self, key: Param, value: impl AsRef<str>) -> &mut Self {
self.inner.insert(key, value.as_ref().to_string());
pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self {
self.inner.insert(key, value.to_string());
self
}

View File

@@ -196,35 +196,36 @@ impl Peerstate {
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
let res = Peerstate {
addr: row.get(0)?,
last_seen: row.get(1)?,
last_seen_autocrypt: row.get(2)?,
prefer_encrypt: EncryptPreference::from_i32(row.get(3)?).unwrap_or_default(),
addr: row.get("addr")?,
last_seen: row.get("last_seen")?,
last_seen_autocrypt: row.get("last_seen_autocrypt")?,
prefer_encrypt: EncryptPreference::from_i32(row.get("prefer_encrypted")?)
.unwrap_or_default(),
public_key: row
.get(4)
.get("public_key")
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
public_key_fingerprint: row
.get::<_, Option<String>>(7)?
.get::<_, Option<String>>("public_key_fingerprint")?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_key: row
.get(6)
.get("gossip_key")
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
gossip_key_fingerprint: row
.get::<_, Option<String>>(8)?
.get::<_, Option<String>>("gossip_key_fingerprint")?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),
gossip_timestamp: row.get(5)?,
gossip_timestamp: row.get("gossip_timestamp")?,
verified_key: row
.get(9)
.get("verified_key")
.ok()
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
verified_key_fingerprint: row
.get::<_, Option<String>>(10)?
.get::<_, Option<String>>("verified_key_fingerprint")?
.map(|s| s.parse::<Fingerprint>())
.transpose()
.unwrap_or_default(),

View File

@@ -44,12 +44,12 @@ impl PlainText {
let line = line.to_string().replace('\r', "");
let mut line = LINKIFY_MAIL_RE
.replace_all(&*line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.replace_all(&line, "\rLTa href=\rQUOTmailto:$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();
line = LINKIFY_URL_RE
.replace_all(&*line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.replace_all(&line, "\rLTa href=\rQUOT$1\rQUOT\rGT$1\rLT/a\rGT")
.as_ref()
.to_string();

View File

@@ -230,7 +230,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
.await
.with_context(|| format!("can't check if address {:?} is our address", addr))?
{
if token::exists(context, token::Namespace::InviteNumber, &*invitenumber).await {
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
Ok(Qr::WithdrawVerifyGroup {
grpname,
grpid,
@@ -260,7 +260,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
})
}
} else if context.is_self_addr(addr).await? {
if token::exists(context, token::Namespace::InviteNumber, &*invitenumber).await {
if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await {
Ok(Qr::WithdrawVerifyContact {
contact_id,
fingerprint,

View File

@@ -57,7 +57,7 @@ impl From<&str> for Reaction {
.split_ascii_whitespace()
.filter(|&emoji| emoji.len() < 30)
.collect();
emojis.sort();
emojis.sort_unstable();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }
@@ -84,7 +84,7 @@ impl Reaction {
pub fn add(&self, other: Self) -> Self {
let mut emojis: Vec<&str> = self.emojis();
emojis.append(&mut other.emojis());
emojis.sort();
emojis.sort_unstable();
emojis.dedup();
let reaction = emojis.join(" ");
Self { reaction }

View File

@@ -173,10 +173,13 @@ pub(crate) async fn receive_imf_inner(
.await?;
let rcvd_timestamp = smeared_time(context).await;
// Sender timestamp is allowed to be a bit in the future due to
// unsynchronized clocks, but not too much.
let sent_timestamp = mime_parser
.get_header(HeaderDef::Date)
.and_then(|value| mailparse::dateparse(value).ok())
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp));
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
// Add parts
let received_msg = add_parts(
@@ -1901,7 +1904,7 @@ async fn apply_mailinglist_changes(
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::load_from_db(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, &listid);
contact.param.set(Param::ListId, listid);
contact.update_param(context).await?;
}
@@ -1909,7 +1912,7 @@ async fn apply_mailinglist_changes(
if list_post != old_list_post {
// Apparently the mailing list is using a different List-Post header in each message.
// Make the mailing list read-only because we would't know which message the user wants to reply to.
chat.param.set(Param::ListPost, "");
chat.param.remove(Param::ListPost);
chat.update_param(context).await?;
}
} else {
@@ -2986,7 +2989,7 @@ mod tests {
assert!(chat.can_send(&t.ctx).await?);
assert_eq!(
chat.get_mailinglist_addr(),
"reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com"
Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com")
);
assert_eq!(chat.name, "deltachat/deltachat-core-rust");
assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1);
@@ -2995,7 +2998,7 @@ mod tests {
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?;
assert!(!chat.can_send(&t.ctx).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
assert_eq!(chats.len(), 1);
@@ -3054,7 +3057,7 @@ mod tests {
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
assert_eq!(chat.name, "delta-dev");
assert!(chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "delta@codespeak.net");
assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net"));
let msg = get_chat_msg(&t, chat_id, 0, 1).await;
let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap();
@@ -3146,6 +3149,7 @@ Hello mailinglist!\r\n"
.unwrap();
receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap();
t.evtracker.wait_next_incoming_message().await;
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 1);
let chat_id = chats.get_chat_id(0).unwrap();
@@ -3158,7 +3162,6 @@ Hello mailinglist!\r\n"
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
assert_eq!(chats.len(), 0); // Test that the message disappeared
t.evtracker.consume_events();
receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap();
// Check that no notification is displayed for blocked mailing list message.
@@ -3317,7 +3320,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.name, "ola");
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1);
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
// receive another message with no sender name but the same address,
// make sure this lands in the same chat
@@ -3370,7 +3373,7 @@ Hello mailinglist!\r\n"
);
assert_eq!(chat.name, "Atlas Obscura");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
Ok(())
}
@@ -3399,7 +3402,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de");
assert_eq!(chat.name, "DHL Paket");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
Ok(())
}
@@ -3428,7 +3431,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de");
assert_eq!(chat.name, "DPD");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
Ok(())
}
@@ -3449,7 +3452,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "96540.xt.local");
assert_eq!(chat.name, "Microsoft Store");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
receive_imf(
&t,
@@ -3462,7 +3465,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "121231234.xt.local");
assert_eq!(chat.name, "DER SPIEGEL Kundenservice");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
Ok(())
}
@@ -3485,7 +3488,7 @@ Hello mailinglist!\r\n"
assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com");
assert_eq!(chat.name, "xing.com");
assert!(!chat.can_send(&t).await?);
assert_eq!(chat.get_mailinglist_addr(), "");
assert_eq!(chat.get_mailinglist_addr(), None);
Ok(())
}

View File

@@ -5,6 +5,7 @@ use futures_lite::FutureExt;
use tokio::task;
use crate::config::Config;
use crate::contact::{ContactId, RecentlySeenLoop};
use crate::context::Context;
use crate::ephemeral::{self, delete_expired_imap_messages};
use crate::imap::Imap;
@@ -35,6 +36,8 @@ pub(crate) struct Scheduler {
ephemeral_interrupt_send: Sender<()>,
location_handle: task::JoinHandle<()>,
location_interrupt_send: Sender<()>,
recently_seen_loop: RecentlySeenLoop,
}
impl Context {
@@ -79,6 +82,12 @@ impl Context {
scheduler.interrupt_location();
}
}
pub(crate) async fn interrupt_recently_seen(&self, contact_id: ContactId, timestamp: i64) {
if let Some(scheduler) = &*self.scheduler.read().await {
scheduler.interrupt_recently_seen(contact_id, timestamp);
}
}
}
async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConnectionHandlers) {
@@ -157,112 +166,117 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
.await;
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder: Config) -> InterruptInfo {
match ctx.get_config(folder).await {
Ok(Some(watch_folder)) => {
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages failed")
{
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
if connection.can_idle() {
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{}", err);
InterruptInfo::new(false)
}
}
} else {
connection.fake_idle(ctx, Some(watch_folder)).await
}
}
Ok(None) => {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder);
connection.fake_idle(ctx, None).await
}
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) -> InterruptInfo {
let folder = match ctx.get_config(folder_config).await {
Ok(folder) => folder,
Err(err) => {
warn!(
ctx,
"Can not watch {} folder, failed to retrieve config: {:?}", folder, err
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
);
connection.fake_idle(ctx, None).await
return connection.fake_idle(ctx, None).await;
}
};
let watch_folder = if let Some(watch_folder) = folder {
watch_folder
} else {
connection.connectivity.set_not_configured(ctx).await;
info!(ctx, "Can not watch {} folder, not set", folder_config);
return connection.fake_idle(ctx, None).await;
};
// connect and fake idle if unable to connect
if let Err(err) = connection.prepare(ctx).await {
warn!(ctx, "imap connection failed: {}", err);
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
if folder_config == Config::ConfiguredInboxFolder {
if let Err(err) = connection
.store_seen_flags_on_imap(ctx)
.await
.context("store_seen_flags_on_imap failed")
{
warn!(ctx, "{:#}", err);
}
}
// Fetch the watched folder.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
.context("fetch_move_delete")
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
// Mark expired messages for deletion. Marked messages will be deleted from the server
// on the next iteration of `fetch_move_delete`. `delete_expired_imap_messages` is not
// called right before `fetch_move_delete` because it is not well optimized and would
// otherwise slow down message fetching.
if let Err(err) = delete_expired_imap_messages(ctx)
.await
.context("delete_expired_imap_messages")
{
warn!(ctx, "{:#}", err);
}
// Scan additional folders only after finishing fetching the watched folder.
//
// On iOS the application has strictly limited time to work in background, so we may not
// be able to scan all folders before time is up if there are many of them.
if folder_config == Config::ConfiguredInboxFolder {
// Only scan on the Inbox thread in order to prevent parallel scans, which might lead to duplicate messages
match connection.scan_folders(ctx).await.context("scan_folders") {
Err(err) => {
// Don't reconnect, if there is a problem with the connection we will realize this when IDLEing
// but maybe just one folder can't be selected or something
warn!(ctx, "{:#}", err);
}
Ok(true) => {
// Fetch the watched folder again in case scanning other folder moved messages
// there.
//
// In most cases this will select the watched folder and return because there are
// no new messages. We want to select the watched folder anyway before going IDLE
// there, so this does not take additional protocol round-trip.
if let Err(err) = connection
.fetch_move_delete(ctx, &watch_folder, false)
.await
.context("fetch_move_delete after scan_folders")
{
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
return InterruptInfo::new(false);
}
}
Ok(false) => {}
}
}
// Synchronize Seen flags.
connection
.sync_seen_flags(ctx, &watch_folder)
.await
.context("sync_seen_flags")
.ok_or_log(ctx);
connection.connectivity.set_connected(ctx).await;
// idle
if !connection.can_idle() {
return connection.fake_idle(ctx, Some(watch_folder)).await;
}
match connection.idle(ctx, Some(watch_folder)).await {
Ok(v) => v,
Err(err) => {
connection.trigger_reconnect(ctx).await;
warn!(ctx, "{:#}", err);
InterruptInfo::new(false)
}
}
}
@@ -271,11 +285,11 @@ async fn simple_imap_loop(
ctx: Context,
started: Sender<()>,
inbox_handlers: ImapConnectionHandlers,
folder: Config,
folder_config: Config,
) {
use futures::future::FutureExt;
info!(ctx, "starting simple loop for {}", folder.as_ref());
info!(ctx, "starting simple loop for {}", folder_config);
let ImapConnectionHandlers {
mut connection,
stop_receiver,
@@ -291,7 +305,7 @@ async fn simple_imap_loop(
}
loop {
fetch_idle(&ctx, &mut connection, folder).await;
fetch_idle(&ctx, &mut connection, folder_config).await;
}
};
@@ -472,6 +486,8 @@ impl Scheduler {
})
};
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());
let res = Self {
inbox,
mvbox,
@@ -485,6 +501,7 @@ impl Scheduler {
ephemeral_interrupt_send,
location_handle,
location_interrupt_send,
recently_seen_loop,
};
// wait for all loops to be started
@@ -539,6 +556,10 @@ impl Scheduler {
self.location_interrupt_send.try_send(()).ok();
}
fn interrupt_recently_seen(&self, contact_id: ContactId, timestamp: i64) {
self.recently_seen_loop.interrupt(contact_id, timestamp);
}
/// Halt the scheduler.
///
/// It consumes the scheduler and never fails to stop it. In the worst case, long-running tasks
@@ -574,6 +595,7 @@ impl Scheduler {
.ok_or_log(context);
self.ephemeral_handle.abort();
self.location_handle.abort();
self.recently_seen_loop.abort();
}
}

View File

@@ -405,7 +405,7 @@ impl Context {
ret += " <b>";
ret += &*escaper::encode_minimal(&foldername);
ret += ":</b> ";
ret += &*escaper::encode_minimal(&*detailed.to_string_imap(self).await);
ret += &*escaper::encode_minimal(&detailed.to_string_imap(self).await);
ret += "</li>";
folder_added = true;

View File

@@ -609,6 +609,13 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
92
).await?;
}
if dbversion < 93 {
sql.execute_migration(
"CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
93,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)

View File

@@ -74,6 +74,14 @@ impl TestContextManager {
.await
}
/// Creates a new unconfigured test account.
pub async fn unconfigured(&mut self) -> TestContext {
TestContext::builder()
.with_log_sink(self.log_tx.clone())
.build()
.await
}
/// Writes info events to the log that mark a section, e.g.:
///
/// ========== `msg` goes here ==========
@@ -89,19 +97,23 @@ impl TestContextManager {
/// - Let the other TestContext receive it and accept the chat
/// - Assert that the message arrived
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
let received_msg = self.try_send_recv(from, to, msg).await;
assert_eq!(received_msg.text.as_ref().unwrap(), msg);
received_msg.chat_id.accept(to).await.unwrap();
}
/// - Let one TestContext send a message
/// - Let the other TestContext receive it
pub async fn try_send_recv(&self, from: &TestContext, to: &TestContext, msg: &str) -> Message {
self.section(&format!(
"{} sends a message '{}' to {}",
from.name(),
msg,
to.name()
));
let chat = from.create_chat(to).await;
let sent = from.send_text(chat.id, msg).await;
let received_msg = to.recv_msg(&sent).await;
received_msg.chat_id.accept(to).await.unwrap();
assert_eq!(received_msg.text.unwrap(), msg);
to.recv_msg(&sent).await
}
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
@@ -369,6 +381,12 @@ impl TestContext {
///
/// Panics if there is no message or on any error.
pub async fn pop_sent_msg(&self) -> SentMessage {
self.pop_sent_msg_opt(Duration::from_secs(3))
.await
.expect("no sent message found in jobs table")
}
pub async fn pop_sent_msg_opt(&self, timeout: Duration) -> Option<SentMessage> {
let start = Instant::now();
let (rowid, msg_id, payload, recipients) = loop {
let row = self
@@ -393,25 +411,25 @@ impl TestContext {
if let Some(row) = row {
break row;
}
if start.elapsed() < Duration::from_secs(3) {
if start.elapsed() < timeout {
tokio::time::sleep(Duration::from_millis(100)).await;
} else {
panic!("no sent message found in jobs table");
return None;
}
};
self.ctx
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![rowid])
.execute("DELETE FROM smtp WHERE id=?;", paramsv![rowid])
.await
.expect("failed to remove job");
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
SentMessage {
Some(SentMessage {
payload,
sender_msg_id: msg_id,
recipients,
}
})
}
/// Parses a message.
@@ -725,7 +743,7 @@ impl Drop for LogSink {
/// passed through a SMTP-IMAP pipeline.
#[derive(Debug, Clone)]
pub struct SentMessage {
payload: String,
pub payload: String,
recipients: String,
pub sender_msg_id: MsgId,
}
@@ -858,9 +876,10 @@ impl EventTracker {
.await
}
/// Consumes all pending events.
pub fn consume_events(&self) {
while self.try_recv().is_ok() {}
/// Wait for the next IncomingMsg event.
pub async fn wait_next_incoming_message(&self) {
self.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
}
}

View File

@@ -12,7 +12,7 @@ use std::time::{Duration, SystemTime};
use anyhow::{bail, Error, Result};
use chrono::{Local, TimeZone};
use futures::StreamExt;
use futures::{StreamExt, TryStreamExt};
use mailparse::dateparse;
use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
@@ -272,7 +272,7 @@ pub(crate) fn create_id() -> String {
rng.fill(&mut arr[..]);
// Take 11 base64 characters containing 66 random bits.
base64::encode_config(&arr, base64::URL_SAFE)
base64::encode_config(arr, base64::URL_SAFE)
.chars()
.take(11)
.collect()
@@ -495,6 +495,13 @@ pub fn open_file_std<P: AsRef<std::path::Path>>(
}
}
pub async fn read_dir(path: &Path) -> Result<Vec<fs::DirEntry>> {
let res = tokio_stream::wrappers::ReadDirStream::new(fs::read_dir(path).await?)
.try_collect()
.await?;
Ok(res)
}
pub(crate) fn time() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
@@ -715,7 +722,9 @@ hi
Message-ID: 2dfdbde7@example.org
Hop: From: localhost; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:22 +0000
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000";
Hop: From: hq5.merlinux.eu; By: hq5.merlinux.eu; Date: Sat, 14 Sep 2019 17:00:25 +0000
DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
check_parse_receive_headers_integration(raw, expected).await;
let raw = include_bytes!("../test-data/message/encrypted_with_received_headers.eml");
@@ -732,7 +741,9 @@ Message-ID: Mr.adQpEwndXLH.LPDdlFVJ7wG@example.net
Hop: From: [127.0.0.1]; By: mail.example.org; Date: Mon, 27 Dec 2021 11:21:21 +0000
Hop: From: mout.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000";
Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22 +0000
DKIM Results: Passed=true, Works=true, Allow_Keychange=true";
check_parse_receive_headers_integration(raw, expected).await;
}

View File

@@ -496,7 +496,7 @@ impl Context {
for update_item in updates.updates {
self.create_status_update_record(
&mut instance,
&*serde_json::to_string(&update_item)?,
&serde_json::to_string(&update_item)?,
timestamp,
can_info_msg,
from_id,
@@ -545,7 +545,7 @@ impl Context {
let (update_item_str, serial) = row;
let update_item = StatusUpdateItemAndSerial
{
item: serde_json::from_str(&*update_item_str)?,
item: serde_json::from_str(&update_item_str)?,
serial,
max_serial,
};
@@ -553,7 +553,7 @@ impl Context {
if !json.is_empty() {
json.push_str(",\n");
}
json.push_str(&*serde_json::to_string(&update_item)?);
json.push_str(&serde_json::to_string(&update_item)?);
}
Ok(json)
},
@@ -1825,7 +1825,7 @@ sth_for_the = "future""#
let instance = t.get_last_msg().await;
let html = instance.get_webxdc_blob(&t, "index.html").await?;
assert!(String::from_utf8_lossy(&*html).contains("requires a newer Delta Chat version"));
assert!(String::from_utf8_lossy(&html).contains("requires a newer Delta Chat version"));
Ok(())
}

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas106.aol.mail.ne1.yahoo.com;
dkim=pass header.i=@buzon.uy header.s=2019;
spf=pass smtp.mailfrom=buzon.uy;
dmarc=pass(p=REJECT) header.from=buzon.uy;
From: <alice@buzon.uy>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
dkim=unknown;
spf=none smtp.mailfrom=delta.blinzeln.de;
dmarc=unknown header.from=delta.blinzeln.de;
From: <alice@delta.blinzeln.de>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas210.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@disroot.org header.s=mail;
spf=pass smtp.mailfrom=disroot.org;
dmarc=pass(p=QUARANTINE) header.from=disroot.org;
From: <alice@disroot.org>
To: <alice@aol.com>

View File

@@ -0,0 +1,7 @@
Authentication-Results: atlas105.aol.mail.ne1.yahoo.com;
dkim=pass header.i=@fastmail.com header.s=fm2;
dkim=pass header.i=@messagingengine.com header.s=fm2;
spf=pass smtp.mailfrom=fastmail.com;
dmarc=pass(p=NONE,sp=NONE) header.from=fastmail.com;
From: <alice@fastmail.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas-baseline-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@gmail.com header.s=20210112;
spf=pass smtp.mailfrom=gmail.com;
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=gmail.com;
From: <alice@gmail.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,8 @@
Authentication-Results: atlas112.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@hotmail.com header.s=selector1;
spf=pass smtp.mailfrom=hotmail.com;
dmarc=pass(p=NONE) header.from=hotmail.com;
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@hotmail.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas101.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@icloud.com header.s=1a1hai;
spf=pass smtp.mailfrom=icloud.com;
dmarc=pass(p=QUARANTINE,sp=QUARANTINE) header.from=icloud.com;
From: <alice@icloud.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@ik.me header.s=20200325;
spf=pass smtp.mailfrom=ik.me;
dmarc=pass(p=REJECT) header.from=ik.me;
From: <alice@ik.me>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas104.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@mail.ru header.s=mail4;
spf=pass smtp.mailfrom=mail.ru;
dmarc=pass(p=REJECT) header.from=mail.ru;
From: <alice@mail.ru>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas211.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@mailo.com header.s=mailo;
spf=pass smtp.mailfrom=mailo.com;
dmarc=pass(p=NONE) header.from=mailo.com;
From: <alice@mailo.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,8 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@outlook.com header.s=selector1;
spf=pass smtp.mailfrom=outlook.com;
dmarc=pass(p=NONE,sp=QUARANTINE) header.from=outlook.com;
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@outlook.com>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas-production.v2-mail-prod1-gq1.omega.yahoo.com;
dkim=pass header.i=@posteo.de header.s=2017;
spf=pass smtp.mailfrom=posteo.de;
dmarc=pass(p=NONE) header.from=posteo.de;
From: <alice@posteo.de>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas114.aol.mail.bf1.yahoo.com;
dkim=pass header.i=@yandex.ru header.s=mail;
spf=pass smtp.mailfrom=yandex.ru;
dmarc=pass(p=NONE) header.from=yandex.ru;
From: <alice@yandex.ru>
To: <alice@aol.com>

View File

@@ -0,0 +1,6 @@
Authentication-Results: atlas206.aol.mail.ne1.yahoo.com;
dkim=unknown;
spf=none smtp.mailfrom=delta.blinzeln.de;
dmarc=unknown header.from=delta.blinzeln.de;
From: forged-authres-added@example.com
Authentication-Results: aaa.com; dkim=pass header.i=@example.com

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=aol.com header.i=@aol.com header.b="sjmqxpKe";
dkim-atps=neutral
From: <alice@aol.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,3 @@
From: <alice@delta.blinzeln.de>
To: <alice@buzon.uy>
Authentication-Results: secure-mailgate.com; auth=pass smtp.auth=91.203.111.88@webbox222.server-home.org

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="L9SmOHOj";
dkim-atps=neutral
From: <alice@disroot.org>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,6 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=fastmail.com header.i=@fastmail.com header.b="kLB05is1";
dkim=pass (2048-bit key; unprotected) header.d=messagingengine.com header.i=@messagingengine.com header.b="B8mfR89g";
dkim-atps=neutral
From: <alice@fastmail.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="Ngf1X5eN";
dkim-atps=neutral
From: <alice@gmail.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,7 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=hotmail.com header.i=@hotmail.com header.b="dEHn9Szj";
dkim-atps=neutral
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@hotmail.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=icloud.com header.i=@icloud.com header.b="rAXD4xVN";
dkim-atps=neutral
From: <alice@icloud.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (1024-bit key; secure) header.d=ik.me header.i=@ik.me header.b="EWWQpVZX";
dkim-atps=neutral
From: <alice@ik.me>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; secure) header.d=mail.de header.i=@mail.de header.b="18cRkjHf";
dkim-atps=neutral
From: <alice@mail.de>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,6 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=mail.ru header.i=@mail.ru header.b="uXBGAnnn";
dkim-atps=neutral
From: <alice@mail.ru>
To: <alice@buzon.uy>
Authentication-Results: smtpng1.m.smailru.net; auth=pass smtp.auth=alice@mail.ru smtp.mailfrom=alice@mail.ru

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (1024-bit key; unprotected) header.d=mailo.com header.i=@mailo.com header.b="awx9eOw9";
dkim-atps=neutral
From: <alice@mailo.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,7 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=outlook.com header.i=@outlook.com header.b="Uq5LH/n/";
dkim-atps=neutral
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
dkim=none; arc=none
From: <alice@outlook.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; secure) header.d=posteo.de header.i=@posteo.de header.b="AyOucyBM";
dkim-atps=neutral
From: <alice@posteo.de>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (1024-bit key; secure) header.d=riseup.net header.i=@riseup.net header.b="eQhRD1BM";
dkim-atps=neutral
From: <alice@riseup.net>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,5 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (2048-bit key; unprotected) header.d=yahoo.com header.i=@yahoo.com header.b="a1T2PpDI";
dkim-atps=neutral
From: <alice@yahoo.com>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,6 @@
Authentication-Results: mail.buzon.uy;
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.b="oGBtMMzR";
dkim-atps=neutral
Authentication-Results: iva4-143b1447cf50.qloud-c.yandex.net; dkim=pass header.i=@yandex.ru
From: <alice@yandex.ru>
To: <alice@buzon.uy>

View File

@@ -0,0 +1,3 @@
From: forged-authres-added@example.com
Authentication-Results: aaa.com; dkim=pass header.i=@example.com
Authentication-Results: aaa.com; dkim=pass header.i=@example.com

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