Compare commits

..

211 Commits

Author SHA1 Message Date
Hocuri
8d4af85cb1 Filter unconventional commits, they cluttered the changelog too much 2023-05-06 19:22:10 +02:00
Hocuri
f637278cb1 Make PR references clickable 2023-05-06 18:44:35 +02:00
Hocuri
a202771d36 Improve template: Always include lines that start with 'BREAKING CHANGE' 2023-05-06 18:41:48 +02:00
Hocuri
833d30123c feat: Commmmmmit
BREAKING CHANGE: this breaks everything
2023-05-06 17:47:16 +02:00
Hocuri
b6c0b5b66e Rename mergable check to "Conventional Commits" 2023-05-06 15:30:52 +02:00
Hocuri
f30a1a3865 chore: Add git-cliff as a changelog generation tool
Changelogs will be generated using commit messages by
`scripts/release.py`.

Both the commit message and the PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)
2023-05-06 15:20:58 +02:00
dependabot[bot]
3a25d6a44e Merge pull request #4362 from deltachat/dependabot/cargo/human-panic-1.1.4 2023-05-06 11:35:30 +00:00
dependabot[bot]
0688895022 Merge pull request #4360 from deltachat/dependabot/cargo/axum-0.6.18 2023-05-05 21:35:26 +00:00
dependabot[bot]
ce0e5416e6 cargo: bump human-panic from 1.1.3 to 1.1.4
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/rust-cli/human-panic/releases)
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v1.1.3...v1.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-05 21:24:47 +00:00
link2xt
7918a6801e scripts/deny.sh: update package cache before running cargo deny 2023-05-05 21:23:10 +00:00
link2xt
6af631e8df Add JSON-RPC API to get reactions 2023-05-05 20:49:55 +00:00
dependabot[bot]
625ecaa9b5 cargo: bump axum from 0.6.12 to 0.6.18
Bumps [axum](https://github.com/tokio-rs/axum) from 0.6.12 to 0.6.18.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.6.12...axum-v0.6.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-05 20:31:03 +00:00
dependabot[bot]
24fe1b9c15 Merge pull request #4364 from deltachat/dependabot/cargo/tokio-1.28.0 2023-05-05 20:29:57 +00:00
link2xt
ba36d09c70 Make the bots automatically accept group chat contact requests 2023-05-05 16:37:34 +00:00
link2xt
f57be7187e Update deny.toml 2023-05-05 13:23:44 +00:00
dependabot[bot]
6a00338f79 cargo: bump tokio from 1.27.0 to 1.28.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.27.0 to 1.28.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.27.0...tokio-1.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-05 10:48:24 +00:00
dependabot[bot]
16906210e1 Merge pull request #4354 from deltachat/dependabot/cargo/tokio-util-0.7.8 2023-05-05 10:47:12 +00:00
dependabot[bot]
f9b4540387 Merge pull request #4358 from deltachat/dependabot/cargo/syn-2.0.15 2023-05-05 10:46:48 +00:00
dependabot[bot]
9755438d0d Merge pull request #4366 from deltachat/dependabot/cargo/serde-1.0.160 2023-05-05 10:44:58 +00:00
dependabot[bot]
fe9534ed7d cargo: bump serde from 1.0.159 to 1.0.160
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.159 to 1.0.160.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.159...v1.0.160)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 10:55:01 +00:00
dependabot[bot]
134c9ada68 cargo: bump syn from 2.0.13 to 2.0.15
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.13 to 2.0.15.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.13...2.0.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 10:54:57 +00:00
dependabot[bot]
a3240452ff cargo: bump tokio-util from 0.7.7 to 0.7.8
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.7 to 0.7.8.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.7...tokio-util-0.7.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 10:54:42 +00:00
dependabot[bot]
84beb6647d Merge pull request #4357 from deltachat/dependabot/cargo/serde_json-1.0.96 2023-05-03 10:53:51 +00:00
dependabot[bot]
ecf7e2d909 Merge pull request #4359 from deltachat/dependabot/cargo/libc-0.2.142 2023-05-03 10:10:06 +00:00
dependabot[bot]
fcfcf4bbf3 Merge pull request #4365 from deltachat/dependabot/cargo/quick-xml-0.28.2 2023-05-03 10:09:37 +00:00
dependabot[bot]
c62b6d77b7 Merge pull request #4368 from deltachat/dependabot/cargo/anyhow-1.0.71 2023-05-03 10:09:14 +00:00
dependabot[bot]
7e51b9686f cargo: bump quick-xml from 0.28.1 to 0.28.2
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.28.1 to 0.28.2.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.28.1...v0.28.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 01:54:26 +00:00
dependabot[bot]
542ec4cac4 cargo: bump anyhow from 1.0.70 to 1.0.71
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.70 to 1.0.71.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.70...1.0.71)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 01:54:07 +00:00
dependabot[bot]
64b25d9ec0 cargo: bump serde_json from 1.0.95 to 1.0.96
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.95 to 1.0.96.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.95...v1.0.96)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 01:54:04 +00:00
dependabot[bot]
f91b6fbdf0 Merge pull request #4363 from deltachat/dependabot/cargo/reqwest-0.11.17 2023-05-03 01:53:12 +00:00
dependabot[bot]
41445a506e Merge pull request #4369 from deltachat/dependabot/cargo/futures-lite-1.13.0 2023-05-03 01:52:22 +00:00
dependabot[bot]
798db9d019 Merge pull request #4367 from deltachat/dependabot/cargo/tokio-stream-0.1.14 2023-05-02 23:48:34 +00:00
dependabot[bot]
2e860c32ab Merge pull request #4355 from deltachat/dependabot/cargo/uuid-1.3.2 2023-05-02 23:46:59 +00:00
dependabot[bot]
0e1faed6e5 cargo: bump futures-lite from 1.12.0 to 1.13.0
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v1.12.0...v1.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:03:16 +00:00
dependabot[bot]
f5de3be977 cargo: bump tokio-stream from 0.1.12 to 0.1.14
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.12 to 0.1.14.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Changelog](https://github.com/tokio-rs/tokio/blob/tokio-0.1.14/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.12...tokio-0.1.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:02:36 +00:00
dependabot[bot]
4b0a30eb66 cargo: bump reqwest from 0.11.16 to 0.11.17
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.16 to 0.11.17.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.16...v0.11.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 22:00:52 +00:00
dependabot[bot]
7710467571 cargo: bump libc from 0.2.140 to 0.2.142
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.140 to 0.2.142.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.140...0.2.142)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:59:05 +00:00
dependabot[bot]
1687794b81 cargo: bump uuid from 1.3.0 to 1.3.2
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.3.0 to 1.3.2.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.3.0...1.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-01 21:57:42 +00:00
link2xt
158541f05c Document deltachat_rpc_client installation 2023-04-29 13:53:57 +00:00
link2xt
eb28899cd0 Add 1.112.8 link to the changelog 2023-04-27 10:28:38 +00:00
link2xt
432046225a Fix 1.114.0 link in the changelog 2023-04-27 10:27:39 +00:00
Simon Laux
bf958ce6c1 More context for chat and chatlist errors (#3932) 2023-04-25 17:20:26 -04:00
Simon Laux
ea8ee4e67d jsonrpc: Remove message id from chatlist item (#3071)
Why? because desktop currently fetches the chatlist multiple times, even though it just needs the
chatlistitem for one chat.

Note: @r10s was worried before that exposing the method to get a single updated chatlistitem could
lead to race conditions where the chatlist item is newer than the chatlist order. But I don't think
this will change anything for desktop besides making it a little faster (because currently desktop
fetches the whole chatlist instead of just the entry it needs when an entry updates).
2023-04-25 17:20:26 -04:00
link2xt
edfdbbdc90 Release 1.114.0 2023-04-24 22:31:17 +00:00
Simon Laux
1b8bfef441 jsonrpc: make event loop in ts client optional (#4347)
* jsonrpc: make event loop in ts client optional

* jsonrpc: ts: fix tests and make specifying `startEventLoop` mandatory

* add changelog entry

* Update CHANGELOG.md
2023-04-24 23:18:55 +02:00
link2xt
514074de8b JSON-RPC: retrieve events via long polling
This way is more compatible to JSON-RPC libraries
that do not support receiving notifications from the server
and allows describing event types in the OpenRPC specification.

Event thread converting events to notifications in the FFI
is removed, so it is now possible to construct a dc_jsonrpc_instance_t
while still retrieving events via dc_event_emitter_t.
2023-04-22 16:42:59 +00:00
link2xt
e7aab5c67c Remove newline from release-date.in 2023-04-20 15:40:09 +00:00
link2xt
40484e875e deltachat-ffi: update read_url_blob for new .log_err() 2023-04-20 15:23:20 +00:00
link2xt
fc215ceb63 Merge v1.112.8 2023-04-20 15:09:55 +00:00
link2xt
2701c135db Prepare 1.112.8 2023-04-20 15:05:07 +00:00
link2xt
3a8df3e673 Do not use println! in JSON-RPC
This may break the output of deltachat-rpc-server
2023-04-20 14:15:18 +00:00
link2xt
fa95b269a5 Backport new set_core_version.py 2023-04-20 14:03:47 +00:00
B. Petersen
0e9f8c4726 describe dc_http_response_t, needed for doxygen's @memberof 2023-04-20 14:01:14 +00:00
link2xt
a8d4cbd5c1 Add C API to get HTTP responses 2023-04-20 14:01:14 +00:00
link2xt
f68a2fc387 JSON-RPC: return mimetype and encoding for HTTP blobs 2023-04-20 14:00:16 +00:00
link2xt
ede4e8109e Release 1.113.0 2023-04-18 18:54:41 +00:00
link2xt
663df6bdfd Replace 404 link with a web archive version 2023-04-18 18:53:39 +00:00
link2xt
d97bdd9fd0 Add more links to configure documentation 2023-04-18 18:53:39 +00:00
Hocuri
a806a218bf Clippy; remove forgotten dbg! (#4338) 2023-04-18 18:52:01 +00:00
link2xt
903633f422 Add regression test 2023-04-18 16:39:19 +00:00
link2xt
3c774b02e5 Only reset status to "" if a text/plain part without signature is received
Otherwise sending a message without plaintext part
resets the signature. It is particularly dangerous
in multidevice case, because it's easy to accidentally
reset the signature on your other device with a non-text message.
2023-04-18 16:39:19 +00:00
iequidoo
4aae48b0a1 Gracefully terminate deltachat-rpc-server on ctrl-c and SIGTERM (#4323) 2023-04-18 12:10:39 -04:00
iequidoo
a8b790a5db deltachat-rpc-server: Drop Accounts object when terminating (#4323)
Otherwise it continues to keep the sender side of the event emitter which prevents events_task from
stopping.
2023-04-18 12:10:39 -04:00
Hocuri
6a6ceb6875 Add send_recv() to test_utils.rs
link2xt approved orally
2023-04-18 16:08:19 +02:00
link2xt
37503dd3e8 JSON-RPC: add get_http_blob API 2023-04-18 13:50:50 +00:00
link2xt
3f615c8de6 Update h2 from 0.3.17 to 0.3.18
Version 0.3.17 is yanked.
2023-04-18 11:05:59 +00:00
link2xt
2ef5f2eb52 scheduler: use oneshot channel for "started" notifications
The senders are not used more than once.
2023-04-17 17:41:33 +00:00
link2xt
f267f6f756 Move event payloads to a submodule 2023-04-17 17:41:00 +00:00
link2xt
538db53887 accounts: comment fixes 2023-04-17 16:20:21 +00:00
link2xt
21349abed8 Fix newlines with prettier 2023-04-17 16:07:05 +00:00
link2xt
d2fb2bb2ca Merge 1.112.7 into master 2023-04-17 15:24:15 +00:00
link2xt
0b832fb9de Prepare 1.112.7 2023-04-17 15:18:37 +00:00
link2xt
179b9ba2cb Update to released async-imap 0.8.0
It fixes important bug in `ensure_capacity()` that
sometimes resulted in erroneous detection of EOF in IMAP response.
2023-04-17 10:50:13 +00:00
link2xt
9150e9fb38 Update crossbeam-channel from 0.5.7 to 0.5.8
0.5.7 is yanked.
2023-04-17 10:48:35 +00:00
link2xt
4716fcef94 Use sync RwLock for debug_logging
This avoids the need for potentially expensive block_in_place(),
but is unlikely to actually block the thread as holding write lock is rare.
2023-04-16 19:58:19 +00:00
link2xt
2b7ee85e30 debug_logging: use Context.emit_event() 2023-04-16 19:58:19 +00:00
link2xt
2b8888350b Debug logging refactoring
Move DebugLogging into debug logging module.
Remove Context.send_log_event().
Use blocking_read() instead of try_read() to
get read lock on debug_logging() in synchronous code.
Do not try to log events while holding a lock
on DebugLogging.
2023-04-16 19:58:19 +00:00
iequidoo
4dfe34eedc fix review comments 2023-04-16 15:32:36 -04:00
iequidoo
430a71288f Reduce + recode images to JPEG if they are too huge (#3956)
I.e. > 500K for the balanced media quality and 130K for the worse one. This can remove animation and
transparency from PNG/WebP, but then a user always can send an image as a file.

Also don't reduce wide/high images if they aren't huge. Among other benefits, this way most of PNG
screenshots aren't touched.

Also remove Exif from all images, not from JPEGs only.
2023-04-16 15:32:36 -04:00
iequidoo
350509d5d1 Remove metadata from avatars and JPEG images before sending (#4027)
If there's an Exif, rewrite the file to remove it. This implies recoding now though.
2023-04-16 15:32:36 -04:00
link2xt
5403fd849c Comment fixes 2023-04-16 00:47:01 +00:00
link2xt
fa87d2e225 New APIs for message processing loops
This patch adds new C APIs
dc_get_next_msgs() and dc_wait_next_msgs(),
and their JSON-RPC counterparts
get_next_msgs() and wait_next_msgs().

New configuration "last_msg_id"
tracks the last message ID processed by the bot.
get_next_msgs() returns message IDs above
the "last_msg_id".
wait_next_msgs() waits for new message notification
and calls get_next_msgs().
wait_next_msgs() can be used to build
a separate message processing loop
independent of the event loop.

Async Python API get_fresh_messages_in_arrival_order()
is deprecated in favor of get_next_messages().

Introduced Python APIs:
- Account.wait_next_incoming_message()
- Message.is_from_self()
- Message.is_from_device()

Introduced Rust APIs:
- Context.set_config_u32()
- Context.get_config_u32()
2023-04-15 21:27:45 +00:00
link2xt
28a13e98a6 Add JSON-RPC API can_send() 2023-04-14 21:09:32 +00:00
link2xt
b369a30544 Pass scripts/run-python-test.sh arguments to pytest 2023-04-14 16:24:08 +00:00
link2xt
318ed4e6e1 Fix pip install argument in python README
Attempt to run `pip install python` will
fail trying to install package from PyPI.
2023-04-14 16:23:34 +00:00
Hocuri
c6a64e8d93 Don't let blocking be bypassed using groups (#4316)
* Don't let blocking be bypassed using groups

Fix #4313

* Fix another bug: A blocked group was sometimes not unblocked when an unblocked contact sent a message into it.
2023-04-13 22:45:47 +02:00
dependabot[bot]
efd6937dfa Merge pull request #4321 from deltachat/dependabot/cargo/fuzz/h2-0.3.17 2023-04-13 20:09:29 +00:00
dependabot[bot]
36bf1fe3f6 Merge pull request #4322 from deltachat/dependabot/cargo/h2-0.3.17 2023-04-13 20:09:17 +00:00
dependabot[bot]
4da0e9ac64 cargo: bump h2 from 0.3.16 to 0.3.17
Bumps [h2](https://github.com/hyperium/h2) from 0.3.16 to 0.3.17.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.16...v0.3.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 17:07:57 +00:00
dependabot[bot]
436766f002 build(deps): bump h2 from 0.3.15 to 0.3.17 in /fuzz
Bumps [h2](https://github.com/hyperium/h2) from 0.3.15 to 0.3.17.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.15...v0.3.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-13 17:04:54 +00:00
Hocuri
d4f2507288 Remove confusing log "ignoring unsolicited response Recent(…)" (#3934)
It doesn't seem to add value and gives the impression that something
went wrong.
2023-04-13 16:27:25 +00:00
Hocuri
28fd27476f Small verification fixes (#4317)
* Small performance improvement by not unnecessarily loading the peerstate

* Remove wrong info message "{contact} verified" when scanning a QR code with just an email

I think that this was a bug in the original C code and then slipped
through two refactorings.
2023-04-13 16:14:23 +00:00
link2xt
619b849ce7 sql: cleanup usage of ToSql
Moved custom ToSql trait including Send + Sync from lib.rs to sql.rs.
Replaced most params! and paramsv! macro usage with tuples.

Replaced paramsv! and params_iterv! with params_slice!,
because there is no need to construct a vector.
2023-04-13 12:48:00 +00:00
iequidoo
f1eeb1df8c Cleanly terminate deltachat-rpc-server (#4234)
Do it as per "Thread safety" section in deltachat-ffi/deltachat.h. Also terminate on ctrl-c.
2023-04-12 17:20:42 -04:00
link2xt
cecc080931 Update crossbeam-channel to 0.5.8
0.5.7 is yanked
2023-04-11 18:45:37 +00:00
link2xt
11e6a325a2 ci: remove references to nonexistent matrix.rust 2023-04-08 18:27:35 +00:00
Sebastian Klähn
eed8e08145 Protect against RTLO attacks (#3609)
Protect against RTLO attackts
2023-04-07 08:36:37 +00:00
iequidoo
36bec9c295 Update "accounts.toml" atomically (#4295) 2023-04-06 22:52:25 -04:00
iequidoo
c8988f5a55 maybe_add_time_based_warnings(): Use release date instead of the provider DB update one (#4213)
bjoern <r10s@b44t.com> wrote:
> maybe_add_time_based_warnings() requires some date guaranteed to be in the near past. based on
this known date we check if the system clock is wrong (if earlier than known date) and if the used
Delta Chat version may be outdated (1 year passed since known date). while this does not catch all
situations, it catches quite some errors with comparable few effort.
>
> figuring out the date guaranteed to be in the near past is a bit tricky. that time, we added
get_provider_update_timestamp() for exactly that purpose - it is checked manually by some dev and
updated from time to time, usually before a release.
>
> however, meanwhile, the provider-db gets updated less frequently - things might be settled a bit
more - and, get_provider_update_timestamp() was also changed to return the date of the last commit,
instead of last run. while that seem to be more on-purpose, we cannot even do an “empty” database
update to update the known date.
>
> as get_provider_update_timestamp() is not used for anything else, maybe we should completely
remove that function and replace it by get_last_release_timestamp that is then updated by
./scripts/set_core_version.py - the result of that is reviewed manually anyway, so that seems to be
a good place (i prefer manual review here and mistrust further automation as also dev or ci clocks
may be wrong :)
2023-04-06 22:00:02 -04:00
Floris Bruynooghe
86c18fb3ae ref: More logging for ongoing and get_backup (#4289)
This adds a few log items for imex::transfer::get_backup and the
ongoing process to give some more insights.

IO is now also paused after the ongoing process is allocated in
get_backup to avoid needlessly pausing IO.
2023-04-04 18:11:45 +02:00
link2xt
2ba4381c39 Merge branch 'stable-1.112' into 'master' 2023-04-04 15:01:37 +00:00
link2xt
185a0193cc Fix newline at the end of package.json 2023-04-04 16:47:25 +02:00
B. Petersen
1b00334281 bump version to 1.112.6 2023-04-04 16:47:25 +02:00
B. Petersen
0eb2f5bf52 fix some CHANGELOG typos 2023-04-04 16:47:25 +02:00
B. Petersen
78d1aa46a1 update changelog for 1.112.6 2023-04-04 16:47:25 +02:00
link2xt
26403a1599 Update spin from 0.9.7 to 0.9.8
`spin` 0.9.7 is yanked: https://rustsec.org/advisories/RUSTSEC-2023-0031
2023-04-04 12:54:23 +00:00
B. Petersen
a1f112470e add a device message when backup transfer ends
that way, UI can just close the transfer dialog,
so that, at the end, both devices end in the chatlist.

we can also use this for troubleshooting -
if the device message is not present,
transfer did not succeed completely.

(a separate device message may be nice in that case -
but that is another effort,
same for making the device message reappear after deletion
or after some time)
2023-04-04 14:17:26 +02:00
dependabot[bot]
2ab7a37d85 Merge pull request #4297 from deltachat/dependabot/cargo/iroh-0.4.1 2023-04-04 09:02:11 +00:00
link2xt
4b933ed2ef Update to iroh 0.4.1 2023-04-04 09:00:19 +00:00
dependabot[bot]
0d1c12115d Merge pull request #4298 from deltachat/dependabot/cargo/fast-socks5-0.8.2 2023-04-04 08:34:30 +00:00
link2xt
3bced80b23 imap: use try_next() to avoid the need to transpose Option and Result 2023-04-04 08:23:37 +00:00
link2xt
f46f3e939e clippy fix 2023-04-04 09:50:42 +02:00
B. Petersen
9d8e836fdd show some text instead of nothing on empty quota list
some providers say that they support QUOTA in the IMAP CAPABILITY,
but return an empty list without any quota information then.

in our "Connectivity", this looks a bit of an error.

i have not seen this error often - only for testrun.org -
if it is usual, we could also just say "not supported"
(as we do in case QUOTA is not returned).
a translations seems not to be needed for now,
it seems unusual, and all other errors are not translated as well.
2023-04-04 09:50:42 +02:00
dependabot[bot]
132d34db5c cargo: bump fast-socks5 from 0.8.1 to 0.8.2
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.8.1 to 0.8.2.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/compare/v0.8.1...v0.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 22:22:51 +00:00
dependabot[bot]
16edb7b35a cargo: bump iroh from 0.4.0 to 0.4.1
Bumps [iroh](https://github.com/n0-computer/iroh) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/n0-computer/iroh/releases)
- [Changelog](https://github.com/n0-computer/iroh/blob/main/CHANGELOG.md)
- [Commits](https://github.com/n0-computer/iroh/compare/v0.4.0...v0.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 22:21:52 +00:00
link2xt
044478a044 Update fd-lock, is-terminal and tempfile 2023-04-03 21:56:16 +00:00
dependabot[bot]
f6ea48d666 Merge pull request #4275 from deltachat/dependabot/cargo/axum-0.6.12 2023-04-03 21:43:43 +00:00
dependabot[bot]
6ed3ed1617 cargo: bump axum from 0.6.11 to 0.6.12
Bumps [axum](https://github.com/tokio-rs/axum) from 0.6.11 to 0.6.12.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.6.11...axum-v0.6.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 19:48:44 +00:00
link2xt
9c9c401e66 ci: use --all-targets --all-features for cargo check 2023-04-03 19:27:11 +00:00
link2xt
72031edfbe ci: split CI into more parallel jobs
With this, all the builds happen in parallel in 3-6 minutes,
and then python tests run for 8-9 minutes.

Before, it was 6-17 minutes for compilation,
then 19-31 minutes for python tests.
Compilation of the library and test binary was not parallelized,
and then python tests included async python tests
and deltachat-rpc-server binary compilation.

Now, deltachat-rpc-server compilation,
Rust testing and async python tests are separate jobs.
2023-04-03 19:27:11 +00:00
iequidoo
41456aa2ab jsonrpc API: Fix documentation of search_messages() (#4288) 2023-04-03 14:17:39 -04:00
link2xt
a70f29381a location: remove bitflags dependency 2023-04-03 16:56:37 +00:00
dependabot[bot]
3b0ad73732 Merge pull request #4270 from deltachat/dependabot/cargo/async_zip-0.0.12 2023-04-03 16:08:41 +00:00
dependabot[bot]
8c302648bb Merge pull request #4263 from deltachat/dependabot/cargo/tokio-1.27.0 2023-04-03 16:06:43 +00:00
dependabot[bot]
01b888c341 cargo: bump tokio from 1.26.0 to 1.27.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.26.0...tokio-1.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 12:33:21 +00:00
dependabot[bot]
f8e87a8b6c cargo: bump async_zip from 0.0.11 to 0.0.12
Bumps [async_zip](https://github.com/Majored/rs-async-zip) from 0.0.11 to 0.0.12.
- [Release notes](https://github.com/Majored/rs-async-zip/releases)
- [Commits](https://github.com/Majored/rs-async-zip/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-03 12:33:19 +00:00
link2xt
01af83946c Update to released version of async-imap 2023-04-03 12:31:16 +00:00
link2xt
386a9ad0b0 Update from yanked spin 0.9.7 to 0.9.8 2023-04-03 10:21:11 +00:00
Floris Bruynooghe
c6c20d8f3c ref(scheduler): Make InnerSchedulerState an enum (#4251)
This is more verbose, but makes reasoning about things easier.
2023-04-03 11:13:44 +02:00
dependabot[bot]
40b072711e Merge pull request #4283 from deltachat/dependabot/cargo/rusqlite-0.29.0 2023-04-03 08:25:57 +00:00
Floris Bruynooghe
2469964a44 ref(sql): Do not warn if BLOBS_BACKUP_NAME does not exist (#4256)
ref(sql): Dop not warn if BLOBS_BACKUP_NAME does not exist

It is perfectly normal for this directory to not exist.
2023-04-03 09:28:17 +02:00
link2xt
40e0924768 Add bitflags exception to deny.toml 2023-04-03 07:25:44 +00:00
dependabot[bot]
e016440fb3 Merge pull request #4267 from deltachat/dependabot/cargo/serde_json-1.0.95 2023-04-02 22:06:59 +00:00
dependabot[bot]
a37aaa5585 Merge pull request #4271 from deltachat/dependabot/cargo/image-0.24.6 2023-04-02 22:06:26 +00:00
dependabot[bot]
1ee551d53e cargo: bump serde_json from 1.0.93 to 1.0.95
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.93 to 1.0.95.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.93...v1.0.95)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 19:55:56 +00:00
dependabot[bot]
2341eed796 Merge pull request #4282 from deltachat/dependabot/cargo/futures-0.3.28 2023-04-02 19:54:10 +00:00
dependabot[bot]
49cfd79505 Merge pull request #4265 from deltachat/dependabot/cargo/serde-1.0.159 2023-04-02 19:41:36 +00:00
dependabot[bot]
9ac0d1a1ce Merge pull request #4280 from deltachat/dependabot/cargo/thiserror-1.0.40 2023-04-02 19:00:26 +00:00
dependabot[bot]
6046dfabe9 cargo: bump rusqlite from 0.28.0 to 0.29.0
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.28.0...v0.29.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 18:32:07 +00:00
dependabot[bot]
c2269bf777 cargo: bump futures from 0.3.26 to 0.3.28
Bumps [futures](https://github.com/rust-lang/futures-rs) from 0.3.26 to 0.3.28.
- [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.26...0.3.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 17:40:49 +00:00
dependabot[bot]
b81a958021 Merge pull request #4278 from deltachat/dependabot/cargo/walkdir-2.3.3 2023-04-02 17:16:24 +00:00
dependabot[bot]
9e37a5643c cargo: bump thiserror from 1.0.38 to 1.0.40
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.38 to 1.0.40.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.38...1.0.40)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 16:42:22 +00:00
dependabot[bot]
50a213c2e1 cargo: bump serde from 1.0.152 to 1.0.159
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.152 to 1.0.159.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.152...v1.0.159)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 16:42:10 +00:00
link2xt
c6e9fbd501 Update syn crate in deltachat_derive 2023-04-02 16:40:23 +00:00
dependabot[bot]
720b5aa7c1 Merge pull request #4284 from deltachat/dependabot/cargo/toml-0.7.3 2023-04-02 16:30:33 +00:00
dependabot[bot]
7be7640d3f Merge pull request #4266 from deltachat/dependabot/cargo/reqwest-0.11.16 2023-04-02 16:07:43 +00:00
dependabot[bot]
e4d97278ee cargo: bump reqwest from 0.11.14 to 0.11.16
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.14 to 0.11.16.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.14...v0.11.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 15:07:16 +00:00
dependabot[bot]
793ad82d42 Merge pull request #4277 from deltachat/dependabot/cargo/libc-0.2.140 2023-04-02 15:04:51 +00:00
dependabot[bot]
2b4d846f0e Merge pull request #4285 from deltachat/dependabot/cargo/regex-1.7.3 2023-04-02 15:03:47 +00:00
dependabot[bot]
379eadab4f Merge pull request #4281 from deltachat/dependabot/cargo/anyhow-1.0.70 2023-04-02 14:45:24 +00:00
dependabot[bot]
53a41fcffe cargo: bump anyhow from 1.0.69 to 1.0.70
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.69 to 1.0.70.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.69...1.0.70)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 09:52:31 +00:00
link2xt
5e597e1f09 Merge tag 'v1.112.5' 2023-04-02 09:50:16 +00:00
link2xt
c9879f863b Prepare 1.112.5 release 2023-04-02 09:48:39 +00:00
dependabot[bot]
e1b260d0ec cargo: bump regex from 1.7.1 to 1.7.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.7.1 to 1.7.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.7.1...1.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 09:19:42 +00:00
dependabot[bot]
480b05063d cargo: bump libc from 0.2.139 to 0.2.140
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.139 to 0.2.140.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.139...0.2.140)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-02 09:17:54 +00:00
dependabot[bot]
8477f7b472 Merge pull request #4276 from deltachat/dependabot/cargo/quick-xml-0.28.1 2023-04-02 09:17:40 +00:00
dependabot[bot]
91ae4eab06 Merge pull request #4268 from deltachat/dependabot/cargo/dirs-5.0.0 2023-04-02 09:14:21 +00:00
link2xt
f6d27516cb Run migrations before delete_and_reset_all_device_msgs()
Both when importing a backup from file or receiving it over the network.
2023-04-02 08:39:13 +00:00
dependabot[bot]
7b86681cb2 Merge pull request #4274 from deltachat/dependabot/cargo/testdir-0.7.3 2023-04-02 08:38:19 +00:00
dependabot[bot]
1de815441a Merge pull request #4272 from deltachat/dependabot/cargo/chrono-0.4.24 2023-04-02 08:37:11 +00:00
link2xt
d6426dc1b6 imex: run migrations after receiving a transferred backup
Similarly to how `imex_inner()` runs migrations
after successful call to `import_backup()`,
migrations should be run after receiving a backup
using `transfer_from_provider()`.
2023-04-01 23:56:36 +00:00
dependabot[bot]
44a36025b5 cargo: bump toml from 0.7.2 to 0.7.3
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.2 to 0.7.3.
- [Release notes](https://github.com/toml-rs/toml/releases)
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.2...toml-v0.7.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:04:59 +00:00
dependabot[bot]
00017ab653 cargo: bump walkdir from 2.3.2 to 2.3.3
Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/BurntSushi/walkdir/releases)
- [Commits](https://github.com/BurntSushi/walkdir/compare/2.3.2...2.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:03:47 +00:00
dependabot[bot]
411adc861e cargo: bump quick-xml from 0.27.1 to 0.28.1
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.27.1 to 0.28.1.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.27.1...v0.28.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:03:25 +00:00
dependabot[bot]
c5b3939ddb cargo: bump testdir from 0.7.2 to 0.7.3
Bumps testdir from 0.7.2 to 0.7.3.

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:02:46 +00:00
dependabot[bot]
a9e3ea56ec cargo: bump chrono from 0.4.23 to 0.4.24
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.23 to 0.4.24.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.23...v0.4.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:02:27 +00:00
dependabot[bot]
6a0f17a983 cargo: bump image from 0.24.5 to 0.24.6
Bumps [image](https://github.com/image-rs/image) from 0.24.5 to 0.24.6.
- [Release notes](https://github.com/image-rs/image/releases)
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:02:14 +00:00
dependabot[bot]
28e6f457b1 cargo: bump dirs from 4.0.0 to 5.0.0
Bumps [dirs](https://github.com/soc/dirs-rs) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/soc/dirs-rs/releases)
- [Commits](https://github.com/soc/dirs-rs/commits)

---
updated-dependencies:
- dependency-name: dirs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-01 22:01:31 +00:00
link2xt
fdf46054e2 ci: limit artifact retention time for libdeltachat.a to 1 day
These artifacts are only needed to be downloaded
by the python test job immediately after building.
2023-04-01 17:37:52 +00:00
link2xt
47a3ee1ff1 ci: run mypy and doc with installed library
These require that package can be installed.
2023-04-01 13:29:55 +00:00
link2xt
a0b7b4b5c4 ci: use ' instead of " for condition quote 2023-04-01 12:58:29 +00:00
link2xt
b9bc0a4047 ci: fixup "if" expression 2023-04-01 12:56:36 +00:00
link2xt
e57abf92f3 ci: split library build and python tests into separate jobs
This way if flaky python tests fail,
it is possible to rerun them without having to rerun library compilation.
Python test jobs will redownload already built library artifact
and compile the bindings against it.
2023-04-01 12:53:49 +00:00
iequidoo
b56a7bc139 Fix CHANGELOG.md after c401780c 2023-04-01 09:13:08 -03:00
iequidoo
c401780c15 Compress mime_headers column with HTML emails stored in database (#4077)
Co-authored-by: bjoern <r10s@b44t.com>
2023-03-31 19:45:34 -03:00
iequidoo
df96a1daac test_moved_markseen: Expect DC_EVENT_MSGS_CHANGED also 2023-03-31 17:15:02 -03:00
iequidoo
0a2200c2c8 python: Wait for initial Resync in the bot tests 2023-03-31 17:15:02 -03:00
Floris Bruynooghe
61b8d04418 ref(logging): remove LogExt::log_or_ok (#4250)
This further reduces the cognitive overload of having many ways to do
something.  The same is very easily done using composition.  Followup
from 82ace72527.
2023-03-31 12:15:17 +02:00
link2xt
fd7cc83537 Merge tag 'v1.112.4'
Release 1.112.4
2023-03-31 01:13:56 +00:00
link2xt
f24843fbb1 Release 1.112.4 2023-03-31 01:12:26 +00:00
link2xt
6c57bc9438 Fix call to auditwheel in scripts/run_all.sh
This bugs prevents CI from building wheels.
2023-03-31 01:02:41 +00:00
link2xt
ae5f72cf4f Remove install_python_bindings.py script and update docs
`install_python_bindings.py` was not used by CI
and scripts, except for `scripts/run-python-test.sh`
which only used it to invoke `cargo`.

Instead of using an additional script,
run cargo directly.

The documentation is updated to remove
references to `install_python_bindings.py`.
The section "Installing bindings from source"
was in fact incorrect as it suggested
running `tox --devenv` first and only then
compiling the library with `install_python_bindings.py`.
Now it explicitly says to run cargo first
and then install the package without using `tox`.
`tox` is still used for running the tests
and setting up development environment.
2023-03-31 00:27:35 +00:00
link2xt
3e65b6f3a6 Update to rPGP 0.10.1 2023-03-30 21:09:29 +00:00
link2xt
a4a53d5299 Increase MSRV to 1.65.0 2023-03-30 21:09:29 +00:00
link2xt
4223cac7a5 set_core_version.py: add newline to the end of package.json 2023-03-30 20:38:47 +00:00
link2xt
0073a09da6 Merge v1.112.3 2023-03-30 20:37:43 +00:00
link2xt
e612927c5d Fix links in the changelog 2023-03-30 20:36:30 +00:00
link2xt
aff951440c Release 1.112.3 2023-03-30 20:32:49 +00:00
Floris Bruynooghe
ef63e01632 fix(imex): transfer::get_backup must always free ongoing process (#4249)
* fix(imex): transfer::get_backup must always free ongoing process

When the ongoing process is cancelled it is still the responsibility
of whoever took out the ongoing process to free it.  This code was
only freeing the ongoing process when completed normally but not when
cancelled.

* add changelog
2023-03-30 20:23:50 +00:00
link2xt
c4cf0f12c9 Prettier lint fix 2023-03-30 16:08:02 +00:00
link2xt
68635be8a2 Merge v1.112.2 into master 2023-03-30 15:42:27 +00:00
link2xt
776df7505e Remove upper limit on the attachment size
Recommended file size is used for recoding media.
For the upper size, we rely on the provider to tell us back
if the message cannot be delivered to some recipients.
This allows to send large files, such as APKs,
when using providers that don't have such strict limits.
2023-03-30 15:01:02 +00:00
link2xt
d6fdc7cb67 Release 1.112.2 2023-03-30 14:57:32 +00:00
Floris Bruynooghe
91c10b3ac6 feat(scheduler): Allow overlapping IoPauseGuards (#4246)
This enables using multiple pause guards at the same time.
2023-03-30 12:40:13 +02:00
Floris Bruynooghe
82ace72527 ref(logging): Remove message from LogExt::log_err (#4235)
This removes the message that needed to be supplied to LogExt::log_err
calls.  This was from a time before we adopted anyhow and now we are
better off using anyhow::Context::context for the message: it is more
consistent, composes better and is less custom.

The benefit of the composition can be seen in the FFI calls which need
to both log the error as well as return it to the caller via
the set_last_error mechanism.

It also removes the LogExt::ok_or_log_msg funcion for the same reason,
the message is obsoleted by anyhow's context.
2023-03-30 10:13:07 +02:00
link2xt
ea87c78d34 Do not return media from trashed messages in the "All media" view. 2023-03-30 08:10:06 +00:00
link2xt
585b8ece58 Remove outdated comment. 2023-03-30 08:07:21 +00:00
Floris Bruynooghe
a2927a6586 ref(deps): Upgrade to iroh 0.4.0 (#4245)
This moves us back to a released version;

- Ticket is now opaque, need to use accessor functions.

- Ticket now ensures it is valid itself, no need to inspect it's
  inners.  Deserialisation would fail if it was bad.

- The git version was accidentally used with default-features enabled
  and thus pulled in a few too many dependencies.  They are now gone.
2023-03-29 15:45:35 +02:00
Floris Bruynooghe
943c8a1ab3 feat(imex): Cancel BackupProvider when dropped (#4242)
This ensures that the BackupProvider will be stopped as soon as the
struct is dropped and the imex progress error event is emitted.  This
makes it easier to use and also makes sure that the ffi call
dc_backup_provider_unref() does not lead to dangling resources.
2023-03-29 14:51:08 +02:00
link2xt
3400f5641e Build zig-rpc-server for i686
cargo-zigbuild is replaced with a simple zig-cc wrapper.
cargo-zigbuild still tries to use i386-linux-musl triple,
while in zig 0.11 x86-linux-musl should be used [1].

This also gives us more control over compilation flags used
and prevents changes due to cargo-zigbuild upgrades,
as its version was not pinned.

[1] Merged PR "all: rename i386 to x86 "
    <https://github.com/ziglang/zig/pull/13101>
2023-03-29 10:56:22 +00:00
link2xt
5068648960 Update yanked spin 0.9.6
Otherwise cargo-deny complains.
2023-03-29 10:56:02 +00:00
Floris Bruynooghe
5be558ea68 feat(imex) Connect to all provider addresses concurrently (#4240)
This uses the new iroh API to connect to all provider addresses
concurrently.  It simplifies the implementation as well as we no
longer need to try the addresses manually.
2023-03-29 09:47:00 +02:00
link2xt
fc25bba514 Add changelog for unreleased 1.112.2 2023-03-28 10:44:07 +00:00
Floris Bruynooghe
20b326415a deps: Update iroh, remove default-net patch (#4239)
* deps: Update iroh, remove default-net patch

The released version of default-net is now sufficient and iroh makes
sure this dependency is recent enough.

* Update cargo-deny config

* Newer version of spin, previous has been yanked
2023-03-28 10:28:38 +00:00
link2xt
1bf5064039 Add unreleased section to changelog 2023-03-27 16:13:11 +00:00
link2xt
edf0c02bc8 Release 1.112.1 2023-03-27 15:22:31 +00:00
link2xt
1d42907114 deltachat-ffi: document thread safety guarantees 2023-03-27 14:47:40 +00:00
bjoern
070d832580 check against null in dc_backup_provider_unref() (#4232)
this is what we're doing in most comparable unref() functions.
2023-03-26 19:25:23 +02:00
link2xt
0dfec83b0f deltachat-rpc-client: configure setuptools_scm
This makes `python -m build` produce wheels with a version
other than 0.0.0.
2023-03-26 10:00:01 +00:00
link2xt
c84155cbd4 Add scripts/deny.sh
This can be used to manually run `cargo deny`
without specifying all the parameters manually.

Similar to existing scripts/clippy.sh
2023-03-25 19:54:24 +00:00
iequidoo
eb5ddf270f receive_imf: Mark special messages as seen (#3054)
Exactly: delivery reports, webxdc status updates.
2023-03-25 15:47:38 -03:00
136 changed files with 6836 additions and 4725 deletions

25
.github/mergeable.yml vendored
View File

@@ -1,26 +1,15 @@
version: 2
mergeable:
- when: pull_request.*
name: "Changelog check"
name: "Conventional Commits"
validate:
- do: or
validate:
- do: description
must_include:
regex: "#skip-changelog"
- do: and
validate:
- do: dependent
changed:
file: "src/**"
required: ["CHANGELOG.md"]
- do: dependent
changed:
file: "deltachat-ffi/src/**"
required: ["CHANGELOG.md"]
- do: title
begins_with:
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore']
fail:
- do: checks
status: "action_required"
payload:
title: Changelog might need an update
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
title: PR title should follow conventional commits
summary: "PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' (for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)"

View File

@@ -16,11 +16,11 @@ env:
RUSTFLAGS: -Dwarnings
jobs:
lint:
name: Rustfmt and Clippy
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.68.0
RUSTUP_TOOLCHAIN: 1.68.2
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -31,6 +31,8 @@ jobs:
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- name: Check
run: cargo check --workspace --all-targets --all-features
cargo_deny:
name: cargo deny
@@ -64,34 +66,28 @@ jobs:
- name: Rustdoc
run: cargo doc --document-private-items --no-deps
build_and_test:
name: Build and test
rust_tests:
name: Rust tests
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
rust: 1.68.0
python: 3.9
rust: 1.68.2
- os: windows-latest
rust: 1.68.0
python: false # Python bindings compilation on Windows is not supported.
rust: 1.68.2
- os: macos-latest
rust: 1.68.0
python: 3.9
rust: 1.68.2
# Minimum Supported Rust Version = 1.64.0
# Minimum Supported Rust Version = 1.65.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.64.0
python: 3.7
rust: 1.65.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v3
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
@@ -100,64 +96,176 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Check
run: cargo check --workspace --bins --examples --tests --benches
- name: Tests
run: cargo test --workspace
- name: Test cargo vendor
run: cargo vendor
c_library:
name: Build C library
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Build C library
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Upload C library
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
retention-days: 1
rpc_server:
name: Build deltachat-rpc-server
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Build deltachat-rpc-server
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug/deltachat-rpc-server
retention-days: 1
python_lint:
name: Python lint
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install tox
run: pip install tox
- name: Lint Python bindings
working-directory: python
run: tox -e lint
- name: Lint deltachat-rpc-client
working-directory: deltachat-rpc-client
run: tox -e lint
python_tests:
name: Python tests
needs: ["c_library", "python_lint"]
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
- os: macos-latest
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
- os: macos-latest
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Download libdeltachat.a
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install tox
if: ${{ matrix.python }}
run: pip install tox
- name: Build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Run python tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,mypy,doc,py3
run: tox -e mypy,doc,py
- name: Build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
aysnc_python_tests:
name: Async Python tests
needs: ["python_lint", "rpc_server"]
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.11
- os: macos-latest
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.9
- os: macos-latest
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install tox
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v3
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug
- name: Make deltachat-rpc-server executable
run: chmod +x target/debug/deltachat-rpc-server
- name: Add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3,lint
- name: Install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: "pypy${{ matrix.python }}"
- name: Run pypy tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e pypy3
run: tox -e py

View File

@@ -15,7 +15,7 @@ jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Cross-compile deltachat-rpc-server for x86_64, aarch64 and armv7 Linux
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
@@ -30,6 +30,13 @@ jobs:
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload i686 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-i686
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
@@ -90,7 +97,7 @@ jobs:
- name: Compose dist/ directory
run: |
mkdir dist
for x in x86_64 aarch64 armv7 win32.exe win64.exe; do
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe; do
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
done

View File

@@ -1,6 +1,110 @@
# Changelog
## [Unreleased]
## Unreleased
### Changes
- BREAKING: jsonrpc:
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`
- JSON-RPC: add API to get reactions outside the message snapshot
### Fixes
- Make the bots automatically accept group chat contact requests. #4377
## [1.114.0] - 2023-04-24
### Changes
- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
This better corresponds to JSON-RPC 2.0 server-client distinction
and is expected to simplify writing new bindings
because dispatching events can be done on higher level.
- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
### Fixes
- JSON-RPC: do not print to stdout on failure to find an account.
## [1.113.0] - 2023-04-18
### Added
- New JSON-RPC API `can_send()`.
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
These APIs can be used by bots to get all unprocessed messages
in the order of their arrival and wait for them without relying on events.
- New Python bindings API `Account.wait_next_incoming_message()`.
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
### Changes
- Increase MSRV to 1.65.0. #4236
- Remove upper limit on the attachment size. #4253
- Update rPGP to 0.10.1. #4236
- Compress HTML emails stored in the `mime_headers` column of the database.
- Strip BIDI characters in system messages, files, group names and contact names. #3479
- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
in favor of `get_next_msgs()` and `wait_next_msgs()`.
- Remove metadata from avatars and JPEG images before sending. #4037
- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
### Fixes
- Don't let blocking be bypassed using groups. #4316
- Show a warning if quota list is empty. #4261
- Do not reset status on other devices when sending signed reaction messages. #3692
- Update `accounts.toml` atomically.
- Fix python bindings README documentation on installing the bindings from source.
- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
## [1.112.8] - 2023-04-20
### Changes
- Add `get_http_response` JSON-RPC API.
- Add C API to get HTTP responses.
## [1.112.7] - 2023-04-17
### Fixes
- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
## [1.112.6] - 2023-04-04
### Changes
- Add a device message after backup transfer #4301
### Fixed
- Updated `iroh` from 0.4.0 to 0.4.1 to fix transfer of large accounts with many blob files.
## [1.112.5] - 2023-04-02
### Fixes
- Run SQL database migrations after receiving a backup from the network. #4287
## [1.112.4] - 2023-03-31
### Fixes
- Fix call to `auditwheel` in `scripts/run_all.sh`.
## [1.112.3] - 2023-03-30
### Fixes
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
## [1.112.2] - 2023-03-30
### Changes
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
### Fixes
- Do not return media from trashed messages in the "All media" view. #4247
## [1.112.1] - 2023-03-27
### Changes
- Add support for `--version` argument to `deltachat-rpc-server`. #4224
@@ -8,6 +112,7 @@
### Fixes
- deltachat-rpc-client: fix bug in `Chat.send_message()`: invalid `MessageData` field `quotedMsg` instead of `quotedMsgId`
- `receive_imf`: Mark special messages as seen. Exactly: delivery reports, webxdc status updates. #4230
## [1.112.0] - 2023-03-23
@@ -38,8 +143,6 @@
It is used to fix race condition between fetching
existing messages and starting the test. #4208
### API-Changes
- Use `tracing` crate for logging. #3960
## [1.111.0] - 2023-03-05
@@ -170,7 +273,6 @@
- Do not treat invalid email addresses as an exception #3942
- Add timeouts to HTTP requests #3948
## 1.105.0
### Changes
@@ -256,7 +358,6 @@
- Disable read timeout during IMAP IDLE #3826
- Bots automatically accept mailing lists #3831
## 1.102.0
### Changes
@@ -2331,6 +2432,15 @@ For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...HEAD
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
[1.112.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.1...v1.112.2
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.113.0
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0

1330
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.112.0"
version = "1.114.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.64"
rust-version = "1.65"
[profile.dev]
debug = 0
@@ -25,7 +25,6 @@ panic = 'abort'
opt-level = "z"
[patch.crates-io]
default-net = { git = "https://github.com/dignifiedquire/default-net.git", rev="7a257095bac009c4be0b93c2979801624fdd337b" }
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
@@ -36,47 +35,43 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.8.0", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.11", default-features = false, features = ["deflate", "fs"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
bitflags = "1.3"
brotli = "3.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
futures = "0.3"
futures-lite = "1.12.0"
futures-lite = "1.13.0"
hex = "0.4.0"
humansize = "2"
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
# iroh = { version = "0.3.0", default-features = false }
iroh = { git = 'https://github.com/n0-computer/iroh', branch = "main" }
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.1", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.15"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.17.0"
percent-encoding = "2.2"
parking_lot = "0.12"
pgp = { version = "0.9", default-features = false }
pin-project-lite = { version = "0.2.9" }
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features=false, features = ["std", "env-filter", "registry", "fmt"] }
tracing-futures = "0.2"
quick-xml = "0.27"
quick-xml = "0.28"
rand = "0.8"
regex = "1.7"
reqwest = { version = "0.11.14", features = ["json"] }
rusqlite = { version = "0.28", features = ["sqlcipher"] }
reqwest = { version = "0.11.17", features = ["json"] }
rusqlite = { version = "0.29", features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.4"
serde_json = "1.0"
@@ -89,10 +84,11 @@ strum_macros = "0.24"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.14", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.8"
toml = "0.7"
trust-dns-resolver = "0.22"
url = "2"
@@ -101,10 +97,12 @@ uuid = { version = "1", features = ["serde", "v4"] }
[dev-dependencies]
ansi_term = "0.12.0"
criterion = { version = "0.4.0", features = ["async_tokio"] }
futures-lite = "1.12"
futures-lite = "1.13"
log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.7.2"
testdir = "0.7.3"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
[workspace]

View File

@@ -19,7 +19,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ cargo run -p deltachat-repl -- ~/deltachat-db
$ RUST_LOG=deltachat_repl=info cargo run -p deltachat-repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.

View File

@@ -2,15 +2,15 @@ use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::{Event, EventType, Events};
use deltachat::{info, Event, EventType, Events};
use tempfile::tempdir;
async fn send_events_benchmark(context: &Context) {
let emitter = context.get_event_emitter();
for _i in 0..1_000_000 {
context.emit_event(EventType::Info("interesting event...".to_string()));
info!(context, "interesting event...");
}
context.emit_event(EventType::Info("DONE".to_string()));
info!(context, "DONE");
loop {
match emitter.recv().await.unwrap() {

75
cliff.toml Normal file
View File

@@ -0,0 +1,75 @@
# configuration file for git-cliff
# see https://github.com/orhun/git-cliff#configuration-file
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/PR/${2}))"}, # replace pull request / issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features / Changes"},
{ message = "^fix", group = "Fixes"},
{ message = "^api", group = "API-Changes" },
{ message = "^refactor", group = "Refactor"},
{ message = "^perf", group = "Performance"},
{ message = "^test", group = "Tests"},
{ message = "^style", group = "Styling"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
# { body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = true
# filter out the commits that are not matched by commit parsers
filter_commits = true
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
#skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42
[changelog]
# changelog header
header = """
# Changelog\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.112.0"
version = "1.114.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -25,8 +25,6 @@ anyhow = "1"
thiserror = "1"
rand = "0.8"
once_cell = "1.17.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features=false, features = ["std", "registry"] }
[features]
default = ["vendored"]

View File

@@ -25,6 +25,7 @@ typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
typedef struct _dc_http_response dc_http_response_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -72,7 +73,6 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
*
* The example above uses "pthreads",
* however, you can also use anything else for thread handling.
* All deltachat-core functions, unless stated otherwise, are thread-safe.
*
* Now you can **configure the context:**
*
@@ -142,6 +142,72 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
* ~~~
*
*
* ## Thread safety
*
* All deltachat-core functions, unless stated otherwise, are thread-safe.
* In particular, it is safe to pass the same dc_context_t pointer
* to multiple functions running concurrently in different threads.
*
* All the functions are guaranteed not to use the reference passed to them
* after returning. If the function spawns a long-running process,
* such as dc_configure() or dc_imex(), it will ensure that the objects
* passed to them are not deallocated as long as they are needed.
* For example, it is safe to call dc_imex(context, ...) and
* call dc_context_unref(context) immediately after return from dc_imex().
* It is however **not safe** to call dc_context_unref(context) concurrently
* until dc_imex() returns, because dc_imex() may have not increased
* the reference count of dc_context_t yet.
*
* This means that the context may be still in use after
* dc_context_unref() call.
* For example, it is possible to start the import/export process,
* call dc_context_unref(context) immediately after
* and observe #DC_EVENT_IMEX_PROGRESS events via the event emitter.
* Once dc_get_next_event() returns NULL,
* it is safe to terminate the application.
*
* It is recommended to create dc_context_t in the main thread
* and only call dc_context_unref() once other threads that may use it,
* such as the event loop thread, are terminated.
* Common mistake is to use dc_context_unref() as a way
* to cause dc_get_next_event() return NULL and terminate event loop this way.
* If event loop thread is inside a function taking dc_context_t
* as an argument at the moment dc_context_unref() is called on the main thread,
* the behavior is undefined.
*
* Recommended way to safely terminate event loop
* and shutdown the application is
* to use a boolean variable
* indicating that the event loop should stop
* and check it in the event loop thread
* every time before calling dc_get_next_event().
* To terminate the event loop, main thread should:
* 1. Notify background threads,
* such as event loop (blocking in dc_get_next_event())
* and message processing loop (blocking in dc_wait_next_msgs()),
* that they should terminate by atomically setting the
* boolean flag in the memory
* shared between the main thread and background loop threads.
* 2. Call dc_stop_io() or dc_accounts_stop_io(), depending
* on whether a single account or account manager is used.
* Stopping I/O is guaranteed to emit at least one event
* and interrupt the event loop even if it was blocked on dc_get_next_event().
* Stopping I/O is guaranteed to interrupt a single dc_wait_next_msgs().
* 3. Wait until the event loop thread notices the flag,
* exits the event loop and terminates.
* 4. Call dc_context_unref() or dc_accounts_unref().
* 5. Keep calling dc_get_next_event() in a loop until it returns NULL,
* indicating that the contexts are deallocated.
* 6. Terminate the application.
*
* When using C API via FFI in runtimes that use automatic memory management,
* such as CPython, JVM or Node.js, take care to ensure the correct
* shutdown order and avoid calling dc_context_unref() or dc_accounts_unref()
* on the objects still in use in other threads,
* e.g. by keeping a reference to the wrapper object.
* The details depend on the runtime being used.
*
*
* ## Class reference
*
* For a class reference, see the "Classes" link atop.
@@ -397,6 +463,16 @@ char* dc_get_blobdir (const dc_context_t* context);
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
* - `last_msg_id` = database ID of the last message processed by the bot.
* This ID and IDs below it are guaranteed not to be returned
* by dc_get_next_msgs() and dc_wait_next_msgs().
* The value is updated automatically
* when dc_markseen_msgs() is called,
* but the bot can also set it manually if it processed
* the message but does not want to mark it as seen.
* For most bots calling `dc_markseen_msgs()` is the
* recommended way to update this value
* even for self-sent messages.
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
* 0=do not fetch existing messages on configure.
* In both cases, existing recipients are added to the contact database.
@@ -1283,6 +1359,56 @@ int dc_estimate_deletion_cnt (dc_context_t* context, int from_ser
dc_array_t* dc_get_fresh_msgs (dc_context_t* context);
/**
* Returns the message IDs of all messages of any chat
* with a database ID higher than `last_msg_id` config value.
*
* This function is intended for use by bots.
* Self-sent messages, device messages,
* messages from contact requests
* and muted chats are included,
* but messages from explicitly blocked contacts
* and chats are ignored.
*
* This function may be called as a part of event loop
* triggered by DC_EVENT_INCOMING_MSG if you are only interested
* in the incoming messages.
* Otherwise use a separate message processing loop
* calling dc_wait_next_msgs() in a separate thread.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_get_next_msgs (dc_context_t* context);
/**
* Waits for notification of new messages
* and returns an array of new message IDs.
* See the documentation for dc_get_next_msgs()
* for the details of return value.
*
* This function waits for internal notification of
* a new message in the database and returns afterwards.
* Notification is also sent when I/O is started
* to allow processing new messages
* and when I/O is stopped using dc_stop_io() or dc_accounts_stop_io()
* to allow for manual interruption of the message processing loop.
* The function may return an empty array if there are
* no messages after notification,
* which may happen on start or if the message is quickly deleted
* after adding it to the database.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @return An array of message IDs, must be dc_array_unref()'d when no longer used.
* On errors, the list is empty. NULL is never returned.
*/
dc_array_t* dc_wait_next_msgs (dc_context_t* context);
/**
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
@@ -1882,6 +2008,11 @@ int dc_resend_msgs (dc_context_t* context, const uint3
* Moreover, timer is started for incoming ephemeral messages.
* This also happens for contact requests chats.
*
* This function updates last_msg_id configuration value
* to the maximum of the current value and IDs passed to this function.
* Bots which mark messages as seen can rely on this side effect
* to avoid updating last_msg_id value manually.
*
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
*
* @memberof dc_context_t
@@ -2715,6 +2846,12 @@ void dc_backup_provider_wait (dc_backup_provider_t* backup_provider);
/**
* Frees a dc_backup_provider_t object.
*
* If the provider has not yet finished, as indicated by
* dc_backup_provider_wait() or the #DC_EVENT_IMEX_PROGRESS event with value
* of 0 (failed) or 1000 (succeeded), this will also abort any in-progress
* transfer. If this aborts the provider a #DC_EVENT_IMEX_PROGRESS event with
* value 0 (failed) will be emitted.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
@@ -4991,6 +5128,72 @@ int dc_provider_get_status (const dc_provider_t* prov
void dc_provider_unref (dc_provider_t* provider);
/**
* Return an HTTP(S) GET response.
* This function can be used to download remote content for HTML emails.
*
* @memberof dc_context_t
* @param context The context object to take proxy settings from.
* @param url HTTP or HTTPS URL.
* @return The response must be released using dc_http_response_unref() after usage.
* NULL is returned on errors.
*/
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
/**
* @class dc_http_response_t
*
* An object containing an HTTP(S) GET response.
* Created by dc_get_http_response().
*/
/**
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
/**
* Returns HTTP response encoding, e.g. "utf-8".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_encoding (const dc_http_response_t* response);
/**
* Returns HTTP response contents.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
*/
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
/**
* Returns HTTP response content size.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob size.
*/
size_t dc_http_response_get_size (const dc_http_response_t* response);
/**
* Free an HTTP response object.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
*/
void dc_http_response_unref (const dc_http_response_t* response);
/**
* @class dc_lot_t
*
@@ -5468,7 +5671,6 @@ void dc_reactions_unref (dc_reactions_t* reactions);
*/
/**
* @class dc_jsonrpc_instance_t
*
@@ -7003,6 +7205,11 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by name and address of the account.
#define DC_STR_BACKUP_TRANSFER_QR 162
/// "Account transferred to your second device."
///
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/**
* @}
*/

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.112.0"
version = "1.114.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -19,21 +19,21 @@ serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.26" }
serde_json = "1.0.91"
futures = { version = "0.3.28" }
serde_json = "1.0.96"
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.25.0" }
tokio = { version = "1.28.0" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.11", optional = true, features = ["ws"] }
axum = { version = "0.6.18", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -1,19 +1,28 @@
use deltachat::{Event, EventType};
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use serde::Serialize;
use serde_json::{json, Value};
use typescript_type_def::TypeDef;
pub fn event_to_json_rpc_notification(event: Event) -> Value {
let id: JSONRPCEventType = event.typ.into();
json!({
"event": id,
"contextId": event.id,
})
#[derive(Serialize, TypeDef)]
pub struct Event {
/// Event payload.
event: EventType,
/// Account ID.
context_id: u32,
}
impl From<CoreEvent> for Event {
fn from(event: CoreEvent) -> Self {
Event {
event: event.typ.into(),
context_id: event.id,
}
}
}
#[derive(Serialize, TypeDef)]
#[serde(tag = "type", rename = "Event")]
pub enum JSONRPCEventType {
#[serde(tag = "type")]
pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
@@ -286,27 +295,27 @@ pub enum JSONRPCEventType {
},
}
impl From<EventType> for JSONRPCEventType {
fn from(event: EventType) -> Self {
use JSONRPCEventType::*;
impl From<CoreEventType> for EventType {
fn from(event: CoreEventType) -> Self {
use EventType::*;
match event {
EventType::Info(msg) => Info { msg },
EventType::SmtpConnected(msg) => SmtpConnected { msg },
EventType::ImapConnected(msg) => ImapConnected { msg },
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
EventType::ImapInboxIdle => ImapInboxIdle,
EventType::NewBlobFile(file) => NewBlobFile { file },
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
EventType::Warning(msg) => Warning { msg },
EventType::Error(msg) => Error { msg },
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
CoreEventType::Info(msg) => Info { msg },
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
CoreEventType::ImapInboxIdle => ImapInboxIdle,
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
CoreEventType::Warning(msg) => Warning { msg },
CoreEventType::Error(msg) => Error { msg },
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ReactionsChanged {
CoreEventType::ReactionsChanged {
chat_id,
msg_id,
contact_id,
@@ -315,92 +324,76 @@ impl From<EventType> for JSONRPCEventType {
msg_id: msg_id.to_u32(),
contact_id: contact_id.to_u32(),
},
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::MsgRead { chat_id, msg_id } => MsgRead {
CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
EventType::ChatModified(chat_id) => ChatModified {
CoreEventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
},
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(),
timer: timer.to_u32(),
}
}
EventType::ContactsChanged(contact) => ContactsChanged {
CoreEventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::LocationChanged(contact) => LocationChanged {
CoreEventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()),
},
EventType::ConfigureProgress { progress, comment } => {
CoreEventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment }
}
EventType::ImexProgress(progress) => ImexProgress { progress },
EventType::ImexFileWritten(path) => ImexFileWritten {
CoreEventType::ImexProgress(progress) => ImexProgress { progress },
CoreEventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(),
},
EventType::SecurejoinInviterProgress {
CoreEventType::SecurejoinInviterProgress {
contact_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::SecurejoinJoinerProgress {
CoreEventType::SecurejoinJoinerProgress {
contact_id,
progress,
} => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(),
progress,
},
EventType::ConnectivityChanged => ConnectivityChanged,
EventType::SelfavatarChanged => SelfavatarChanged,
EventType::WebxdcStatusUpdate {
CoreEventType::ConnectivityChanged => ConnectivityChanged,
CoreEventType::SelfavatarChanged => SelfavatarChanged,
CoreEventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
} => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
}
}
}
#[cfg(test)]
#[test]
fn generate_events_ts_types_definition() {
let events = {
let mut buf = Vec::new();
let options = typescript_type_def::DefinitionFileOptions {
root_namespace: None,
..typescript_type_def::DefinitionFileOptions::default()
};
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
.unwrap();
String::from_utf8(buf).unwrap()
};
std::fs::write("typescript/generated/events.ts", events).unwrap();
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime};
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, bail, Context as _, Result};
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype;
@@ -53,7 +53,9 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::load_from_db(context, *contact_id).await?,
Contact::load_from_db(context, *contact_id)
.await
.context("failed to load contact")?,
)
.await?,
)
@@ -73,7 +75,8 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::load_from_db(context, *contact)
.await?
.await
.context("failed to load contact for was_seen_recently")?
.was_seen_recently(),
None => false,
}

View File

@@ -1,23 +1,18 @@
use anyhow::Result;
use anyhow::{Context, Result};
use deltachat::chat::{Chat, ChatId};
use deltachat::chatlist::get_last_message_for_chat;
use deltachat::constants::*;
use deltachat::contact::{Contact, ContactId};
use deltachat::{
chat::{get_chat_contacts, ChatVisibility},
chatlist::Chatlist,
};
use deltachat::{
chat::{Chat, ChatId},
message::MsgId,
};
use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize};
use serde::Serialize;
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
#[derive(Deserialize, Serialize, TypeDef)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef)]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {
@@ -56,14 +51,9 @@ pub enum ChatListItemFetchResult {
pub(crate) async fn get_chat_list_item_by_id(
ctx: &deltachat::context::Context,
entry: &ChatListEntry,
entry: u32,
) -> Result<ChatListItemFetchResult> {
let chat_id = ChatId::new(entry.0);
let last_msgid = match entry.1 {
0 => None,
_ => Some(MsgId::new(entry.1)),
};
let chat_id = ChatId::new(entry);
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
if chat_id.is_archived_link() {
@@ -72,8 +62,12 @@ pub(crate) async fn get_chat_list_item_by_id(
});
}
let chat = Chat::load_from_db(ctx, chat_id).await?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
.await
.context("summary")?;
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
let summary_text2 = summary.text.to_owned();
@@ -101,7 +95,8 @@ pub(crate) async fn get_chat_list_item_by_id(
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::load_from_db(ctx, *contact)
.await?
.await
.context("contact")?
.was_seen_recently(),
None => false,
};

View File

@@ -0,0 +1,29 @@
use deltachat::net::HttpResponse as CoreHttpResponse;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef)]
pub struct HttpResponse {
/// base64-encoded response body.
blob: String,
/// MIME type, e.g. "text/plain" or "text/html".
mimetype: Option<String>,
/// Encoding, e.g. "utf-8".
encoding: Option<String>,
}
impl From<CoreHttpResponse> for HttpResponse {
fn from(response: CoreHttpResponse) -> Self {
use base64::{engine::general_purpose, Engine as _};
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
let mimetype = response.mimetype;
let encoding = response.encoding;
HttpResponse {
blob,
mimetype,
encoding,
}
}
}

View File

@@ -2,6 +2,7 @@ pub mod account;
pub mod chat;
pub mod chat_list;
pub mod contact;
pub mod http;
pub mod location;
pub mod message;
pub mod provider_info;

View File

@@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession};
mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808;
@@ -44,12 +43,5 @@ async fn main() -> Result<(), std::io::Error> {
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone());
tokio::spawn(async move {
let events = api.accounts.read().await.get_event_emitter();
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
client.send_notification("event", Some(event)).await.ok();
}
});
handle_ws_rpc(ws, out_receiver, session).await
}

View File

@@ -67,7 +67,7 @@ async function run() {
null,
null
);
for (const [chatId, _messageId] of chats) {
for (const chatId of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds(

View File

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

View File

@@ -1,7 +1,6 @@
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 { WebsocketTransport, BaseTransport, Request } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
@@ -36,27 +35,33 @@ export class BaseDeltaChat<
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
constructor(public transport: Transport) {
//@ts-ignore
private eventTask: Promise<void>;
constructor(public transport: Transport, startEventLoop: boolean) {
super();
this.rpc = new RawClient(this.transport);
this.transport.on("request", (request: Request) => {
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
if (startEventLoop) {
this.eventTask = this.eventLoop();
}
}
if (this.contextEmitters[event.contextId]) {
this.contextEmitters[event.contextId].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.contextId].emit("ALL", event.event);
}
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
this.emit(event.event.type, event.context_id, event.event as any);
this.emit("ALL", event.context_id, event.event as any);
if (this.contextEmitters[event.context_id]) {
this.contextEmitters[event.context_id].emit(
event.event.type,
//@ts-ignore
event.event as any
);
this.contextEmitters[event.context_id].emit("ALL", event.event as any);
}
});
}
}
async listAccounts(): Promise<T.Account[]> {
@@ -75,10 +80,12 @@ export class BaseDeltaChat<
export type Opts = {
url: string;
startEventLoop: boolean;
};
export const DEFAULT_OPTS: Opts = {
url: "ws://localhost:20808/ws",
startEventLoop: true,
};
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
opts: Opts;
@@ -86,20 +93,24 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
this.transport.close();
}
constructor(opts?: Opts | string) {
if (typeof opts === "string") opts = { url: opts };
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
else opts = { ...DEFAULT_OPTS };
if (typeof opts === "string") {
opts = { ...DEFAULT_OPTS, url: opts };
} else if (opts) {
opts = { ...DEFAULT_OPTS, ...opts };
} else {
opts = { ...DEFAULT_OPTS };
}
const transport = new WebsocketTransport(opts.url);
super(transport);
super(transport, opts.startEventLoop);
this.opts = opts;
}
}
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
close() {}
constructor(input: any, output: any) {
constructor(input: any, output: any, startEventLoop: boolean) {
const transport = new StdioTransport(input, output);
super(transport);
super(transport, startEventLoop);
}
}

View File

@@ -1,6 +1,5 @@
export * as RPC from "../generated/jsonrpc.js";
export * as T from "../generated/types.js";
export * from "../generated/events.js";
export { RawClient } from "../generated/client.js";
export * from "./client.js";
export * as yerpc from "yerpc";

View File

@@ -12,7 +12,7 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
// dc.on("ALL", (event) => {
//console.log("event", event);
// });

View File

@@ -27,7 +27,7 @@ describe("online tests", function () {
this.skip();
}
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
dc.on("ALL", (contextId, { type }) => {
if (type !== "Info") console.log(contextId, type);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.112.0"
version = "1.114.0"
license = "MPL-2.0"
edition = "2021"
@@ -8,15 +8,12 @@ edition = "2021"
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "4"
dirs = "5"
log = "0.4.16"
pretty_env_logger = "0.4"
rusqlite = "0.28"
rusqlite = "0.29"
rustyline = "11"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features=false, features = ["std", "env-filter", "registry", "fmt"] }
tracing-log = "0.1.3"
[features]
default = ["vendored"]

View File

@@ -465,7 +465,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
continue_key_transfer(&context, MsgId::new(arg1.parse()?), arg2).await?;
}
"has-backup" => {
has_backup(blobdir).await?;
has_backup(&context, blobdir).await?;
}
"export-backup" => {
let dir = dirs::home_dir().unwrap_or_default();
@@ -508,7 +508,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"export-setup" => {
let setup_code = create_setup_code();
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(&context, &setup_code).await?;
fs::write(&file_name, file_content).await?;
@@ -563,7 +563,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await;
}
"housekeeping" => {
sql::housekeeping(&context).await.ok_or_log(&context);
sql::housekeeping(&context).await.log_err(&context).ok();
}
"listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" {
@@ -1244,7 +1244,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let socks5_enabled = context
.get_config_bool(config::Config::Socks5Enabled)
.await?;
match provider::get_provider_info(arg1, socks5_enabled).await {
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("status: {}", info.status as u32);

View File

@@ -22,6 +22,7 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
@@ -32,9 +33,6 @@ use rustyline::{
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing::{error, info, warn};
use tracing_log::LogTracer;
use tracing_subscriber::{fmt, EnvFilter};
mod cmdline;
use self::cmdline::*;
@@ -483,12 +481,7 @@ async fn handle_cmd(
#[tokio::main]
async fn main() -> Result<(), Error> {
// Convert `log` records into `tracing` events.
LogTracer::init()?;
// Setup `tracing` subscriber according to `RUST_LOG` environment variable.
let filter = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
fmt().with_env_filter(filter).with_writer(io::stderr).init();
let _ = pretty_env_logger::try_init();
let args = std::env::args().collect();
start(args).await?;

View File

@@ -5,9 +5,23 @@ and provides asynchronous interface to it.
## Getting started
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
or download a prebuilt release.
Install it anywhere in your `PATH`.
[Create a virtual environment](https://docs.python.org/3/library/venv.html)
if you don't have one already and activate it.
```
$ python -m venv env
$ . env/bin/activate
```
Install `deltachat-rpc-client` from source:
```
$ cd deltachat-rpc-client
$ pip install .
```
## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.

View File

@@ -6,7 +6,7 @@ import asyncio
import logging
import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
async def main():
@@ -30,9 +30,9 @@ async def main():
await deltachat.start_io()
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
for message in await account.get_next_messages():
snapshot = await message.get_snapshot()
if not snapshot.is_bot and not snapshot.is_info:
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()

View File

@@ -21,6 +21,9 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.setuptools_scm]
root = ".."
[tool.black]
line-length = 120

View File

@@ -3,7 +3,7 @@ from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat
from .client import Bot, Client
from .const import EventType
from .const import EventType, SpecialContactId
from .contact import Contact
from .deltachat import DeltaChat
from .message import Message
@@ -19,6 +19,7 @@ __all__ = [
"DeltaChat",
"EventType",
"Message",
"SpecialContactId",
"Rpc",
"run_bot_cli",
"run_client_cli",

View File

@@ -1,5 +1,6 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from warnings import warn
from ._utils import AttrDict
from .chat import Chat
@@ -176,7 +177,7 @@ class Account:
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry[0]) for entry in entries]
return [Chat(self, entry) for entry in entries]
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
@@ -239,7 +240,22 @@ class Account:
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
async def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = await self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn(
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]

View File

@@ -105,6 +105,10 @@ class Chat:
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
async def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
return await self._rpc.can_send(self.account.id, self.id)
async def send_message(
self,
text: Optional[str] = None,

View File

@@ -20,7 +20,7 @@ from ._utils import (
parse_system_image_changed,
parse_system_title_changed,
)
from .const import COMMAND_PREFIX, EventType, SystemMessageType
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType
from .events import (
EventFilter,
GroupImageChanged,
@@ -189,9 +189,10 @@ class Client:
async def _process_messages(self) -> None:
if self._should_process_messages:
for message in await self.account.get_fresh_messages_in_arrival_order():
for message in await self.account.get_next_messages():
snapshot = await message.get_snapshot()
await self._on_new_msg(snapshot)
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
await self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()

View File

@@ -1,6 +1,6 @@
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Union
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
from .contact import Contact
@@ -35,6 +35,13 @@ class Message:
snapshot["message"] = self
return snapshot
async def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
async def mark_seen(self) -> None:
"""Mark the message as seen."""
await self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -23,7 +23,9 @@ class Rpc:
self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.closing: bool
self.reader_task: asyncio.Task
self.events_task: asyncio.Task
async def start(self) -> None:
self.process = await asyncio.create_subprocess_exec(
@@ -35,10 +37,15 @@ class Rpc:
self.id = 0
self.event_queues = {}
self.request_events = {}
self.closing = False
self.reader_task = asyncio.create_task(self.reader_loop())
self.events_task = asyncio.create_task(self.events_loop())
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
await self.stop_io_for_all_accounts()
await self.events_task
self.process.terminate()
await self.reader_task
@@ -58,21 +65,28 @@ class Rpc:
if "id" in response:
fut = self.request_events.pop(response["id"])
fut.set_result(response)
elif response["method"] == "event":
# An event notification.
params = response["params"]
account_id = params["contextId"]
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
await self.event_queues[account_id].put(params["event"])
else:
print(response)
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = asyncio.Queue()
return self.event_queues[account_id]
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
while True:
if self.closing:
return
event = await self.get_next_event()
account_id = event["context_id"]
queue = await self.get_queue(account_id)
await queue.put(event["event"])
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
if account_id in self.event_queues:
return await self.event_queues[account_id].get()
return None
queue = await self.get_queue(account_id)
return await queue.get()
def __getattr__(self, attr: str):
async def method(*args, **kwargs) -> Any:

View File

@@ -98,8 +98,8 @@ async def test_account(acfactory) -> None:
assert await alice.get_chatlist()
assert await alice.get_chatlist(snapshot=True)
assert await alice.get_qr_code()
await alice.get_fresh_messages()
await alice.get_fresh_messages_in_arrival_order()
assert await alice.get_fresh_messages()
assert await alice.get_next_messages()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
@@ -147,7 +147,9 @@ async def test_chat(acfactory) -> None:
assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob)
await alice_chat_bob.delete()
assert not await bob_chat_alice.can_send()
await bob_chat_alice.accept()
assert await bob_chat_alice.can_send()
await bob_chat_alice.block()
bob_chat_alice = await snapshot.sender.create_chat()
await bob_chat_alice.mute()
@@ -237,6 +239,10 @@ async def test_message(acfactory) -> None:
await message.mark_seen()
await message.send_reaction("😎")
reactions = await message.get_reactions()
assert reactions
snapshot = await message.get_snapshot()
assert reactions == snapshot.reactions
@pytest.mark.asyncio()
@@ -303,3 +309,29 @@ async def test_bot(acfactory) -> None:
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id)
@pytest.mark.asyncio()
async def test_wait_next_messages(acfactory) -> None:
alice = await acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = await acfactory.new_preconfigured_account()
await bot.set_config("bot", "1")
await bot.configure()
# There are no old messages and the call returns immediately.
assert not await bot.wait_next_messages()
# Bot starts waiting for messages.
next_messages_task = asyncio.create_task(bot.wait_next_messages())
bot_addr = await bot.get_config("addr")
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
alice_chat_bot = await alice_contact_bot.create_chat()
await alice_chat_bot.send_text("Hello!")
next_messages = await next_messages_task
assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.112.0"
version = "1.114.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -14,12 +14,13 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = "1"
futures-lite = "1.12.0"
serde_json = "1.0.91"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
log = "0.4"
serde_json = "1.0.96"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.25.0", features = ["io-std"] }
tracing-subscriber = { version = "0.3", default-features=false, features = ["std", "env-filter", "fmt"] }
tracing = { version = "0.1" }
tokio = { version = "1.28.0", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
[features]

View File

@@ -3,20 +3,33 @@ use std::env;
///!
///! It speaks JSON Lines over stdio.
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, Context as _, Result};
use deltachat::constants::DC_VERSION_STR;
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};
#[cfg(target_family = "unix")]
use tokio::signal::unix as signal_unix;
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use tracing::{info, trace};
use tracing_subscriber::{fmt, EnvFilter};
use tokio_util::sync::CancellationToken;
use yerpc::{RpcClient, RpcSession};
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
async fn main() {
let r = main_impl().await;
// From tokio documentation:
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
// until the user presses enter."
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
async fn main_impl() -> Result<()> {
let mut args = env::args_os();
let _program_name = args.next().context("no command line arguments found")?;
if let Some(first_arg) = args.next() {
@@ -34,63 +47,97 @@ async fn main() -> Result<()> {
return Err(anyhow!("Unrecognized argument {:?}", arg));
}
let filter = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?;
fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
// Install signal handlers early so that the shutdown is graceful starting from here.
let _ctrl_c = tokio::signal::ctrl_c();
#[cfg(target_family = "unix")]
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
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());
info!("Starting with accounts directory `{}`.", path);
log::info!("Starting with accounts directory `{}`.", path);
let accounts = Accounts::new(PathBuf::from(&path)).await?;
let events = accounts.get_event_emitter();
info!("Creating JSON-RPC API.");
let state = CommandApi::new(accounts);
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));
let state = CommandApi::from_arc(accounts.clone());
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(())
});
let session = RpcSession::new(client.clone(), state.clone());
let main_cancel = CancellationToken::new();
// Send task prints JSON responses to stdout.
let cancel = main_cancel.clone();
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)?;
trace!("RPC send {message}");
let _cancel_guard = cancel.clone().drop_guard();
loop {
let message = tokio::select! {
_ = cancel.cancelled() => break,
message = out_receiver.next() => match message {
None => break,
Some(message) => serde_json::to_string(&message)?,
}
};
log::trace!("RPC send {}", message);
println!("{message}");
}
Ok(())
});
let cancel = main_cancel.clone();
let sigterm_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
#[cfg(target_family = "unix")]
{
let _cancel_guard = cancel.clone().drop_guard();
tokio::select! {
_ = cancel.cancelled() => (),
_ = sigterm.recv() => {
log::info!("got SIGTERM");
}
}
}
let _ = cancel;
Ok(())
});
// Receiver task reads JSON requests from stdin.
let cancel = main_cancel.clone();
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
let _cancel_guard = cancel.clone().drop_guard();
let stdin = io::stdin();
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
trace!("RPC recv {}", message);
loop {
let message = tokio::select! {
_ = cancel.cancelled() => break,
_ = tokio::signal::ctrl_c() => {
log::info!("got ctrl-c event");
break;
}
message = lines.next_line() => match message? {
None => {
log::info!("EOF reached on stdin");
break;
}
Some(message) => message,
}
};
log::trace!("RPC recv {}", message);
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
}
info!("EOF reached on stdin");
Ok(())
});
// Wait for the end of stdin.
main_cancel.cancelled().await;
accounts.read().await.stop_io().await;
drop(accounts);
drop(state);
send_task.await??;
sigterm_task.await??;
recv_task.await??;
// Shutdown the server.
send_task.abort();
events_task.abort();
Ok(())
}

View File

@@ -8,5 +8,5 @@ license = "MPL-2.0"
proc-macro = true
[dependencies]
syn = "1"
syn = "2"
quote = "1"

View File

@@ -11,42 +11,53 @@ ignore = [
# Accept some duplicate versions, ideally we work towards this list
# becoming empty. Adding versions forces us to revisit this at least
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "darling", version = "<0.14" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
{ name = "convert_case", version = "0.4.0" },
{ name = "curve25519-dalek", version = "3.2.0" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "<0.10" },
{ name = "getrandom", version = "<0.2" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "humantime", version = "<2.1" },
{ name = "idna", version = "<0.3" },
{ name = "nom", version = "<7.1" },
{ name = "libm", version = "0.1.4" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand", version = "<0.8" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "time", version = "<0.3" },
{ name = "version_check", version = "<0.9" },
{ name = "wasi", version = "<0.11" },
{ name = "windows-sys", version = "<0.45" },
{ name = "windows_x86_64_msvc", version = "<0.42" },
{ name = "windows_x86_64_gnu", version = "<0.42" },
{ name = "windows_i686_msvc", version = "<0.42" },
{ name = "windows_i686_gnu", version = "<0.42" },
{ name = "windows_aarch64_msvc", version = "<0.42" },
{ name = "unicode-xid", version = "<0.2.4" },
{ name = "syn", version = "<1.0" },
{ name = "quote", version = "<1.0" },
{ name = "proc-macro2", version = "<1.0" },
{ name = "portable-atomic", version = "<1.0" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "convert_case", version = "0.4.0" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
]
@@ -78,7 +89,5 @@ license-files = [
github = [
"async-email",
"deltachat",
"n0-computer",
"quinn-rs",
"dignifiedquire",
]

View File

@@ -1,5 +1,3 @@
use tempfile::tempdir;
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::*;
use deltachat::config;
@@ -8,25 +6,24 @@ use deltachat::context::*;
use deltachat::message::Message;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use tracing::{error, info, warn};
use tracing_subscriber::{fmt, EnvFilter};
use tempfile::tempdir;
fn cb(event: EventType) {
match event {
EventType::ConfigureProgress { progress, .. } => {
info!("progress: {progress}");
log::info!("progress: {}", progress);
}
EventType::Info(msg) => {
info!("{msg}");
log::info!("{}", msg);
}
EventType::Warning(msg) => {
warn!("{msg}");
log::warn!("{}", msg);
}
EventType::Error(msg) => {
error!("{msg}");
log::error!("{}", msg);
}
event => {
info!("{event:?}");
log::info!("{:?}", event);
}
}
}
@@ -34,22 +31,16 @@ fn cb(event: EventType) {
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
#[tokio::main]
async fn main() {
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
fmt()
.with_env_filter(filter)
.with_writer(std::io::stderr)
.init();
pretty_env_logger::try_init_timed().ok();
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
info!("creating database {:?}", dbfile);
log::info!("creating database {:?}", dbfile);
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
.await
.expect("Failed to create context");
let info = ctx.get_info().await;
info!("info: {:#?}", info);
log::info!("info: {:#?}", info);
let events = ctx.get_event_emitter();
let events_spawn = tokio::task::spawn(async move {
@@ -58,7 +49,7 @@ async fn main() {
}
});
info!("configuring");
log::info!("configuring");
let args = std::env::args().collect::<Vec<String>>();
assert_eq!(args.len(), 3, "requires email password");
let email = args[1].clone();
@@ -72,9 +63,9 @@ async fn main() {
ctx.configure().await.unwrap();
info!("------ RUN ------");
log::info!("------ RUN ------");
ctx.start_io().await;
info!("--- SENDING A MESSAGE ---");
log::info!("--- SENDING A MESSAGE ---");
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
.await
@@ -82,7 +73,7 @@ async fn main() {
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
for i in 0..1 {
info!("sending message {}", i);
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
.await
.unwrap();
@@ -91,19 +82,19 @@ async fn main() {
// wait for the message to be sent out
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
info!("fetching chats..");
log::info!("fetching chats..");
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
for i in 0..chats.len() {
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
.await
.unwrap();
info!("[{i}] msg: {msg:?}");
log::info!("[{}] msg: {:?}", i, msg);
}
info!("stopping");
log::info!("stopping");
ctx.stop_io().await;
info!("closing");
log::info!("closing");
drop(ctx);
events_spawn.await.unwrap();
}

900
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -151,6 +151,7 @@ module.exports = {
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
DC_STR_ARCHIVEDCHATS: 40,
DC_STR_AUDIO: 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
DC_STR_BACKUP_TRANSFER_QR: 162,
DC_STR_BAD_TIME_MSG_BODY: 85,
DC_STR_BROADCAST_LIST: 115,

View File

@@ -151,6 +151,7 @@ export enum C {
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
DC_STR_ARCHIVEDCHATS = 40,
DC_STR_AUDIO = 11,
DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
DC_STR_BACKUP_TRANSFER_QR = 162,
DC_STR_BAD_TIME_MSG_BODY = 85,
DC_STR_BROADCAST_LIST = 115,

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.112.0"
}
"version": "1.114.0"
}

View File

@@ -12,17 +12,17 @@ a low-level Chat/Contact/Message API to user interfaces and bots.
Installing pre-built packages (Linux-only)
==========================================
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
If you have a Linux system you may install the ``deltachat`` binary "wheel" packages
without any "build-from-source" steps.
Otherwise you need to `compile the Delta Chat bindings yourself`__.
__ sourceinstall_
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
then create a fresh Python virtual environment and activate it in your shell::
We recommend to first create a fresh Python virtual environment
and activate it in your shell::
virtualenv env # or: python -m venv
source env/bin/activate
python -m venv env
source env/bin/activate
Afterwards, invoking ``python`` or ``pip install`` only
modifies files in your ``env`` directory and leaves
@@ -40,16 +40,14 @@ To verify it worked::
Running tests
=============
Recommended way to run tests is using `tox <https://tox.wiki>`_.
After successful binding installation you can install tox
and run the tests::
Recommended way to run tests is using `scripts/run-python-test.sh`
script provided in the core repository.
pip install tox
tox -e py3
This will run all "offline" tests and skip all functional
This script compiles the library in debug mode and runs the tests using `tox`_.
By default it will run all "offline" tests and skip all functional
end-to-end tests that require accounts on real e-mail servers.
.. _`tox`: https://tox.wiki
.. _livetests:
Running "live" tests with temporary accounts
@@ -61,13 +59,32 @@ Please feel free to contact us through a github issue or by e-mail and we'll sen
export DCC_NEW_TMP_EMAIL=<URL you got from us>
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
One hour is enough to invoke pytest and run all offline and online tests::
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server.
These accounts are removed automatically as they expire.
After setting the variable, either rerun `scripts/run-python-test.sh`
or run offline and online tests with `tox` directly::
tox -e py3
tox -e py
Each test run creates new accounts.
Developing the bindings
-----------------------
If you want to develop or debug the bindings,
you can create a testing development environment using `tox`::
tox -c python --devenv env
. env/bin/activate
Inside this environment the bindings are installed
in editable mode (as if installed with `python -m pip install -e`)
together with the testing dependencies like `pytest` and its plugins.
You can then edit the source code in the development tree
and quickly run `pytest` manually without waiting for `tox`
to recreating the virtual environment each time.
.. _sourceinstall:
Installing bindings from source
@@ -89,20 +106,34 @@ To install the Delta Chat Python bindings make sure you have Python3 installed.
E.g. on Debian-based systems `apt install python3 python3-pip
python3-venv` should give you a usable python installation.
Ensure you are in the deltachat-core-rust/python directory, create the
virtual environment with dependencies using tox
and activate it in your shell::
First, build the core library::
cd python
tox --devenv env
cargo build --release -p deltachat_ffi --features jsonrpc
`jsonrpc` feature is required even if not used by the bindings
because `deltachat.h` includes JSON-RPC functions unconditionally.
Create the virtual environment and activate it:
python -m venv env
source env/bin/activate
You should now be able to build the python bindings using the supplied script::
Build and install the bindings:
python3 install_python_bindings.py
export DCC_RS_DEV="$PWD"
export DCC_RS_TARGET=release
python -m pip install ./python
The core compilation and bindings building might take a while,
depending on the speed of your machine.
`DCC_RS_DEV` environment variable specifies the location of
the core development tree. If this variable is not set,
`libdeltachat` library and `deltachat.h` header are expected
to be installed system-wide.
When `DCC_RS_DEV` is set, `DCC_RS_TARGET` specifies
the build profile name to look up the artifacts
in the target directory.
In this case setting it can be skipped because
`DCC_RS_TARGET=release` is the default.
Building manylinux based wheels
===============================

View File

@@ -24,6 +24,8 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating a temp account to contact the bot")
(ac1,) = acfactory.get_online_accounts(1)
botproc.await_resync()
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
bot_chat = bot_contact.create_chat()
@@ -40,7 +42,7 @@ def test_echo_quit_plugin(acfactory, lp):
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
botproc = acfactory.run_bot_process(group_tracking)
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -52,6 +54,8 @@ def test_group_tracking_plugin(acfactory, lp):
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
botproc.await_resync()
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env python3
"""
setup a python binding development in-place install with cargo debug symbols.
"""
import os
import subprocess
import sys
if __name__ == "__main__":
target = os.environ.get("DCC_RS_TARGET")
if target is None:
os.environ["DCC_RS_TARGET"] = target = "debug"
if "DCC_RS_DEV" not in os.environ:
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.environ["DCC_RS_DEV"] = dn
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
if target == "release":
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
cmd.append("--release")
print("running:", " ".join(cmd))
subprocess.check_call(cmd)
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll", shell=True)
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
subprocess.check_call([sys.executable, "-m", "pip", "install", "-e", "."])

View File

@@ -95,7 +95,6 @@ class Account:
ptr,
lib.dc_context_unref,
)
self._id = lib.dc_get_id(self._dc_context)
self._shutdown_event = Event()
self._event_thread = EventThread(self)
@@ -140,10 +139,6 @@ class Account:
"""return dictionary of built config parameters."""
return get_dc_info_as_dict(self._dc_context)
def get_id(self) -> int:
"""Return account ID."""
return self._id
def dump_account_info(self, logfile):
def log(*args, **kwargs):
kwargs["file"] = logfile
@@ -381,6 +376,22 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()

View File

@@ -263,7 +263,6 @@ class EventThread(threading.Thread):
self._process_event(event)
def _process_event(self, event) -> None:
account_id = lib.dc_event_get_account_id(event)
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
@@ -273,7 +272,6 @@ class EventThread(threading.Thread):
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
assert account_id == self.account.get_id(), f"data2={data2}"
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)

View File

@@ -344,6 +344,16 @@ class Message:
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self.account, contact_id)
def is_from_self(self):
"""Return true if the message is sent by self."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_SELF
def is_from_device(self):
"""Return true if the message is sent by the device."""
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return contact_id == const.DC_CONTACT_ID_DEVICE
#
# Message State query methods
#

View File

@@ -676,6 +676,13 @@ class BotProcess:
print("+++IGN:", line)
ignored.append(line)
def await_resync(self):
self.fnmatch_lines(
"""
*Resync: collected * message IDs in folder INBOX*
""",
)
@pytest.fixture()
def tmp_db_path(tmpdir):

View File

@@ -44,21 +44,21 @@ def test_configure_generate_key(acfactory, lp):
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2._evtracker.wait_next_incoming_message()
msg_in = ac2.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1._evtracker.wait_next_incoming_message()
msg2_in = ac1.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2._evtracker.wait_next_incoming_message()
msg3_in = ac2.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -319,6 +319,9 @@ def test_webxdc_message(acfactory, data, lp):
assert msg2.text == "message1"
assert msg2.is_webxdc()
assert msg2.filename
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
ac2.direct_imap.select_folder("Inbox")
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_mvbox_sentbox_threads(acfactory, lp):
@@ -564,7 +567,8 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
msg = ac2._evtracker.wait_next_incoming_message()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
# Accept the contact request.
msg.chat.accept()

View File

@@ -43,7 +43,7 @@ deps =
pygments
restructuredtext_lint
commands =
black --quiet --check --diff setup.py install_python_bindings.py src/deltachat examples/ tests/
black --quiet --check --diff setup.py src/deltachat examples/ tests/
ruff src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst

1
release-date.in Normal file
View File

@@ -0,0 +1 @@
2023-04-24

View File

@@ -8,6 +8,8 @@ and an own build machine.
- `clippy.sh` runs `cargo clippy` for all Rust code in the project.
- `deny.sh` runs `cargo deny` for all Rust code in the project.
- `../.github/workflows` contains jobs run by GitHub Actions.
- `remote_tests_python.sh` rsyncs to a build machine and runs

View File

@@ -233,7 +233,7 @@ if __name__ == "__main__":
else:
now = datetime.datetime.fromisoformat(sys.argv[2])
out_all += (
"pub static PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
+ str(now.year)
+ ", "

6
scripts/deny.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Update package cache without changing the lockfile.
cargo update --dry-run
cargo deny --workspace --all-features check -D warnings

39
scripts/release.py Normal file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
import sys
import subprocess
import set_core_version
from argparse import ArgumentParser
# update the version
parser = ArgumentParser(prog="release")
parser.add_argument("newversion")
try:
newversion = parser.parse_args().newversion
except SystemExit:
newversion = None
set_core_version.set_version(newversion)
tag = "v" + newversion
# TODO would be nice to automatically checkout the correct branch
# update the changelog
print(f"Updating CHANGELOG.md using git cliff --unreleased --tag {newversion} --prepend CHANGELOG.md")
changelog = subprocess.run(["git", "cliff", "--unreleased", "--tag", newversion, "--prepend", "CHANGELOG.md"]).stdout.strip()
subprocess.run(["git", "add", "-A"])
subprocess.run(["git", "commit", "-m", f"chore(release): prepare for {tag}"])
subprocess.run(["git", "show"])
# create a tag
subprocess.run(
[
"git", "tag", "-a", tag, "-m", f"Release {tag}", "-m", changelog
]
)
subprocess.run(["git", "tag", "-v", tag])
print("Done!")
print(f"Now push the commit (git push origin {tag}).")

View File

@@ -12,7 +12,7 @@ export DCC_RS_DEV=`pwd`
cd python
python install_python_bindings.py onlybuild
cargo build -p deltachat_ffi --features jsonrpc
# remove and inhibit writing PYC files
rm -rf tests/__pycache__
@@ -22,4 +22,5 @@ export PYTHONDONTWRITEBYTECODE=1
# run python tests (tox invokes pytest to run tests in python/tests)
#TOX_PARALLEL_NO_SPINNER=1 tox -e lint,doc
tox -e lint
tox -e doc,py3
tox -e doc
tox -e py -- "$@"

View File

@@ -33,7 +33,7 @@ unset DCC_NEW_TMP_EMAIL
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR/wheelhouse/deltachat*" -w "$TOXWORKDIR/wheelhouse"
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
echo -----------------------

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env python3
import datetime
import json
import os
import pathlib
@@ -8,7 +9,14 @@ import subprocess
from argparse import ArgumentParser
rex = re.compile(r'version = "(\S+)"')
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",
"deltachat-repl/Cargo.toml",
]
def regex_matches(relpath, regex=rex):
p = pathlib.Path(relpath)
@@ -56,33 +64,19 @@ def update_package_json(relpath, newversion):
json_data = json.loads(f.read())
json_data["version"] = newversion
with open(p, "w") as f:
f.write(json.dumps(json_data, sort_keys=True, indent=2))
json.dump(json_data, f, sort_keys=True, indent=2)
f.write("\n")
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
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",
"deltachat-repl/Cargo.toml",
]
try:
opts = parser.parse_args()
except SystemExit:
def set_version(newversion):
if newversion is None:
print()
for x in toml_list:
print(f"{x}: {read_toml_version(x)}")
for x in json_list:
print(f"{x}: {read_json_version(x)}")
print()
raise SystemExit("need argument: new version, example: 1.25.0")
raise SystemExit("need argument: newversion, example: 1.25.0")
newversion = opts.newversion
if newversion.count(".") < 2:
raise SystemExit("need at least two dots in version")
@@ -90,15 +84,25 @@ def main():
ffi_toml = read_toml_version("deltachat-ffi/Cargo.toml")
assert core_toml == ffi_toml, (core_toml, ffi_toml)
today = datetime.date.today().isoformat()
if "alpha" not in newversion:
for line in open("CHANGELOG.md"):
changelog_name = "CHANGELOG.md"
changelog_tmpname = changelog_name + ".tmp"
changelog_tmp = open(changelog_tmpname, "w")
found = False
for line in open(changelog_name):
## 1.25.0
if line.startswith("## [") and line[4:].strip().startswith(newversion):
break
else:
if line == f"## [{newversion}]\n":
line = f"## [{newversion}] - {today}\n"
found = True
changelog_tmp.write(line)
if not found:
raise SystemExit(
f"CHANGELOG.md contains no entry for version: {newversion}"
f"{changelog_name} contains no entry for version: {newversion}"
)
changelog_tmp.close()
os.rename(changelog_tmpname, changelog_name)
for toml_filename in toml_list:
replace_toml_version(toml_filename, newversion)
@@ -106,6 +110,9 @@ def main():
for json_filename in json_list:
update_package_json(json_filename, newversion)
with open("release-date.in", "w") as f:
f.write(today)
print("running cargo check")
subprocess.call(["cargo", "check"])
@@ -119,6 +126,17 @@ def main():
print(f" git push origin v{newversion}")
print("")
def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
try:
newversion = parser.parse_args().newversion
except SystemExit:
newversion = None
set_version(newversion)
if __name__ == "__main__":
main()

32
scripts/zig-cc Executable file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env python
import subprocess
import sys
import os
def flag_filter(flag: str) -> bool:
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
return False
if "self-contained" in flag and "crt" in flag:
return False
return True
def main():
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
zig_target = os.environ["ZIG_TARGET"]
zig_cpu = os.environ.get("ZIG_CPU")
if zig_cpu:
zig_cpu_args = ["-mcpu=" + zig_cpu]
args = [x for x in args if not x.startswith("-march")]
else:
zig_cpu_args = []
subprocess.run(
["zig", "cc", "-target", zig_target, *zig_cpu_args, *args], check=True
)
main()

View File

@@ -1,12 +1,15 @@
#!/bin/sh
#
# Build statically linked deltachat-rpc-server using cargo-zigbuild.
# Build statically linked deltachat-rpc-server using zig.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.68.1
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
@@ -15,9 +18,35 @@ wget "https://ziglang.org/builds/zig-linux-x86_64-$ZIG_VERSION.tar.xz"
tar xf "zig-linux-x86_64-$ZIG_VERSION.tar.xz"
export PATH="$PWD/zig-linux-x86_64-$ZIG_VERSION:$PATH"
cargo install cargo-zigbuild
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86-linux-musl" \
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
for TARGET in x86_64-unknown-linux-musl aarch64-unknown-linux-musl armv7-unknown-linux-musleabihf; do
rustup target add "$TARGET"
cargo zigbuild --release --target "$TARGET" -p deltachat-rpc-server --features vendored
done
rustup target add armv7-unknown-linux-musleabihf
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="arm-linux-musleabihf" \
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add aarch64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored

View File

@@ -6,6 +6,7 @@ use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use uuid::Uuid;
use crate::context::Context;
@@ -17,6 +18,7 @@ use crate::stock_str::StockStrings;
pub struct Accounts {
dir: PathBuf,
config: Config,
/// Map from account ID to the account.
accounts: BTreeMap<u32, Context>,
/// Event channel to emit account manager errors.
@@ -77,12 +79,12 @@ impl Accounts {
})
}
/// Get an account by its `id`:
/// Returns an account by its `id`:
pub fn get_account(&self, id: u32) -> Option<Context> {
self.accounts.get(&id).cloned()
}
/// Get the currently selected account.
/// Returns the currently selected account.
pub fn get_selected_account(&self) -> Option<Context> {
let id = self.config.get_selected_account();
self.accounts.get(&id).cloned()
@@ -96,14 +98,14 @@ impl Accounts {
}
}
/// Select the given account.
/// Selects the given account.
pub async fn select_account(&mut self, id: u32) -> Result<()> {
self.config.select_account(id).await?;
Ok(())
}
/// Add a new account and opens it.
/// Adds a new account and opens it.
///
/// Returns account ID.
pub async fn add_account(&mut self) -> Result<u32> {
@@ -138,7 +140,7 @@ impl Accounts {
Ok(account_config.id)
}
/// Remove an account.
/// Removes an account.
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
let ctx = self
.accounts
@@ -159,7 +161,7 @@ impl Accounts {
Ok(())
}
/// Migrate an existing account into this structure.
/// Migrates an existing account into this structure.
///
/// Returns the ID of new account.
pub async fn migrate_account(&mut self, dbfile: PathBuf) -> Result<u32> {
@@ -262,7 +264,7 @@ impl Accounts {
pub async fn stop_io(&self) {
// Sending an event here wakes up event loop even
// if there are no accounts.
self.emit_event(EventType::Info("Stopping IO for all accounts.".to_string()));
info!(self, "Stopping IO for all accounts.");
for account in self.accounts.values() {
account.stop_io().await;
}
@@ -291,11 +293,6 @@ impl Accounts {
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
/// Returns event channel.
pub fn events(&self) -> Events {
self.events.clone()
}
}
/// Configuration file name.
@@ -306,7 +303,7 @@ pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
struct Config {
file: PathBuf,
inner: InnerConfig,
}
@@ -330,10 +327,8 @@ impl Config {
selected_account: 0,
next_id: 1,
};
let cfg = Config {
file: dir.join(CONFIG_NAME),
inner,
};
let file = dir.join(CONFIG_NAME);
let mut cfg = Self { file, inner };
cfg.sync().await?;
@@ -341,10 +336,24 @@ impl Config {
}
/// Sync the inmemory representation to disk.
async fn sync(&self) -> Result<()> {
fs::write(&self.file, toml::to_string_pretty(&self.inner)?)
/// Takes a mutable reference because the saved file is a part of the `Config` state. This
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
.await
.context("failed to write config")
.context("failed to create a tmp config")?;
file.write_all(toml::to_string_pretty(&self.inner)?.as_bytes())
.await
.context("failed to write a tmp config")?;
file.sync_data()
.await
.context("failed to sync a tmp config")?;
drop(file);
fs::rename(&tmp_path, &self.file)
.await
.context("failed to rename config")?;
Ok(())
}
/// Read a configuration from the given file into memory.
@@ -364,7 +373,7 @@ impl Config {
}
}
let config = Self { file, inner };
let mut config = Self { file, inner };
if modified {
config.sync().await?;
}
@@ -508,17 +517,19 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts1");
let mut accounts1 = Accounts::new(p.clone()).await.unwrap();
accounts1.add_account().await.unwrap();
{
let mut accounts = Accounts::new(p.clone()).await.unwrap();
accounts.add_account().await.unwrap();
let accounts2 = Accounts::open(p).await.unwrap();
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
{
let accounts = Accounts::open(p).await.unwrap();
assert_eq!(accounts1.accounts.len(), 1);
assert_eq!(accounts1.config.get_selected_account(), 1);
assert_eq!(accounts1.dir, accounts2.dir);
assert_eq!(accounts1.config, accounts2.config,);
assert_eq!(accounts1.accounts.len(), accounts2.accounts.len());
assert_eq!(accounts.accounts.len(), 1);
assert_eq!(accounts.config.get_selected_account(), 1);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -308,7 +308,7 @@ async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i6
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
paramsv![from_domain],
(from_domain,),
)
.await?
.unwrap_or(0);
@@ -325,7 +325,7 @@ async fn set_dkim_works_timestamp(
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
paramsv![from_domain, timestamp],
(from_domain, timestamp),
)
.await?;
Ok(())

View File

@@ -9,22 +9,17 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use futures::StreamExt;
use image::{DynamicImage, ImageFormat};
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
use tokio_stream::wrappers::ReadDirStream;
use tracing::{error, info, warn};
use crate::config::Config;
use crate::constants::{
MediaQuality, BALANCED_AVATAR_SIZE, BALANCED_IMAGE_SIZE, WORSE_AVATAR_SIZE, WORSE_IMAGE_SIZE,
};
use crate::constants::{self, MediaQuality};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::message;
use crate::message::Viewtype;
/// Represents a file in the blob directory.
///
@@ -93,7 +88,7 @@ impl<'a> BlobObject<'a> {
if attempt >= MAX_ATTEMPT {
return Err(err).context("failed to create file");
} else if attempt == 1 && !dir.exists() {
fs::create_dir_all(dir).await.ok_or_log(context);
fs::create_dir_all(dir).await.log_err(context).ok();
} else {
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
}
@@ -324,114 +319,149 @@ impl<'a> BlobObject<'a> {
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_AVATAR_SIZE,
MediaQuality::Worse => WORSE_AVATAR_SIZE,
MediaQuality::Balanced => constants::BALANCED_AVATAR_SIZE,
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
};
let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self.recode_to_size(blob_abs, img_wh, Some(20_000))? {
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
{
self.name = new_name;
}
Ok(())
}
pub async fn recode_to_image_size(&self, context: &Context) -> Result<()> {
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
if message::guess_msgtype_from_suffix(Path::new(&blob_abs))
!= Some((Viewtype::Image, "image/jpeg"))
{
return Ok(());
}
let img_wh =
let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
.unwrap_or_default()
{
MediaQuality::Balanced => BALANCED_IMAGE_SIZE,
MediaQuality::Worse => WORSE_IMAGE_SIZE,
MediaQuality::Balanced => (
constants::BALANCED_IMAGE_SIZE,
constants::BALANCED_IMAGE_BYTES,
),
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
if self.recode_to_size(blob_abs, img_wh, None)?.is_some() {
return Err(format_err!(
"Internal error: recode_to_size(..., None) shouldn't change the name of the image"
));
let strict_limits = false;
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
{
self.name = new_name;
}
Ok(())
}
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
/// proceed with the result.
fn recode_to_size(
&self,
context: &Context,
mut blob_abs: PathBuf,
mut img_wh: u32,
max_bytes: Option<usize>,
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image recode failure")?;
let orientation = self.get_exif_orientation();
let mut img = image::open(&blob_abs).context("image decode failure")?;
let (nr_bytes, exif) = self.metadata()?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
let exceeds_width = img.width() > img_wh || img.height() > img_wh;
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
Some(270) => img.rotate270(),
_ => img,
};
let do_scale =
exceeds_width || encoded_img_exceeds_bytes(&img, max_bytes, &mut encoded)?;
let do_rotate = matches!(orientation, Ok(90) | Ok(180) | Ok(270));
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
if do_scale || do_rotate {
if do_rotate {
img = match orientation {
Ok(90) => img.rotate90(),
Ok(180) => img.rotate180(),
Ok(270) => img.rotate270(),
_ => img,
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
_ => {
let jpeg_quality = 75;
ImageOutputFormat::Jpeg(jpeg_quality)
}
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
//
// TODO: Fix lost animation and transparency when recoding using the `image` crate. And
// also `Viewtype::Gif` (maybe renamed to `Animation`) should be used for animated
// images.
let do_scale = exceeds_max_bytes
|| strict_limits
&& (exceeds_wh
|| exif.is_some()
&& encoded_img_exceeds_bytes(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?);
if do_scale {
if !exceeds_wh {
img_wh = max(img.width(), img.height());
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
// crate when recoding, so don't scale them down.
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
if do_scale {
if !exceeds_width {
// The image is already smaller than img_wh, but exceeds max_bytes
// We can directly start with trying to scale down to 2/3 of its current width
img_wh = max(img.width(), img.height()) * 2 / 3
}
loop {
let new_img = img.thumbnail(img_wh, img_wh);
loop {
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(&new_img, max_bytes, &mut encoded)? {
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B",
max_bytes.unwrap_or_default()
));
}
img_wh = img_wh * 2 / 3;
} else {
if encoded.is_empty() {
encode_img(&new_img, &mut encoded)?;
}
info!(
"Final scaled-down image size: {}B ({}px).",
encoded.len(),
img_wh
);
break;
if encoded_img_exceeds_bytes(
context,
&new_img,
ofmt.clone(),
max_bytes,
&mut encoded,
)? && strict_limits
{
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B.",
max_bytes,
));
}
img_wh = img_wh * 2 / 3;
} else {
info!(
context,
"Final scaled-down image size: {}B ({}px).",
encoded.len(),
img_wh
);
break;
}
}
}
// The file format is JPEG now, we may have to change the file extension
if !matches!(ImageFormat::from_path(&blob_abs), Ok(ImageFormat::Jpeg)) {
if do_scale || exif.is_some() {
// The file format is JPEG/PNG now, we may have to change the file extension
if !matches!(fmt, Ok(ImageFormat::Jpeg))
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
{
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No avatar file name (???)")?;
let file_name = blob_abs.file_name().context("No image file name (???)")?;
let file_name = file_name.to_str().context("Filename is no UTF-8 (???)")?;
changed_name = Some(format!("$BLOBDIR/{file_name}"));
}
if encoded.is_empty() {
encode_img(&img, &mut encoded)?;
encode_img(&img, ofmt, &mut encoded)?;
}
std::fs::write(&blob_abs, &encoded)
@@ -442,25 +472,30 @@ impl<'a> BlobObject<'a> {
})
}
pub fn get_exif_orientation(&self) -> Result<i32> {
/// Returns image file size and Exif.
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
let file = std::fs::File::open(self.to_abs_path())?;
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(&file);
let exifreader = exif::Reader::new();
let exif = exifreader.read_from_container(&mut bufreader)?;
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return Ok(180),
Some(6) => return Ok(90),
Some(8) => return Ok(270),
other => warn!("Exif orientation value ignored: {other:?}."),
}
}
Ok(0)
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
if let Some(orientation) = exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
// possible orientation values are described at http://sylvana.net/jpegcrop/exif_orientation.html
// we only use rotation, in practise, flipping is not used.
match orientation.value.get_uint(0) {
Some(3) => return 180,
Some(6) => return 90,
Some(8) => return 270,
other => warn!(context, "Exif orientation value ignored: {other:?}."),
}
}
0
}
impl<'a> fmt::Display for BlobObject<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "$BLOBDIR/{}", self.name)
@@ -486,7 +521,7 @@ impl<'a> BlobDirContents<'a> {
match entry {
Ok(entry) => Some(entry),
Err(err) => {
error!("Failed to read blob file: {err:#}.");
error!(context, "Failed to read blob file: {err}.");
None
}
}
@@ -496,6 +531,7 @@ impl<'a> BlobDirContents<'a> {
true => Some(entry.path()),
false => {
warn!(
context,
"Export: Found blob dir entry {} that is not a file, ignoring.",
entry.path().display()
);
@@ -538,7 +574,7 @@ impl<'a> Iterator for BlobDirIter<'a> {
// silently skipping them is fine.
match BlobObject::from_path(self.context, path) {
Ok(blob) => return Some(blob),
Err(err) => warn!("{err:#}"),
Err(err) => warn!(self.context, "{err}"),
}
}
None
@@ -547,29 +583,35 @@ impl<'a> Iterator for BlobDirIter<'a> {
impl FusedIterator for BlobDirIter<'_> {}
fn encode_img(img: &DynamicImage, encoded: &mut Vec<u8>) -> anyhow::Result<()> {
fn encode_img(
img: &DynamicImage,
fmt: ImageOutputFormat,
encoded: &mut Vec<u8>,
) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, image::ImageFormat::Jpeg)?;
img.write_to(&mut buf, fmt)?;
Ok(())
}
fn encoded_img_exceeds_bytes(
context: &Context,
img: &DynamicImage,
max_bytes: Option<usize>,
fmt: ImageOutputFormat,
max_bytes: usize,
encoded: &mut Vec<u8>,
) -> anyhow::Result<bool> {
if let Some(max_bytes) = max_bytes {
encode_img(img, encoded)?;
if encoded.len() > max_bytes {
info!(
"Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
encode_img(img, fmt, encoded)?;
if encoded.len() > max_bytes {
info!(
context,
"Image size {}B ({}x{}px) exceeds {}B, need to scale down.",
encoded.len(),
img.width(),
img.height(),
max_bytes,
);
return Ok(true);
}
Ok(false)
}
@@ -582,7 +624,7 @@ mod tests {
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
use crate::message::Message;
use crate::message::{Message, Viewtype};
use crate::test_utils::{self, TestContext};
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
@@ -807,7 +849,11 @@ mod tests {
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
check_image_size(avatar_src, 1000, 1000);
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
check_image_size(
&avatar_blob,
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_SIZE,
);
async fn file_size(path_buf: &Path) -> u64 {
let file = File::open(path_buf).await.unwrap();
@@ -815,8 +861,8 @@ mod tests {
}
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
blob.recode_to_size(blob.to_abs_path(), 1000, Some(3000))
let strict_limits = true;
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
@@ -843,10 +889,14 @@ mod tests {
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
assert_eq!(
avatar_cfg,
avatar_src.with_extension("jpg").to_str().unwrap()
avatar_src.with_extension("png").to_str().unwrap()
);
check_image_size(avatar_cfg, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
check_image_size(
avatar_cfg,
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_SIZE,
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -872,18 +922,29 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
// BALANCED_IMAGE_SIZE > 1000, the original image size, so the image is not scaled down:
send_image_check_mediaquality(Some("0"), bytes, 1000, 1000, 0, 1000, 1000)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
Some("0"),
bytes,
"jpg",
true, // has Exif
1000,
1000,
0,
WORSE_IMAGE_SIZE,
WORSE_IMAGE_SIZE,
1000,
1000,
)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
"jpg",
true, // has Exif
1000,
1000,
0,
1000,
1000,
)
.await
.unwrap();
@@ -896,69 +957,87 @@ mod tests {
let img_rotated = send_image_check_mediaquality(
Some("0"),
bytes,
"jpg",
true, // has Exif
2000,
1800,
270,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
1800,
2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
let mut buf = Cursor::new(vec![]);
img_rotated
.write_to(&mut buf, image::ImageFormat::Jpeg)
.unwrap();
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
let bytes = buf.into_inner();
// Do this in parallel to speed up the test a bit
// (it still takes very long though)
let bytes2 = bytes.clone();
let join_handle = tokio::task::spawn(async move {
let img_rotated = send_image_check_mediaquality(
Some("0"),
&bytes2,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
0,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
});
let img_rotated = send_image_check_mediaquality(
Some("1"),
&bytes,
BALANCED_IMAGE_SIZE * 1800 / 2000,
BALANCED_IMAGE_SIZE,
"jpg",
false, // no Exif
1800,
2000,
0,
WORSE_IMAGE_SIZE * 1800 / 2000,
WORSE_IMAGE_SIZE,
1800,
2000,
)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
join_handle.await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_3() {
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("0"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
async fn test_recode_image_balanced_png() {
let bytes = include_bytes!("../test-data/image/screenshot.png");
let bytes = include_bytes!("../test-data/image/rectangle200x180-rotated.jpg");
let img_rotated = send_image_check_mediaquality(Some("1"), bytes, 200, 180, 270, 180, 200)
.await
.unwrap();
assert_correct_rotation(&img_rotated);
send_image_check_mediaquality(
Some("0"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
send_image_check_mediaquality(
Some("1"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
constants::WORSE_IMAGE_SIZE,
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
send_image_check_mediaquality(
Some("0"),
bytes,
"jpg",
true, // has Exif
1920,
1080,
0,
constants::BALANCED_IMAGE_SIZE,
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
)
.await
.unwrap();
}
fn assert_correct_rotation(img: &DynamicImage) {
@@ -978,9 +1057,12 @@ mod tests {
assert_eq!(luma, 0);
}
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
media_quality_config: Option<&str>,
bytes: &[u8],
extension: &str,
has_exif: bool,
original_width: u32,
original_height: u32,
orientation: i32,
@@ -992,7 +1074,7 @@ mod tests {
alice
.set_config(Config::MediaQuality, media_quality_config)
.await?;
let file = alice.get_blobdir().join("file.jpg");
let file = alice.get_blobdir().join("file").with_extension(extension);
fs::write(&file, &bytes)
.await
@@ -1000,7 +1082,13 @@ mod tests {
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
assert_eq!(blob.get_exif_orientation().unwrap_or(0), orientation);
let (_, exif) = blob.metadata()?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
} else {
assert!(exif.is_none());
}
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
@@ -1021,7 +1109,8 @@ mod tests {
let file = bob_msg.get_file(&bob).unwrap();
let blob = BlobObject::new_from_path(&bob, &file).await?;
assert_eq!(blob.get_exif_orientation().unwrap_or(0), 0);
let (_, exif) = blob.metadata()?;
assert!(exif.is_none());
let img = check_image_size(file, compressed_width, compressed_height);
Ok(img)

View File

@@ -10,7 +10,6 @@ use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
@@ -36,8 +35,9 @@ use crate::scheduler::InterruptInfo;
use crate::smtp::send_msg_to_smtp;
use crate::stock_str;
use crate::tools::{
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
strip_rtlo_characters, time, IsNoneOrEmpty,
};
use crate::webxdc::WEBXDC_SUFFIX;
use crate::{location, sql};
@@ -246,7 +246,10 @@ impl ChatId {
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
chat_id
} else {
warn!("Cannot create chat, contact {contact_id} does not exist.");
warn!(
context,
"Cannot create chat, contact {contact_id} does not exist."
);
bail!("Can not create chat for non-existing contact");
}
}
@@ -266,23 +269,29 @@ impl ChatId {
create_protected: ProtectionStatus,
param: Option<String>,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
paramsv![
(
chattype,
grpname,
&grpname,
grpid,
create_blocked,
create_smeared_timestamp(context),
create_protected,
param.unwrap_or_default(),
],
),
).await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
info!(
"Created group/mailinglist '{grpname}' grpid={grpid} as {chat_id}, blocked={create_blocked}."
context,
"Created group/mailinglist '{}' grpid={} as {}, blocked={}.",
&grpname,
grpid,
chat_id,
create_blocked,
);
Ok(chat_id)
@@ -295,7 +304,7 @@ impl ChatId {
"UPDATE contacts
SET selfavatar_sent=?
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=?);",
paramsv![timestamp, self],
(timestamp, self),
)
.await?;
Ok(())
@@ -312,7 +321,7 @@ impl ChatId {
.sql
.execute(
"UPDATE chats SET blocked=?1 WHERE id=?2 AND blocked != ?1",
paramsv![new_blocked, self],
(new_blocked, self),
)
.await?;
Ok(count > 0)
@@ -329,13 +338,16 @@ impl ChatId {
Chattype::Single => {
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != ContactId::SELF {
info!("Blocking the contact {contact_id} to block 1:1 chat.");
info!(
context,
"Blocking the contact {contact_id} to block 1:1 chat."
);
Contact::block(context, contact_id).await?;
}
}
}
Chattype::Group => {
info!("Can't block groups yet, deleting the chat.");
info!(context, "Can't block groups yet, deleting the chat.");
self.delete(context).await?;
}
Chattype::Mailinglist => {
@@ -400,7 +412,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
if protect == chat.protected {
info!("Protection status unchanged for {self}.");
info!(context, "Protection status unchanged for {}.", self);
return Ok(());
}
@@ -423,10 +435,7 @@ impl ChatId {
context
.sql
.execute(
"UPDATE chats SET protected=? WHERE id=?;",
paramsv![protect, self],
)
.execute("UPDATE chats SET protected=? WHERE id=?;", (protect, self))
.await?;
context.emit_event(EventType::ChatModified(self));
@@ -490,7 +499,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, self).await?;
if let Err(e) = self.inner_set_protection(context, protect).await {
error!("Cannot set protection: {e:#}."); // make error user-visible
error!(context, "Cannot set protection: {e:#}."); // make error user-visible
return Err(e);
}
@@ -512,12 +521,12 @@ impl ChatId {
if visibility == ChatVisibility::Archived {
transaction.execute(
"UPDATE msgs SET state=? WHERE chat_id=? AND state=?;",
paramsv![MessageState::InNoticed, self, MessageState::InFresh],
(MessageState::InNoticed, self, MessageState::InFresh),
)?;
}
transaction.execute(
"UPDATE chats SET archived=? WHERE id=?;",
paramsv![visibility, self],
(visibility, self),
)?;
Ok(())
})
@@ -546,7 +555,7 @@ impl ChatId {
.execute(
"UPDATE chats SET archived=0 WHERE id=? AND archived=1 \
AND NOT(muted_until=-1 OR muted_until>?)",
paramsv![self, time()],
(self, time()),
)
.await?;
return Ok(());
@@ -564,7 +573,7 @@ impl ChatId {
WHERE state=?
AND hidden=0
AND chat_id=?",
paramsv![MessageState::InFresh, self],
(MessageState::InFresh, self),
)
.await?;
if unread_cnt == 1 {
@@ -575,7 +584,7 @@ impl ChatId {
}
context
.sql
.execute("UPDATE chats SET archived=0 WHERE id=?", paramsv![self])
.execute("UPDATE chats SET archived=0 WHERE id=?", (self,))
.await?;
Ok(())
}
@@ -604,26 +613,23 @@ impl ChatId {
.sql
.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?);",
paramsv![self],
(self,),
)
.await?;
context
.sql
.execute("DELETE FROM msgs WHERE chat_id=?;", paramsv![self])
.execute("DELETE FROM msgs WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute(
"DELETE FROM chats_contacts WHERE chat_id=?;",
paramsv![self],
)
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (self,))
.await?;
context
.sql
.execute("DELETE FROM chats WHERE id=?;", paramsv![self])
.execute("DELETE FROM chats WHERE id=?;", (self,))
.await?;
context.emit_msgs_changed_without_ids();
@@ -679,7 +685,7 @@ impl ChatId {
.sql
.query_get_value(
"SELECT id FROM msgs WHERE chat_id=? AND state=?;",
paramsv![self, MessageState::OutDraft],
(self, MessageState::OutDraft),
)
.await?;
Ok(msg_id)
@@ -761,14 +767,14 @@ impl ChatId {
"UPDATE msgs
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
WHERE id=?;",
paramsv![
(
time(),
msg.viewtype,
msg.text.as_deref().unwrap_or(""),
msg.param.to_string(),
msg.in_reply_to.as_deref().unwrap_or_default(),
msg.id
],
msg.id,
),
)
.await?;
return Ok(true);
@@ -792,7 +798,7 @@ impl ChatId {
hidden,
mime_in_reply_to)
VALUES (?,?,?, ?,?,?,?,?,?);",
paramsv![
(
self,
ContactId::SELF,
time(),
@@ -802,7 +808,7 @@ impl ChatId {
msg.param.to_string(),
1,
msg.in_reply_to.as_deref().unwrap_or_default(),
],
),
)
.await?;
msg.id = MsgId::new(row_id.try_into()?);
@@ -815,7 +821,7 @@ impl ChatId {
.sql
.count(
"SELECT COUNT(*) FROM msgs WHERE hidden=0 AND chat_id=?",
paramsv![self],
(self,),
)
.await?;
Ok(count)
@@ -858,7 +864,7 @@ impl ChatId {
WHERE state=?
AND hidden=0
AND chat_id=?;",
paramsv![MessageState::InFresh, self],
(MessageState::InFresh, self),
)
.await?
};
@@ -868,7 +874,7 @@ impl ChatId {
pub(crate) async fn get_param(self, context: &Context) -> Result<Params> {
let res: Option<String> = context
.sql
.query_get_value("SELECT param FROM chats WHERE id=?", paramsv![self])
.query_get_value("SELECT param FROM chats WHERE id=?", (self,))
.await?;
Ok(res
.map(|s| s.parse().unwrap_or_default())
@@ -913,13 +919,13 @@ impl ChatId {
let row = sql
.query_row_optional(
&query,
paramsv![
(
self,
MessageState::OutPreparing,
MessageState::OutDraft,
MessageState::OutPending,
MessageState::OutFailed
],
MessageState::OutFailed,
),
f,
)
.await?;
@@ -1041,10 +1047,7 @@ impl ChatId {
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
let timestamp: Option<i64> = context
.sql
.query_get_value(
"SELECT gossiped_timestamp FROM chats WHERE id=?;",
paramsv![self],
)
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
.await?;
Ok(timestamp.unwrap_or_default())
}
@@ -1058,13 +1061,16 @@ impl ChatId {
!self.is_special(),
"can not set gossiped timestamp for special chats"
);
info!("Set gossiped_timestamp for chat {self} to {timestamp}.");
info!(
context,
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
);
context
.sql
.execute(
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
paramsv![timestamp, self],
(timestamp, self),
)
.await?;
@@ -1160,7 +1166,7 @@ impl Chat {
c.blocked, c.locations_send_until, c.muted_until, c.protected
FROM chats c
WHERE c.id=?;",
paramsv![chat_id],
(chat_id,),
|row| {
let c = Chat {
id: chat_id,
@@ -1196,7 +1202,10 @@ impl Chat {
}
}
Err(err) => {
error!("Failed to load contacts for {}: {:#}.", chat.id, err);
error!(
context,
"Failed to load contacts for {}: {:#}.", chat.id, err
);
}
}
chat.name = chat_name;
@@ -1271,7 +1280,7 @@ impl Chat {
.sql
.execute(
"UPDATE chats SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id],
(self.param.to_string(), self.id),
)
.await?;
Ok(())
@@ -1453,13 +1462,16 @@ impl Chat {
.sql
.query_get_value(
"SELECT contact_id FROM chats_contacts WHERE chat_id=?;",
paramsv![self.id],
(self.id,),
)
.await?
{
to_id = id;
} else {
error!("Cannot send message, contact for {} not found.", self.id,);
error!(
context,
"Cannot send message, contact for {} not found.", self.id,
);
bail!("Cannot set message, contact for {} not found.", self.id);
}
} else if self.typ == Chattype::Group
@@ -1531,13 +1543,13 @@ impl Chat {
"INSERT INTO locations \
(timestamp,from_id,chat_id, latitude,longitude,independent)\
VALUES (?,?,?, ?,?,1);",
paramsv![
(
timestamp,
ContactId::SELF,
self.id,
msg.param.get_float(Param::SetLatitude).unwrap_or_default(),
msg.param.get_float(Param::SetLongitude).unwrap_or_default(),
],
),
)
.await
{
@@ -1561,7 +1573,12 @@ impl Chat {
} else {
msg.param.get(Param::SendHtml).map(|s| s.to_string())
};
html.map(|html| new_html_mimepart(html).build().as_string())
match html {
Some(html) => Some(tokio::task::block_in_place(move || {
buf_compress(new_html_mimepart(html).build().as_string().as_bytes())
})?),
None => None,
}
} else {
None
};
@@ -1575,9 +1592,10 @@ impl Chat {
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
state=?, txt=?, subject=?, param=?,
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
mime_headers=?, location_id=?, ephemeral_timer=?, ephemeral_timestamp=?
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
ephemeral_timestamp=?
WHERE id=?;",
paramsv![
params_slice![
new_rfc724_mid,
self.id,
ContactId::SELF,
@@ -1621,11 +1639,12 @@ impl Chat {
mime_references,
mime_modified,
mime_headers,
mime_compressed,
location_id,
ephemeral_timer,
ephemeral_timestamp)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
paramsv![
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
params_slice![
new_rfc724_mid,
self.id,
ContactId::SELF,
@@ -1647,6 +1666,7 @@ impl Chat {
],
)
.await?;
context.new_msgs_notify.notify_one();
msg.id = MsgId::new(u32::try_from(raw_id)?);
maybe_set_logging_xdc(context, msg, self.id).await?;
@@ -1836,7 +1856,7 @@ async fn update_special_chat_name(
.sql
.execute(
"UPDATE chats SET name=? WHERE id=? AND name!=?",
paramsv![name, chat_id, name],
(&name, chat_id, &name),
)
.await?;
}
@@ -1899,7 +1919,7 @@ impl ChatIdBlocked {
WHERE c.type=100 -- 100 = Chattype::Single
AND c.id>9 -- 9 = DC_CHAT_ID_LAST_SPECIAL
AND j.contact_id=?;",
paramsv![contact_id],
(contact_id,),
|row| {
let id: ChatId = row.get(0)?;
let blocked: Blocked = row.get(1)?;
@@ -1950,13 +1970,13 @@ impl ChatIdBlocked {
"INSERT INTO chats
(type, name, param, blocked, created_timestamp)
VALUES(?, ?, ?, ?, ?)",
params![
(
Chattype::Single,
chat_name,
params.to_string(),
create_blocked as u8,
create_smeared_timestamp(context)
],
create_smeared_timestamp(context),
),
)?;
let chat_id = ChatId::new(
transaction
@@ -1969,7 +1989,7 @@ impl ChatIdBlocked {
"INSERT INTO chats_contacts
(chat_id, contact_id)
VALUES((SELECT last_insert_rowid()), ?)",
params![contact_id],
(contact_id,),
)?;
Ok(chat_id)
@@ -2006,7 +2026,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let blob = msg
let mut blob = msg
.param
.get_blob(Param::File, context, !msg.is_increation())
.await?
@@ -2014,7 +2034,10 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Image {
if let Err(err) = blob.recode_to_image_size(context).await {
warn!("Cannot recode image, using original data: {err:#}.");
warn!(
context,
"Cannot recode image, using original data: {err:#}."
);
}
}
msg.param.set(Param::File, blob.as_name());
@@ -2054,6 +2077,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
}
info!(
context,
"Attaching \"{}\" for message type #{}.",
blob.to_abs_path().display(),
msg.viewtype
@@ -2125,7 +2149,7 @@ pub async fn is_contact_in_chat(
.sql
.exists(
"SELECT COUNT(*) FROM chats_contacts WHERE chat_id=? AND contact_id=?;",
paramsv![chat_id, contact_id],
(chat_id, contact_id),
)
.await?;
Ok(exists)
@@ -2177,6 +2201,13 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
}
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
// protect all system messages againts RTLO attacks
if msg.is_system_message() {
if let Some(text) = &msg.text {
msg.text = Some(strip_rtlo_characters(text.as_ref()));
}
}
if prepare_send_msg(context, chat_id, msg).await?.is_some() {
context.emit_msgs_changed(msg.chat_id, msg.id);
@@ -2236,7 +2267,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!("SMTP job cannot get selfavatar-state: {err:#}.");
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
false
}
};
@@ -2261,7 +2292,10 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!("Message {msg_id} has no recipient, skipping smtp-send.");
info!(
context,
"Message {msg_id} has no recipient, skipping smtp-send."
);
msg_id.set_delivered(context).await?;
return Ok(None);
}
@@ -2295,27 +2329,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
if 0 != rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
error!("Failed to set kml sent_timestamp: {err:#}.");
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id)
.await
{
error!("Failed to set msg_location_id: {err:#}.");
error!(context, "Failed to set msg_location_id: {err:#}.");
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
if let Err(err) = context.delete_sync_ids(sync_ids).await {
error!("Failed to delete sync ids: {err:#}.");
error!(context, "Failed to delete sync ids: {err:#}.");
}
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
error!("Failed to set selfavatar timestamp: {err:#}.");
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
}
@@ -2336,12 +2370,12 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result<Option<
.insert(
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
VALUES (?1, ?2, ?3, ?4)",
paramsv![
(
&rendered_msg.rfc724_mid,
recipients,
&rendered_msg.message,
msg_id
],
msg_id,
),
)
.await?;
Ok(Some(row_id))
@@ -2507,7 +2541,7 @@ pub async fn get_chat_msgs_ex(
OR m.from_id == ?
OR m.to_id == ?
);",
paramsv![chat_id, ContactId::INFO, ContactId::INFO],
(chat_id, ContactId::INFO, ContactId::INFO),
process_row,
process_rows,
)
@@ -2520,7 +2554,7 @@ pub async fn get_chat_msgs_ex(
FROM msgs m
WHERE m.chat_id=?
AND m.hidden=0;",
paramsv![chat_id],
(chat_id,),
process_row,
process_rows,
)
@@ -2538,7 +2572,7 @@ pub(crate) async fn marknoticed_chat_if_older_than(
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
paramsv![chat_id],
(chat_id,),
)
.await?
{
@@ -2588,7 +2622,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
.sql
.exists(
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
paramsv![MessageState::InFresh, chat_id],
(MessageState::InFresh, chat_id),
)
.await?;
if !exists {
@@ -2603,7 +2637,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
WHERE state=?
AND hidden=0
AND chat_id=?;",
paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id],
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?;
}
@@ -2652,12 +2686,12 @@ pub(crate) async fn mark_old_messages_as_noticed(
AND hidden=0
AND chat_id=?
AND timestamp<=?;",
paramsv![
(
MessageState::InNoticed,
MessageState::InFresh,
msg.chat_id,
msg.sort_timestamp
],
msg.sort_timestamp,
),
)?;
if changed_rows > 0 {
changed_chats.push(msg.chat_id);
@@ -2669,6 +2703,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
if !changed_chats.is_empty() {
info!(
context,
"Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
);
}
@@ -2700,12 +2735,14 @@ pub async fn get_chat_media(
"SELECT id
FROM msgs
WHERE (1=? OR chat_id=?)
AND chat_id != ?
AND (type=? OR type=? OR type=?)
AND hidden=0
ORDER BY timestamp, id;",
paramsv![
(
chat_id.is_none(),
chat_id.unwrap_or_else(|| ChatId::new(0)),
DC_CHAT_ID_TRASH,
msg_type,
if msg_type2 != Viewtype::Unknown {
msg_type2
@@ -2717,7 +2754,7 @@ pub async fn get_chat_media(
} else {
msg_type
},
],
),
|row| row.get::<_, MsgId>(0),
|ids| Ok(ids.flatten().collect()),
)
@@ -2795,7 +2832,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
ON c.id=cc.contact_id
WHERE cc.chat_id=?
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
paramsv![chat_id],
(chat_id,),
|row| row.get::<_, ContactId>(0),
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
@@ -2821,12 +2858,12 @@ pub async fn create_group_chat(
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![
(
Chattype::Group,
chat_name,
grpid,
create_smeared_timestamp(context),
],
),
)
.await?;
@@ -2859,7 +2896,7 @@ async fn find_unused_broadcast_list_name(context: &Context) -> Result<String> {
.sql
.exists(
"SELECT COUNT(*) FROM chats WHERE type=? AND name=?;",
paramsv![Chattype::Broadcast, better_name],
(Chattype::Broadcast, &better_name),
)
.await?
{
@@ -2879,12 +2916,12 @@ pub async fn create_broadcast_list(context: &Context) -> Result<ChatId> {
"INSERT INTO chats
(type, name, grpid, param, created_timestamp)
VALUES(?, ?, ?, \'U=1\', ?);",
paramsv![
(
Chattype::Broadcast,
chat_name,
grpid,
create_smeared_timestamp(context),
],
),
)
.await?;
let chat_id = ChatId::new(u32::try_from(row_id)?);
@@ -2905,7 +2942,7 @@ pub(crate) async fn add_to_chat_contacts_table(
for contact_id in contact_ids {
transaction.execute(
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
paramsv![chat_id, contact_id],
(chat_id, contact_id),
)?;
}
Ok(())
@@ -2925,7 +2962,7 @@ pub(crate) async fn remove_from_chat_contacts_table(
.sql
.execute(
"DELETE FROM chats_contacts WHERE chat_id=? AND contact_id=?",
paramsv![chat_id, contact_id],
(chat_id, contact_id),
)
.await?;
Ok(())
@@ -2988,7 +3025,10 @@ pub(crate) async fn add_contact_to_chat_ex(
if context.is_self_addr(contact.get_addr()).await? {
// ourself is added using ContactId::SELF, do not add this address explicitly.
// if SELF is not in the group, members cannot be added at all.
warn!("Invalid attempt to add self e-mail address to group.");
warn!(
context,
"Invalid attempt to add self e-mail address to group."
);
return Ok(false);
}
@@ -3001,7 +3041,10 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.is_protected()
&& contact.is_verified(context).await? != VerifiedStatus::BidirectVerified
{
error!("Only bidirectional verified contacts can be added to protected chats.");
error!(
context,
"Only bidirectional verified contacts can be added to protected chats."
);
return Ok(false);
}
if is_contact_in_chat(context, chat_id, contact_id).await? {
@@ -3037,7 +3080,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
FROM chats_contacts cc
LEFT JOIN contacts c ON c.id=cc.contact_id
WHERE cc.chat_id=? AND cc.contact_id!=?;",
paramsv![chat_id, ContactId::SELF],
(chat_id, ContactId::SELF),
|row| Ok(row.get::<_, i64>(0)),
|rows| {
let mut needs_attach = false;
@@ -3110,7 +3153,7 @@ pub async fn set_muted(context: &Context, chat_id: ChatId, duration: MuteDuratio
.sql
.execute(
"UPDATE chats SET muted_until=? WHERE id=?;",
paramsv![duration, chat_id],
(duration, chat_id),
)
.await
.context(format!("Failed to set mute duration for {chat_id}"))?;
@@ -3197,10 +3240,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
if !is_group_explicitly_left(context, grpid).await? {
context
.sql
.execute(
"INSERT INTO leftgrps (grpid) VALUES(?);",
paramsv![grpid.to_string()],
)
.execute("INSERT INTO leftgrps (grpid) VALUES(?);", (grpid,))
.await?;
}
@@ -3210,10 +3250,7 @@ async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()>
pub(crate) async fn is_group_explicitly_left(context: &Context, grpid: &str) -> Result<bool> {
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM leftgrps WHERE grpid=?;",
paramsv![grpid],
)
.exists("SELECT COUNT(*) FROM leftgrps WHERE grpid=?;", (grpid,))
.await?;
Ok(exists)
}
@@ -3245,7 +3282,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
.sql
.execute(
"UPDATE chats SET name=? WHERE id=?;",
paramsv![new_name.to_string(), chat_id],
(new_name.to_string(), chat_id),
)
.await?;
if chat.is_promoted()
@@ -3498,7 +3535,7 @@ pub(crate) async fn get_chat_id_by_grpid(
.sql
.query_row_optional(
"SELECT id, blocked, protected FROM chats WHERE grpid=?;",
paramsv![grpid],
(grpid,),
|row| {
let chat_id = row.get::<_, ChatId>(0)?;
@@ -3531,7 +3568,7 @@ pub async fn add_device_msg_with_importance(
if let Some(label) = label {
if was_device_msg_ever_added(context, label).await? {
info!("Device-message {label} already added.");
info!(context, "Device-message {label} already added.");
return Ok(msg_id);
}
}
@@ -3552,7 +3589,7 @@ pub async fn add_device_msg_with_importance(
.sql
.query_get_value(
"SELECT MAX(timestamp) FROM msgs WHERE chat_id=?",
paramsv![chat_id],
(chat_id,),
)
.await?
{
@@ -3577,7 +3614,7 @@ pub async fn add_device_msg_with_importance(
param,
rfc724_mid)
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
paramsv![
(
chat_id,
ContactId::DEVICE,
ContactId::SELF,
@@ -3589,9 +3626,10 @@ pub async fn add_device_msg_with_importance(
msg.text.as_ref().cloned().unwrap_or_default(),
msg.param.to_string(),
rfc724_mid,
],
),
)
.await?;
context.new_msgs_notify.notify_one();
msg_id = MsgId::new(u32::try_from(row_id)?);
if !msg.hidden {
@@ -3602,10 +3640,7 @@ pub async fn add_device_msg_with_importance(
if let Some(label) = label {
context
.sql
.execute(
"INSERT INTO devmsglabels (label) VALUES (?);",
paramsv![label.to_string()],
)
.execute("INSERT INTO devmsglabels (label) VALUES (?);", (label,))
.await?;
}
@@ -3632,7 +3667,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
.sql
.exists(
"SELECT COUNT(label) FROM devmsglabels WHERE label=?",
paramsv![label],
(label,),
)
.await?;
@@ -3649,10 +3684,7 @@ pub async fn was_device_msg_ever_added(context: &Context, label: &str) -> Result
pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Result<()> {
context
.sql
.execute(
"DELETE FROM msgs WHERE from_id=?;",
paramsv![ContactId::DEVICE],
)
.execute("DELETE FROM msgs WHERE from_id=?;", (ContactId::DEVICE,))
.await?;
context.sql.execute("DELETE FROM devmsglabels;", ()).await?;
@@ -3695,7 +3727,7 @@ pub(crate) async fn add_info_msg_with_cmd(
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
paramsv![
(
chat_id,
from_id.unwrap_or(ContactId::INFO),
ContactId::INFO,
@@ -3709,8 +3741,9 @@ pub(crate) async fn add_info_msg_with_cmd(
ephemeral_timer,
param.to_string(),
parent.map(|msg|msg.rfc724_mid.clone()).unwrap_or_default()
]
)
).await?;
context.new_msgs_notify.notify_one();
let msg_id = MsgId::new(row_id.try_into()?);
context.emit_msgs_changed(chat_id, msg_id);
@@ -3749,7 +3782,7 @@ pub(crate) async fn update_msg_text_and_timestamp(
.sql
.execute(
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
paramsv![text, timestamp, msg_id],
(text, timestamp, msg_id),
)
.await?;
context.emit_msgs_changed(chat_id, msg_id);
@@ -3762,8 +3795,10 @@ mod tests {
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::TestContext;
use tokio::fs;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_chat_info() {
@@ -5944,7 +5979,7 @@ mod tests {
include_bytes!("../test-data/image/avatar64x64.png"),
)
.await?;
send_media(
let second_image_msg_id = send_media(
&t,
chat_id2,
Viewtype::Image,
@@ -6046,6 +6081,47 @@ mod tests {
4
);
// Delete an image.
delete_msgs(&t, &[second_image_msg_id]).await?;
assert_eq!(
get_chat_media(
&t,
None,
Viewtype::Image,
Viewtype::Sticker,
Viewtype::Webxdc,
)
.await?
.len(),
3
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_blob_renaming() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
add_contact_to_chat(
&alice,
chat_id,
Contact::create(&alice, "bob", "bob@example.net").await?,
)
.await?;
let dir = tempfile::tempdir()?;
let file = dir.path().join("harmless_file.\u{202e}txt.exe");
fs::write(&file, "aaa").await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file(file.to_str().unwrap(), None);
let msg = bob.recv_msg(&alice.send_msg(chat_id, &mut msg).await).await;
// the file bob receives should not contain BIDI-control characters
assert_eq!(
Some("$BLOBDIR/harmless_file.txt.exe"),
msg.param.get(Param::File),
);
Ok(())
}
}

View File

@@ -1,7 +1,6 @@
//! # Chat list module.
use anyhow::{ensure, Context as _, Result};
use tracing::warn;
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
use crate::constants::{
@@ -138,7 +137,7 @@ impl Chatlist {
AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2)
GROUP BY c.id
ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned],
(MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
@@ -165,7 +164,7 @@ impl Chatlist {
AND c.archived=1
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft],
(MessageState::OutDraft,),
process_row,
process_rows,
)
@@ -177,7 +176,7 @@ impl Chatlist {
// allow searching over special names that may change at any time
// when the ui calls set_stock_translation()
if let Err(err) = update_special_chat_names(context).await {
warn!("Cannot update special chat names: {err:#}.")
warn!(context, "Cannot update special chat names: {err:#}.")
}
let str_like_cmd = format!("%{query}%");
@@ -199,7 +198,7 @@ impl Chatlist {
AND c.name LIKE ?3
GROUP BY c.id
ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, str_like_cmd],
(MessageState::OutDraft, skip_id, str_like_cmd),
process_row,
process_rows,
)
@@ -229,7 +228,7 @@ impl Chatlist {
AND NOT c.archived=?4
GROUP BY c.id
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
paramsv![MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned],
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
process_row,
process_rows,
).await?;
@@ -312,13 +311,17 @@ impl Chatlist {
};
let (lastmsg, lastcontact) = if let Some(lastmsg_id) = lastmsg_id {
let lastmsg = Message::load_from_db(context, lastmsg_id).await?;
let lastmsg = Message::load_from_db(context, lastmsg_id)
.await
.context("loading message failed")?;
if lastmsg.from_id == ContactId::SELF {
(Some(lastmsg), None)
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?;
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
}
Chattype::Single | Chattype::Undefined => (Some(lastmsg), None),
@@ -357,12 +360,31 @@ pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
.sql
.count(
"SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
paramsv![Blocked::Yes, ChatVisibility::Archived],
(Blocked::Yes, ChatVisibility::Archived),
)
.await?;
Ok(count)
}
/// Gets the last message of a chat, the message that would also be displayed in the ChatList
/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
pub async fn get_last_message_for_chat(
context: &Context,
chat_id: ChatId,
) -> Result<Option<MsgId>> {
context
.sql
.query_get_value(
"SELECT id
FROM msgs
WHERE chat_id=?2
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1",
(MessageState::OutDraft, chat_id),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -308,6 +308,9 @@ pub enum Config {
/// This value is used internally to remember the MsgId of the logging xdc
#[strum(props(default = "0"))]
DebugLogging,
/// Last message processed by the bot.
LastMsgId,
}
impl Context {
@@ -358,6 +361,11 @@ impl Context {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
}
/// Returns 32-bit unsigned integer configuration value for the given key.
pub async fn get_config_u32(&self, key: Config) -> Result<u32> {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
}
/// Returns 64-bit signed integer configuration value for the given key.
pub async fn get_config_i64(&self, key: Config) -> Result<i64> {
Ok(self.get_config_parsed(key).await?.unwrap_or_default())
@@ -459,6 +467,12 @@ impl Context {
Ok(())
}
/// Set the given config to an unsigned 32-bit integer value.
pub async fn set_config_u32(&self, key: Config, value: u32) -> Result<()> {
self.set_config(key, Some(&value.to_string())).await?;
Ok(())
}
/// Set the given config to a boolean value.
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { Some("0") })

View File

@@ -1,8 +1,16 @@
//! Email accounts autoconfiguration process module.
//! # Email accounts autoconfiguration process.
//!
//! The module provides automatic lookup of configuration
//! for email providers based on the built-in [provider database],
//! [Mozilla Thunderbird Autoconfiguration protocol]
//! and [Outlook's Autodiscover].
//!
//! [provider database]: crate::provider
//! [Mozilla Thunderbird Autoconfiguration protocol]: auto_mozilla
//! [Outlook's Autodiscover]: auto_outlook
mod auto_mozilla;
mod auto_outlook;
mod read_url;
mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
@@ -13,7 +21,6 @@ use futures_lite::FutureExt as _;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use tracing::{info, warn};
use crate::config::Config;
use crate::contact::addr_cmp;
@@ -101,7 +108,7 @@ impl Context {
}
async fn inner_configure(&self) -> Result<()> {
info!("Configure ...");
info!(self, "Configure ...");
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
@@ -126,10 +133,13 @@ async fn on_configure_completed(
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !context.config_exists(def.key).await? {
info!("apply config_defaults {}={}", def.key, def.value);
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
} else {
info!("skip already set config_defaults {}={}", def.key, def.value);
info!(
context,
"skip already set config_defaults {}={}", def.key, def.value
);
}
}
}
@@ -141,7 +151,7 @@ async fn on_configure_completed(
.await
.is_err()
{
warn!("cannot add after_login_hint as core-provider-info");
warn!(context, "cannot add after_login_hint as core-provider-info");
}
}
}
@@ -154,7 +164,9 @@ async fn on_configure_completed(
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.ok_or_log_msg(context, "Cannot add AEAP explanation");
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
}
}
}
@@ -184,7 +196,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.await?
.and_then(|e| e.parse().ok())
{
info!("Authorized address is {oauth2_addr}");
info!(ctx, "Authorized address is {}", oauth2_addr);
param.addr = oauth2_addr;
ctx.sql
.set_raw_config("addr", Some(param.addr.as_str()))
@@ -213,17 +225,22 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
{
// no advanced parameters entered by the user: query provider-database or do Autoconfig
info!("checking internal provider-info for offline autoconfig");
info!(
ctx,
"checking internal provider-info for offline autoconfig"
);
if let Some(provider) = provider::get_provider_info(&param_domain, socks5_enabled).await {
if let Some(provider) =
provider::get_provider_info(ctx, &param_domain, socks5_enabled).await
{
param.provider = Some(provider);
match provider.status {
provider::Status::Ok | provider::Status::Preparation => {
if provider.server.is_empty() {
info!("offline autoconfig found, but no servers defined");
info!(ctx, "offline autoconfig found, but no servers defined");
param_autoconfig = None;
} else {
info!("offline autoconfig found");
info!(ctx, "offline autoconfig found");
let servers = provider
.server
.iter()
@@ -250,17 +267,17 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
provider::Status::Broken => {
info!("offline autoconfig found, provider is broken");
info!(ctx, "offline autoconfig found, provider is broken");
param_autoconfig = None;
}
}
} else {
// Try receiving autoconfig
info!("no offline autoconfig found");
info!(ctx, "no offline autoconfig found");
param_autoconfig = if socks5_enabled {
// Currently we can't do http requests through socks5, to not leak
// the ip, just don't do online autoconfig
info!("socks5 enabled, skipping autoconfig");
info!(ctx, "socks5 enabled, skipping autoconfig");
None
} else {
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await
@@ -458,7 +475,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 920);
e2ee::ensure_secret_key_exists(ctx).await?;
info!("key generation completed");
info!(ctx, "key generation completed");
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
.await?;
@@ -571,13 +588,13 @@ async fn try_imap_one_param(
"None".to_string()
}
);
info!("Trying: {}", inf);
info!(context, "Trying: {}", inf);
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
Err(err) => {
info!("failure: {:#}", err);
info!(context, "failure: {:#}", err);
return Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
@@ -588,14 +605,14 @@ async fn try_imap_one_param(
match imap.connect(context).await {
Err(err) => {
info!("failure: {:#}", err);
info!(context, "failure: {:#}", err);
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
}
Ok(()) => {
info!("success: {}", inf);
info!(context, "success: {}", inf);
Ok(imap)
}
}
@@ -623,19 +640,19 @@ async fn try_smtp_one_param(
"None".to_string()
}
);
info!("Trying: {inf}");
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, socks5_config, addr, provider_strict_tls)
.await
{
info!("failure: {err:#}");
info!(context, "failure: {}", err);
Err(ConfigurationError {
config: inf,
msg: format!("{err:#}"),
})
} else {
info!("success: {inf}");
info!(context, "success: {}", inf);
smtp.disconnect().await;
Ok(())
}

View File

@@ -1,15 +1,15 @@
//! # Thunderbird's Autoconfiguration implementation
//!
//! Documentation: <https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use quick_xml::events::{BytesStart, Event};
//! Documentation: <https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration>
use std::io::BufRead;
use std::str::FromStr;
use tracing::warn;
use super::read_url::read_url;
use quick_xml::events::{BytesStart, Event};
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
#[derive(Debug)]
@@ -263,7 +263,10 @@ pub(crate) async fn moz_autoconfigure(
let res = parse_serverparams(&param_in.addr, &xml_raw);
if let Err(err) = &res {
warn!("Failed to parse Thunderbird autoconfiguration XML: {err:#}");
warn!(
context,
"Failed to parse Thunderbird autoconfiguration XML: {}", err
);
}
res
}

View File

@@ -6,11 +6,10 @@
use std::io::BufRead;
use quick_xml::events::Event;
use tracing::warn;
use super::read_url::read_url;
use super::{Error, ServerParams};
use crate::context::Context;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
/// Result of parsing a single `Protocol` tag.
@@ -203,7 +202,7 @@ pub(crate) async fn outlk_autodiscover(
let xml_raw = read_url(context, &url).await?;
let res = parse_xml(&xml_raw);
if let Err(err) = &res {
warn!("{err:#}");
warn!(context, "{}", err);
}
match res? {
ParsingResult::RedirectUrl(redirect_url) => url = redirect_url,

View File

@@ -1,45 +0,0 @@
use anyhow::{anyhow, format_err};
use tracing::info;
use crate::context::Context;
use crate::socks::Socks5Config;
pub async fn read_url(context: &Context, url: &str) -> anyhow::Result<String> {
match read_url_inner(context, url).await {
Ok(s) => {
info!("Successfully read url {url}");
Ok(s)
}
Err(e) => {
info!("Can't read URL {url}: {e:#}");
Err(format_err!("Can't read URL {url}: {e:#}"))
}
}
}
pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result<String> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let client = crate::http::get_client(socks5_config)?;
let mut url = url.to_string();
// Follow up to 10 http-redirects
for _i in 0..10 {
let response = client.get(&url).send().await?;
if response.status().is_redirection() {
let headers = response.headers();
let header = headers
.get_all("location")
.iter()
.last()
.ok_or_else(|| anyhow!("Redirection doesn't have a target location"))?
.to_str()?;
info!("Following redirect to {header}");
url = header.to_string();
continue;
}
return response.text().await.map_err(Into::into);
}
Err(format_err!("Followed 10 redirections"))
}

View File

@@ -192,11 +192,15 @@ pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
/// How many existing messages shall be fetched after configuration.
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
// max. weight of images to send w/o recoding
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
pub const WORSE_IMAGE_BYTES: usize = 130_000;
// max. width/height of an avatar
pub(crate) const BALANCED_AVATAR_SIZE: u32 = 256;
pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
// max. width/height of images
// max. width/height of images scaled down because of being too huge
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;

View File

@@ -17,16 +17,12 @@ use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::task;
use tokio::time::{timeout, Duration};
use tracing::instrument::WithSubscriber;
use tracing::Instrument;
use tracing::{info, warn};
use crate::aheader::EncryptPreference;
use crate::chat::ChatId;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
use crate::context::future::ContextIdFutureExt;
use crate::context::Context;
use crate::events::EventType;
use crate::key::{DcKey, SignedPublicKey};
@@ -36,7 +32,10 @@ use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::sql::{self, params_iter};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress,
};
use crate::{chat, stock_str};
/// Time during which a contact is considered as seen recently.
@@ -348,7 +347,7 @@ impl Contact {
c.authname, c.param, c.status
FROM contacts c
WHERE c.id=?;",
paramsv![contact_id],
(contact_id,),
|row| {
let name: String = row.get(0)?;
let addr: String = row.get(1)?;
@@ -461,7 +460,7 @@ impl Contact {
.sql
.execute(
"UPDATE msgs SET state=? WHERE from_id=? AND state=?;",
paramsv![MessageState::InNoticed, id, MessageState::InFresh],
(MessageState::InNoticed, id, MessageState::InFresh),
)
.await?;
Ok(())
@@ -494,7 +493,7 @@ impl Contact {
"SELECT id FROM contacts \
WHERE addr=?1 COLLATE NOCASE \
AND id>?2 AND origin>=?3 AND blocked=0;",
paramsv![addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32,],
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
)
.await?;
Ok(id)
@@ -540,7 +539,7 @@ impl Contact {
return Ok((ContactId::SELF, sth_modified));
}
let mut name = name;
let mut name = strip_rtlo_characters(name);
#[allow(clippy::collapsible_if)]
if origin <= Origin::OutgoingTo {
// The user may accidentally have written to a "noreply" address with another MUA:
@@ -550,12 +549,12 @@ impl Contact {
// Filter out use-once addresses (like reply+AEJDGPOECLAP...@reply.github.com):
|| (addr.len() > 50 && addr.contains('+'))
{
info!("hiding contact {addr}");
info!(context, "hiding contact {}", addr);
origin = Origin::Hidden;
// For these kind of email addresses, sender and address often don't belong together
// (like hocuri <notifications@github.com>). In this example, hocuri shouldn't
// be saved as the displayname for notifications@github.com.
name = "";
name = "".to_string();
}
}
@@ -609,7 +608,7 @@ impl Contact {
transaction
.execute(
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
paramsv![
(
new_name,
if update_addr {
addr.to_string()
@@ -627,7 +626,7 @@ impl Contact {
row_authname
},
row_id
],
),
)?;
if update_name || update_authname {
@@ -635,7 +634,7 @@ impl Contact {
// This is one of the few duplicated data, however, getting the chat list is easier this way.
let chat_id: Option<ChatId> = transaction.query_row(
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
params![Chattype::Single, isize::try_from(row_id)?],
(Chattype::Single, isize::try_from(row_id)?),
|row| {
let chat_id: ChatId = row.get(0)?;
Ok(chat_id)
@@ -649,7 +648,7 @@ impl Contact {
"SELECT addr, name, authname
FROM contacts
WHERE id=?",
params![contact_id],
(contact_id,),
|row| {
let addr: String = row.get(0)?;
let name: String = row.get(1)?;
@@ -667,7 +666,7 @@ impl Contact {
let count = transaction.execute(
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
params![chat_name, chat_id])?;
(chat_name, chat_id))?;
if count > 0 {
// Chat name updated
@@ -685,7 +684,7 @@ impl Contact {
.execute(
"INSERT INTO contacts (name, addr, origin, authname)
VALUES (?, ?, ?, ?);",
params![
(
if update_name {
name.to_string()
} else {
@@ -698,12 +697,12 @@ impl Contact {
} else {
"".to_string()
}
],
),
)?;
sth_modified = Modifier::Created;
row_id = u32::try_from(transaction.last_insert_rowid())?;
info!("added contact id={} addr={}", row_id, &addr);
info!(context, "added contact id={} addr={}", row_id, &addr);
}
Ok(row_id)
}).await?;
@@ -743,12 +742,15 @@ impl Contact {
}
}
Err(err) => {
warn!("Failed to add address {addr} from address book: {err:#}");
warn!(
context,
"Failed to add address {} from address book: {}", addr, err
);
}
}
}
Err(err) => {
warn!("{err:#}.");
warn!(context, "{:#}.", err);
}
}
}
@@ -796,12 +798,12 @@ impl Contact {
ORDER BY c.last_seen DESC, c.id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 },
if flag_verified_only { 0i32 } else { 1i32 }
])),
|row| row.get::<_, ContactId>(0),
|ids| {
@@ -848,7 +850,7 @@ impl Contact {
ORDER BY last_seen DESC, id DESC;",
sql::repeat_vars(self_addrs.len())
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo
])),
@@ -881,7 +883,7 @@ impl Contact {
.transaction(move |transaction| {
let mut stmt = transaction
.prepare("SELECT name, grpid FROM chats WHERE type=? AND blocked=?")?;
let rows = stmt.query_map(params![Chattype::Mailinglist, Blocked::Yes], |row| {
let rows = stmt.query_map((Chattype::Mailinglist, Blocked::Yes), |row| {
let name: String = row.get(0)?;
let grpid: String = row.get(1)?;
Ok((name, grpid))
@@ -903,7 +905,7 @@ impl Contact {
// Always do an update in case the blocking is reset or name is changed.
transaction.execute(
"UPDATE contacts SET name=?, origin=?, blocked=1 WHERE addr=?",
params![&name, Origin::MailinglistAddress, &grpid],
(&name, Origin::MailinglistAddress, &grpid),
)?;
}
Ok(())
@@ -918,7 +920,7 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
paramsv![ContactId::LAST_SPECIAL],
(ContactId::LAST_SPECIAL,),
)
.await?;
Ok(count)
@@ -934,7 +936,7 @@ impl Contact {
.sql
.query_map(
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
paramsv![ContactId::LAST_SPECIAL],
(ContactId::LAST_SPECIAL,),
|row| row.get::<_, ContactId>(0),
|ids| {
ids.collect::<std::result::Result<Vec<_>, _>>()
@@ -1028,12 +1030,12 @@ impl Contact {
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],
(contact_id, contact_id),
)?;
if deleted_contacts == 0 {
transaction.execute(
"UPDATE contacts SET origin=? WHERE id=?;",
paramsv![Origin::Hidden, contact_id],
(Origin::Hidden, contact_id),
)?;
}
Ok(())
@@ -1056,24 +1058,24 @@ impl Contact {
}
/// Updates `param` column in the database.
pub(crate) async fn update_param(&self, context: &Context) -> Result<()> {
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET param=? WHERE id=?",
paramsv![self.param.to_string(), self.id],
(self.param.to_string(), self.id),
)
.await?;
Ok(())
}
/// Updates `status` column in the database.
pub(crate) async fn update_status(&self, context: &Context) -> Result<()> {
pub async fn update_status(&self, context: &Context) -> Result<()> {
context
.sql
.execute(
"UPDATE contacts SET status=? WHERE id=?",
paramsv![self.status, self.id],
(&self.status, self.id),
)
.await?;
Ok(())
@@ -1193,9 +1195,7 @@ impl Contact {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
}
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
} else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.verified_key.is_some() {
return Ok(VerifiedStatus::BidirectVerified);
}
@@ -1231,7 +1231,7 @@ impl Contact {
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>?;",
paramsv![ContactId::LAST_SPECIAL],
(ContactId::LAST_SPECIAL,),
)
.await?;
Ok(count)
@@ -1245,10 +1245,7 @@ impl Contact {
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM contacts WHERE id=?;",
paramsv![contact_id],
)
.exists("SELECT COUNT(*) FROM contacts WHERE id=?;", (contact_id,))
.await?;
Ok(exists)
}
@@ -1263,7 +1260,7 @@ impl Contact {
.sql
.execute(
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
paramsv![origin, contact_id, origin],
(origin, contact_id, origin),
)
.await?;
Ok(())
@@ -1292,18 +1289,20 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str()))
strip_rtlo_characters(
&captures
.get(1)
.map_or("".to_string(), |m| normalize_name(m.as_str())),
)
} else {
name.to_string()
strip_rtlo_characters(name)
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(name.to_string(), addr.to_string())
(strip_rtlo_characters(name), addr.to_string())
}
}
@@ -1325,7 +1324,7 @@ async fn set_block_contact(
.sql
.execute(
"UPDATE contacts SET blocked=? WHERE id=?;",
paramsv![i32::from(new_blocking), contact_id],
(i32::from(new_blocking), contact_id),
)
.await?;
@@ -1344,7 +1343,7 @@ WHERE type=? AND id IN (
SELECT chat_id FROM chats_contacts WHERE contact_id=?
);
"#,
paramsv![new_blocking, Chattype::Single, contact_id],
(new_blocking, Chattype::Single, contact_id),
)
.await
.is_ok()
@@ -1389,7 +1388,7 @@ pub(crate) async fn set_profile_image(
.set_config(Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!("Do not use unencrypted selfavatar.");
info!(context, "Do not use unencrypted selfavatar.");
}
} else {
contact.param.set(Param::ProfileImage, profile_image);
@@ -1401,7 +1400,7 @@ pub(crate) async fn set_profile_image(
if was_encrypted {
context.set_config(Config::Selfavatar, None).await?;
} else {
info!("Do not use unencrypted selfavatar deletion.");
info!(context, "Do not use unencrypted selfavatar deletion.");
}
} else {
contact.param.remove(Param::ProfileImage);
@@ -1461,7 +1460,7 @@ pub(crate) async fn update_last_seen(
.sql
.execute(
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
paramsv![timestamp, contact_id],
(timestamp, contact_id),
)
.await?
> 0
@@ -1490,7 +1489,7 @@ pub fn normalize_name(full_name: &str) -> String {
match full_name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
.get(1..full_name.len() - 1)
.map_or("".to_string(), |s| s.trim().into()),
.map_or("".to_string(), |s| s.trim().to_string()),
_ => full_name.to_string(),
}
}
@@ -1556,12 +1555,7 @@ impl RecentlySeenLoop {
pub(crate) fn new(context: Context) -> Self {
let (interrupt_send, interrupt_recv) = channel::bounded(1);
let handle = task::spawn(
Self::run(context, interrupt_recv)
.with_current_subscriber()
.bind_current_context_id()
.in_current_span(),
);
let handle = task::spawn(Self::run(context, interrupt_recv));
Self {
handle,
interrupt_send,
@@ -1581,7 +1575,7 @@ impl RecentlySeenLoop {
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
paramsv![time() - SEEN_RECENTLY_SECONDS],
(time() - SEEN_RECENTLY_SECONDS,),
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;
@@ -1613,6 +1607,7 @@ impl RecentlySeenLoop {
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Recently seen loop waiting for {} or interrupt",
duration_to_str(duration)
);
@@ -1626,7 +1621,10 @@ impl RecentlySeenLoop {
}
}
Ok(Err(err)) => {
warn!("Error receiving an interruption in recently seen loop: {err:#}.");
warn!(
context,
"Error receiving an interruption in recently seen loop: {}", err
);
// Maybe the sender side is closed.
// Terminate the loop to avoid looping indefinitely.
return;
@@ -1640,7 +1638,10 @@ impl RecentlySeenLoop {
}
}
} else {
info!("Recently seen loop is not waiting, event is already due.");
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 {

View File

@@ -11,16 +11,14 @@ use std::time::{Duration, Instant, SystemTime};
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
use ratelimit::Ratelimit;
use tokio::sync::{Mutex, RwLock};
use tokio::task;
use tracing::{info, warn};
use tokio::sync::{Mutex, Notify, RwLock};
use crate::chat::{get_chat_cnt, ChatId};
use crate::config::Config;
use crate::constants::DC_VERSION_STR;
use crate::contact::Contact;
use crate::debug_logging::DebugEventLogData;
use crate::events::{Event, EventEmitter, EventLayer, EventType, Events};
use crate::debug_logging::DebugLogging;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{DcKey, SignedPublicKey};
use crate::login_param::LoginParam;
use crate::message::{self, MessageState, MsgId};
@@ -31,8 +29,6 @@ use crate::stock_str::StockStrings;
use crate::timesmearing::SmearedTimestamp;
use crate::tools::{duration_to_str, time};
pub mod future;
/// Builder for the [`Context`].
///
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
@@ -221,6 +217,11 @@ pub struct InnerContext {
/// IMAP UID resync request.
pub(crate) resync_request: AtomicBool,
/// Notify about new messages.
///
/// This causes [`Context::wait_next_msgs`] to wake up.
pub(crate) new_msgs_notify: Notify,
/// Server ID response if ID capability is supported
/// and the server returned non-NIL on the inbox connection.
/// <https://datatracker.ietf.org/doc/html/rfc2971>
@@ -236,18 +237,16 @@ pub struct InnerContext {
creation_time: SystemTime,
pub(crate) debug_logging: RwLock<Option<DebugLogging>>,
}
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: std::sync::RwLock<String>,
#[derive(Debug)]
pub(crate) struct DebugLogging {
/// The message containing the logging xdc
pub(crate) msg_id: MsgId,
/// Handle to the background task responsible for sending
pub(crate) loop_handle: task::JoinHandle<()>,
/// Channel that log events should be send to
/// A background loop will receive and handle them
pub(crate) sender: Sender<DebugEventLogData>,
/// If debug logging is enabled, this contains all necessary information
///
/// Standard RwLock instead of [`tokio::sync::RwLock`] is used
/// because the lock is used from synchronous [`Context::emit_event`].
pub(crate) debug_logging: std::sync::RwLock<Option<DebugLogging>>,
}
/// The state of ongoing process.
@@ -257,7 +256,7 @@ enum RunningState {
Running { cancel_sender: Sender<()> },
/// Cancel signal has been sent, waiting for ongoing process to be freed.
ShallStop,
ShallStop { request: Instant },
/// There is no ongoing process, a new one can be allocated.
Stopped,
@@ -293,7 +292,7 @@ impl Context {
events: Events,
stock_strings: StockStrings,
) -> Result<Context> {
let context = Self::new_closed(dbfile, id, events.clone(), stock_strings).await?;
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
// Open the database if is not encrypted.
if context.check_passphrase("".to_string()).await? {
@@ -302,11 +301,6 @@ impl Context {
Ok(context)
}
/// Returns `tracing` subscriber layer.
pub fn to_layer(&self) -> EventLayer {
self.events.to_layer()
}
/// Creates new context without opening the database.
pub async fn new_closed(
dbfile: &Path,
@@ -365,6 +359,11 @@ impl Context {
blobdir.display()
);
let new_msgs_notify = Notify::new();
// Notify once immediately to allow processing old messages
// without starting I/O.
new_msgs_notify.notify_one();
let inner = InnerContext {
id,
blobdir,
@@ -381,10 +380,12 @@ impl Context {
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
new_msgs_notify,
server_id: RwLock::new(None),
creation_time: std::time::SystemTime::now(),
last_full_folder_scan: Mutex::new(None),
debug_logging: RwLock::new(None),
last_error: std::sync::RwLock::new("".to_string()),
debug_logging: std::sync::RwLock::new(None),
};
let ctx = Context {
@@ -397,7 +398,7 @@ impl Context {
/// Starts the IO scheduler.
pub async fn start_io(&self) {
if let Ok(false) = self.is_configured().await {
warn!("can not start io on a context that is not configured");
warn!(self, "can not start io on a context that is not configured");
return;
}
self.scheduler.start(self.clone()).await;
@@ -439,41 +440,18 @@ impl Context {
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
if self
.debug_logging
.try_read()
.ok()
.map(|inner| inner.is_some())
== Some(true)
{
self.send_log_event(event.clone()).ok();
};
let lock = self.debug_logging.read().expect("RwLock is poisoned");
if let Some(debug_logging) = &*lock {
debug_logging.log_event(event.clone());
}
}
self.events.emit(Event {
id: self.id,
typ: event,
});
}
pub(crate) fn send_log_event(&self, event: EventType) -> anyhow::Result<()> {
if let Ok(lock) = self.debug_logging.try_read() {
if let Some(DebugLogging {
msg_id: xdc_id,
sender,
..
}) = &*lock
{
let event_data = DebugEventLogData {
time: time(),
msg_id: *xdc_id,
event,
};
sender.try_send(event_data).ok();
}
}
Ok(())
}
/// Emits a generic MsgsChanged event (without chat or message id)
pub fn emit_msgs_changed_without_ids(&self) {
self.emit_event(EventType::MsgsChanged {
@@ -531,6 +509,9 @@ impl Context {
pub(crate) async fn free_ongoing(&self) {
let mut s = self.running_state.write().await;
if let RunningState::ShallStop { request } = *s {
info!(self, "Ongoing stopped in {:?}", request.elapsed());
}
*s = RunningState::Stopped;
}
@@ -540,13 +521,15 @@ impl Context {
match &*s {
RunningState::Running { cancel_sender } => {
if let Err(err) = cancel_sender.send(()).await {
warn!("could not cancel ongoing: {:#}", err);
warn!(self, "could not cancel ongoing: {:#}", err);
}
info!("Signaling the ongoing process to stop ASAP.",);
*s = RunningState::ShallStop;
info!(self, "Signaling the ongoing process to stop ASAP.",);
*s = RunningState::ShallStop {
request: Instant::now(),
};
}
RunningState::ShallStop | RunningState::Stopped => {
info!("No ongoing process to stop.",);
RunningState::ShallStop { .. } | RunningState::Stopped => {
info!(self, "No ongoing process to stop.",);
}
}
}
@@ -555,7 +538,7 @@ impl Context {
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
match &*self.running_state.read().await {
RunningState::Running { .. } => false,
RunningState::ShallStop | RunningState::Stopped => true,
RunningState::ShallStop { .. } | RunningState::Stopped => true,
}
}
@@ -768,6 +751,10 @@ impl Context {
"debug_logging",
self.get_config_int(Config::DebugLogging).await?.to_string(),
);
res.insert(
"last_msg_id",
self.get_config_int(Config::LastMsgId).await?.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
@@ -800,7 +787,7 @@ impl Context {
" AND NOT(c.muted_until=-1 OR c.muted_until>?)",
" ORDER BY m.timestamp DESC,m.id DESC;"
),
paramsv![MessageState::InFresh, time()],
(MessageState::InFresh, time()),
|row| row.get::<_, MsgId>(0),
|rows| {
let mut list = Vec::new();
@@ -814,6 +801,66 @@ impl Context {
Ok(list)
}
/// Returns a list of messages with database ID higher than requested.
///
/// Blocked contacts and chats are excluded,
/// but self-sent messages and contact requests are included in the results.
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
Some(s) => MsgId::new(s.parse()?),
None => MsgId::new_unset(),
};
let list = self
.sql
.query_map(
"SELECT m.id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
LEFT JOIN chats c
ON m.chat_id=c.id
WHERE m.id>?
AND m.hidden=0
AND m.chat_id>9
AND ct.blocked=0
AND c.blocked!=1
ORDER BY m.id ASC",
(
last_msg_id.to_u32(), // Explicitly convert to u32 because 0 is allowed.
),
|row| {
let msg_id: MsgId = row.get(0)?;
Ok(msg_id)
},
|rows| {
let mut list = Vec::new();
for row in rows {
list.push(row?);
}
Ok(list)
},
)
.await?;
Ok(list)
}
/// Returns a list of messages with database ID higher than last marked as seen.
///
/// This function is supposed to be used by bot to request messages
/// that are not processed yet.
///
/// Waits for notification and returns a result.
/// Note that the result may be empty if the message is deleted
/// shortly after notification or notification is manually triggered
/// to interrupt waiting.
/// Notification may be manually triggered by calling [`Self::stop_io`].
pub async fn wait_next_msgs(&self) -> Result<Vec<MsgId>> {
self.new_msgs_notify.notified().await;
let list = self.get_next_msgs().await?;
Ok(list)
}
/// Searches for messages containing the query string.
///
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
@@ -825,24 +872,10 @@ impl Context {
}
let str_like_in_text = format!("%{real_query}%");
let do_query = |query, params| {
self.sql.query_map(
query,
params,
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
};
let list = if let Some(chat_id) = chat_id {
do_query(
"SELECT m.id AS id
self.sql
.query_map(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -851,9 +884,17 @@ impl Context {
AND ct.blocked=0
AND txt LIKE ?
ORDER BY m.timestamp,m.id;",
paramsv![chat_id, str_like_in_text],
)
.await?
(chat_id, str_like_in_text),
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
} else {
// For performance reasons results are sorted only by `id`, that is in the order of
// message reception.
@@ -865,8 +906,9 @@ impl Context {
// of unwanted results that are discarded moments later, we added `LIMIT 1000`.
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
// The limit is documented and UI may add a hint when getting 1000 results.
do_query(
"SELECT m.id AS id
self.sql
.query_map(
"SELECT m.id AS id
FROM msgs m
LEFT JOIN contacts ct
ON m.from_id=ct.id
@@ -878,9 +920,17 @@ impl Context {
AND ct.blocked=0
AND m.txt LIKE ?
ORDER BY m.id DESC LIMIT 1000",
paramsv![str_like_in_text],
)
.await?
(str_like_in_text,),
|row| row.get::<_, MsgId>("id"),
|rows| {
let mut ret = Vec::new();
for id in rows {
ret.push(id?);
}
Ok(ret)
},
)
.await?
};
Ok(list)
@@ -1090,7 +1140,7 @@ mod tests {
t.sql
.execute(
"UPDATE chats SET muted_until=? WHERE id=?;",
paramsv![time() - 3600, bob.id],
(time() - 3600, bob.id),
)
.await
.unwrap();
@@ -1107,10 +1157,7 @@ mod tests {
// to test get_fresh_msgs() with invalid mute_until (everything < -1),
// that results in "muted forever" by definition.
t.sql
.execute(
"UPDATE chats SET muted_until=-2 WHERE id=?;",
paramsv![bob.id],
)
.execute("UPDATE chats SET muted_until=-2 WHERE id=?;", (bob.id,))
.await
.unwrap();
let bob = Chat::load_from_db(&t, bob.id).await.unwrap();
@@ -1445,4 +1492,38 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_next_msgs() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
assert!(alice.get_next_msgs().await?.is_empty());
assert!(bob.get_next_msgs().await?.is_empty());
let sent_msg = alice.send_text(alice_chat.id, "Hi Bob").await;
let received_msg = bob.recv_msg(&sent_msg).await;
let bob_next_msg_ids = bob.get_next_msgs().await?;
assert_eq!(bob_next_msg_ids.len(), 1);
assert_eq!(bob_next_msg_ids.get(0), Some(&received_msg.id));
bob.set_config_u32(Config::LastMsgId, received_msg.id.to_u32())
.await?;
assert!(bob.get_next_msgs().await?.is_empty());
// Next messages include self-sent messages.
let alice_next_msg_ids = alice.get_next_msgs().await?;
assert_eq!(alice_next_msg_ids.len(), 1);
assert_eq!(alice_next_msg_ids.get(0), Some(&sent_msg.sender_msg_id));
alice
.set_config_u32(Config::LastMsgId, sent_msg.sender_msg_id.to_u32())
.await?;
assert!(alice.get_next_msgs().await?.is_empty());
Ok(())
}
}

View File

@@ -1,83 +0,0 @@
//! Futures extensions to track current context ID.
use pin_project_lite::pin_project;
use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::thread_local;
thread_local! {
static THREAD_CONTEXT_ID: RefCell<u32> = RefCell::new(0);
}
pub(crate) struct ContextIdGuard {
previous: u32,
}
pub(crate) fn current_context_id() -> u32 {
THREAD_CONTEXT_ID.with(|context_id| *context_id.borrow())
}
impl ContextIdGuard {
fn new(context_id: u32) -> Self {
let previous = THREAD_CONTEXT_ID.with(|prev_context_id| {
let ret = *prev_context_id.borrow();
*prev_context_id.borrow_mut() = context_id;
ret
});
Self { previous }
}
}
impl Drop for ContextIdGuard {
fn drop(&mut self) {
THREAD_CONTEXT_ID.with(|context_id| {
*context_id.borrow_mut() = self.previous;
})
}
}
pin_project! {
/// A future with attached context ID.
#[derive(Debug, Clone)]
pub struct ContextIdFuture<F> {
context_id: u32,
#[pin]
future: F,
}
}
impl<F> ContextIdFuture<F> {
/// Wraps a future.
pub fn new(context_id: u32, future: F) -> Self {
Self { context_id, future }
}
}
impl<F: Future> Future for ContextIdFuture<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let context_id = self.context_id;
let this = self.project();
let _guard = ContextIdGuard::new(context_id);
this.future.poll(cx)
}
}
/// Future extension to bind context ID.
pub trait ContextIdFutureExt: Sized {
/// Binds context ID to the future.
fn bind_context_id(self, context_id: u32) -> ContextIdFuture<Self> {
ContextIdFuture::new(context_id, self)
}
/// Binds current context ID to the future.
fn bind_current_context_id(self) -> ContextIdFuture<Self> {
self.bind_context_id(current_context_id())
}
}
impl<F> ContextIdFutureExt for F where F: Future {}

View File

@@ -2,17 +2,40 @@
use crate::{
chat::ChatId,
config::Config,
context::{Context, DebugLogging},
context::Context,
message::{Message, MsgId, Viewtype},
param::Param,
tools::time,
webxdc::StatusUpdateItem,
Event, EventType,
EventType,
};
use async_channel::{self as channel, Receiver};
use async_channel::{self as channel, Receiver, Sender};
use serde_json::json;
use std::path::PathBuf;
use tokio::task;
use tracing::info;
#[derive(Debug)]
pub(crate) struct DebugLogging {
/// The message containing the logging xdc
pub(crate) msg_id: MsgId,
/// Handle to the background task responsible for sending
pub(crate) loop_handle: task::JoinHandle<()>,
/// Channel that log events should be sent to.
/// A background loop will receive and handle them.
pub(crate) sender: Sender<DebugEventLogData>,
}
impl DebugLogging {
pub(crate) fn log_event(&self, event: EventType) {
let event_data = DebugEventLogData {
time: time(),
msg_id: self.msg_id,
event,
};
self.sender.try_send(event_data).ok();
}
}
/// Store all information needed to log an event to a webxdc.
pub struct DebugEventLogData {
@@ -49,12 +72,9 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
eprintln!("Can't log event to webxdc status update: {err:#}");
}
Ok(serial) => {
context.events.emit(Event {
id: context.id,
typ: EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
},
context.emit_event(EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
});
}
}
@@ -113,35 +133,37 @@ pub(crate) async fn set_debug_logging_xdc(ctx: &Context, id: Option<MsgId>) -> a
Some(msg_id.to_string().as_ref()),
)
.await?;
let debug_logging = &mut *ctx.debug_logging.write().await;
match debug_logging {
// Switch logging xdc
Some(debug_logging) => debug_logging.msg_id = msg_id,
// Bootstrap background loop for message forwarding
None => {
let (sender, debug_logging_recv) = channel::bounded(1000);
let loop_handle = {
let ctx = ctx.clone();
task::spawn(
async move { debug_logging_loop(&ctx, debug_logging_recv).await },
)
};
*debug_logging = Some(DebugLogging {
msg_id,
loop_handle,
sender,
});
{
let debug_logging = &mut *ctx.debug_logging.write().expect("RwLock is poisoned");
match debug_logging {
// Switch logging xdc
Some(debug_logging) => debug_logging.msg_id = msg_id,
// Bootstrap background loop for message forwarding
None => {
let (sender, debug_logging_recv) = channel::bounded(1000);
let loop_handle = {
let ctx = ctx.clone();
task::spawn(async move {
debug_logging_loop(&ctx, debug_logging_recv).await
})
};
*debug_logging = Some(DebugLogging {
msg_id,
loop_handle,
sender,
});
}
}
}
info!("replacing logging webxdc");
info!(ctx, "replacing logging webxdc");
}
// Delete current debug logging
None => {
ctx.sql
.set_raw_config(Config::DebugLogging.as_ref(), None)
.await?;
*ctx.debug_logging.write().await = None;
info!("removing logging webxdc");
*ctx.debug_logging.write().expect("RwLock is poisoned") = None;
info!(ctx, "removing logging webxdc");
}
}
Ok(())

View File

@@ -5,7 +5,6 @@ use std::str::FromStr;
use anyhow::Result;
use mailparse::ParsedMail;
use tracing::{info, warn};
use crate::aheader::Aheader;
use crate::authres::handle_authres;
@@ -25,6 +24,7 @@ use crate::pgp;
///
/// If the message is wrongly signed, HashSet will be empty.
pub fn try_decrypt(
context: &Context,
mail: &ParsedMail<'_>,
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
@@ -36,7 +36,7 @@ pub fn try_decrypt(
None => return Ok(None),
Some(res) => res,
};
info!("Detected Autocrypt-mime message");
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
encrypted_data_part,
@@ -54,6 +54,7 @@ pub(crate) async fn prepare_decryption(
if mail.headers.get_header(HeaderDef::ListPost).is_some() {
if mail.headers.get_header(HeaderDef::Autocrypt).is_some() {
info!(
context,
"Ignoring autocrypt header since this is a mailing list message. \
NOTE: For privacy reasons, the mailing list software should remove Autocrypt headers."
);
@@ -77,13 +78,13 @@ pub(crate) async fn prepare_decryption(
Ok(header) if addr_cmp(&header.addr, from) => Some(header),
Ok(header) => {
warn!(
"Autocrypt header address {:?} is not {:?}.",
header.addr, from
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from
);
None
}
Err(err) => {
warn!("Failed to parse Autocrypt header: {:#}.", err);
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
}
@@ -318,8 +319,8 @@ pub(crate) async fn get_autocrypt_peerstate(
peerstate.save_to_db(&context.sql).await?;
} else {
info!(
"Refusing to update existing peerstate of {}",
&peerstate.addr
context,
"Refusing to update existing peerstate of {}", &peerstate.addr
);
}
}

View File

@@ -6,7 +6,6 @@ use std::collections::BTreeMap;
use anyhow::{anyhow, Result};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use crate::config::Config;
use crate::context::Context;
@@ -102,7 +101,7 @@ impl MsgId {
.sql
.execute(
"UPDATE msgs SET download_state=? WHERE id=?;",
paramsv![download_state, self],
(download_state, self),
)
.await?;
context.emit_event(EventType::MsgsChanged {
@@ -125,7 +124,7 @@ impl Job {
/// Called in response to `Action::DownloadMsg`.
pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(context).await {
warn!("download: could not connect: {:#}", err);
warn!(context, "download: could not connect: {:#}", err);
return Status::RetryNow;
}
@@ -135,7 +134,7 @@ impl Job {
.sql
.query_row_optional(
"SELECT uid, folder FROM imap WHERE rfc724_mid=? AND target=folder",
paramsv![msg.rfc724_mid],
(&msg.rfc724_mid,),
|row| {
let server_uid: u32 = row.get(0)?;
let server_folder: String = row.get(1)?;
@@ -196,7 +195,7 @@ impl Imap {
}
// we are connected, and the folder is selected
info!("Downloading message {}/{} fully...", folder, uid);
info!(context, "Downloading message {}/{} fully...", folder, uid);
let mut uid_message_ids: BTreeMap<u32, String> = BTreeMap::new();
uid_message_ids.insert(uid, rfc724_mid);
@@ -241,7 +240,7 @@ impl MimeMessage {
text += format!(" [{until}]").as_str();
};
info!("Partial download: {}", text);
info!(context, "Partial download: {}", text);
self.parts.push(Part {
typ: Viewtype::Text,

View File

@@ -2,7 +2,6 @@
use anyhow::{format_err, Context as _, Result};
use num_traits::FromPrimitive;
use tracing::info;
use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
@@ -52,6 +51,7 @@ impl EncryptHelper {
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
pub fn should_encrypt(
&self,
context: &Context,
e2ee_guaranteed: bool,
peerstates: &[(Option<Peerstate>, &str)],
) -> Result<bool> {
@@ -63,7 +63,10 @@ impl EncryptHelper {
for (peerstate, addr) in peerstates {
match peerstate {
Some(peerstate) => {
info!("peerstate for {:?} is {}", addr, peerstate.prefer_encrypt);
info!(
context,
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
);
match peerstate.prefer_encrypt {
EncryptPreference::NoPreference => {}
EncryptPreference::Mutual => prefer_encrypt_count += 1,
@@ -79,7 +82,7 @@ impl EncryptHelper {
if e2ee_guaranteed {
return Err(format_err!("{}", msg));
} else {
info!("{}", msg);
info!(context, "{}", msg);
return Ok(false);
}
}
@@ -319,22 +322,22 @@ Sent with my Delta Chat Messenger: https://delta.chat";
// test with EncryptPreference::NoPreference:
// if e2ee_eguaranteed is unset, there is no encryption as not more than half of peers want encryption
let ps = new_peerstates(EncryptPreference::NoPreference);
assert!(encrypt_helper.should_encrypt(true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(false, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Reset
let ps = new_peerstates(EncryptPreference::Reset);
assert!(encrypt_helper.should_encrypt(true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(false, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with EncryptPreference::Mutual (self is also Mutual)
let ps = new_peerstates(EncryptPreference::Mutual);
assert!(encrypt_helper.should_encrypt(true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(false, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, true, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
// test with missing peerstate
let ps = vec![(None, "bob@foo.bar")];
assert!(encrypt_helper.should_encrypt(true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(false, &ps).unwrap());
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
}
}

View File

@@ -72,7 +72,6 @@ use anyhow::{ensure, Result};
use async_channel::Receiver;
use serde::{Deserialize, Serialize};
use tokio::time::timeout;
use tracing::{error, info, warn};
use crate::chat::{send_msg, ChatId};
use crate::constants::{DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH};
@@ -175,10 +174,7 @@ impl ChatId {
pub async fn get_ephemeral_timer(self, context: &Context) -> Result<Timer> {
let timer = context
.sql
.query_get_value(
"SELECT ephemeral_timer FROM chats WHERE id=?;",
paramsv![self],
)
.query_get_value("SELECT ephemeral_timer FROM chats WHERE id=?;", (self,))
.await?;
Ok(timer.unwrap_or_default())
}
@@ -200,7 +196,7 @@ impl ChatId {
"UPDATE chats
SET ephemeral_timer=?
WHERE id=?;",
paramsv![timer, self],
(timer, self),
)
.await?;
@@ -226,8 +222,8 @@ impl ChatId {
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
"Failed to send a message about ephemeral message timer change: {:?}",
err
context,
"Failed to send a message about ephemeral message timer change: {:?}", err
);
}
}
@@ -292,10 +288,7 @@ impl MsgId {
pub(crate) async fn ephemeral_timer(self, context: &Context) -> Result<Timer> {
let res = match context
.sql
.query_get_value(
"SELECT ephemeral_timer FROM msgs WHERE id=?",
paramsv![self],
)
.query_get_value("SELECT ephemeral_timer FROM msgs WHERE id=?", (self,))
.await?
{
None | Some(0) => Timer::Disabled,
@@ -315,7 +308,7 @@ impl MsgId {
"UPDATE msgs SET ephemeral_timestamp = ? \
WHERE (ephemeral_timestamp == 0 OR ephemeral_timestamp > ?) \
AND id = ?",
paramsv![ephemeral_timestamp, ephemeral_timestamp, self],
(ephemeral_timestamp, ephemeral_timestamp, self),
)
.await?;
context.scheduler.interrupt_ephemeral_task().await;
@@ -339,8 +332,8 @@ pub(crate) async fn start_ephemeral_timers_msgids(
sql::repeat_vars(msg_ids.len())
),
rusqlite::params_from_iter(
std::iter::once(&now as &dyn crate::ToSql)
.chain(std::iter::once(&now as &dyn crate::ToSql))
std::iter::once(&now as &dyn crate::sql::ToSql)
.chain(std::iter::once(&now as &dyn crate::sql::ToSql))
.chain(params_iter(msg_ids)),
),
)
@@ -370,7 +363,7 @@ WHERE
AND ephemeral_timestamp <= ?
AND chat_id != ?
"#,
paramsv![now, DC_CHAT_ID_TRASH],
(now, DC_CHAT_ID_TRASH),
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
@@ -403,12 +396,12 @@ WHERE
AND chat_id != ?
AND chat_id != ?
"#,
paramsv![
(
threshold_timestamp,
DC_CHAT_ID_LAST_SPECIAL,
self_chat_id,
device_chat_id
],
device_chat_id,
),
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
@@ -434,7 +427,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
let rows = select_expired_messages(context, now).await?;
if !rows.is_empty() {
info!("Attempting to delete {} messages.", rows.len());
info!(context, "Attempting to delete {} messages.", rows.len());
let (msgs_changed, webxdc_deleted) = context
.sql
@@ -450,7 +443,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
SET chat_id=?, txt='', subject='', txt_raw='',
mime_headers='', from_id=0, to_id=0, param=''
WHERE id=?",
params![DC_CHAT_ID_TRASH, msg_id],
(DC_CHAT_ID_TRASH, msg_id),
)?;
msgs_changed.push((chat_id, msg_id));
@@ -495,7 +488,7 @@ async fn next_delete_device_after_timestamp(context: &Context) -> Result<Option<
AND chat_id != ?
AND chat_id != ?;
"#,
paramsv![DC_CHAT_ID_TRASH, self_chat_id, device_chat_id],
(DC_CHAT_ID_TRASH, self_chat_id, device_chat_id),
)
.await?;
@@ -519,12 +512,12 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
WHERE ephemeral_timestamp != 0
AND chat_id != ?;
"#,
paramsv![DC_CHAT_ID_TRASH], // Trash contains already deleted messages, skip them
(DC_CHAT_ID_TRASH,), // Trash contains already deleted messages, skip them
)
.await
{
Err(err) => {
warn!("Can't calculate next ephemeral timeout: {}", err);
warn!(context, "Can't calculate next ephemeral timeout: {}", err);
None
}
Ok(ephemeral_timestamp) => ephemeral_timestamp,
@@ -534,8 +527,8 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
match next_delete_device_after_timestamp(context).await {
Err(err) => {
warn!(
"Can't calculate timestamp of the next message expiration: {}",
err
context,
"Can't calculate timestamp of the next message expiration: {}", err
);
None
}
@@ -564,6 +557,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
if let Ok(duration) = until.duration_since(now) {
info!(
context,
"Ephemeral loop waiting for deletion in {} or interrupt",
duration_to_str(duration)
);
@@ -575,7 +569,8 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
delete_expired_messages(context, time())
.await
.ok_or_log(context);
.log_err(context)
.ok();
}
}
@@ -604,12 +599,12 @@ pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()
(download_state != 0 AND timestamp < ?) OR
(ephemeral_timestamp != 0 AND ephemeral_timestamp <= ?))
)",
paramsv![
target,
(
&target,
threshold_timestamp,
threshold_timestamp_extended,
now,
],
),
)
.await?;
@@ -634,12 +629,12 @@ pub(crate) async fn start_ephemeral_timers(context: &Context) -> Result<()> {
WHERE ephemeral_timer > 0 \
AND ephemeral_timestamp = 0 \
AND state NOT IN (?, ?, ?)",
paramsv![
(
time(),
MessageState::InFresh,
MessageState::InNoticed,
MessageState::OutDraft
],
MessageState::OutDraft,
),
)
.await?;
@@ -1105,7 +1100,7 @@ mod tests {
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", paramsv![msg_id])
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
.await
.unwrap();
assert!(rawtxt.is_none_or_empty(), "{rawtxt:?}");
@@ -1130,13 +1125,13 @@ mod tests {
t.sql
.execute(
"INSERT INTO msgs (id, rfc724_mid, timestamp, ephemeral_timestamp) VALUES (?,?,?,?);",
paramsv![id, message_id, timestamp, ephemeral_timestamp],
(id, &message_id, timestamp, ephemeral_timestamp),
)
.await?;
t.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
paramsv![message_id, id],
(&message_id, id),
)
.await?;
}
@@ -1147,7 +1142,7 @@ mod tests {
.sql
.count(
"SELECT COUNT(*) FROM imap WHERE target='' AND rfc724_mid=?",
paramsv![id.to_string()],
(id.to_string(),),
)
.await?,
1
@@ -1158,10 +1153,7 @@ mod tests {
async fn remove_uid(context: &Context, id: u32) -> Result<()> {
context
.sql
.execute(
"DELETE FROM imap WHERE rfc724_mid=?",
paramsv![id.to_string()],
)
.execute("DELETE FROM imap WHERE rfc724_mid=?", (id.to_string(),))
.await?;
Ok(())
}

View File

@@ -1,33 +1,16 @@
#![allow(missing_docs)]
//! # Events specification.
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use async_channel::{self as channel, Receiver, Sender, TrySendError};
use serde::Serialize;
use tracing::Level;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
mod payload;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
pub use self::payload::EventType;
/// Event channel.
#[derive(Debug, Clone)]
pub struct Events {
receiver: Receiver<Event>,
sender: Sender<Event>,
/// The text of the last error logged and emitted as an event.
/// If the ui wants to display an error after a failure,
/// `last_error` should be used to avoid races with the event thread.
pub(crate) last_error: Arc<RwLock<String>>,
}
impl Default for Events {
@@ -41,26 +24,13 @@ impl Events {
pub fn new() -> Self {
let (sender, receiver) = channel::bounded(1_000);
Self {
receiver,
sender,
last_error: Arc::new(RwLock::new("".to_string())),
}
}
/// Set last error string.
/// Implemented as blocking as used from macros in different, not always async blocks.
pub fn set_last_error(&self, error: &str) {
let error = error.to_string();
let mut last_error = self.last_error.write().unwrap();
*last_error = error;
Self { receiver, sender }
}
/// Emits an event into event channel.
///
/// If the channel is full, deletes the oldest event first.
pub fn emit(&self, event: Event) {
if let EventType::Error(formatted) = &event.typ {
self.set_last_error(formatted);
}
match self.sender.try_send(event) {
Ok(()) => {}
Err(TrySendError::Full(event)) => {
@@ -76,93 +46,10 @@ impl Events {
}
}
/// Retrieve the event emitter.
/// Creates an event emitter.
pub fn get_emitter(&self) -> EventEmitter {
EventEmitter(self.receiver.clone())
}
/// Returns `tracing` subscriber layer.
pub fn to_layer(&self) -> EventLayer {
EventLayer::new(self.sender.clone(), self.last_error.clone())
}
}
#[derive(Debug)]
struct MessageStorage<'a>(&'a mut String);
impl tracing::field::Visit for MessageStorage<'_> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
write!(self.0, "{value:?}").ok();
}
}
}
/// Tracing subscriber layer converting logs into Delta Chat events.
#[derive(Debug)]
pub struct EventLayer {
/// Event channel for event submission.
sender: Sender<Event>,
last_error: Arc<RwLock<String>>,
}
impl EventLayer {
pub(crate) fn new(sender: Sender<Event>, last_error: Arc<RwLock<String>>) -> Self {
Self { sender, last_error }
}
fn set_last_error(&self, error: &str) {
let error = error.to_string();
let mut last_error = self.last_error.write().unwrap();
*last_error = error;
}
}
impl<S> Layer<S> for EventLayer
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let context_id = crate::context::future::current_context_id();
let &level = event.metadata().level();
let mut message = "".to_string();
let mut visitor = MessageStorage(&mut message);
event.record(&mut visitor);
match level {
Level::ERROR => {
self.set_last_error(&message);
self.sender
.try_send(Event {
id: context_id,
typ: EventType::Error(message),
})
.ok();
}
Level::WARN => {
self.sender
.try_send(Event {
id: context_id,
typ: EventType::Warning(message),
})
.ok();
}
Level::INFO => {
self.sender
.try_send(Event {
id: context_id,
typ: EventType::Info(message),
})
.ok();
}
Level::TRACE | Level::DEBUG => {}
}
}
}
/// A receiver of events from a [`Context`].
@@ -218,251 +105,3 @@ pub struct Event {
/// These are documented in `deltachat.h` as the `DC_EVENT_*` constants.
pub typ: EventType,
}
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info(String),
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
ImapMessageMoved(String),
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile(String),
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning(String),
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
ErrorSelfNotInGroup(String),
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
MsgsChanged {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId,
/// ID of the message.
msg_id: MsgId,
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
ChatModified(ChatId),
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
ContactsChanged(Option<ContactId>),
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
ImexFileWritten(PathBuf),
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
SecurejoinInviterProgress {
/// ID of the contact that wants to join.
contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
SecurejoinJoinerProgress {
/// ID of the inviting contact.
contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId,
/// Status update ID.
status_update_serial: StatusUpdateSerial,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.
msg_id: MsgId,
},
}

258
src/events/payload.rs Normal file
View File

@@ -0,0 +1,258 @@
//! # Event payloads.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use crate::chat::ChatId;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
use crate::webxdc::StatusUpdateSerial;
/// Event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventType {
/// The library-user may write an informational string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info(String),
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected(String),
/// Emitted when IMAP connection is established and login was successful.
ImapConnected(String),
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent(String),
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted(String),
/// Emitted when an IMAP message has been moved
ImapMessageMoved(String),
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile(String),
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile(String),
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning(String),
/// The library-user should report an error to the end-user.
///
/// As most things are asynchronous, things may go wrong at any time and the user
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
///
/// However, for ongoing processes (eg. configure())
/// or for functions that are expected to fail (eg. dc_continue_key_transfer())
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error(String),
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// dc_set_chat_name(), dc_set_chat_profile_image(),
/// dc_add_contact_to_chat(), dc_remove_contact_from_chat(),
/// dc_send_text_msg() or another sending function.
ErrorSelfNotInGroup(String),
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
/// - Messages sent, received or removed
/// - Chats created, deleted or archived
/// - A draft has been set
///
MsgsChanged {
/// Set if only a single chat is affected by the changes, otherwise 0.
chat_id: ChatId,
/// Set if only a single message is affected by the changes, otherwise 0.
msg_id: MsgId,
},
/// Reactions for the message changed.
ReactionsChanged {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message for which reactions were changed.
msg_id: MsgId,
/// ID of the contact whose reaction set is changed.
contact_id: ContactId,
},
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
IncomingMsg {
/// ID of the chat where the message is assigned.
chat_id: ChatId,
/// ID of the message.
msg_id: MsgId,
},
/// Downloading a bunch of messages just finished.
IncomingMsgBunch {
/// List of incoming message IDs.
msg_ids: Vec<MsgId>,
},
/// Messages were seen or noticed.
/// chat id is always set.
MsgsNoticed(ChatId),
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see dc_msg_get_state().
MsgDelivered {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was successfully sent.
msg_id: MsgId,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see dc_msg_get_state().
MsgFailed {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that could not be sent.
msg_id: MsgId,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see dc_msg_get_state().
MsgRead {
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// ID of the message that was read.
msg_id: MsgId,
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
/// and dc_remove_contact_from_chat().
///
/// This event does not include ephemeral timer modification, which
/// is a separate event.
ChatModified(ChatId),
/// Chat ephemeral timer changed.
ChatEphemeralTimerModified {
/// Chat ID.
chat_id: ChatId,
/// New ephemeral timer value.
timer: EphemeralTimer,
},
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
ContactsChanged(Option<ContactId>),
/// Location of one or more contact has changed.
///
/// @param data1 (u32) contact_id of the contact for which the location has changed.
/// If the locations of several contacts have been changed,
/// eg. after calling dc_delete_all_locations(), this parameter is set to `None`.
LocationChanged(Option<ContactId>),
/// Inform about the configuration progress started by configure().
ConfigureProgress {
/// Progress.
///
/// 0=error, 1-999=progress in permille, 1000=success and done
progress: usize,
/// Progress comment or error, something to display to the user.
comment: Option<String>,
},
/// Inform about the import/export progress started by imex().
///
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
ImexProgress(usize),
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
///
/// A typical purpose for a handler of this event may be to make the file public to some system
/// services.
///
/// @param data2 0
ImexFileWritten(PathBuf),
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by dc_get_securejoin_qr().
SecurejoinInviterProgress {
/// ID of the contact that wants to join.
contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
/// The events are typically sent while dc_join_securejoin(), which
/// may take some time, is executed.
SecurejoinJoinerProgress {
/// ID of the inviting contact.
contact_id: ContactId,
/// Progress as:
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
progress: usize,
},
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
/// and possibly the connectivtiy HTML; see dc_get_connectivity() and
/// dc_get_connectivity_html() for details.
ConnectivityChanged,
/// The user's avatar changed.
SelfavatarChanged,
/// Webxdc status update received.
WebxdcStatusUpdate {
/// Message ID.
msg_id: MsgId,
/// Status update ID.
status_update_serial: StatusUpdateSerial,
},
/// Inform that a message containing a webxdc instance has been deleted.
WebxdcInstanceDeleted {
/// ID of the deleted message.
msg_id: MsgId,
},
}

View File

@@ -7,6 +7,7 @@
//! `MsgId.get_html()` will return HTML -
//! this allows nice quoting, handling linebreaks properly etc.
use std::future::Future;
use std::pin::Pin;
@@ -16,7 +17,6 @@ use futures::future::FutureExt;
use lettre_email::mime::{self, Mime};
use lettre_email::PartBuilder;
use mailparse::ParsedContentType;
use tracing::warn;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::message::{Message, MsgId};
@@ -88,7 +88,7 @@ impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
pub async fn from_bytes(rawmime: &[u8]) -> Result<Self> {
pub async fn from_bytes(context: &Context, rawmime: &[u8]) -> Result<Self> {
let mut parser = HtmlMsgParser {
html: "".to_string(),
plain: None,
@@ -103,7 +103,7 @@ impl HtmlMsgParser {
parser.html = plain.to_html();
}
} else {
parser.cid_to_data_recursive(&parsedmail).await?;
parser.cid_to_data_recursive(context, &parsedmail).await?;
}
Ok(parser)
@@ -174,6 +174,7 @@ impl HtmlMsgParser {
/// This allows the final html-file to be self-contained.
fn cid_to_data_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
@@ -181,7 +182,7 @@ impl HtmlMsgParser {
match get_mime_multipart_type(&mail.ctype) {
MimeMultipartType::Multiple => {
for cur_data in &mail.subparts {
self.cid_to_data_recursive(cur_data).await?;
self.cid_to_data_recursive(context, cur_data).await?;
}
Ok(())
}
@@ -191,7 +192,7 @@ impl HtmlMsgParser {
return Ok(());
}
let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
self.cid_to_data_recursive(&mail).await
self.cid_to_data_recursive(context, &mail).await
}
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -214,8 +215,10 @@ impl HtmlMsgParser {
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}",
re_string, e
re_string,
e
),
}
}
@@ -247,15 +250,15 @@ impl MsgId {
let rawmime = message::get_mime_headers(context, self).await?;
if !rawmime.is_empty() {
match HtmlMsgParser::from_bytes(&rawmime).await {
match HtmlMsgParser::from_bytes(context, &rawmime).await {
Err(err) => {
warn!("get_html: parser error: {:#}", err);
warn!(context, "get_html: parser error: {:#}", err);
Ok(None)
}
Ok(parser) => Ok(Some(parser.html)),
}
} else {
warn!("get_html: no mime for {}", self);
warn!(context, "get_html: no mime for {}", self);
Ok(None)
}
}
@@ -284,8 +287,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_plain_unspecified() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
@@ -302,8 +306,9 @@ This message does not have Content-Type nor Subject.<br/>
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_plain_iso88591() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_iso88591.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
@@ -320,8 +325,9 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_plain_flowed() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_plain_flowed.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
@@ -342,8 +348,9 @@ and will be wrapped as usual.<br/>
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_alt_plain() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r##"<!DOCTYPE html>
@@ -363,8 +370,9 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_html.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
// on windows, `\r\n` linends are returned from mimeparser,
// however, rust multiline-strings use just `\n`;
@@ -380,8 +388,9 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_alt_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_html.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -394,8 +403,9 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_htmlparse_alt_plain_html() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html.replace('\r', ""), // see comment in test_htmlparse_html()
r##"<html>
@@ -412,6 +422,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
async fn test_htmlparse_apple_cid_jpg() {
// load raw mime html-data with related image-part (cid:)
// and make sure, Content-Id has angle-brackets that are removed correctly.
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/apple_cid_jpg.eml");
let test = String::from_utf8_lossy(raw);
assert!(test.contains("Content-Id: <8AE052EF-BC90-486F-BB78-58D3590308EC@fritz.box>"));
@@ -419,7 +430,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert!(test.find("data:").is_none());
// parsing converts cid: to data:
let parser = HtmlMsgParser::from_bytes(raw).await.unwrap();
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert!(parser.html.contains("<html>"));
assert!(!parser.html.contains("Content-Id:"));
assert!(parser.html.contains("data:image/jpeg;base64,/9j/4AAQ"));

View File

@@ -1,24 +0,0 @@
//! # HTTP module.
use std::time::Duration;
use anyhow::Result;
use crate::socks::Socks5Config;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
builder.proxy(proxy)
} else {
// Disable usage of "system" proxy configured via environment variables.
// It is enabled by default in `reqwest`, see
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
// for documentation.
builder.no_proxy()
};
Ok(builder.build()?)
}

View File

@@ -14,9 +14,8 @@ use std::{
use anyhow::{bail, format_err, Context as _, Result};
use async_channel::Receiver;
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use futures::StreamExt;
use futures::{StreamExt, TryStreamExt};
use num_traits::FromPrimitive;
use tracing::{error, info, warn};
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::config::Config;
@@ -307,7 +306,7 @@ impl Imap {
let oauth2 = self.config.lp.oauth2;
info!("Connecting to IMAP server");
info!(context, "Connecting to IMAP server");
let connection_res: Result<Client> = if self.config.lp.security == Socket::Starttls
|| self.config.lp.security == Socket::Plain
{
@@ -364,7 +363,7 @@ impl Imap {
let imap_pw: &str = config.lp.password.as_ref();
let login_res = if oauth2 {
info!("Logging into IMAP server with OAuth 2");
info!(context, "Logging into IMAP server with OAuth 2");
let addr: &str = config.addr.as_ref();
let token = get_oauth2_access_token(context, addr, imap_pw, true)
@@ -376,7 +375,7 @@ impl Imap {
};
client.authenticate("XOAUTH2", auth).await
} else {
info!("Logging into IMAP server with LOGIN");
info!(context, "Logging into IMAP server with LOGIN");
client.login(imap_user, imap_pw).await
};
@@ -392,7 +391,7 @@ impl Imap {
"IMAP-LOGIN as {}",
self.config.lp.user
)));
info!("Successfully logged into IMAP server");
info!(context, "Successfully logged into IMAP server");
Ok(())
}
@@ -400,7 +399,7 @@ impl Imap {
let imap_user = self.config.lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;
warn!("{} ({:#})", message, err);
warn!(context, "{} ({:#})", message, err);
let lock = context.wrong_pw_warning_mutex.lock().await;
if self.login_failed_once
@@ -408,7 +407,7 @@ impl Imap {
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
{
if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await {
warn!("{:#}", e);
warn!(context, "{:#}", e);
}
drop(lock);
@@ -418,7 +417,7 @@ impl Imap {
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await
{
warn!("{:#}", e);
warn!(context, "{:#}", e);
}
} else {
self.login_failed_once = true;
@@ -446,8 +445,8 @@ impl Imap {
/// Drops the session without disconnecting properly.
/// Useful in case of an IMAP error, when it's unclear if it's in a correct state and it's
/// easier to setup a new connection.
pub fn trigger_reconnect(&mut self) {
info!("Dropping an IMAP connection.");
pub fn trigger_reconnect(&mut self, context: &Context) {
info!(context, "Dropping an IMAP connection.");
self.session = None;
}
@@ -511,18 +510,17 @@ impl Imap {
.as_mut()
.context("IMAP No connection established")?;
session.select_folder(Some(folder)).await?;
session.select_folder(context, Some(folder)).await?;
let mut list = session
.uid_fetch("1:*", RFC724MID_UID)
.await
.with_context(|| format!("can't resync folder {folder}"))?;
while let Some(fetch) = list.next().await {
let fetch = fetch?;
while let Some(fetch) = list.try_next().await? {
let headers = match get_fetch_headers(&fetch) {
Ok(headers) => headers,
Err(err) => {
warn!("Failed to parse FETCH headers: {}", err);
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
@@ -540,6 +538,7 @@ impl Imap {
}
info!(
context,
"Resync: collected {} message IDs in folder {}",
msgs.len(),
folder,
@@ -551,7 +550,7 @@ impl Imap {
context
.sql
.transaction(move |transaction| {
transaction.execute("DELETE FROM imap WHERE folder=?", params![folder])?;
transaction.execute("DELETE FROM imap WHERE folder=?", (folder,))?;
for (uid, (rfc724_mid, target)) in &msgs {
// This may detect previously undetected moved
// messages, so we update server_folder too.
@@ -561,7 +560,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
params![rfc724_mid, folder, uid, uid_validity, target],
(rfc724_mid, folder, uid, uid_validity, target),
)?;
}
Ok(())
@@ -582,7 +581,7 @@ impl Imap {
) -> Result<bool> {
let session = self.session.as_mut().context("no session")?;
let newly_selected = session
.select_or_create_folder(folder)
.select_or_create_folder(context, folder)
.await
.with_context(|| format!("failed to select or create folder {folder}"))?;
let mailbox = session
@@ -611,6 +610,7 @@ impl Imap {
} else if let Some(uid_next) = mailbox.uid_next {
if uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {} from {} to {} without changing validity ({}), resyncing UIDs...",
folder, old_uid_next, uid_next, new_uid_validity,
);
@@ -628,7 +628,7 @@ impl Imap {
set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!("Folder \"{}\" is empty.", folder);
info!(context, "Folder \"{}\" is empty.", folder);
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
@@ -644,7 +644,10 @@ impl Imap {
let new_uid_next = match mailbox.uid_next {
Some(uid_next) => uid_next,
None => {
warn!("IMAP folder has no uid_next, fall back to fetching");
warn!(
context,
"IMAP folder has no uid_next, fall back to fetching"
);
// note that we use fetch by sequence number
// and thus we only need to get exactly the
// last-index message.
@@ -656,7 +659,7 @@ impl Imap {
.context("Error fetching UID")?;
let mut new_last_seen_uid = None;
while let Some(fetch) = list.next().await.transpose()? {
while let Some(fetch) = list.try_next().await? {
if fetch.message == mailbox.exists && fetch.uid.is_some() {
new_last_seen_uid = fetch.uid;
}
@@ -673,7 +676,7 @@ impl Imap {
.sql
.execute(
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
paramsv![folder, new_uid_validity],
(&folder, new_uid_validity),
)
.await?;
@@ -681,8 +684,13 @@ impl Imap {
job::schedule_resync(context).await?;
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}",
folder, new_uid_next, new_uid_validity, old_uid_next, old_uid_validity,
folder,
new_uid_next,
new_uid_validity,
old_uid_next,
old_uid_validity,
);
Ok(false)
}
@@ -698,7 +706,7 @@ impl Imap {
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder, folder_meaning).await? {
info!("Not fetching from {}", folder);
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
@@ -708,7 +716,7 @@ impl Imap {
.with_context(|| format!("failed to select folder {folder}"))?;
if !new_emails && !fetch_existing_msgs {
info!("No new emails in folder {}", folder);
info!(context, "No new emails in folder {}", folder);
return Ok(false);
}
@@ -734,7 +742,7 @@ impl Imap {
let headers = match get_fetch_headers(fetch_response) {
Ok(headers) => headers,
Err(err) => {
warn!("Failed to parse FETCH headers: {}", err);
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
@@ -751,7 +759,7 @@ impl Imap {
ON CONFLICT(folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
paramsv![message_id, folder, uid, uid_validity, &target],
(&message_id, &folder, uid, uid_validity, &target),
)
.await?;
@@ -834,7 +842,7 @@ impl Imap {
set_uid_next(context, folder, new_uid_next).await?;
}
info!("{} mails read from \"{}\".", read_cnt, folder);
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
let msg_ids: Vec<MsgId> = received_msgs
.iter()
@@ -881,7 +889,10 @@ impl Imap {
None => continue,
};
if let Some(folder) = context.get_config(config).await? {
info!("Fetching existing messages from folder \"{}\"", folder);
info!(
context,
"Fetching existing messages from folder \"{}\"", folder
);
self.fetch_new_messages(context, &folder, meaning, true)
.await
.context("could not fetch existing messages")?;
@@ -889,7 +900,7 @@ impl Imap {
}
}
info!("Done fetching existing messages.");
info!(context, "Done fetching existing messages.");
Ok(())
}
@@ -974,15 +985,21 @@ impl Session {
Err(err) => {
if context.should_delete_to_trash().await? {
error!(
context,
"Cannot move messages {} to {}, no fallback to COPY/DELETE because \
delete_to_trash is set. Error: {:#}",
set, target, err,
set,
target,
err,
);
return Err(err.into());
}
warn!(
context,
"Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
set, target, err
set,
target,
err
);
}
}
@@ -993,14 +1010,14 @@ impl Session {
let copy = !context.is_trash(target).await?;
if copy {
info!(
"Server does not support MOVE, fallback to COPY/DELETE {} to {}",
set, target
context,
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
);
self.uid_copy(&set, &target).await?;
} else {
error!(
"Server does not support MOVE, fallback to DELETE {} to {}",
set, target,
context,
"Server does not support MOVE, fallback to DELETE {} to {}", set, target,
);
}
context
@@ -1033,7 +1050,7 @@ impl Session {
WHERE folder = ?
AND target != folder
ORDER BY target, uid",
paramsv![folder],
(folder,),
|row| {
let rowid: i64 = row.get(0)?;
let uid: u32 = row.get(1)?;
@@ -1049,7 +1066,7 @@ impl Session {
// MOVE/DELETE operations. This does not result in multiple SELECT commands
// being sent because `select_folder()` does nothing if the folder is already
// selected.
self.select_folder(Some(folder)).await?;
self.select_folder(context, Some(folder)).await?;
// Empty target folder name means messages should be deleted.
if target.is_empty() {
@@ -1070,8 +1087,8 @@ impl Session {
// Expunge folder if needed, e.g. if some jobs have
// deleted messages on the server.
if let Err(err) = self.maybe_close_folder().await {
warn!("failed to close folder: {:?}", err);
if let Err(err) = self.maybe_close_folder(context).await {
warn!(context, "failed to close folder: {:?}", err);
}
Ok(())
@@ -1097,17 +1114,23 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
self.select_folder(Some(&folder))
self.select_folder(context, Some(&folder))
.await
.context("failed to select folder")?;
if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {} in folder {} as seen, will retry later: {}.",
uid_set, folder, err
uid_set,
folder,
err
);
} else {
info!("Marked messages {} in folder {} as seen.", uid_set, folder);
info!(
context,
"Marked messages {} in folder {} as seen.", uid_set, folder
);
context
.sql
.execute(
@@ -1135,12 +1158,15 @@ impl Imap {
.with_context(|| format!("No IMAP connection established, folder: {folder}"))?;
if !session.can_condstore() {
info!("Server does not support CONDSTORE, skipping flag synchronization.");
info!(
context,
"Server does not support CONDSTORE, skipping flag synchronization."
);
return Ok(());
}
session
.select_folder(Some(folder))
.select_folder(context, Some(folder))
.await
.context("failed to select folder")?;
@@ -1153,8 +1179,8 @@ impl Imap {
// We are not interested in actual value of HIGHESTMODSEQ.
if mailbox.highest_modseq.is_none() {
info!(
"Mailbox {} does not support mod-sequences, skipping flag synchronization.",
folder
context,
"Mailbox {} does not support mod-sequences, skipping flag synchronization.", folder
);
return Ok(());
}
@@ -1171,12 +1197,15 @@ impl Imap {
.await
.context("failed to fetch flags")?;
while let Some(fetch) = list.next().await {
let fetch = fetch.context("failed to get FETCH result")?;
while let Some(fetch) = list
.try_next()
.await
.context("failed to get FETCH result")?
{
let uid = if let Some(uid) = fetch.uid {
uid
} else {
info!("FETCH result contains no UID, skipping");
info!(context, "FETCH result contains no UID, skipping");
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
@@ -1196,7 +1225,7 @@ impl Imap {
highest_modseq = modseq;
}
} else {
warn!("FETCH result contains no MODSEQ");
warn!(context, "FETCH result contains no MODSEQ");
}
}
@@ -1231,8 +1260,7 @@ impl Imap {
.await
.context("IMAP Could not fetch")?;
while let Some(fetch) = list.next().await {
let msg = fetch?;
while let Some(msg) = list.try_next().await? {
match get_fetch_headers(&msg) {
Ok(headers) => {
if let Some(from) = mimeparser::get_from(&headers) {
@@ -1242,7 +1270,7 @@ impl Imap {
}
}
Err(err) => {
warn!("{}", err);
warn!(context, "{}", err);
continue;
}
};
@@ -1267,8 +1295,7 @@ impl Imap {
.context("IMAP could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(fetch) = list.next().await {
let msg = fetch?;
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
@@ -1305,8 +1332,7 @@ impl Imap {
.context("IMAP Could not fetch")?;
let mut msgs = BTreeMap::new();
while let Some(fetch) = list.next().await {
let msg = fetch?;
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
@@ -1339,6 +1365,7 @@ impl Imap {
let session = self.session.as_mut().context("no IMAP session")?;
for (request_uids, set) in build_sequence_sets(&request_uids)? {
info!(
context,
"Starting a {} FETCH of message set \"{}\".",
if fetch_partially { "partial" } else { "full" },
set
@@ -1379,7 +1406,7 @@ impl Imap {
let next_fetch_response = match next_fetch_response {
Ok(next_fetch_response) => next_fetch_response,
Err(err) => {
warn!("Failed to process IMAP FETCH result: {}.", err);
warn!(context, "Failed to process IMAP FETCH result: {}.", err);
continue;
}
};
@@ -1395,21 +1422,24 @@ impl Imap {
// another client changes \Seen flag on a message after we do a prefetch but
// before fetch. It's not an error if we receive such unsolicited response.
info!(
"Skipping not requested FETCH response for UID {}.",
next_uid
context,
"Skipping not requested FETCH response for UID {}.", next_uid
);
} else if uid_msgs.insert(next_uid, next_fetch_response).is_some() {
warn!("Got duplicated UID {}.", next_uid);
warn!(context, "Got duplicated UID {}.", next_uid);
}
} else {
info!("Skipping FETCH response without UID.");
info!(context, "Skipping FETCH response without UID.");
}
}
let fetch_response = match fetch_response {
Some(fetch) => fetch,
None => {
warn!("Missed UID {} in the server response.", request_uid);
warn!(
context,
"Missed UID {} in the server response.", request_uid
);
continue;
}
};
@@ -1423,7 +1453,7 @@ impl Imap {
};
if is_deleted {
info!("Not processing deleted msg {}.", request_uid);
info!(context, "Not processing deleted msg {}.", request_uid);
last_uid = Some(request_uid);
continue;
}
@@ -1431,7 +1461,10 @@ impl Imap {
let body = if let Some(body) = body {
body
} else {
info!("Not processing message {} without a BODY.", request_uid);
info!(
context,
"Not processing message {} without a BODY.", request_uid
);
last_uid = Some(request_uid);
continue;
};
@@ -1442,13 +1475,17 @@ impl Imap {
rfc724_mid
} else {
error!(
context,
"No Message-ID corresponding to UID {} passed in uid_messsage_ids.",
request_uid
);
continue;
};
info!("Passing message UID {} to receive_imf().", request_uid);
info!(
context,
"Passing message UID {} to receive_imf().", request_uid
);
match receive_imf_inner(
context,
rfc724_mid,
@@ -1465,7 +1502,7 @@ impl Imap {
}
}
Err(err) => {
warn!("receive_imf error: {:#}.", err);
warn!(context, "receive_imf error: {:#}.", err);
}
};
last_uid = Some(request_uid)
@@ -1477,13 +1514,18 @@ impl Imap {
if count != request_uids.len() {
warn!(
context,
"Failed to fetch all UIDs: got {}, requested {}, we requested the UIDs {:?}.",
count,
request_uids.len(),
request_uids,
);
} else {
info!("Successfully received {} UIDs.", request_uids.len());
info!(
context,
"Successfully received {} UIDs.",
request_uids.len()
);
}
}
@@ -1524,7 +1566,7 @@ impl Imap {
return Some(ImapActionResult::RetryLater);
}
if let Err(err) = self.prepare(context).await {
warn!("prepare_imap_op failed: {}", err);
warn!(context, "prepare_imap_op failed: {}", err);
return Some(ImapActionResult::RetryLater);
}
@@ -1534,24 +1576,24 @@ impl Imap {
.context("no IMAP connection established")
{
Err(err) => {
error!("Failed to prepare IMAP operation: {:#}", err);
error!(context, "Failed to prepare IMAP operation: {:#}", err);
return Some(ImapActionResult::Failed);
}
Ok(session) => session,
};
match session.select_folder(Some(folder)).await {
match session.select_folder(context, Some(folder)).await {
Ok(_) => None,
Err(select_folder::Error::ConnectionLost) => {
warn!("Lost imap connection");
warn!(context, "Lost imap connection");
Some(ImapActionResult::RetryLater)
}
Err(select_folder::Error::BadFolderName(folder_name)) => {
warn!("invalid folder name: {:?}", folder_name);
warn!(context, "invalid folder name: {:?}", folder_name);
Some(ImapActionResult::Failed)
}
Err(err) => {
warn!("failed to select folder {:?}: {:#}", folder, err);
warn!(context, "failed to select folder {:?}: {:#}", folder, err);
Some(ImapActionResult::RetryLater)
}
}
@@ -1580,6 +1622,7 @@ impl Imap {
/// Returns first found or created folder name.
async fn configure_mvbox<'a>(
&mut self,
context: &Context,
folders: &[&'a str],
create_mvbox: bool,
) -> Result<Option<&'a str>> {
@@ -1590,15 +1633,15 @@ impl Imap {
// Close currently selected folder if needed.
// We are going to select folders using low-level EXAMINE operations below.
session.select_folder(None).await?;
session.select_folder(context, None).await?;
for folder in folders {
info!("Looking for MVBOX-folder \"{}\"...", &folder);
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
let res = session.examine(&folder).await;
if res.is_ok() {
info!(
"MVBOX-folder {:?} successfully selected, using it.",
&folder
context,
"MVBOX-folder {:?} successfully selected, using it.", &folder
);
session.close().await?;
return Ok(Some(folder));
@@ -1609,11 +1652,11 @@ impl Imap {
for folder in folders {
match session.create(&folder).await {
Ok(_) => {
info!("MVBOX-folder {} created.", &folder);
info!(context, "MVBOX-folder {} created.", &folder);
return Ok(Some(folder));
}
Err(err) => {
warn!("Cannot create MVBOX-folder {:?}: {}", &folder, err);
warn!(context, "Cannot create MVBOX-folder {:?}: {}", &folder, err);
}
}
}
@@ -1636,9 +1679,8 @@ impl Imap {
let mut delimiter_is_default = true;
let mut folder_configs = BTreeMap::new();
while let Some(folder) = folders.next().await {
let folder = folder?;
info!("Scanning folder: {:?}", folder);
while let Some(folder) = folders.try_next().await? {
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
@@ -1662,11 +1704,11 @@ impl Imap {
}
drop(folders);
info!("Using \"{}\" as folder-delimiter.", delimiter);
info!(context, "Using \"{}\" as folder-delimiter.", delimiter);
let fallback_folder = format!("INBOX{delimiter}DeltaChat");
let mvbox_folder = self
.configure_mvbox(&["DeltaChat", &fallback_folder], create_mvbox)
.configure_mvbox(context, &["DeltaChat", &fallback_folder], create_mvbox)
.await
.context("failed to configure mvbox")?;
@@ -1674,7 +1716,7 @@ impl Imap {
.set_config(Config::ConfiguredInboxFolder, Some("INBOX"))
.await?;
if let Some(mvbox_folder) = mvbox_folder {
info!("Setting MVBOX FOLDER TO {}", &mvbox_folder);
info!(context, "Setting MVBOX FOLDER TO {}", &mvbox_folder);
context
.set_config(Config::ConfiguredMvboxFolder, Some(mvbox_folder))
.await?;
@@ -1687,7 +1729,7 @@ impl Imap {
.set_raw_config_int("folders_configured", DC_FOLDERS_CONFIGURED_VERSION)
.await?;
info!("FINISHED configuring IMAP-folders.");
info!(context, "FINISHED configuring IMAP-folders.");
Ok(())
}
}
@@ -1697,15 +1739,38 @@ impl Session {
/// Drains all responses from `session.unsolicited_responses` in the process.
/// If this returns `true`, this means that new emails arrived and you should
/// fetch again, even if you just fetched.
fn server_sent_unsolicited_exists(&self) -> Result<bool> {
fn server_sent_unsolicited_exists(&self, context: &Context) -> Result<bool> {
use async_imap::imap_proto::Response;
use async_imap::imap_proto::ResponseCode;
use UnsolicitedResponse::*;
let mut unsolicited_exists = false;
while let Ok(response) = self.unsolicited_responses.try_recv() {
match response {
UnsolicitedResponse::Exists(_) => {
info!("Need to fetch again, got unsolicited EXISTS {:?}", response);
Exists(_) => {
info!(
context,
"Need to fetch again, got unsolicited EXISTS {:?}", response
);
unsolicited_exists = true;
}
_ => info!("ignoring unsolicited response {:?}", response),
// We are not interested in the following responses and they are are
// sent quite frequently, so, we ignore them without logging them
Expunge(_) | Recent(_) => {}
Other(response_data)
if matches!(
response_data.parsed(),
Response::Fetch { .. }
| Response::Done {
code: Some(ResponseCode::CopyUid(_, _, _)),
..
}
) => {}
_ => {
info!(context, "got unsolicited response {:?}", response)
}
}
}
Ok(unsolicited_exists)
@@ -1749,8 +1814,8 @@ async fn should_move_out_of_spam(
Some(res) => res,
None => {
warn!(
"Contact with From address {:?} cannot exist, not moving out of spam",
from
context,
"Contact with From address {:?} cannot exist, not moving out of spam", from
);
return Ok(false);
}
@@ -2073,7 +2138,7 @@ pub(crate) async fn prefetch_should_download(
// (prevent_rename is the last argument of from_field_to_contact_id())
if flags.any(|f| f == Flag::Draft) {
info!("Ignoring draft message");
info!(context, "Ignoring draft message");
return Ok(false);
}
@@ -2123,7 +2188,7 @@ async fn mark_seen_by_uid(
AND uid=?3
LIMIT 1
)",
paramsv![&folder, uid_validity, uid],
(&folder, uid_validity, uid),
|row| {
let msg_id: MsgId = row.get(0)?;
let chat_id: ChatId = row.get(1)?;
@@ -2139,12 +2204,12 @@ async fn mark_seen_by_uid(
"UPDATE msgs SET state=?1
WHERE (state=?2 OR state=?3)
AND id=?4",
paramsv![
(
MessageState::InSeen,
MessageState::InFresh,
MessageState::InNoticed,
msg_id
],
msg_id,
),
)
.await
.with_context(|| format!("failed to update msg {msg_id} state"))?
@@ -2174,7 +2239,7 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
.execute(
"INSERT OR IGNORE INTO imap_markseen (id)
SELECT id FROM imap WHERE rfc724_mid=?",
paramsv![message_id],
(message_id,),
)
.await?;
context
@@ -2194,7 +2259,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
paramsv![folder, uid_next],
(folder, uid_next),
)
.await?;
Ok(())
@@ -2208,10 +2273,7 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value(
"SELECT uid_next FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
.await?
.unwrap_or(0))
}
@@ -2226,7 +2288,7 @@ pub(crate) async fn set_uidvalidity(
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
paramsv![folder, uidvalidity],
(folder, uidvalidity),
)
.await?;
Ok(())
@@ -2237,7 +2299,7 @@ async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
.sql
.query_get_value(
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
paramsv![folder],
(folder,),
)
.await?
.unwrap_or(0))
@@ -2249,7 +2311,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
paramsv![folder, modseq],
(folder, modseq),
)
.await?;
Ok(())
@@ -2258,10 +2320,7 @@ pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) ->
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
Ok(context
.sql
.query_get_value(
"SELECT modseq FROM imap_sync WHERE folder=?;",
paramsv![folder],
)
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
.await?
.unwrap_or(0))
}
@@ -2378,8 +2437,8 @@ async fn add_all_recipients_as_contacts(
m
} else {
info!(
"Folder {} is not configured, skipping fetching contacts from it.",
folder
context,
"Folder {} is not configured, skipping fetching contacts from it.", folder
);
return Ok(());
};
@@ -2403,8 +2462,10 @@ async fn add_all_recipients_as_contacts(
let recipient_addr = match ContactAddress::new(&recipient.addr) {
Err(err) => {
warn!(
context,
"Could not add contact for recipient with address {:?}: {:#}",
recipient.addr, err
recipient.addr,
err
);
continue;
}

View File

@@ -4,7 +4,6 @@ use anyhow::{bail, Context as _, Result};
use async_channel::Receiver;
use async_imap::extensions::idle::IdleResponse;
use futures_lite::FutureExt;
use tracing::{error, info, warn};
use super::session::Session;
use super::Imap;
@@ -16,6 +15,7 @@ const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
impl Session {
pub async fn idle(
mut self,
context: &Context,
idle_interrupt_receiver: Receiver<InterruptInfo>,
watch_folder: Option<String>,
) -> Result<(Self, InterruptInfo)> {
@@ -27,14 +27,14 @@ impl Session {
let mut info = Default::default();
self.select_folder(watch_folder.as_deref()).await?;
self.select_folder(context, watch_folder.as_deref()).await?;
if self.server_sent_unsolicited_exists()? {
if self.server_sent_unsolicited_exists(context)? {
return Ok((self, info));
}
if let Ok(info) = idle_interrupt_receiver.try_recv() {
info!("skip idle, got interrupt {:?}", info);
info!(context, "skip idle, got interrupt {:?}", info);
return Ok((self, info));
}
@@ -56,7 +56,10 @@ impl Session {
}
let folder_name = watch_folder.as_deref().unwrap_or("None");
info!("{}: Idle entering wait-on-remote state", folder_name);
info!(
context,
"{}: Idle entering wait-on-remote state", folder_name
);
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
let info = idle_interrupt_receiver.recv().await;
@@ -68,20 +71,29 @@ impl Session {
match fut.await {
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
info!("{}: Idle has NewData {:?}", folder_name, x);
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
}
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
info!("{}: Idle-wait timeout or interruption", folder_name);
info!(
context,
"{}: Idle-wait timeout or interruption", folder_name
);
}
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
info!("{}: Idle wait was interrupted manually", folder_name);
info!(
context,
"{}: Idle wait was interrupted manually", folder_name
);
}
Ok(Event::Interrupt(i)) => {
info!("{}: Idle wait was interrupted: {:?}", folder_name, &i);
info!(
context,
"{}: Idle wait was interrupted: {:?}", folder_name, &i
);
info = i;
}
Err(err) => {
warn!("{}: Idle wait errored: {:?}", folder_name, err);
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
}
}
@@ -112,14 +124,14 @@ impl Imap {
let watch_folder = if let Some(watch_folder) = watch_folder {
watch_folder
} else {
info!("IMAP-fake-IDLE: no folder, waiting for interrupt");
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
return self
.idle_interrupt_receiver
.recv()
.await
.unwrap_or_default();
};
info!("IMAP-fake-IDLEing folder={:?}", watch_folder);
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
// check every minute if there are new messages
// TODO: grow sleep durations / make them more flexible
@@ -147,7 +159,7 @@ impl Imap {
// (setup_handle_if_needed might not know about them if we
// never successfully connected)
if let Err(err) = self.prepare(context).await {
warn!("fake_idle: could not connect: {}", err);
warn!(context, "fake_idle: could not connect: {}", err);
continue;
}
if let Some(session) = &self.session {
@@ -156,7 +168,7 @@ impl Imap {
break InterruptInfo::new(false);
}
}
info!("fake_idle is connected");
info!(context, "fake_idle is connected");
// we are connected, let's see if fetching messages results
// in anything. If so, we behave as if IDLE had data but
// will have already fetched the messages so perform_*_fetch
@@ -166,26 +178,27 @@ impl Imap {
.await
{
Ok(res) => {
info!("fetch_new_messages returned {:?}", res);
info!(context, "fetch_new_messages returned {:?}", res);
if res {
break InterruptInfo::new(false);
}
}
Err(err) => {
error!("could not fetch from folder: {:#}", err);
self.trigger_reconnect();
error!(context, "could not fetch from folder: {:#}", err);
self.trigger_reconnect(context);
}
}
}
Event::Interrupt(info) => {
// Interrupt
info!("Fake IDLE interrupted");
info!(context, "Fake IDLE interrupted");
break info;
}
}
};
info!(
context,
"IMAP-fake-IDLE done after {:.4}s",
SystemTime::now()
.duration_since(fake_idle_start_time)

View File

@@ -2,7 +2,6 @@ use std::{collections::BTreeMap, time::Instant};
use anyhow::{Context as _, Result};
use futures::stream::StreamExt;
use tracing::info;
use super::{get_folder_meaning_by_attrs, get_folder_meaning_by_name};
use crate::config::Config;
@@ -25,7 +24,7 @@ impl Imap {
return Ok(false);
}
}
info!("Starting full folder scan");
info!(context, "Starting full folder scan");
self.prepare(context).await?;
let folders = self.list_folders(context).await?;
@@ -66,16 +65,18 @@ impl Imap {
{
let session = self.session.as_mut().context("no session")?;
// Drain leftover unsolicited EXISTS messages
session.server_sent_unsolicited_exists()?;
session.server_sent_unsolicited_exists(context)?;
loop {
self.fetch_move_delete(context, folder.name(), folder_meaning)
.await
.ok_or_log_msg(context, "Can't fetch new msgs in scanned folder");
.context("Can't fetch new msgs in scanned folder")
.log_err(context)
.ok();
let session = self.session.as_mut().context("no session")?;
// If the server sent an unsocicited EXISTS during the fetch, we need to fetch again
if !session.server_sent_unsolicited_exists()? {
if !session.server_sent_unsolicited_exists(context)? {
break;
}
}
@@ -106,7 +107,11 @@ impl Imap {
let list = session
.list(Some(""), Some("*"))
.await?
.filter_map(|f| async { f.ok_or_log_msg(context, "list_folders() can't get folder") });
.filter_map(|f| async {
f.context("list_folders() can't get folder")
.log_err(context)
.ok()
});
Ok(list.collect().await)
}
}

View File

@@ -1,9 +1,9 @@
//! # IMAP folder selection module.
use anyhow::Context as _;
use tracing::info;
use super::session::Session as ImapSession;
use crate::context::Context;
type Result<T> = std::result::Result<T, Error>;
@@ -34,13 +34,13 @@ impl ImapSession {
///
/// CLOSE is considerably faster than an EXPUNGE, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn maybe_close_folder(&mut self) -> anyhow::Result<()> {
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!("Expunge messages in \"{}\".", folder);
info!(context, "Expunge messages in \"{}\".", folder);
self.close().await.context("IMAP close/expunge failed")?;
info!("close/expunge succeeded");
info!(context, "close/expunge succeeded");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
}
@@ -51,7 +51,11 @@ impl ImapSession {
/// Selects a folder, possibly updating uid_validity and, if needed,
/// expunging the folder to remove delete-marked messages.
/// Returns whether a new folder was selected.
pub(super) async fn select_folder(&mut self, folder: Option<&str>) -> Result<NewlySelected> {
pub(super) async fn select_folder(
&mut self,
context: &Context,
folder: Option<&str>,
) -> Result<NewlySelected> {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(folder) = folder {
@@ -63,7 +67,7 @@ impl ImapSession {
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
self.maybe_close_folder().await?;
self.maybe_close_folder(context).await?;
// select new folder
if let Some(folder) = folder {
@@ -100,18 +104,19 @@ impl ImapSession {
/// Selects a folder. Tries to create it once and select again if the folder does not exist.
pub(super) async fn select_or_create_folder(
&mut self,
context: &Context,
folder: &str,
) -> anyhow::Result<NewlySelected> {
match self.select_folder( Some(folder)).await {
match self.select_folder(context, Some(folder)).await {
Ok(newly_selected) => Ok(newly_selected),
Err(err) => match err {
Error::NoFolder(..) => {
info!( "Failed to select folder {} because it does not exist, trying to create it.", folder);
info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder);
self.create(folder).await.with_context(|| {
format!("Couldn't select folder ('{err}'), then create() failed")
})?;
Ok(self.select_folder(Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"))?)
Ok(self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"))?)
}
_ => Err(err).with_context(|| format!("failed to select folder {folder} with error other than NO, not trying to create it")),
},

View File

@@ -11,7 +11,6 @@ use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
use tokio_tar::Archive;
use tracing::{error, info, warn};
use crate::blob::{BlobDirContents, BlobObject};
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
@@ -92,7 +91,7 @@ pub async fn imex(
let cancel = context.alloc_ongoing().await?;
let res = {
let _guard = context.scheduler.pause(context.clone()).await;
let _guard = context.scheduler.pause(context.clone()).await?;
imex_inner(context, what, path, passphrase)
.race(async {
cancel.recv().await.ok();
@@ -104,10 +103,10 @@ pub async fn imex(
if let Err(err) = res.as_ref() {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
error!("IMEX failed to complete: {:#}", err);
error!(context, "IMEX failed to complete: {:#}", err);
context.emit_event(EventType::ImexProgress(0));
} else {
info!("IMEX successfully completed");
info!(context, "IMEX successfully completed");
context.emit_event(EventType::ImexProgress(1000));
}
@@ -115,7 +114,7 @@ pub async fn imex(
}
/// Returns the filename of the backup found (otherwise an error)
pub async fn has_backup(dir_name: &Path) -> Result<String> {
pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
let mut dir_iter = tokio::fs::read_dir(dir_name).await?;
let mut newest_backup_name = "".to_string();
let mut newest_backup_path: Option<PathBuf> = None;
@@ -145,7 +144,7 @@ pub async fn has_backup(dir_name: &Path) -> Result<String> {
///
/// Returns setup code.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
let setup_code = create_setup_code();
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
/* encrypting may also take a while ... */
@@ -227,7 +226,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
}
/// Creates a new setup code for Autocrypt Setup Message.
pub fn create_setup_code() -> String {
pub fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut rng = thread_rng();
let mut ret = String::new();
@@ -344,7 +343,7 @@ async fn set_self_key(
)
.await?;
info!("stored self key: {:?}", keypair.secret.key_id());
info!(context, "stored self key: {:?}", keypair.secret.key_id());
Ok(())
}
@@ -377,7 +376,7 @@ async fn imex_inner(
path: &Path,
passphrase: Option<String>,
) -> Result<()> {
info!("Import/export dir: {}", path.display());
info!(context, "Import/export dir: {}", path.display());
ensure!(context.sql.is_open().await, "Database not opened.");
context.emit_event(EventType::ImexProgress(10));
@@ -398,8 +397,7 @@ async fn imex_inner(
export_backup(context, path, passphrase.unwrap_or_default()).await
}
ImexMode::ImportBackup => {
import_backup(context, path, passphrase.unwrap_or_default()).await?;
context.sql.run_migrations(context).await
import_backup(context, path, passphrase.unwrap_or_default()).await
}
}
}
@@ -427,6 +425,7 @@ async fn import_backup(
let backup_file = File::open(backup_to_import).await?;
let file_size = backup_file.metadata().await?.len();
info!(
context,
"Import \"{}\" ({} bytes) to \"{}\".",
backup_to_import.display(),
file_size,
@@ -468,12 +467,13 @@ async fn import_backup(
if let Some(name) = from_path.file_name() {
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
} else {
warn!("No file name.");
warn!(context, "No file name");
}
}
}
}
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
@@ -527,6 +527,7 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
.context("could not export database")?;
info!(
context,
"Backup '{}' to '{}'.",
context.get_dbfile().display(),
dest_path.display(),
@@ -540,7 +541,7 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
context.emit_event(EventType::ImexFileWritten(dest_path));
}
Err(e) => {
error!("backup failed: {}", e);
error!(context, "backup failed: {}", e);
}
}
@@ -612,7 +613,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
continue;
}
set_default = if name_f.contains("legacy") {
info!("found legacy key '{}'", path_plus_name.display());
info!(context, "found legacy key '{}'", path_plus_name.display());
false
} else {
true
@@ -622,13 +623,17 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
continue;
}
}
info!("considering key file: {}", path_plus_name.display());
info!(
context,
"considering key file: {}",
path_plus_name.display()
);
match read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
info!("set_self_key: {}", err);
info!(context, "set_self_key: {}", err);
continue;
}
}
@@ -673,7 +678,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
error!("Failed to export public key: {:#}.", err);
error!(context, "Failed to export public key: {:#}.", err);
export_errors += 1;
}
} else {
@@ -681,7 +686,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
}
if let Ok(key) = private_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
error!("Failed to export private key: {:#}.", err);
error!(context, "Failed to export private key: {:#}.", err);
export_errors += 1;
}
} else {
@@ -718,6 +723,7 @@ where
dir.join(format!("{}-key-{}.asc", kind, &id))
};
info!(
context,
"Exporting key {:?} to {}",
key.key_id(),
file_name.display()
@@ -753,18 +759,15 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) ->
.with_context(|| format!("path {} is not valid unicode", dest.display()))?;
context.sql.set_raw_config_int("backup_time", now).await?;
sql::housekeeping(context).await.ok_or_log(context);
sql::housekeeping(context).await.log_err(context).ok();
context
.sql
.call_write(|conn| {
conn.execute("VACUUM;", params![])
.map_err(|err| warn!("Vacuum failed, exporting anyway {err:#}."))
conn.execute("VACUUM;", ())
.map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}"))
.ok();
conn.execute(
"ATTACH DATABASE ? AS backup KEY ?",
paramsv![dest, passphrase],
)
.context("failed to attach backup database")?;
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (dest, passphrase))
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(()))
.context("failed to export to attached backup database");
@@ -818,7 +821,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_setup_code() {
let setupcode = create_setup_code();
let t = TestContext::new().await;
let setupcode = create_setup_code(&t);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
@@ -883,7 +887,7 @@ mod tests {
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(backup_dir.path()).await.is_err());
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
// export from context1
assert!(
@@ -897,7 +901,7 @@ mod tests {
.await;
// import to context2
let backup = has_backup(backup_dir.path()).await?;
let backup = has_backup(&context2, backup_dir.path()).await?;
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(imex(
@@ -942,7 +946,7 @@ mod tests {
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None).await?;
// import to context2
let backup = has_backup(backup_dir.path()).await?;
let backup = has_backup(&context2, backup_dir.path()).await?;
let context2_cloned = context2.clone();
let handle = task::spawn(async move {
imex(

View File

@@ -22,7 +22,6 @@
//! getter can not connect to an impersonated provider and the provider does not offer the
//! download to an impersonated getter.
use std::cmp::Ordering;
use std::future::Future;
use std::net::Ipv4Addr;
use std::ops::Deref;
@@ -33,7 +32,8 @@ use std::task::Poll;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use async_channel::Receiver;
use futures_lite::StreamExt;
use iroh::get::{DataStream, Options};
use iroh::blobs::Collection;
use iroh::get::DataStream;
use iroh::progress::ProgressEmitter;
use iroh::protocol::AuthToken;
use iroh::provider::{DataSource, Event, Provider, Ticket};
@@ -44,16 +44,20 @@ use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, Mutex};
use tokio::task::{JoinHandle, JoinSet};
use tokio_stream::wrappers::ReadDirStream;
use tracing::{error, info, warn};
use tokio_util::sync::CancellationToken;
use crate::blob::BlobDirContents;
use crate::chat::delete_and_reset_all_device_msgs;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::context::Context;
use crate::qr::Qr;
use crate::message::{Message, Viewtype};
use crate::qr::{self, Qr};
use crate::stock_str::backup_transfer_msg_body;
use crate::{e2ee, EventType};
use super::{export_database, DBFILE_BACKUP_NAME};
const MAX_CONCURRENT_DIALS: u8 = 16;
/// Provide or send a backup of this device.
///
/// This creates a backup of the current device and starts a service which offers another
@@ -73,6 +77,8 @@ pub struct BackupProvider {
handle: JoinHandle<Result<()>>,
/// The ticket to retrieve the backup collection.
ticket: Ticket,
/// Guard to cancel the provider on drop.
_drop_guard: tokio_util::sync::DropGuard,
}
impl BackupProvider {
@@ -93,15 +99,15 @@ impl BackupProvider {
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let paused_guard = context.scheduler.pause(context.clone()).await;
let paused_guard = context.scheduler.pause(context.clone()).await?;
let context_dir = context
.get_blobdir()
.parent()
.ok_or(anyhow!("Context dir not found"))?;
.ok_or_else(|| anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
warn!("Previous database export deleted.");
warn!(context, "Previous database export deleted");
}
let dbfile = TempPathGuard::new(dbfile);
let res = tokio::select! {
@@ -110,7 +116,7 @@ impl BackupProvider {
match res {
Ok(slf) => Ok(slf),
Err(err) => {
error!("Failed to set up second device setup: {err:#}.");
error!(context, "Failed to set up second device setup: {:#}", err);
Err(err)
},
}
@@ -124,10 +130,12 @@ impl BackupProvider {
return Err(err);
}
};
let drop_token = CancellationToken::new();
let handle = {
let context = context.clone();
let drop_token = drop_token.clone();
tokio::spawn(async move {
let res = Self::watch_provider(&context, provider, cancel_token).await;
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
context.free_ongoing().await;
// Explicit drop to move the guards into this future
@@ -136,7 +144,11 @@ impl BackupProvider {
res
})
};
Ok(Self { handle, ticket })
Ok(Self {
handle,
ticket,
_drop_guard: drop_token.drop_guard(),
})
}
/// Creates the provider task.
@@ -171,8 +183,8 @@ impl BackupProvider {
.auth_token(token)
.spawn()?;
context.emit_event(SendProgress::ProviderListening.into());
info!("Waiting for remote to connect.");
let ticket = provider.ticket(hash);
info!(context, "Waiting for remote to connect");
let ticket = provider.ticket(hash)?;
Ok((provider, ticket))
}
@@ -190,8 +202,8 @@ impl BackupProvider {
context: &Context,
mut provider: Provider,
cancel_token: Receiver<()>,
drop_token: CancellationToken,
) -> Result<()> {
// _dbfile exists so we can clean up the file once it is no longer needed
let mut events = provider.subscribe();
let mut total_size = 0;
let mut current_size = 0;
@@ -251,14 +263,23 @@ impl BackupProvider {
},
_ = cancel_token.recv() => {
provider.shutdown();
break Err(anyhow!("BackupSender cancelled"));
break Err(anyhow!("BackupProvider cancelled"));
},
_ = drop_token.cancelled() => {
provider.shutdown();
break Err(anyhow!("BackupProvider dropped"));
}
}
};
match &res {
Ok(_) => context.emit_event(SendProgress::Completed.into()),
Ok(_) => {
context.emit_event(SendProgress::Completed.into());
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(backup_transfer_msg_body(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
}
Err(err) => {
error!("Backup transfer failure: {err:#}.");
error!(context, "Backup transfer failure: {err:#}");
context.emit_event(SendProgress::Failed.into())
}
}
@@ -372,137 +393,90 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
!context.is_configured().await?,
"Cannot import backups to accounts in use."
);
let _guard = context.scheduler.pause(context.clone()).await;
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
let _guard = context.scheduler.pause(context.clone()).await;
info!(
context,
"Running get_backup for {}",
qr::format_backup(&qr)?
);
let res = tokio::select! {
biased;
res = get_backup_inner(context, qr) => {
context.free_ongoing().await;
res
}
res = get_backup_inner(context, qr) => res,
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
context.free_ongoing().await;
res
}
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
let mut ticket = match qr {
let ticket = match qr {
Qr::Backup { ticket } => ticket,
_ => bail!("QR code for backup must be of type DCBACKUP"),
};
if ticket.addrs.is_empty() {
bail!("ticket is missing addresses to dial");
}
// Crude sorting, most local wifi's are in the 192.168.0.0/24 range so this will try
// them first.
ticket.addrs.sort_by(|a, b| {
let a = a.to_string();
let b = b.to_string();
if a.starts_with("192.168.") && !b.starts_with("192.168.") {
Ordering::Less
} else if b.starts_with("192.168.") && !a.starts_with("192.168.") {
Ordering::Greater
} else {
Ordering::Equal
match transfer_from_provider(context, &ticket).await {
Ok(()) => {
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
Ok(())
}
});
for addr in &ticket.addrs {
let opts = Options {
addr: *addr,
peer_id: Some(ticket.peer),
keylog: false,
};
info!("Attempting to contact {addr}.");
match transfer_from_provider(context, &ticket, opts).await {
Ok(_) => {
delete_and_reset_all_device_msgs(context).await?;
context.emit_event(ReceiveProgress::Completed.into());
return Ok(());
}
Err(TransferError::ConnectionError(err)) => {
warn!("Connection error: {err:#}.");
continue;
}
Err(TransferError::Other(err)) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
Err(err) => {
// Clean up any blobs we already wrote.
let readdir = fs::read_dir(context.get_blobdir()).await?;
let mut readdir = ReadDirStream::new(readdir);
while let Some(dirent) = readdir.next().await {
if let Ok(dirent) = dirent {
fs::remove_file(dirent.path()).await.ok();
}
context.emit_event(ReceiveProgress::Failed.into());
return Err(err);
}
context.emit_event(ReceiveProgress::Failed.into());
Err(err)
}
}
Err(anyhow!("failed to contact provider"))
}
/// Error during a single transfer attempt.
///
/// Mostly exists to distinguish between `ConnectionError` and any other errors.
#[derive(Debug, thiserror::Error)]
enum TransferError {
#[error("connection error")]
ConnectionError(#[source] anyhow::Error),
#[error("other")]
Other(#[source] anyhow::Error),
}
async fn transfer_from_provider(
context: &Context,
ticket: &Ticket,
opts: Options,
) -> Result<(), TransferError> {
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
spawn_progress_proxy(context.clone(), progress.subscribe());
let mut connected = false;
let on_connected = || {
context.emit_event(ReceiveProgress::Connected.into());
connected = true;
async { Ok(()) }
};
let on_collection = |collection: &Collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
async { Ok(()) }
};
let jobs = Mutex::new(JoinSet::default());
let on_blob =
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
let res = iroh::get::run(
ticket.hash,
ticket.token,
opts,
// Perform the transfer.
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
let stats = iroh::get::run_ticket(
ticket,
keylog,
MAX_CONCURRENT_DIALS,
on_connected,
|collection| {
context.emit_event(ReceiveProgress::CollectionReceived.into());
progress.set_total(collection.total_blobs_size());
async { Ok(()) }
},
on_collection,
on_blob,
)
.await;
.await?;
let mut jobs = jobs.lock().await;
while let Some(job) = jobs.join_next().await {
job.context("job failed").map_err(TransferError::Other)?;
job.context("job failed")?;
}
drop(progress);
match res {
Ok(stats) => {
info!(
"Backup transfer finished, transfer rate is {} Mbps.",
stats.mbits()
);
Ok(())
}
Err(err) => match connected {
true => Err(TransferError::Other(err)),
false => Err(TransferError::ConnectionError(err)),
},
}
info!(
context,
"Backup transfer finished, transfer rate was {} Mbps.",
stats.mbits()
);
Ok(())
}
/// Get callback when a blob is received from the provider.
@@ -523,11 +497,11 @@ async fn on_blob(
let context_dir = context
.get_blobdir()
.parent()
.ok_or(anyhow!("Context dir not found"))?;
.ok_or_else(|| anyhow!("Context dir not found"))?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
warn!("Previous database export deleted.");
warn!(context, "Previous database export deleted");
}
dbfile
} else {
@@ -544,14 +518,15 @@ async fn on_blob(
if name.starts_with("db/") {
let context = context.clone();
let token = ticket.token.to_string();
let token = ticket.token().to_string();
jobs.lock().await.spawn(async move {
if let Err(err) = context.sql.import(&path, token).await {
error!("Cannot import database: {err:#?}.");
error!(context, "cannot import database: {:#?}", err);
}
if let Err(err) = fs::remove_file(&path).await {
error!(
"Failed to delete database import file '{}': {:#?}.",
context,
"failed to delete database import file '{}': {:#?}",
path.display(),
err,
);
@@ -705,4 +680,16 @@ mod tests {
assert_eq!(out, EventType::ImexProgress(progress));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_drop_provider() {
let mut tcm = TestContextManager::new();
let ctx = tcm.alice().await;
let provider = BackupProvider::prepare(&ctx).await.unwrap();
drop(provider);
ctx.evtracker
.get_matching(|ev| matches!(ev, EventType::ImexProgress(0)))
.await;
}
}

View File

@@ -11,7 +11,6 @@ use std::sync::atomic::Ordering;
use anyhow::{Context as _, Result};
use deltachat_derive::{FromSql, ToSql};
use rand::{thread_rng, Rng};
use tracing::{error, info, warn};
use crate::context::Context;
use crate::imap::Imap;
@@ -100,7 +99,7 @@ impl Job {
if self.job_id != 0 {
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![self.job_id as i32])
.execute("DELETE FROM jobs WHERE id=?;", (self.job_id as i32,))
.await?;
}
@@ -111,29 +110,29 @@ impl Job {
///
/// The Job is consumed by this method.
pub(crate) async fn save(self, context: &Context) -> Result<()> {
info!("saving job {:?}", self);
info!(context, "saving job {:?}", self);
if self.job_id != 0 {
context
.sql
.execute(
"UPDATE jobs SET desired_timestamp=?, tries=? WHERE id=?;",
paramsv![
(
self.desired_timestamp,
i64::from(self.tries),
self.job_id as i32,
],
),
)
.await?;
} else {
context.sql.execute(
"INSERT INTO jobs (added_timestamp, action, foreign_id, desired_timestamp) VALUES (?,?,?,?);",
paramsv![
(
self.added_timestamp,
self.action,
self.foreign_id,
self.desired_timestamp
]
)
).await?;
}
@@ -154,7 +153,7 @@ impl<'a> Connection<'a> {
}
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!("Job {} started...", &job);
info!(context, "Job {} started...", &job);
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
@@ -166,33 +165,39 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
let tries = job.tries + 1;
if tries < JOB_RETRIES {
info!("Increase job {job} tries to {tries}.");
info!(context, "Increase job {job} tries to {tries}.");
job.tries = tries;
let time_offset = get_backoff_time_offset(tries);
job.desired_timestamp = time() + time_offset;
info!(
context,
"job #{} not succeeded on try #{}, retry in {} seconds.",
job.job_id, tries, time_offset
job.job_id,
tries,
time_offset
);
job.save(context).await.unwrap_or_else(|err| {
error!("Failed to save job: {err:#}.");
error!(context, "Failed to save job: {err:#}.");
});
} else {
info!("Remove job {job} as it exhausted {JOB_RETRIES} retries.");
info!(
context,
"Remove job {job} as it exhausted {JOB_RETRIES} retries."
);
job.delete(context).await.unwrap_or_else(|err| {
error!("Failed to delete job: {err:#}.");
error!(context, "Failed to delete job: {err:#}.");
});
}
}
Status::Finished(res) => {
if let Err(err) = res {
warn!("Remove job {job} as it failed with error {err:#}.");
warn!(context, "Remove job {job} as it failed with error {err:#}.");
} else {
info!("Remove job {job} as it succeeded.");
info!(context, "Remove job {job} as it succeeded.");
}
job.delete(context).await.unwrap_or_else(|err| {
error!("failed to delete job: {:#}", err);
error!(context, "failed to delete job: {:#}", err);
});
}
}
@@ -204,13 +209,13 @@ async fn perform_job_action(
connection: &mut Connection<'_>,
tries: u32,
) -> Status {
info!("Begin immediate try {tries} of job {job}.");
info!(context, "Begin immediate try {tries} of job {job}.");
let try_res = match job.action {
Action::DownloadMsg => job.download_msg(context, connection.inbox()).await,
};
info!("Finished immediate try {tries} of job {job}.");
info!(context, "Finished immediate try {tries} of job {job}.");
try_res
}
@@ -242,7 +247,7 @@ pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
pub async fn add(context: &Context, job: Job) -> Result<()> {
job.save(context).await.context("failed to save job")?;
info!("Interrupt: IMAP.");
info!(context, "Interrupt: IMAP.");
context
.scheduler
.interrupt_inbox(InterruptInfo::new(false))
@@ -256,7 +261,7 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
/// jobs, this is tricky and probably wrong currently. Look at the
/// SQL queries for details.
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
info!("Loading job.");
info!(context, "Loading job.");
let query;
let params;
@@ -272,7 +277,7 @@ WHERE desired_timestamp<=?
ORDER BY action DESC, added_timestamp
LIMIT 1;
"#;
params = paramsv![t];
params = vec![t];
} else {
// processing after call to dc_maybe_network():
// process _all_ pending jobs that failed before
@@ -284,13 +289,13 @@ WHERE tries>0
ORDER BY desired_timestamp, action DESC
LIMIT 1;
"#;
params = paramsv![];
params = vec![];
};
loop {
let job_res = context
.sql
.query_row_optional(query, params.clone(), |row| {
.query_row_optional(query, rusqlite::params_from_iter(params.clone()), |row| {
let job = Job {
job_id: row.get("id")?,
action: row.get("action")?,
@@ -308,17 +313,19 @@ LIMIT 1;
Ok(job) => return Ok(job),
Err(err) => {
// Remove invalid job from the DB
info!("Cleaning up job, because of {err:#}.");
info!(context, "Cleaning up job, because of {err:#}.");
// TODO: improve by only doing a single query
let id = context
.sql
.query_row(query, params.clone(), |row| row.get::<_, i32>(0))
.query_row(query, rusqlite::params_from_iter(params.clone()), |row| {
row.get::<_, i32>(0)
})
.await
.context("failed to retrieve invalid job ID from the database")?;
context
.sql
.execute("DELETE FROM jobs WHERE id=?;", paramsv![id])
.execute("DELETE FROM jobs WHERE id=?;", (id,))
.await
.with_context(|| format!("failed to delete invalid job {id}"))?;
}
@@ -339,7 +346,7 @@ mod tests {
"INSERT INTO jobs
(added_timestamp, action, foreign_id, desired_timestamp)
VALUES (?, ?, ?, ?);",
paramsv![
(
now,
if valid {
Action::DownloadMsg as i32
@@ -347,8 +354,8 @@ mod tests {
-1
},
foreign_id,
now
],
now,
),
)
.await
.unwrap();

View File

@@ -14,7 +14,6 @@ pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
use pgp::ser::Serialize;
use pgp::types::{KeyTrait, SecretKeyTrait};
use tokio::runtime::Handle;
use tracing::info;
use crate::config::Config;
use crate::constants::KeyGenType;
@@ -101,7 +100,7 @@ impl DcKey for SignedPublicKey {
WHERE addr=?
AND is_default=1;
"#,
paramsv![addr],
(addr,),
|row| {
let bytes: Vec<u8> = row.get(0)?;
Ok(bytes)
@@ -212,13 +211,14 @@ async fn generate_keypair(context: &Context) -> Result<KeyPair> {
let start = std::time::SystemTime::now();
let keytype = KeyGenType::from_i32(context.get_config_int(Config::KeyGenType).await?)
.unwrap_or_default();
info!("Generating keypair with type {}", keytype);
info!(context, "Generating keypair with type {}", keytype);
let keypair = Handle::current()
.spawn_blocking(move || crate::pgp::create_keypair(addr, keytype))
.await??;
store_self_keypair(context, &keypair, KeyPairUse::Default).await?;
info!(
context,
"Keypair generated in {:.3}s.",
start.elapsed().unwrap_or_default().as_secs()
);
@@ -240,7 +240,7 @@ pub(crate) async fn load_keypair(
WHERE addr=?1
AND is_default=1;
"#,
paramsv![addr],
(addr,),
|row| {
let pub_bytes: Vec<u8> = row.get(0)?;
let sec_bytes: Vec<u8> = row.get(1)?;
@@ -297,7 +297,7 @@ pub async fn store_self_keypair(
transaction
.execute(
"DELETE FROM keypairs WHERE public_key=? OR private_key=?;",
paramsv![public_key, secret_key],
(&public_key, &secret_key),
)
.context("failed to remove old use of key")?;
if default == KeyPairUse::Default {
@@ -317,7 +317,7 @@ pub async fn store_self_keypair(
.execute(
"INSERT INTO keypairs (addr, is_default, public_key, private_key, created)
VALUES (?,?,?,?,?);",
paramsv![addr, is_default, public_key, secret_key, t],
(addr, is_default, &public_key, &secret_key, t),
)
.context("failed to insert keypair")?;

View File

@@ -35,11 +35,7 @@ extern crate rusqlite;
#[macro_use]
extern crate strum_macros;
#[allow(missing_docs)]
pub trait ToSql: rusqlite::ToSql + Send + Sync {}
impl<T: rusqlite::ToSql + Send + Sync> ToSql for T {}
#[macro_use]
pub mod log;
#[cfg(feature = "internals")]
@@ -67,9 +63,9 @@ mod decrypt;
pub mod download;
mod e2ee;
pub mod ephemeral;
mod http;
mod imap;
pub mod imex;
pub mod release;
mod scheduler;
#[macro_use]
mod job;
@@ -103,7 +99,7 @@ mod dehtml;
mod authres;
mod color;
pub mod html;
mod net;
pub mod net;
pub mod plaintext;
pub mod summary;

View File

@@ -5,10 +5,8 @@ use std::time::Duration;
use anyhow::{ensure, Context as _, Result};
use async_channel::Receiver;
use bitflags::bitflags;
use quick_xml::events::{BytesEnd, BytesStart, BytesText};
use tokio::time::timeout;
use tracing::{info, warn};
use crate::chat::{self, ChatId};
use crate::contact::ContactId;
@@ -79,16 +77,15 @@ pub struct Kml {
pub curr: Location,
}
bitflags! {
#[derive(Default)]
struct KmlTag: i32 {
const UNDEFINED = 0x00;
const PLACEMARK = 0x01;
const TIMESTAMP = 0x02;
const WHEN = 0x04;
const POINT = 0x08;
const COORDINATES = 0x10;
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
enum KmlTag {
#[default]
Undefined,
Placemark,
PlacemarkTimestamp,
PlacemarkTimestampWhen,
PlacemarkPoint,
PlacemarkPointCoordinates,
}
impl Kml {
@@ -129,12 +126,14 @@ impl Kml {
}
fn text_cb(&mut self, event: &BytesText) {
if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) {
if self.tag == KmlTag::PlacemarkTimestampWhen
|| self.tag == KmlTag::PlacemarkPointCoordinates
{
let val = event.unescape().unwrap_or_default();
let val = val.replace(['\n', '\r', '\t', ' '], "");
if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 {
if self.tag == KmlTag::PlacemarkTimestampWhen && val.len() >= 19 {
// YYYY-MM-DDTHH:MM:SSZ
// 0 4 7 10 13 16 19
match chrono::NaiveDateTime::parse_from_str(&val, "%Y-%m-%dT%H:%M:%SZ") {
@@ -148,7 +147,7 @@ impl Kml {
self.curr.timestamp = time();
}
}
} else if self.tag.contains(KmlTag::COORDINATES) {
} else if self.tag == KmlTag::PlacemarkPointCoordinates {
let parts = val.splitn(2, ',').collect::<Vec<_>>();
if let [longitude, latitude] = &parts[..] {
self.curr.longitude = longitude.parse().unwrap_or_default();
@@ -163,17 +162,41 @@ impl Kml {
.trim()
.to_lowercase();
if tag == "placemark" {
if self.tag.contains(KmlTag::PLACEMARK)
&& 0 != self.curr.timestamp
&& 0. != self.curr.latitude
&& 0. != self.curr.longitude
{
self.locations
.push(std::mem::replace(&mut self.curr, Location::new()));
match self.tag {
KmlTag::PlacemarkTimestampWhen => {
if tag == "when" {
self.tag = KmlTag::PlacemarkTimestamp
}
}
self.tag = KmlTag::UNDEFINED;
};
KmlTag::PlacemarkTimestamp => {
if tag == "timestamp" {
self.tag = KmlTag::Placemark
}
}
KmlTag::PlacemarkPointCoordinates => {
if tag == "coordinates" {
self.tag = KmlTag::PlacemarkPoint
}
}
KmlTag::PlacemarkPoint => {
if tag == "point" {
self.tag = KmlTag::Placemark
}
}
KmlTag::Placemark => {
if tag == "placemark" {
if 0 != self.curr.timestamp
&& 0. != self.curr.latitude
&& 0. != self.curr.longitude
{
self.locations
.push(std::mem::replace(&mut self.curr, Location::new()));
}
self.tag = KmlTag::Undefined;
}
}
KmlTag::Undefined => {}
}
}
fn starttag_cb<B: std::io::BufRead>(
@@ -197,19 +220,19 @@ impl Kml {
.map(|a| a.into_owned());
}
} else if tag == "placemark" {
self.tag = KmlTag::PLACEMARK;
self.tag = KmlTag::Placemark;
self.curr.timestamp = 0;
self.curr.latitude = 0.0;
self.curr.longitude = 0.0;
self.curr.accuracy = 0.0
} else if tag == "timestamp" && self.tag.contains(KmlTag::PLACEMARK) {
self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP
} else if tag == "when" && self.tag.contains(KmlTag::TIMESTAMP) {
self.tag = KmlTag::PLACEMARK | KmlTag::TIMESTAMP | KmlTag::WHEN
} else if tag == "point" && self.tag.contains(KmlTag::PLACEMARK) {
self.tag = KmlTag::PLACEMARK | KmlTag::POINT
} else if tag == "coordinates" && self.tag.contains(KmlTag::POINT) {
self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES;
} else if tag == "timestamp" && self.tag == KmlTag::Placemark {
self.tag = KmlTag::PlacemarkTimestamp;
} else if tag == "when" && self.tag == KmlTag::PlacemarkTimestamp {
self.tag = KmlTag::PlacemarkTimestampWhen;
} else if tag == "point" && self.tag == KmlTag::Placemark {
self.tag = KmlTag::PlacemarkPoint;
} else if tag == "coordinates" && self.tag == KmlTag::PlacemarkPoint {
self.tag = KmlTag::PlacemarkPointCoordinates;
if let Some(acc) = event.attributes().find(|attr| {
attr.as_ref()
.map(|a| {
@@ -248,11 +271,11 @@ pub async fn send_locations_to_chat(
SET locations_send_begin=?, \
locations_send_until=? \
WHERE id=?",
paramsv![
(
if 0 != seconds { now } else { 0 },
if 0 != seconds { now + seconds } else { 0 },
chat_id,
],
),
)
.await?;
if 0 != seconds && !is_sending_locations_before {
@@ -287,7 +310,7 @@ pub async fn is_sending_locations_to_chat(
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE id=? AND locations_send_until>?;",
paramsv![chat_id, time()],
(chat_id, time()),
)
.await?
}
@@ -296,7 +319,7 @@ pub async fn is_sending_locations_to_chat(
.sql
.exists(
"SELECT COUNT(id) FROM chats WHERE locations_send_until>?;",
paramsv![time()],
(time(),),
)
.await?
}
@@ -315,7 +338,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
.sql
.query_map(
"SELECT id FROM chats WHERE locations_send_until>?;",
paramsv![time()],
(time(),),
|row| row.get::<_, i32>(0),
|chats| {
chats
@@ -329,18 +352,18 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
if let Err(err) = context.sql.execute(
"INSERT INTO locations \
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
paramsv![
(
latitude,
longitude,
accuracy,
time(),
chat_id,
ContactId::SELF,
]
)
).await {
warn!( "failed to store location {:#}", err);
warn!(context, "failed to store location {:#}", err);
} else {
info!("stored location for chat {}", chat_id);
info!(context, "stored location for chat {}", chat_id);
continue_streaming = true;
}
}
@@ -381,14 +404,14 @@ pub async fn get_range(
AND (? OR l.from_id=?) \
AND (l.independent=1 OR (l.timestamp>=? AND l.timestamp<=?)) \
ORDER BY l.timestamp DESC, l.id DESC, msg_id DESC;",
paramsv![
(
disable_chat_id,
chat_id,
disable_contact_id,
contact_id as i32,
timestamp_from,
timestamp_to,
],
),
|row| {
let msg_id = row.get(6)?;
let txt: String = row.get(9)?;
@@ -448,7 +471,7 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
let (locations_send_begin, locations_send_until, locations_last_sent) = context.sql.query_row(
"SELECT locations_send_begin, locations_send_until, locations_last_sent FROM chats WHERE id=?;",
paramsv![chat_id], |row| {
(chat_id,), |row| {
let send_begin: i64 = row.get(0)?;
let send_until: i64 = row.get(1)?;
let last_sent: i64 = row.get(2)?;
@@ -477,12 +500,12 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
AND independent=0 \
GROUP BY timestamp \
ORDER BY timestamp;",
paramsv![
(
ContactId::SELF,
locations_send_begin,
locations_last_sent,
ContactId::SELF
],
),
|row| {
let location_id: i32 = row.get(0)?;
let latitude: f64 = row.get(1)?;
@@ -552,7 +575,7 @@ pub async fn set_kml_sent_timestamp(
.sql
.execute(
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
paramsv![timestamp, chat_id],
(timestamp, chat_id),
)
.await?;
Ok(())
@@ -564,7 +587,7 @@ pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id:
.sql
.execute(
"UPDATE msgs SET location_id=? WHERE id=?;",
paramsv![location_id, msg_id],
(location_id, msg_id),
)
.await?;
@@ -606,10 +629,10 @@ pub(crate) async fn save(
.prepare_cached("SELECT id FROM locations WHERE timestamp=? AND from_id=?")?;
let mut stmt_insert = conn.prepare_cached(stmt_insert)?;
let exists = stmt_test.exists(paramsv![timestamp, contact_id])?;
let exists = stmt_test.exists((timestamp, contact_id))?;
if independent || !exists {
stmt_insert.execute(paramsv![
stmt_insert.execute((
timestamp,
contact_id,
chat_id,
@@ -617,7 +640,7 @@ pub(crate) async fn save(
longitude,
accuracy,
independent,
])?;
))?;
if timestamp > newest_timestamp {
// okay to drop, as we use cached prepared statements
@@ -640,7 +663,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
loop {
let next_event = match maybe_send_locations(context).await {
Err(err) => {
warn!("maybe_send_locations failed: {:#}", err);
warn!(context, "maybe_send_locations failed: {:#}", err);
Some(60) // Retry one minute later.
}
Ok(next_event) => next_event,
@@ -653,6 +676,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
};
info!(
context,
"Location loop is waiting for {} or interrupt",
duration_to_str(duration)
);
@@ -705,7 +729,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
AND timestamp>=? \
AND timestamp>? \
AND independent=0",
paramsv![ContactId::SELF, locations_send_begin, locations_last_sent,],
(ContactId::SELF, locations_send_begin, locations_last_sent),
)
.await?;
@@ -719,7 +743,10 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
// Send location-only message.
// Pending locations are attached automatically to every message,
// so also to this empty text message.
info!("Chat {} has pending locations, sending them.", chat_id);
info!(
context,
"Chat {} has pending locations, sending them.", chat_id
);
let mut msg = Message::new(Viewtype::Text);
msg.hidden = true;
msg.param.set_cmd(SystemMessage::LocationOnly);
@@ -727,8 +754,8 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
} else {
// Wait until pending locations can be sent.
info!(
"Chat {} has pending locations, but they can't be sent yet.",
chat_id
context,
"Chat {} has pending locations, but they can't be sent yet.", chat_id
);
next_event = next_event
.into_iter()
@@ -737,21 +764,24 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
}
} else {
info!(
"Chat {} has location streaming enabled, but no pending locations.",
chat_id
context,
"Chat {} has location streaming enabled, but no pending locations.", chat_id
);
}
} else {
// Location streaming was either explicitly disabled (locations_send_begin = 0) or
// locations_send_until is in the past.
info!("Disabling location streaming for chat {}.", chat_id);
info!(
context,
"Disabling location streaming for chat {}.", chat_id
);
context
.sql
.execute(
"UPDATE chats \
SET locations_send_begin=0, locations_send_until=0 \
WHERE id=?",
paramsv![chat_id],
(chat_id,),
)
.await
.context("failed to disable location streaming")?;

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