Compare commits

...

278 Commits

Author SHA1 Message Date
link2xt
a9dbf05d8d ci: update to Python 3.13 2024-06-04 16:56:16 +00:00
link2xt
90c30879b1 refactor(imap): make select_folder() accept non-optional folder
If no folder should be selected,
`maybe_close_folder()` can be called directly.
2024-06-04 13:31:40 +00:00
link2xt
0ca1318118 fix: retry sending MDNs on temporary error
Postfix returns 421 4.4.2 Error: timeout exceeded
when overloaded by CI. If MDN is not retried in this case,
`test_qr_readreceipt` fails.
2024-06-04 12:54:47 +00:00
link2xt
0be639b244 chore(release): prepare for 1.140.0 2024-06-04 12:01:55 +00:00
Sebastian Klähn
48b4cfc247 feat: add config option to enable iroh (#5607)
Co-authored-by: link2xt <link2xt@testrun.org>
2024-06-03 22:59:29 +00:00
link2xt
a4037b8278 refactor: put duplicate code into lookup_chat_or_create_adhoc_group 2024-06-03 20:46:30 +00:00
link2xt
30405056e3 fix: Do not fail to send images with wrong extensions
Try to guess the image format based on its content first.

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2024-06-03 14:56:10 +00:00
link2xt
0fbab7147a fix: prefer Chat-Group-ID over references for new groups 2024-06-03 14:10:32 +00:00
link2xt
de57ef5ac7 refactor: factor create_adhoc_group() call out of create_group() 2024-06-03 14:10:32 +00:00
link2xt
f48a047fe0 test: refactor test_alias_* into 8 separate tests 2024-06-03 14:10:32 +00:00
link2xt
8ba08432c5 docs: fix a typo in test_partial_group_consistency() 2024-06-03 14:10:32 +00:00
link2xt
bf34bd3a62 docs: create_group() does not find chats, only creates them 2024-06-03 14:10:32 +00:00
B. Petersen
21845ca5ea docs: add vCard as supported standard 2024-06-02 15:33:56 +02:00
iequidoo
768ef772bb feat: Add a db index for reactions by msg_id (#5507)
This should speed up `get_msg_reactions()` filtering reactions by `msg_id`, but also queries in
other places involving both `msg_id` and `contact_id`.
2024-06-01 11:43:15 -03:00
link2xt
69842c18f7 test: fix logging of TestContext created using TestContext::new_alice()
Before this fix LogSink was dropped immediately,
resulting in no logs printed for contexts created using
TextContext::new_alice(), but printed for contexts created
using TextContext::new().
2024-05-30 21:14:41 +00:00
link2xt
42a7cd3eea fix: allow creation of groups by outgoing messages without recipients
`!to_ids().is_empty()` check is needed in cases of 1:1 chat creation
because otherwise `to_id` is undefined,
but in case of outgoing group message without recipients
observed on a second device creating a group should be allowed.
2024-05-30 18:01:32 +00:00
link2xt
b7e5b906d1 build: unpin OpenSSL version and update to OpenSSL 3.3.0
Previously OpenSSL was pinned due to problems
with Zig toolchain used back then
and then not unpinned even after switching from Zig to Nix
due to <https://github.com/alexcrichton/openssl-src-rs/issues/235>.
The problem should be fixed now
and we can try to unpin OpenSSL again.
2024-05-30 16:56:10 +00:00
link2xt
ad271fac80 ci: remove cargo-nextest bug workaround
The problem should be fixed
since nextest 0.9.72.
2024-05-30 13:46:29 +00:00
iequidoo
70ad323c9a fix: AEAP: Remove old peerstate verified_key instead of removing the whole peerstate (#5535)
When doing an AEAP transition, we mustn't just delete the old peerstate as this would break
encryption to it. This is critical for non-verified groups -- if we can't encrypt to the old
address, we can't securely remove it from the group (to add the new one instead).
2024-05-30 10:38:39 -03:00
iequidoo
27bf4c37a7 feat: Map *.wav to Viewtype::Audio (#5633)
It seems there are no problems with playing WAV on all modern platforms, so such files should be
displayed in the "AUDIO", not "FILES" tab in the UIs.
2024-05-30 10:33:10 -03:00
iequidoo
1cc31c1038 feat: Remove limit on number of email recipients for chatmail clients (#5598) 2024-05-27 16:59:01 -03:00
iequidoo
adb0dd43a7 fix: Set Param::Bot for messages on the sender side as well (#5615) 2024-05-27 16:29:41 -03:00
link2xt
d29538beb0 chore(release): prepare for 1.139.6 2024-05-25 07:05:10 +00:00
link2xt
b99e4649a4 chore(cargo-deny): remove unused entry from deny.toml 2024-05-25 06:50:16 +00:00
link2xt
68daa3550e build: upgrade iroh to 0.17.0 2024-05-25 06:36:34 +00:00
link2xt
9d65282710 build(nix): add iroh-base output hash 2024-05-25 05:59:00 +00:00
link2xt
d8f3368b3c chore: fix python lint 2024-05-25 05:51:37 +00:00
link2xt
5755fe7bef test(deltachat-rpc-client): regression test for double gossip subscription 2024-05-25 05:46:55 +00:00
link2xt
4f071e3b31 fix: acquire write lock on iroh_channels before checking for subscribe_loop 2024-05-25 05:46:55 +00:00
holger krekel
f4dfc79808 test(deltachat-rpc-client): add realtime channel tests
Co-authored-by: link2xt <link2xt@testrun.org>
2024-05-25 05:46:55 +00:00
link2xt
518d5bc4c7 refactor: log IMAP connection type on connection failure
Otherwise it is impossible to distinguish between
failure to establish INBOX and DeltaChat folder connections
in the logs.
2024-05-24 00:13:56 +00:00
link2xt
0e1f62a38d build: update iroh to the git version
This fixes the problem with simultaneous connections in iroh-gossip:
<https://github.com/n0-computer/iroh/pull/2318>
2024-05-23 22:58:45 +00:00
iequidoo
af4b59fe0a test: Viewtype::File attachments are sent unchanged and preserve extensions 2024-05-23 17:10:52 -03:00
link2xt
8c3c0484ed fix(@deltachat/stdio-rpc-server): do not set RUST_LOG to "info" by default
`info` enables info level logging for all libraries,
e.g. iroh starts printing very verbose logs
to stderr this way.
2024-05-23 15:50:29 +00:00
link2xt
97828234dd chore(release): prepare for 1.139.5 2024-05-23 13:15:08 +00:00
iequidoo
20e64c71f8 test: "SecureJoin wait" state and info messages 2024-05-23 14:36:13 +02:00
iequidoo
2214d140c3 fix: Don't recode images in Viewtype::File messages (#5617) 2024-05-23 14:35:38 +02:00
link2xt
907d3efcd0 api(deltachat-rpc-client): add Message.send_webxdc_realtime_data() 2024-05-21 22:15:49 +00:00
link2xt
9573e02c32 api(deltachat-rpc-client): add Message.send_webxdc_realtime_advertisement() 2024-05-21 22:15:49 +00:00
Sebastian Klähn
8cb699290a fix: connect to peers that advertise to you 2024-05-21 22:12:23 +00:00
Friedel Ziegelmayer
31d7b4f9ce feat(deltachat-repl): add start-realtime and send-realtime commands 2024-05-21 22:11:22 +00:00
link2xt
2e5ad3f3a0 test(peer_channels): add test_parallel_connect() 2024-05-21 22:10:15 +00:00
holger krekel
5d3d5d23a1 api(deltachat-rpc-client): add EventType.WEBXDC_REALTIME_DATA constant 2024-05-21 22:10:15 +00:00
link2xt
469ff799ad api: add event channel overflow event 2024-05-21 22:05:48 +00:00
link2xt
18f2a09b35 api(deltachat-ffi): make WebXdcRealtimeData data usable in CFFI
Previously only msg_id was returned to CFFI
without any way to get to the actual received data.
2024-05-21 22:05:34 +00:00
link2xt
81f6aec1a0 chore(release): prepare for 1.139.4 2024-05-21 18:09:55 +00:00
iequidoo
ff60605a7f test: import_vcard() updates only the contact's gossip key 2024-05-21 17:40:07 +00:00
iequidoo
7010e80336 fix: make_vcard: Add authname and key for ContactId::SELF 2024-05-21 17:40:07 +00:00
iequidoo
5f790c1dbc fix(contact-tools): parse_vcard: Support \r\n newlines 2024-05-21 17:40:07 +00:00
iequidoo
8c5d8477fb feat: Add import_vcard() (#5202)
Add a function importing contacts from the given vCard.
2024-05-21 17:40:07 +00:00
iequidoo
10fe6929b0 feat: Scale up contact origins to OutgoingTo when sending a message 2024-05-21 17:40:07 +00:00
link2xt
6fc0000c8a fix: do not log warning if iroh relay metadata is NIL 2024-05-21 15:24:08 +00:00
Sebastian Klähn
e84a5589df nix: add nextest (#5610)
Has some unrelated change that I add the whole .vscode folder to
.gitignore which I also need.
2024-05-21 08:18:05 +00:00
link2xt
e7d9ff12ec chore(release): prepare for 1.139.3 2024-05-20 18:19:27 +00:00
link2xt
607f5959ab fix: always convert absolute paths to relative in accounts.toml
Even if the path does not start with
the directory containing the config,
we want to keep only the last component.
2024-05-20 17:49:21 +00:00
Simon Laux
11546a1ce9 api!(npm rpc server): change api: don't search in path unless options.takeVersionFromPATH is set to true
-remove `DELTA_CHAT_SKIP_PATH` environment variable
- remove version check / search for dc rpc server in $PATH
- remove `options.skipSearchInPath`
- add `options.takeVersionFromPATH`
2024-05-20 18:55:05 +02:00
Simon Laux
ee671836ca fix: npm rpc: set default options for startDeltaChat
this fixes an "undefined" error
2024-05-20 18:55:05 +02:00
Simon Laux
dd77d32446 fix: log/print exit error of deltachat-rpc-server (#5601)
see also #5599 (this logs that error atleast, but does not fix it yet)

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-05-20 10:47:36 +00:00
link2xt
b32fb05ab8 fix: ignore event channel overflows
async-broadcast returns Overflowed error once
if channel overflow happened.
Public APIs such as get_next_event JSON-RPC method
are only expecting an error if the channel is closed,
so we should not propagate overflow error outside.
In particular, Delta Chat Desktop
stop receiving events completely if an error
is returned once.
If overflow happens, we should ignore it
and try again until we get an event or an error because
the channel is closed (in case of recv())
or empty (in case of try_recv()).
2024-05-20 10:44:35 +00:00
link2xt
918d87dcb6 refactor: do not try to lookup group in create_or_lookup_group() 2024-05-20 05:20:38 +00:00
link2xt
98ae05ee59 refactor: stop trying to extract chat ID from Message-IDs 2024-05-20 05:20:38 +00:00
link2xt
cff5c064a6 refactor: use let..else in create_or_lookup_group() 2024-05-20 05:20:38 +00:00
link2xt
e9cef4b0ba refactor(receive_imf): only call create_or_lookup_group() with allow_creation=true 2024-05-20 05:20:38 +00:00
link2xt
7f2c8ff53d refactor(receive_imf): remove unnecessary check for is_mdn
If message is an MDN, it is already assigned to the trash chat
by now, so it is enough to check if the message
is not assigned to chat yet.
2024-05-20 05:20:38 +00:00
link2xt
46d6b81058 refactor(receive_imf): do not check for ContactId::UNDEFINED
from_id should not be undefined,
we either should create a contact for it by then
or reject the message.
2024-05-20 05:20:38 +00:00
link2xt
6d59fb49aa feat: replace env_logger with tracing_subscriber
This allows to get iroh logs with
RUST_LOG=iroh_net=trace
2024-05-19 23:22:37 +00:00
Simon Laux
97602f3fd7 fix: sql syntax error in db migration 27 2024-05-19 20:15:22 +02:00
Simon Laux
f17987743e fix: db migration version 59, it contained an sql syntax error 2024-05-19 20:15:22 +02:00
link2xt
5767cce178 fix(mimeparser): take the last header of multiple ones with the same name
If multiple headers with the same name are present,
we must take the last one.
This is the one that is DKIM-signed if
this header is DKIM-signed at all.

Ideally servers should prevent adding
more From, To and Cc headers by oversigning
them, but unfortunately it is not common
and OpenDKIM in its default configuration
does not oversign any headers.
2024-05-18 22:24:39 +00:00
link2xt
20a4bb1a88 api(deltachat-rpc-client): add Account.wait_for_incoming_msg() 2024-05-18 22:24:17 +00:00
link2xt
578f29f215 chore(release): prepare for 1.139.2 2024-05-18 20:58:03 +00:00
link2xt
6c9643e39e build: add repository URL to @deltachat/jsonrpc-client 2024-05-18 20:56:11 +00:00
link2xt
502ae7fd9f chore(release): prepare for 1.139.1 2024-05-18 20:40:34 +00:00
link2xt
8cb527342a ci: set --access public when publishing to npm
Otherwise it fails to upload new packages with provenance.
2024-05-18 20:39:41 +00:00
link2xt
964c943dd9 chore(release): prepare for 1.139.0 2024-05-18 19:42:54 +00:00
Simon Laux
a971ad1f85 npm rpc server: fix example (#5580) 2024-05-18 19:58:44 +02:00
link2xt
e66b9de922 fix: do not mark the message as seen if it has location.kml 2024-05-18 16:09:55 +00:00
adbenitez
5db202169b fix: save override sender displayname for outgoing messages
Co-authored-by: link2xt <link2xt@testrun.org>
2024-05-18 16:08:53 +00:00
link2xt
b292b191ff test(deltachat-rpc-client): test sending vCard 2024-05-18 14:00:57 +00:00
link2xt
450ff411ec api(deltachat-rpc-client): add Chat.send_contact() 2024-05-18 14:00:57 +00:00
link2xt
8de92e54eb api(deltachat-rpc-client): add Contact.make_vcard() 2024-05-18 14:00:57 +00:00
link2xt
d0844c3e62 api(deltachat-rpc-client): add ViewType.VCARD constant 2024-05-18 14:00:57 +00:00
link2xt
37d61e41ca api(deltachat-jsonrpc): return vcard contact directly in MessageObject 2024-05-17 23:37:29 +00:00
Simon Laux
0c7dad961d npm rpc: fix convert_platform.py: 32bit i32 -> ia32 (#5589) 2024-05-17 23:35:50 +02:00
Sebastian Klähn
36f1fc4f9d feat: ephemeral peer channels (#5346)
Co-authored-by: link2xt <link2xt@testrun.org>
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-05-17 20:13:21 +00:00
Simon Laux
517cb821fb jsonrpc: add api migrate_account and get_blob_dir (#5584)
closes #5533

adds the functions that were still missing for migration to jsonrpc (the
ones that the cffi already had, so just should be quick to review ;)
2024-05-17 21:33:59 +02:00
Simon Laux
ef6c3f8476 fix: rpc npm: fix local desktop development (#5583)
typescript was complaining about missing `@deltachat/jsonrpc-client`
when it wasn't installed locally
2024-05-17 21:28:33 +02:00
link2xt
f84f0d5ad9 ci: check that constants are always up-to-date 2024-05-17 19:23:27 +00:00
Simon Laux
d8e98279c4 rpc npm: rename shutdown method to close and add muteStdErr option to mute the stderr output (#5588) 2024-05-17 21:14:38 +02:00
link2xt
424ac606d8 ci: publish @deltachat/jsonrpc-client directly to npm 2024-05-17 19:10:39 +00:00
Sebastian Klähn
2f35d9a013 use rust-analyzer nightly 2024-05-17 17:37:51 +00:00
Sebastian Klähn
e5259176c9 nix: add git-cliff to flake 2024-05-17 17:36:02 +00:00
link2xt
c370195698 chore(cargo): downgrade libc from 0.2.154 to 0.2.153
0.2.154 is yanked.
2024-05-17 13:52:19 +00:00
Simon Laux
0ba0bd3d77 fix(@deltachat/stdio-rpc-server): fix version check when deltachat-rpc-server is found in path (#5579) 2024-05-17 11:11:11 +02:00
link2xt
d23a7b8523 chore(release): prepare for 1.138.5 2024-05-16 15:07:46 +00:00
link2xt
935f503bc7 chore: rebuild node constants 2024-05-16 15:01:56 +00:00
link2xt
a0f0a8e021 build: add repository URL to deltachat-rpc-server packages 2024-05-16 14:59:45 +00:00
iequidoo
6290ed8752 api: Add make_vcard() (#5203)
Add a function returning a vCard containing contacts with the given ids.
2024-05-15 21:49:59 -03:00
iequidoo
a38f0ba09e refactor: VcardContact: Change timestamp type to i64
- u64 only adds unnecessary conversions.
- `Contact::last_seen` is also `i64`, so make timestamps such everywhere.
2024-05-15 21:07:24 -03:00
iequidoo
191624f334 refactor(contact-tools): VcardContact: rename display_name to authname
It's actually `deltachat::contact::Contact::authname` by semantics. `display_name`/`name` shouldn't
be shared, it's the name given by the user locally.
2024-05-15 21:07:24 -03:00
Hocuri
5292a49bb1 fix: parsing vCards with avatars exported by Android's "Contacts" app 2024-05-15 21:07:24 -03:00
iequidoo
22f01a2699 api: Add Viewtype::Vcard (#5202)
Co-authored-by: Hocuri <hocuri@gmx.de>
2024-05-15 21:07:24 -03:00
iequidoo
95238b6e17 api(jsonrpc): Add parse_vcard() (#5202)
Add a function parsing a vCard file at the given path.

Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: Asiel Díaz Benítez <asieldbenitez@gmail.com>
2024-05-15 21:07:24 -03:00
link2xt
4a738ebd19 chore(release): prepare for 1.138.4 2024-05-15 21:59:28 +00:00
link2xt
d02eccd303 ci: run actions/setup-node before npm publish
Otherwise it does not try to use NODE_AUTH_TOKEN.
2024-05-15 21:57:24 +00:00
link2xt
f1fa053f9f chore(release): prepare for 1.138.3 2024-05-15 20:42:34 +00:00
link2xt
38c1caf180 ci: give CI job permission to publish binaries to the release
Otherwise it fails on `gh release upload` step.
2024-05-15 20:38:59 +00:00
link2xt
97d2812644 chore(release): prepare for 1.138.2 2024-05-15 18:34:06 +00:00
link2xt
2ab713d968 ci: add npm token to publish deltachat-rpc-server packages 2024-05-15 18:08:58 +00:00
link2xt
b7a25d5092 api(deltachat-rpc-client): add CONFIG_SYNCED constant 2024-05-15 17:20:06 +00:00
link2xt
8cd85fa7a4 feat: reset more settings when configuring a chatmail account 2024-05-15 06:30:17 +00:00
link2xt
7cfab9a931 test: set configuration after configure() finishes
This allows to overwrite settings changed
when XCHATMAIL capability is detected.
2024-05-15 06:27:57 +00:00
link2xt
30086038e6 chore(release): prepare for 1.138.1 2024-05-14 22:25:13 +00:00
link2xt
eec1062619 feat: detect XCHATMAIL capability and expose it as is_chatmail config 2024-05-14 22:17:46 +00:00
link2xt
07ceabdf85 refactor: resultify token::lookup_or_new() 2024-05-14 20:32:23 +00:00
Simon Laux
c349bf8e0c ci(deltachat-rpc-server): fix upload of npm packages to github releases (#5564) 2024-05-14 18:48:34 +00:00
link2xt
21eb4f6648 chore(cargo): bump brotli from 5.0.0 to 6.0.0 2024-05-14 17:27:33 +00:00
dependabot[bot]
10fed7d7de chore(cargo): bump human-panic from 1.2.3 to 2.0.0
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 1.2.3 to 2.0.0.
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v1.2.3...v2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:29:22 +00:00
dependabot[bot]
b08a283fe5 chore(cargo): bump serde_json from 1.0.115 to 1.0.116
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.115 to 1.0.116.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.115...v1.0.116)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:12:52 +00:00
dependabot[bot]
45a2805100 chore(cargo): bump hickory-resolver from 0.24.0 to 0.24.1
Bumps [hickory-resolver](https://github.com/hickory-dns/hickory-dns) from 0.24.0 to 0.24.1.
- [Release notes](https://github.com/hickory-dns/hickory-dns/releases)
- [Changelog](https://github.com/hickory-dns/hickory-dns/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hickory-dns/hickory-dns/compare/v0.24.0...v0.24.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:11:48 +00:00
dependabot[bot]
cc8157ecf1 chore(cargo): bump libc from 0.2.153 to 0.2.154
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.153 to 0.2.154.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.153...0.2.154)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:11:30 +00:00
dependabot[bot]
0c98aca5f0 chore(cargo): bump parking_lot from 0.12.1 to 0.12.2
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.12.1 to 0.12.2.
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.1...0.12.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:11:12 +00:00
link2xt
170e4b3530 refactor(sql): make open flags immutable 2024-05-14 14:55:04 +00:00
link2xt
5ed91e9f6e refactor: make MimeMessage.get_header() return Option<&str> 2024-05-13 17:07:58 +00:00
link2xt
2779737c56 ci: set RUSTUP_WINDOWS_PATH_ADD_BIN
This is a workaround for
<https://github.com/nextest-rs/nextest/issues/1493>
2024-05-13 14:29:33 +00:00
link2xt
0d3c0a3d8f fix: ignore parent message if message references itself
When there are no parent references,
Delta Chat inserts Message-ID into References.
Such references should be ignored
because otherwise fully downloaded message
may be assigned to the same chat as previously incorrectly assigned
partially downloaded message.

Fixes receive_imf::tests::test_create_group_with_big_msg
2024-05-13 13:29:06 +00:00
link2xt
8e38e7220b fix: always prefer Chat-Group-ID over In-Reply-To and References
Chat-Group-ID always correctly identifies the chat
message was sent to, while In-Reply-To and References
may point to a message that has itself been incorrectly
assigned to a chat.
2024-05-13 13:29:06 +00:00
link2xt
acfde3cb7b fix: never treat message with Chat-Group-ID as a private reply 2024-05-13 13:29:06 +00:00
link2xt
b6a461e3b7 refactor: add MimeMessage.get_chat_group_id() 2024-05-13 13:29:06 +00:00
B. Petersen
0541ecf22c chore(release): prepare for 1.138.0 2024-05-13 12:47:11 +02:00
B. Petersen
77af0a2114 update node constants 2024-05-13 12:47:11 +02:00
B. Petersen
2f679bc21a add new securejoin strings to deltachat.h 2024-05-13 12:08:36 +02:00
iequidoo
518db9a20f feat: Make one-to-one chats read-only the first seconds of a SecureJoin (#5512)
This protects Bob (the joiner) of sending unexpected-unencrypted messages during an otherwise nicely
running SecureJoin.

If things get stuck, however, we do not want to block communication -- the chat is just
opportunistic as usual, but that needs to be communicated:
1. If Bob's chat with Alice is `Unprotected` and a SecureJoin is started, then add info-message
   "Establishing guaranteed end-to-end encryption, please wait..." and let `Chat::can_send()` return
   `false`.
2. Once the info-message "Messages are guaranteed to be e2ee from now on" is added, let
   `Chat::can_send()` return `true`.
3. If after SECUREJOIN_WAIT_TIMEOUT seconds `2.` did not happen, add another info-message "Could not
   yet establish guaranteed end-to-end encryption but you may already send a message" and also let
   `Chat::can_send()` return `true`.

Both `2.` and `3.` require the event `ChatModified` being sent out so that UI pick up the change wrt
`Chat::can_send()` (this is the same way how groups become updated wrt `can_send()` changes).

SECUREJOIN_WAIT_TIMEOUT should be 10-20 seconds so that we are reasonably sure that the app remains
active and receiving also on mobile devices. If the app is killed during this time then we may need
to do step 3 for any pending Bob-join chats (right now, Bob can only join one chat at a time).
2024-05-13 12:08:36 +02:00
link2xt
edf8aafbdc api!(jsonrpc): replace EphemeralTimer tag "variant" with "kind"
We use "kind" everywhere else.
2024-05-12 01:15:31 +00:00
iequidoo
ab1583eef9 fix: Use ChatIdBlocked::lookup_by_contact() instead of ChatId's method when applicable
`ChatId::lookup_by_contact()` returns `None` for blocked chats, so it should be only used if we need
to filter out blocked chats, e.g. when building a chatlist.
2024-05-11 17:37:12 -03:00
iequidoo
e3cb9b894b fix: Update special chats icons even if they are blocked (#5509)
E.g. the multi-device synchronisation creates the "Saved Messages" chat as blocked, in this case the
chat icon wasn't updated before and the user avatar was displayed instead.
2024-05-11 17:37:12 -03:00
Simon Laux
c375c03d8e stdio jsonrpc server npm package (#5332)
- [x] figgure out how to build the packages (that it installs native
optional package automatically)
- [X] Make the gluecode
- [x] expose both the lowerlevel api that desktop uses (~~send objects
and receive objects~~, getting path of rpc-server is enough)
  - [X] and the higher level api needed for bots (jsonrpc client)
  - [X] typescript types
- [x] automatically pick the right binary from npm or allow getting it
from env var, or give out an error (throw error)
- [x] find out how to dev locally (use local built core in dc desktop) -
there is the question of how to link the typescript client and the task
to add a search in the cargo target folder for a debug build or a
different way, find out some good flow that we can use and document for
dc desktop + locally built core development
- [x] build the packages in ci
- [x] fix that deltachat-rpc-server is not executable

postponed:
- [ ] publish from ci
   - [ ] add key/token to deploy to npm 

Closes #4694

## Related prs
- https://github.com/deltachat-bot/echo/pull/69
- https://github.com/deltachat/deltachat-desktop/pull/3567

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2024-05-11 20:54:11 +02:00
bjoern
14aaab05b0 limit quote replies (#5543)
this PR checks if the quotes are used in a reasonable way:

- quoted messages should be send to the same chat
- or to one-to-one chats

if the quote's chat ID is not the same than the sending chat _and_ the
sending chat is not a one-to-one chat, sending is aborted.

usually, the UIs does not allow quoting in other ways, so, this check is
only a "last defence line" to avoid leaking data in case the UI has
bugs, as recently in
https://github.com/deltachat/deltachat-android/issues/3032

@iequidoo @link2xt @adbenitez i am not 100% sure about this PR, maybe
i've overseen a reasonable usecase where other quotes make sense

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2024-05-10 16:43:49 +02:00
link2xt
72c09feb64 feat: do not add location markers to messages with non-POI location 2024-05-10 14:04:16 +00:00
Nico de Haen
8a4dff2212 Add webxdc internal integration commands in jsonrpc (#5541)
Adds 
_setWebxdcIntegration_ 
_initWebxdcIntegration_
2024-05-08 07:53:04 +02:00
dependabot[bot]
022f836d35 chore(cargo): bump schemars from 0.8.16 to 0.8.19
Bumps [schemars](https://github.com/GREsau/schemars) from 0.8.16 to 0.8.19.
- [Release notes](https://github.com/GREsau/schemars/releases)
- [Changelog](https://github.com/GREsau/schemars/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GREsau/schemars/compare/v0.8.16...v0.8.19)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-08 03:42:51 +00:00
link2xt
636ab4a9e5 fix: delete non-POI locations after delete_device_after, not immediately 2024-05-06 18:32:04 +00:00
link2xt
2bddefa1ab refactor: remove allow_keychange 2024-05-05 21:23:32 +00:00
dependabot[bot]
7d67100a3c Merge pull request #5523 from deltachat/dependabot/cargo/mailparse-0.15.0 2024-05-05 21:23:10 +00:00
dependabot[bot]
1043916411 chore(cargo): bump mailparse from 0.14.1 to 0.15.0
Bumps [mailparse](https://github.com/staktrace/mailparse) from 0.14.1 to 0.15.0.
- [Commits](https://github.com/staktrace/mailparse/compare/v0.14.1...v0.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-05 20:59:52 +00:00
dependabot[bot]
f4e58e90ae chore(cargo): bump syn from 2.0.57 to 2.0.60
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.57 to 2.0.60.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.57...2.0.60)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-05 13:50:07 +00:00
link2xt
e4f10b32dd chore(cargo): bump imap-proto from 0.16.4 to 0.16.5 2024-05-04 22:41:30 +00:00
dependabot[bot]
e9431888a6 Merge pull request #5522 from deltachat/dependabot/cargo/chrono-0.4.38 2024-05-04 22:40:17 +00:00
dependabot[bot]
1649073c0f chore(cargo): bump anyhow from 1.0.81 to 1.0.82
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.81 to 1.0.82.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.81...1.0.82)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-04 03:23:17 -03:00
dependabot[bot]
b2cf18d8b3 chore(cargo): bump chrono from 0.4.37 to 0.4.38
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.37 to 0.4.38.
- [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.37...v0.4.38)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-04 02:53:42 +00:00
iequidoo
2eceb4be29 feat(contact-tools): Add make_vcard()
Add a function making a vCard containing given `VcardContact`s.
2024-05-03 23:52:51 -03:00
iequidoo
ae7ff17ba2 feat(contact-tools): Support parsing profile images from "PHOTO:data:image/jpeg;base64,..." 2024-05-03 23:52:51 -03:00
Hocuri
026f678452 feat: Parsing vCards for contacts sharing (#5482)
Co-authored-by: iequidoo <dgreshilov@gmail.com>
2024-05-03 22:44:09 -03:00
link2xt
add8c0680f ci: update Rust to 1.78.0 2024-05-03 02:11:07 +00:00
dependabot[bot]
aee2b81c06 chore(cargo): bump thiserror from 1.0.58 to 1.0.59
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.58 to 1.0.59.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.59)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 20:35:31 -03:00
dependabot[bot]
3624aad1b5 chore(cargo): bump async-channel from 2.2.0 to 2.2.1
Bumps [async-channel](https://github.com/smol-rs/async-channel) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.2.0...v2.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 20:24:27 -03:00
link2xt
299d994d4b ci: replace black with ruff format
We use `ruff` anyway, so it is one less dependency.
2024-05-02 14:44:11 +02:00
dependabot[bot]
5e0f5ec390 chore(cargo): bump serde from 1.0.197 to 1.0.200
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.197 to 1.0.200.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.197...v1.0.200)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 04:27:42 -03:00
dependabot[bot]
c318ca5d1a chore(cargo): bump base64 from 0.22.0 to 0.22.1
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.22.0 to 0.22.1.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.22.0...v0.22.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 04:18:18 -03:00
dependabot[bot]
38a2e07194 Merge pull request #5515 from deltachat/dependabot/cargo/quote-1.0.36 2024-05-02 06:22:56 +00:00
dependabot[bot]
1ff6740938 chore(cargo): bump quote from 1.0.35 to 1.0.36
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.35 to 1.0.36.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.35...1.0.36)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-01 21:52:09 +00:00
Christian Hagenest
402d5bed85 rm npm install 2024-04-30 21:10:36 +02:00
missytake
57bc046381 ci: use rsync instead of 3rd party github action 2024-04-30 21:10:36 +02:00
Christian Hagenest
0617236eb0 rm leading slash 2024-04-30 21:10:36 +02:00
Christian Hagenest
8c5ffe0237 upload-docs npm run => npm run build 2024-04-30 21:10:36 +02:00
Christian Hagenest
39f977c1e6 add : to upload-docs.yml 2024-04-30 21:10:36 +02:00
Christian Hagenest
ec03614cae add npm install to upload-docs.yml 2024-04-30 21:10:36 +02:00
Christian Hagenest
ea0b063c19 test ts build on branch 2024-04-30 21:10:36 +02:00
Christian Hagenest
98d7a93909 correct folder for js.jsonrpc docs 2024-04-30 21:10:36 +02:00
Christian Hagenest
49bf8414ed rm unneeded rust install from ts docs ci 2024-04-30 21:10:36 +02:00
Christian Hagenest
1e7dbea351 Implement jsonrpc-docs build in github action 2024-04-30 21:10:36 +02:00
Christian Hagenest
0412244646 Revert "WIP: build ts docs with ci + nix"
This reverts commit 00d486e58f.
2024-04-30 21:10:36 +02:00
Christian Hagenest
bbd854d7bc Revert "push docs to delta.chat instead of codespeak"
This reverts commit c04188ce56.
2024-04-30 21:10:36 +02:00
Christian Hagenest
ba2bb517f7 Revert "WIP npm2nix, error: no source/nix"
This reverts commit 83bfba53de.
2024-04-30 21:10:36 +02:00
Christian Hagenest
0ae831eca0 WIP npm2nix, error: no source/nix 2024-04-30 21:10:36 +02:00
Christian Hagenest
ab494ae786 push docs to delta.chat instead of codespeak 2024-04-30 21:10:36 +02:00
Christian Hagenest
8a58ae8a3a WIP: build ts docs with ci + nix 2024-04-30 21:10:36 +02:00
link2xt
cf84255e99 test: test that POIs are deleted when ephemeral message expires 2024-04-29 22:35:59 +00:00
link2xt
462bd63065 docs: add location module documentation 2024-04-29 22:35:59 +00:00
link2xt
6bfbf6547b feat: delete orphaned POI locations during housekeeping 2024-04-29 22:35:59 +00:00
link2xt
13802bab42 fix: delete POI location when disappearing message expires 2024-04-29 22:35:59 +00:00
link2xt
adb2e4ea32 refactor: move delete_poi_location to location module and document it 2024-04-29 22:35:59 +00:00
link2xt
421a7b277d docs: remove Doxygen markup from Message.has_location() 2024-04-29 22:35:59 +00:00
link2xt
14d8139883 feat: delete expired path locations in ephemeral loop 2024-04-29 22:35:59 +00:00
link2xt
062905924c docs: fix references in Message.set_location() documentation 2024-04-29 22:35:59 +00:00
iequidoo
20d79970a2 fix: Correct message viewtype before recoding image blob (#5496)
Otherwise, e.g. if a message is a large GIF, but its viewtype is set to `Image` by the app, this GIF
will be recoded to JPEG to reduce its size. GIFs and other special viewtypes must be always detected
and sent as is.
2024-04-27 23:44:50 -03:00
link2xt
f49588e64e fix: interrupt location loop when new location is stored
Otherwise location-only messages
that should be sent every 60 seconds
are never sent because location loop
waits until the end of location streaming
and is only interrupted by location streaming
ending in other chats or being enabled in other chats.
2024-04-27 20:55:05 +00:00
link2xt
496a8e3810 test: test that member is added even if "Member added" is lost
This is similar to `test_modify_chat_disordered`,
but tests that recovery works in the simplest case
where next message is not modifying group membership.
2024-04-27 07:51:08 +00:00
link2xt
94dc65c1a2 refactor(python): fix ruff 0.4.2 warnings 2024-04-25 20:45:54 +00:00
link2xt
4fe7fa3148 fix: never prepend subject to message text when bot receives it 2024-04-25 12:15:54 +00:00
link2xt
4cf923ccb9 fix: do not fail to send encrypted quotes to unencrypted chats
Replace quote text with "..." instead.
2024-04-25 09:00:49 +00:00
iequidoo
56b86adf18 api: Add dc_msg_save_file() which saves file copy at the provided path (#4309)
... and fails if file already exists. The UI should open the file saving dialog, defaulting to
Downloads and original filename, when asked to save the file. After confirmation it should call
dc_msg_save_file().
2024-04-24 16:38:25 -03:00
iequidoo
cfccee2ad4 fix: Message::set_file_from_bytes(): Set Param::Filename 2024-04-24 16:38:25 -03:00
Hocuri
37d92e3fa5 test: Explain test_was_seen_recently false-positive and give workaround instructions (#5474)
Until the issue is fixed, keep others from running into the same issue.
2024-04-24 14:55:15 +00:00
link2xt
a1ee2b463f chore(release): prepare for 1.137.4 2024-04-24 11:10:25 +00:00
link2xt
8df3b1bb1b fix: use only CRLF in Autocrypt Setup Message 2024-04-24 01:50:28 +00:00
iequidoo
22f240dd4d feat: Add progressive backoff for failing IMAP connection attempts (#5443)
This way we avoid an immediate retry if the network is not yet ready exhausting the ratelimiter's
quota of two connection attempts. Also notify the ratelimiter only after a successful connection so
that it only limits the server load, but not connection attempts.
2024-04-23 22:00:47 -03:00
iequidoo
ae10ed5c40 refactor: Imap: remove RwLock from ratelimit 2024-04-23 22:00:47 -03:00
link2xt
aff6bf9402 fix: convert images to RGB8 (without alpha) before encoding into JPEG
Otherwise an error
"The encoder or decoder for Jpeg does not support the color type `Rgba8`"
is returned if image has an alpha channel.

This is caused by the recent change of JPEG encoder
in `image` crate: <https://github.com/image-rs/image/issues/2211>
2024-04-23 23:37:58 +00:00
link2xt
43fc55e542 test: test recoding RGBA image 2024-04-23 23:37:58 +00:00
link2xt
7ea05cb8a0 test: add screenshot-rgba.png
Created by `convert -alpha deactivate screenshot.png screenshot-rbga.png`.
2024-04-23 23:37:58 +00:00
link2xt
d036ad5853 fix: do not fail if Autocrypt Setup Message has no encryption preference
According to Autocrypt specification
Autocrypt Setup Message SHOULD
contain Autocrypt-Prefer-Encrypt header,
but K-9 6.802 does not include it.
2024-04-23 22:16:54 +00:00
link2xt
e9280b8413 refactor: group use at the top of the test modules 2024-04-23 21:07:50 +00:00
link2xt
2108a8ba94 fix(node): undefine NAPI_EXPERIMENTAL
This fixes build with new clang
which treats -Wincompatible-function-pointer-types as an error.

Related upstream issue: <https://github.com/nodejs/node/issues/52229>
2024-04-23 11:34:03 +00:00
link2xt
34f4ec02f6 feat: replace event channel with broadcast channel
This makes `EventTracker` receive events immediately
instead of being moved from event emitter to event tracker
by a task spawned from `TestContext::new_internal`.

This makes `EventTracker.clear_events` reliable
as it is guaranteed to remove all events emitted
by the time it is called rather than only events
that have been moved already.
2024-04-22 07:44:21 +00:00
link2xt
72d5a387fb api!: remove Stream implementation for EventEmitter 2024-04-22 07:44:21 +00:00
link2xt
d17d89ea8f fix: mark contact request messages as seen on IMAP 2024-04-21 02:17:52 +00:00
link2xt
d2aa76c0ca refactor(deltachat-ffi): remove ResultNullableExt 2024-04-20 21:04:01 +00:00
link2xt
406031773b docs: update references to SecureJoin protocols 2024-04-20 20:17:56 +02:00
bjoern
242547f1e9 feat: experimental Webxdc Integration API, Maps Integration (#5461)
as discussed in several chats, this PR starts making it possible to use
Webxdc as integrations to the main app. In other word: selected parts of
the main app can be integrated as Webxdc, eg. Maps [^1]

this PR contains two parts:

- draft an Webxdc Integration API
- use the Webxdc Integration API to create a Maps Integration

to be clear: a Webxdc is not part of this PR. the PR is about marking a
Webxdc being used as a Map - and core then feeds the Webxdc with
location data. from the view of the Webxdc, the normal
`sendUpdate()`/`setUpdateListener()` is used.

things are still marked as "experimental", idea is to get that in to
allow @adbenitez and @nicodh to move forward on the integrations into
android and desktop, as well as improving the maps.xdc itself.
good news is that we currently can change the protocol between Webxdc
and core at any point :)


# Webxdc Integration API

see `dc_init_webxdc_integration()` in `deltachat.h` for overview and
documentation.

rust code is mostly in `webxdc/integration.rs` that is called by other
places as needed. current [user of the API is
deltachat-ios](https://github.com/deltachat/deltachat-ios/pull/1912),
android/desktop will probably follow.

the jsonrpc part is missing and can come in another PR when things are
settled and desktop is really starting [^2] (so we won't need to do all
iterations twice :) makes also sense, when this is done by someone
actually trying that out on desktop

while the API is prepared to allow other types of integrations (photo
editor, compose tools ...) internally, we currently ignore the type. if
that gets more crazy, we probably also need a dedicated table for the
integrations and not just a single param.

# Maps Integration

rust code is mostly in `webxdc/maps_integration.rs` that is called by
`webxdc/integration.rs` as needed.

EDIT: the idea of having a split here, is that
`webxdc/maps_integration.rs` really can focus on the json part, on the
communication with the .xdc, including tests

this PR is basic implementation, enabling to move forward on
integrations on iOS, but also on desktop and android.

the current implementation allows already the following:
- global and per-chat maps
- add and display POIs
- show positions and tracks of the last 24 hours

the current maps.xdc uses leaflet, and is in some regards better than
the current android/desktop implementations (much faster, show age of
positions, fade out positions, always show names of POIs, clearer UI).
however, we are also not bound to leaflet, it can be anything

> [**screenshots of the current
state**](https://github.com/deltachat/deltachat-ios/pull/1912)
> 👆

to move forward faster and to keep this PR small, the following will go
to a subsequent PR:

- consider allowing webxdc to use a different timewindow for the
location
- delete POIs
- jsonrpc 


[^1]: maps are a good example as anyways barely native (see android
app), did cause a lot of pain on many levels in the past (technically,
bureaucratically), and have a comparable simple api
[^2]: only going for jsonrpc would only make sense if large parts of
android/ios would use jsonrpc, we're not there

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2024-04-20 18:09:35 +02:00
link2xt
f43f5c6c0f build(nix): add ./deltachat-contact-tools to sources 2024-04-20 06:01:53 +00:00
link2xt
910e4bfa37 docs: fix broken references in documentation comments 2024-04-20 05:57:26 +00:00
link2xt
ecf4e651ee bulid(nix): update nix flake 2024-04-20 05:56:34 +00:00
link2xt
7b724fa75a build(nix): add imap-tools as deltachat-rpc-client dependency 2024-04-20 05:56:26 +00:00
link2xt
09776ae71c chore(deps): update rustls to 0.21.11 2024-04-20 05:35:26 +00:00
iequidoo
47bea5f8fb feat(ffi): Warning instead of error if dc_get_msg() could not retrieve message
As trashed messages can't be loaded from the db now by `Message::load_from_db()` returning an error
for such messages, errors from `Message::load_from_db()` should be logged as warnings. If it's
really an error like a db failure, it should be logged internally.
2024-04-20 05:33:56 +00:00
iequidoo
99cd6d10da fix(python): _map_ffi_event: Always check if get_message_by_id() returned None
As trashed messages can't be loaded from the db now, `get_message_by_id()` returns None in some
tests e.g. in `test_deleted_msgs_dont_reappear()`. A `PerAccount` hook shouldn't be called if so.
2024-04-20 05:33:56 +00:00
iequidoo
fae4cb33bc fix(python): Message::is_outgoing: Don't reload message from db
There's no need to load an updated message state from the db to implement `is_outgoing()` and also
this function is implicitly called in some tests where a message is already trashed and a call to
`dc_get_msg()` generates an unexpected error.
2024-04-20 05:33:56 +00:00
iequidoo
7a3be74350 fix(python): Use cached message object if dc_get_msg() returns NULL
Particularly, in _msgstate() and download_state(). If the message has been trashed, dc_get_msg()
returns NULL now.
2024-04-20 05:33:56 +00:00
link2xt
20a64ec357 refactor: remove duplicate clippy exceptions 2024-04-20 03:04:24 +00:00
iequidoo
92bf48684a fix: Don't set is_bot for webxdc status updates (#5445)
Webxdc status update messages are always "Auto-Submitted: auto-generated", so they must be excluded
for is_bot flag calculation.
2024-04-18 23:19:34 -03:00
link2xt
17701b78d6 chore(release): prepare for 1.137.3 2024-04-18 02:33:34 +00:00
link2xt
ff0d506c95 ci: allow older versions of Sphinx than 7.2.6
Version 7.2.6 does not supported by older Python.
2024-04-18 02:32:37 +00:00
link2xt
8ff3f08c2f test: make EventTracker.clear_events() reliable 2024-04-18 02:32:37 +00:00
link2xt
7a32bcc1f4 test: fix flaky chatlist_events test test_update_after_ephemeral_messages
Previously test did not trigger
deletion of ephemeral messages
and worked because clear_events() did not
remove just emitted events from `send_text_msg`.
2024-04-18 02:32:37 +00:00
link2xt
65822e53e6 build(python): pin sphinx version 2024-04-17 11:54:45 +00:00
Simon Laux
ac508a9e9c replace tokio sleep with SystemTime::shift 2024-04-17 10:29:53 +00:00
Simon Laux
225112a8fe fix test events::chatlist_events::test_chatlist_events::test_secure_join_group 2024-04-17 10:29:53 +00:00
Hocuri
5d34b225b7 Split off functional contact tools into its own crate (#5444)
I would like to implement
https://github.com/deltachat/deltachat-core-rust/issues/5422 in its own
crate, but it will depend on some functions that are in the `deltachat`
crate.

So, this PR extracts these functions into its own crate so that I can
add https://github.com/deltachat/deltachat-core-rust/issues/5422 into
the new crate.
2024-04-16 19:01:25 +02:00
link2xt
6ca6a439bd test(node): increase 'static tests' timeout to 5 minutes
It keeps timing out with the default timeout of 2 s on macOS runners.

Also fix comment in the integration test which
said that timeout is 1 minute but sets it to 3 minutes.
Set this timeout to 5 minutes as well.
2024-04-16 10:08:00 +00:00
Simon Laux
f9465f7512 api: add ChatListChanged and ChatListItemChanged events (#4476) 2024-04-15 22:35:19 +00:00
link2xt
489eae5d66 fix: format error context in Message::load_from_db 2024-04-14 20:40:27 +00:00
link2xt
b6c6a63a39 refactor: do not check for is_trash() in get_last_reaction_if_newer_than()
`Message::load_from_db_optional` does not return trashed messages anymore.
2024-04-14 18:46:40 +00:00
link2xt
c069190b68 api: don't load trashed messages with Message::load_from_db
API now pretends that trashed messages don't exist.
This way callers don't have to check if loaded message
belongs to trash chat.
If message may be trashed by the time it is attempted to be loaded,
callers should use Message::load_from_db_optional.

Most changes are around receive_status_update() function
because previously it relied on loading trashed status update
messages immediately after adding them to the database.
2024-04-14 17:43:41 +00:00
Hocuri
94ac2b1097 ci: Run doc tests with cargo test --workspace --doc (#5459)
Nextest doesn't run doc tests, so we need to run them with `cargo test
--workspace --doc`.

See https://github.com/nextest-rs/nextest/issues/16

Follow-up for #5457
2024-04-14 18:54:20 +02:00
link2xt
6080a52024 chore(cargo): update brotli from 4 to 5 2024-04-13 18:27:26 +00:00
link2xt
0aea7d1e02 fix: do not create ad-hoc groups from partial downloads 2024-04-12 01:19:16 +00:00
link2xt
08cbc54c00 fix: assign messages to chats based on not fully downloaded references 2024-04-12 01:19:16 +00:00
link2xt
9731ec419e build(nix): use stable Rust in flake.nix devshell
This way nightly clippy warnings are not generated
when devshell is used.

Nighly Rust is also not cached, e.g. rust-analyzer has to be rebuilt
if version from fenix is used.
2024-04-11 22:25:06 +00:00
iequidoo
e9cfcd9d1b fix: Don't try to do fetch_move_delete() if Trash is needed but not yet configured
This fixes things for Gmail f.e. Before, `Imap::fetch_move_delete()` was called before looking for
Trash and returned an error because of that failing the whole `fetch_idle()` which prevented
configuring Trash in turn.
2024-04-10 21:06:43 -03:00
link2xt
d39cbcdc8d ci: use cargo-nextest instead of cargo-test 2024-04-10 23:57:43 +00:00
link2xt
fbbefe6b49 chore: fix nightly clippy warnings 2024-04-10 15:41:48 +00:00
Sebastian Klähn
bab311730c ci: typos in ci files (#5453) 2024-04-10 15:20:48 +02:00
Sebastian Klähn
b47cad7e68 refactore: use clone_from() (#5451)
`a.clone_from(&b)` is equivalent to `a = b.clone()` in functionality,
but can be overridden to reuse the resources of a to avoid unnecessary
allocations.
2024-04-10 15:01:11 +02:00
link2xt
a3b62b9743 fix(deltachat-rpc-client): construct Thread with target keyword argument
`run` argument does not exist.

Also add `daemon=True`.
2024-04-09 05:55:38 +00:00
link2xt
9aa4c0e56b refactor(deltachat-rpc-client): use list, set and tuple instead of typing
`typing.List` is deprecated according to https://docs.python.org/3/library/typing.html#typing.List
Similar for `Set` and `Dict`.

`from __future__ import annotations` is for compatibility with Python 3.7.
2024-04-09 00:56:10 +00:00
Lothar
27d2b12e8d Adapt target install path if env var CARGO_BUILD_TARGET is set
When the env var CARGO_BUILD_TARGET is set, cargo will crossbuild for the given arch triplet. In this case, the targets will not be put into target/release/, but target/$CARGO_BUILD_TARGET/release/. Add this subdirectory, if neccessary.
2024-04-08 20:07:20 +00:00
link2xt
c1148e4117 chore(cargo): update env_logger 2024-04-08 19:44:35 +00:00
B. Petersen
295f7a291b api!: remove reactions ffi; all implementations use jsonrpc 2024-04-08 19:11:16 +00:00
link2xt
2be28f1311 test: move reaction tests to JSON-RPC 2024-04-08 19:11:16 +00:00
link2xt
2e42243de8 feat: port direct_imap.py into deltachat-rpc-client 2024-04-08 19:11:16 +00:00
link2xt
00f2585d8c api(deltachat-rpc-client): add ACFactory.get_accepted_chat() 2024-04-08 19:11:16 +00:00
link2xt
0b73f9cebd api(deltachat-rpc-client): add Account.bring_online() 2024-04-08 19:11:16 +00:00
link2xt
f5e8a04fd0 api(deltachat-rpc-client): return Message from Message.send_reaction() 2024-04-08 19:11:16 +00:00
link2xt
6721df7d57 api(deltachat-rpc-client): add Account.wait_for_reactions_changed() 2024-04-08 19:11:16 +00:00
link2xt
18d98d643b api(deltachat-rpc-client): add Chat.send_file() 2024-04-08 19:11:16 +00:00
link2xt
62758658ed api(deltachat-rpc-client): add Message.wait_until_delivered() 2024-04-08 19:11:16 +00:00
link2xt
03bb751a9b api(deltachat-rpc-client): add Account.create_chat() 2024-04-08 19:11:16 +00:00
link2xt
3ebb1ea95f chore(cargo): require tokio 1.37 and chrono 0.4.37
Make deps.rs happy about RUSTSEC-2020-0159 and RUSTSEC-2023-0001
2024-04-08 16:39:30 +00:00
iequidoo
c1d251010f fix: Keep webxdc instance for delete_device_after period after a status update (#5365)
If `delete_device_after` is configured, that period should be counted for webxdc instances from the
last status update, otherwise nothing prevents from deleting them. Use `msgs.timestamp_rcvd` to
store the last status update timestamp, it anyway isn't used for anything except displaying a
detailed message info. Also, as `ephemeral::select_expired_messages()` now also checks
`timestamp_rcvd`, we have an improvement that a message is guaranteed not to be deleted for the
`delete_device_after` period since its receipt. Before only the sort timestamp was checked which is
derived from the "sent" timestamp.
2024-04-07 22:08:48 -03:00
iequidoo
7e5959e495 test: display_chat(): Don't add day markers
Otherwise golden_test_chat() fails when run around midnight.
2024-04-07 21:03:33 -03:00
iequidoo
823da56f2d fix: Add tolerance to MemberListTimestamp (#5366)
Let's add a 1-minute tolerance to `Params::MemberListTimestamp`.

This adds to the group membership consistency algo the following properties:
- If remote group membership changes were made by two members in parallel, both of them are applied,
  no matter in which order the messages are received.
- If we remove a member locally, only explicit remote member additions/removals made in parallel are
  allowed, but not the synchronisation of the member list from "To". Before, if somebody managed to
  reply earlier than receiving our removal of a member, we added it back which doesn't look good.
2024-04-07 21:03:33 -03:00
link2xt
5bcc44ca9b chore: use ruff check instead of ruff
`ruff` without `check` is deprecated.
2024-04-07 19:57:05 +00:00
link2xt
4304e3f0be chore(cargo): require tokio 1.37.0
Make deps.rs happy about RUSTSEC-2023-0001
2024-04-07 19:18:44 +00:00
link2xt
e2e3abdf03 chore(cargo): update base64 to 0.22 2024-04-07 19:16:44 +00:00
link2xt
dcea188b62 chore(cargo): require smallvec 1.13.2
Make deps.rs happy about RUSTSEC-2021-0003
2024-04-07 19:12:34 +00:00
link2xt
5cf725a378 chore(cargo): require kamadak-exif 0.5.3
Make deps.rs happy about RUSTSEC-2021-0143
2024-04-07 19:09:58 +00:00
link2xt
2bf0ea9d91 docs: add deps.rs badge 2024-04-07 19:06:42 +00:00
B. Petersen
1df936aeac add 'Ubuntu Touch' to the list of 'frontend projects' 2024-04-07 16:20:45 +00:00
link2xt
9ab2c6df16 fix(deltachat-jsonrpc): block in inner_get_backup_qr
This change avoids the race between
`provide_backup` changing the state from NoProvider to Pending
and a call to `get_backup_qr` or `get_backup_qr_svg`.
With this change `get_backup_qr` and `get_backup_qr_svg`
always block until QR code is available,
even if `provide_backup` was not called yet.
2024-04-07 14:20:39 +00:00
link2xt
cf11741a8c refactor: do not ignore Contact::get_by_id errors in get_encrinfo 2024-04-07 10:12:17 +00:00
iequidoo
b6a12e3914 fix: Fix emitting ContactsChanged events on "recently seen" status change (#5377)
- Always emit `ContactsChanged` from `contact::update_last_seen()` if a contact was seen recently
  just for simplicity and symmetry with `RecentlySeenLoop::run()` which also may emit several events
  for single contact.
- Fix sleep time calculation in `RecentlySeenLoop::run()` -- `now` must be updated on every
  iteration, before the initial value was used every time which led to progressively long sleeps.
2024-04-06 18:21:12 -03:00
B. Petersen
b753440a68 fix: Message::get_summary() must not return reaction summary 2024-04-06 20:16:18 +02:00
B. Petersen
39abc8344c add a test for Message::get_summary() 2024-04-06 20:16:18 +02:00
link2xt
65c9e72bf4 test: test withdrawing group join QR codes 2024-04-05 22:34:02 +00:00
link2xt
ea4d954c77 fix: do not emit MSGS_CHANGED event for outgoing hidden messages
This includes synchronization messages.
2024-04-05 22:34:02 +00:00
link2xt
43523a96a2 api(deltachat-rpc-client): add check_qr and set_config_from_qr APIs 2024-04-05 22:34:02 +00:00
link2xt
2e2fa9e74f chore: update lockfile in /fuzz 2024-04-05 19:44:51 +00:00
165 changed files with 11445 additions and 2706 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.77.1
RUSTUP_TOOLCHAIN: 1.78.0
steps:
- uses: actions/checkout@v4
with:
@@ -40,6 +40,18 @@ jobs:
- name: Check
run: cargo check --workspace --all-targets --all-features
npm_constants:
name: Check if node constants are up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Rebuild constants
run: npm run build:core:constants
- name: Check that constants are not changed
run: git diff --exit-code
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
@@ -83,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.77.1
rust: 1.78.0
- os: windows-latest
rust: 1.77.1
rust: 1.78.0
- os: macos-latest
rust: 1.77.1
rust: 1.78.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
@@ -105,10 +117,20 @@ jobs:
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Install nextest
uses: taiki-e/install-action@v2
with:
tool: nextest
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace
run: cargo nextest run --workspace
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --doc
- name: Test cargo vendor
run: cargo vendor
@@ -187,11 +209,11 @@ jobs:
fail-fast: false
matrix:
include:
# Currently used Rust version.
# Currently used Python version.
- os: ubuntu-latest
python: 3.12
python: 3.13
- os: macos-latest
python: 3.12
python: 3.13
# PyPy tests
- os: ubuntu-latest
@@ -221,6 +243,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install tox
run: pip install tox
@@ -241,11 +264,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.12
python: 3.13
- os: macos-latest
python: 3.12
python: 3.13
- os: windows-latest
python: 3.12
python: 3.13
# PyPy tests
- os: ubuntu-latest
@@ -267,6 +290,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
allow-prereleases: true
- name: Install tox
run: pip install tox

View File

@@ -266,3 +266,141 @@ jobs:
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
permissions:
id-token: write
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
run: |
cd deltachat-rpc-server/npm-package
python --version
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
ls -lah platform_package
for platform in ./platform_package/*; do npm pack "$platform"; done
npm pack
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
if-no-files-found: error
- name: Upload npm packets to the GitHub release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,82 +1,38 @@
name: "jsonrpc js client build"
name: "Publish @deltachat/jsonrpc-client"
on:
pull_request:
push:
tags:
- "*"
- "!py-*"
workflow_dispatch:
release:
types: [published]
jobs:
pack-module:
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
name: "Publish @deltachat/jsonrpc-client"
runs-on: ubuntu-20.04
permissions:
id-token: write
contents: read
steps:
- name: Install tree
run: sudo apt install tree
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: Get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pull Request ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
shell: bash
working-directory: deltachat-jsonrpc/typescript
run: |
npm run build
npm pack .
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v4
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: Post links to details
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
env:
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,9 +1,10 @@
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
on:
push:
branches:
- main
- build_jsonrpc_docs_ci
jobs:
build-rs:
@@ -17,13 +18,11 @@ jobs:
run: |
cargo doc --package deltachat --no-deps --document-private-items
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
@@ -56,9 +55,37 @@ jobs:
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to py.delta.chat
- name: Upload to c.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
build-ts:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: npm install
run: npm install
- name: npm run build
run: npm run build
- name: Run docs script
run: npm run docs
- name: Upload to js.jsonrpc.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"

View File

@@ -1,5 +1,5 @@
# GitHub Actions workflow
# to build `deltachat_fii` crate documentation
# to build `deltachat_ffi` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat

4
.gitignore vendored
View File

@@ -33,7 +33,7 @@ deltachat-ffi/xml
coverage/
.DS_Store
.vscode/launch.json
.vscode
python/accounts.txt
python/all-testaccounts.txt
tmp/
@@ -50,4 +50,4 @@ result
# direnv
.envrc
.direnv
.direnv

View File

@@ -1,5 +1,488 @@
# Changelog
## [1.140.0] - 2024-06-04
### Features / Changes
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
### Fixes
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
- Allow creation of groups by outgoing messages without recipients.
- Prefer `Chat-Group-ID` over references for new groups.
- Do not fail to send images with wrong extensions.
### Build system
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
### CI
- Remove cargo-nextest bug workaround.
### Documentation
- Add vCard as supported standard.
- Create_group() does not find chats, only creates them.
- Fix a typo in test_partial_group_consistency().
### Refactor
- Factor create_adhoc_group() call out of create_group().
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
### Tests
- Fix logging of TestContext created using TestContext::new_alice().
- Refactor `test_alias_*` into 8 separate tests.
## [1.139.6] - 2024-05-25
### Build system
- Update `iroh` to the git version.
- nix: Add iroh-base output hash.
- Upgrade iroh to 0.17.0.
### Fixes
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
- Acquire write lock on iroh_channels before checking for subscribe_loop.
### Miscellaneous Tasks
- Fix python lint.
- cargo-deny: Remove unused entry from deny.toml.
### Refactor
- Log IMAP connection type on connection failure.
### Tests
- Viewtype::File attachments are sent unchanged and preserve extensions.
- deltachat-rpc-client: Add realtime channel tests.
- deltachat-rpc-client: Regression test for double gossip subscription.
## [1.139.5] - 2024-05-23
### API-Changes
- deltachat-ffi: Make WebXdcRealtimeData data usable in CFFI.
- Add event channel overflow event.
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_DATA constant.
- deltachat-rpc-client: Add Message.send_webxdc_realtime_advertisement().
- deltachat-rpc-client: Add Message.send_webxdc_realtime_data().
### Features / Changes
- deltachat-repl: Add start-realtime and send-realtime commands.
### Fixes
- peer_channels: Connect to peers that advertise to you.
- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)).
### Tests
- peer_channels: Add test_parallel_connect().
- "SecureJoin wait" state and info messages.
## [1.139.4] - 2024-05-21
### Features / Changes
- Scale up contact origins to OutgoingTo when sending a message.
- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
### Fixes
- Do not log warning if iroh relay metadata is NIL.
- contact-tools: Parse_vcard: Support `\r\n` newlines.
- Make_vcard: Add authname and key for ContactId::SELF.
### Other
- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)).
## [1.139.3] - 2024-05-20
### API-Changes
- [**breaking**] @deltachat/stdio-rpc-server: change api: don't search in path unless `options.takeVersionFromPATH` is set to `true`
- @deltachat/stdio-rpc-server: remove `DELTA_CHAT_SKIP_PATH` environment variable
- @deltachat/stdio-rpc-server: remove version check / search for dc rpc server in $PATH
- @deltachat/stdio-rpc-server: remove `options.skipSearchInPath`
- @deltachat/stdio-rpc-server: add `options.takeVersionFromPATH`
- deltachat-rpc-client: Add Account.wait_for_incoming_msg().
### Features / Changes
- Replace env_logger with tracing_subscriber.
### Fixes
- Ignore event channel overflows.
- mimeparser: Take the last header of multiple ones with the same name.
- Db migration version 59, it contained an sql syntax error.
- Sql syntax error in db migration 27.
- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)).
- @deltachat/stdio-rpc-server: set default options for `startDeltaChat`.
- Always convert absolute paths to relative in accounts.toml.
### Refactor
- receive_imf: Do not check for ContactId::UNDEFINED.
- receive_imf: Remove unnecessary check for is_mdn.
- receive_imf: Only call create_or_lookup_group() with allow_creation=true.
- Use let..else in create_or_lookup_group().
- Stop trying to extract chat ID from Message-IDs.
- Do not try to lookup group in create_or_lookup_group().
## [1.139.2] - 2024-05-18
### Build system
- Add repository URL to @deltachat/jsonrpc-client.
## [1.139.1] - 2024-05-18
### CI
- Set `--access public` when publishing to npm.
## [1.139.0] - 2024-05-18
### Features / Changes
- Ephemeral peer channels ([#5346](https://github.com/deltachat/deltachat-core-rust/pull/5346)).
### Fixes
- Save override sender displayname for outgoing messages.
- Do not mark the message as seen if it has `location.kml`.
- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/deltachat/deltachat-core-rust/pull/5579)).
- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/deltachat/deltachat-core-rust/pull/5583)).
- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/deltachat/deltachat-core-rust/pull/5588))
- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/deltachat/deltachat-core-rust/pull/5589))
- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/deltachat/deltachat-core-rust/pull/5580))
### API-Changes
- deltachat-jsonrpc: Return vcard contact directly in MessageObject.
- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/deltachat/deltachat-core-rust/pull/5584)).
- deltachat-rpc-client: Add ViewType.VCARD constant.
- deltachat-rpc-client: Add Contact.make_vcard().
- deltachat-rpc-client: Add Chat.send_contact().
### CI
- Publish @deltachat/jsonrpc-client directly to npm.
- Check that constants are always up-to-date.
### Build system
- nix: Add git-cliff to flake.
- nix: Use rust-analyzer nightly
### Miscellaneous Tasks
- cargo: Downgrade libc from 0.2.154 to 0.2.153.
### Tests
- deltachat-rpc-client: Test sending vCard.
## [1.138.5] - 2024-05-16
### API-Changes
- jsonrpc: Add parse_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
- Add Viewtype::Vcard ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
- Add make_vcard() ([#5203](https://github.com/deltachat/deltachat-core-rust/pull/5203)).
### Build system
- Add repository URL to deltachat-rpc-server packages.
### Fixes
- Parsing vCards with avatars exported by Android's "Contacts" app.
### Miscellaneous Tasks
- Rebuild node constants.
### Refactor
- contact-tools: VcardContact: rename display_name to authname.
- VcardContact: Change timestamp type to i64.
## [1.138.4] - 2024-05-15
### CI
- Run actions/setup-node before npm publish.
## [1.138.3] - 2024-05-15
### CI
- Give CI job permission to publish binaries to the release.
## [1.138.2] - 2024-05-15
### API-Changes
- deltachat-rpc-client: Add CONFIG_SYNCED constant.
### CI
- Add npm token to publish deltachat-rpc-server packages.
### Features / Changes
- Reset more settings when configuring a chatmail account.
### Tests
- Set configuration after configure() finishes.
## [1.138.1] - 2024-05-14
### Features / Changes
- Detect XCHATMAIL capability and expose it as `is_chatmail` config.
### Fixes
- Never treat message with Chat-Group-ID as a private reply.
- Always prefer Chat-Group-ID over In-Reply-To and References.
- Ignore parent message if message references itself.
### CI
- Set RUSTUP_WINDOWS_PATH_ADD_BIN to work around `nextest` issue <https://github.com/nextest-rs/nextest/issues/1493>.
- deltachat-rpc-server: Fix upload of npm packages to github releases ([#5564](https://github.com/deltachat/deltachat-core-rust/pull/5564)).
### Refactor
- Add MimeMessage.get_chat_group_id().
- Make MimeMessage.get_header() return Option<&str>.
- sql: Make open flags immutable.
- Resultify token::lookup_or_new().
### Miscellaneous Tasks
- cargo: Bump parking_lot from 0.12.1 to 0.12.2.
- cargo: Bump libc from 0.2.153 to 0.2.154.
- cargo: Bump hickory-resolver from 0.24.0 to 0.24.1.
- cargo: Bump serde_json from 1.0.115 to 1.0.116.
- cargo: Bump human-panic from 1.2.3 to 2.0.0.
- cargo: Bump brotli from 5.0.0 to 6.0.0.
## [1.138.0] - 2024-05-13
### API-Changes
- Add dc_msg_save_file() which saves file copy at the provided path ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
- Api!(jsonrpc): replace EphemeralTimer tag "variant" with "kind"
### CI
- Use rsync instead of 3rd party github action.
- Replace `black` with `ruff format`.
- Update Rust to 1.78.0.
### Documentation
- Fix references in Message.set_location() documentation.
- Remove Doxygen markup from Message.has_location().
- Add `location` module documentation.
### Features / Changes
- Delete expired path locations in ephemeral loop.
- Delete orphaned POI locations during housekeeping.
- Parsing vCards for contacts sharing ([#5482](https://github.com/deltachat/deltachat-core-rust/pull/5482)).
- contact-tools: Support parsing profile images from "PHOTO:data:image/jpeg;base64,...".
- contact-tools: Add make_vcard().
- Do not add location markers to messages with non-POI location.
- Make one-to-one chats read-only the first seconds of a SecureJoin ([#5512](https://github.com/deltachat/deltachat-core-rust/pull/5512)).
### Fixes
- Message::set_file_from_bytes(): Set Param::Filename.
- Do not fail to send encrypted quotes to unencrypted chats.
- Never prepend subject to message text when bot receives it.
- Interrupt location loop when new location is stored.
- Correct message viewtype before recoding image blob ([#5496](https://github.com/deltachat/deltachat-core-rust/pull/5496)).
- Delete POI location when disappearing message expires.
- Delete non-POI locations after `delete_device_after`, not immediately.
- Update special chats icons even if they are blocked ([#5509](https://github.com/deltachat/deltachat-core-rust/pull/5509)).
- Use ChatIdBlocked::lookup_by_contact() instead of ChatId's method when applicable.
### Miscellaneous Tasks
- cargo: Bump quote from 1.0.35 to 1.0.36.
- cargo: Bump base64 from 0.22.0 to 0.22.1.
- cargo: Bump serde from 1.0.197 to 1.0.200.
- cargo: Bump async-channel from 2.2.0 to 2.2.1.
- cargo: Bump thiserror from 1.0.58 to 1.0.59.
- cargo: Bump anyhow from 1.0.81 to 1.0.82.
- cargo: Bump chrono from 0.4.37 to 0.4.38.
- cargo: Bump imap-proto from 0.16.4 to 0.16.5.
- cargo: Bump syn from 2.0.57 to 2.0.60.
- cargo: Bump mailparse from 0.14.1 to 0.15.0.
- cargo: Bump schemars from 0.8.16 to 0.8.19.
### Other
- Build ts docs with ci + nix.
- Push docs to delta.chat instead of codespeak
- Implement jsonrpc-docs build in github action
- Rm unneeded rust install from ts docs ci
- Correct folder for js.jsonrpc docs
- Add npm install to upload-docs.yml
- Add : to upload-docs.yml
- Upload-docs npm run => npm run build
- Rm leading slash
- Rm npm install
- Merge pull request #5515 from deltachat/dependabot/cargo/quote-1.0.36
- Merge pull request #5522 from deltachat/dependabot/cargo/chrono-0.4.38
- Merge pull request #5523 from deltachat/dependabot/cargo/mailparse-0.15.0
- Add webxdc internal integration commands in jsonrpc ([#5541](https://github.com/deltachat/deltachat-core-rust/pull/5541))
- Limit quote replies ([#5543](https://github.com/deltachat/deltachat-core-rust/pull/5543))
- Stdio jsonrpc server npm package ([#5332](https://github.com/deltachat/deltachat-core-rust/pull/5332))
### Refactor
- python: Fix ruff 0.4.2 warnings.
- Move `delete_poi_location` to location module and document it.
- Remove allow_keychange.
### Tests
- Explain test_was_seen_recently false-positive and give workaround instructions ([#5474](https://github.com/deltachat/deltachat-core-rust/pull/5474)).
- Test that member is added even if "Member added" is lost.
- Test that POIs are deleted when ephemeral message expires.
- Test ts build on branch
## [1.137.4] - 2024-04-24
### API-Changes
- [**breaking**] Remove `Stream` implementation for `EventEmitter`.
- Experimental Webxdc Integration API, Maps Integration ([#5461](https://github.com/deltachat/deltachat-core-rust/pull/5461)).
### Features / Changes
- Add progressive backoff for failing IMAP connection attempts ([#5443](https://github.com/deltachat/deltachat-core-rust/pull/5443)).
- Replace event channel with broadcast channel.
- Mark contact request messages as seen on IMAP.
### Fixes
- Convert images to RGB8 (without alpha) before encoding into JPEG to fix sending of large RGBA images.
- Don't set `is_bot` for webxdc status updates ([#5445](https://github.com/deltachat/deltachat-core-rust/pull/5445)).
- Do not fail if Autocrypt Setup Message has no encryption preference to fix key transfer from K-9 Mail to Delta Chat.
- Use only CRLF in Autocrypt Setup Message.
- python: Use cached message object if `dc_get_msg()` returns `NULL`.
- python: `Message::is_outgoing`: Don't reload message from db.
- python: `_map_ffi_event`: Always check if `get_message_by_id()` returned None.
- node: Undefine `NAPI_EXPERIMENTAL` to fix build with new clang.
### Build system
- nix: Add `imap-tools` as `deltachat-rpc-client` dependency.
- nix: Add `./deltachat-contact-tools` to sources.
- nix: Update nix flake.
- deps: Update rustls to 0.21.11.
### Documentation
- Update references to SecureJoin protocols.
- Fix broken references in documentation comments.
### Refactor
- imap: remove `RwLock` from `ratelimit`.
- deltachat-ffi: Remove unused `ResultNullableExt`.
- Remove duplicate clippy exceptions.
- Group `use` at the top of the test modules.
## [1.137.3] - 2024-04-16
### API-Changes
- [**breaking**] Remove reactions ffi; all implementations use jsonrpc.
- Don't load trashed messages with `Message::load_from_db`.
- Add `ChatListChanged` and `ChatListItemChanged` events ([#4476](https://github.com/deltachat/deltachat-core-rust/pull/4476)).
- deltachat-rpc-client: Add `check_qr` and `set_config_from_qr` APIs.
- deltachat-rpc-client: Add `Account.create_chat()`.
- deltachat-rpc-client: Add `Message.wait_until_delivered()`.
- deltachat-rpc-client: Add `Chat.send_file()`.
- deltachat-rpc-client: Add `Account.wait_for_reactions_changed()`.
- deltachat-rpc-client: Return Message from `Message.send_reaction()`.
- deltachat-rpc-client: Add `Account.bring_online()`.
- deltachat-rpc-client: Add `ACFactory.get_accepted_chat()`.
### Features / Changes
- Port `direct_imap.py` into deltachat-rpc-client.
### Fixes
- Do not emit `MSGS_CHANGED` event for outgoing hidden messages.
- `Message::get_summary()` must not return reaction summary.
- Fix emitting `ContactsChanged` events on "recently seen" status change ([#5377](https://github.com/deltachat/deltachat-core-rust/pull/5377)).
- deltachat-jsonrpc: block in `inner_get_backup_qr`.
- Add tolerance to `MemberListTimestamp` ([#5366](https://github.com/deltachat/deltachat-core-rust/pull/5366)).
- Keep webxdc instance for `delete_device_after` period after a status update ([#5365](https://github.com/deltachat/deltachat-core-rust/pull/5365)).
- Don't try to do `fetch_move_delete()` if Trash is needed but not yet configured.
- Assign messages to chats based on not fully downloaded references.
- Do not create ad-hoc groups from partial downloads.
- deltachat-rpc-client: construct Thread with `target` keyword argument.
- Format error context in `Message::load_from_db`.
### Build system
- cmake: adapt target install path if env var `CARGO_BUILD_TARGET` is set.
- nix: Use stable Rust in flake.nix devshell.
### CI
- Use cargo-nextest instead of cargo-test.
- Run doc tests with cargo test --workspace --doc ([#5459](https://github.com/deltachat/deltachat-core-rust/pull/5459)).
- Typos in CI files ([#5453](https://github.com/deltachat/deltachat-core-rust/pull/5453)).
### Documentation
- Add <https://deps.rs> badge.
- Add 'Ubuntu Touch' to the list of 'frontend projects'
### Refactor
- Do not ignore `Contact::get_by_id` errors in `get_encrinfo`.
- deltachat-rpc-client: Use `list`, `set` and `tuple` instead of `typing`.
- Use `clone_from()` ([#5451](https://github.com/deltachat/deltachat-core-rust/pull/5451)).
- Do not check for `is_trash()` in `get_last_reaction_if_newer_than()`.
- Split off functional contact tools into its own crate ([#5444](https://github.com/deltachat/deltachat-core-rust/pull/5444))
- Fix nightly clippy warnings.
### Tests
- Test withdrawing group join QR codes.
- `display_chat()`: Don't add day markers.
- Move reaction tests to JSON-RPC.
- node: Increase 'static tests' timeout to 5 minutes.
## [1.137.2] - 2024-04-05
### API-Changes
@@ -3872,3 +4355,19 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.137.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.6...v1.137.0
[1.137.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.0...v1.137.1
[1.137.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.1...v1.137.2
[1.137.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.2...v1.137.3
[1.137.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.3...v1.137.4
[1.138.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.137.4...v1.138.0
[1.138.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.0...v1.138.1
[1.138.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.1...v1.138.2
[1.138.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.2...v1.138.3
[1.138.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.3...v1.138.4
[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5
[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0
[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1
[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0

View File

@@ -12,6 +12,12 @@ else()
set(DYNAMIC_EXT "dll")
endif()
if(DEFINED ENV{CARGO_BUILD_TARGET})
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
else()
set(ARCH_DIR "./")
endif()
add_custom_command(
OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
@@ -35,6 +41,6 @@ add_custom_target(
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

2194
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.137.2"
version = "1.140.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -34,19 +34,21 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "2.0.0"
anyhow = { workspace = true }
async-broadcast = "0.7.0"
async-channel = "2.2.1"
async-imap = { version = "0.9.7", 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.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = "0.21"
brotli = { version = "4", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
base64 = "0.22"
brotli = { version = "6", default-features=false, features = ["std"] }
chrono = { workspace = true }
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"
@@ -58,41 +60,42 @@ hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh = { version = "0.4.2", default-features = false }
kamadak-exif = "0.5"
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = "0.17.0"
iroh-gossip = { version = "0.17.0", features = ["net"] }
quinn = "0.10.0"
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
mailparse = "0.14"
mailparse = "0.15"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
num-traits = "0.2"
once_cell = "1.18.0"
once_cell = { workspace = true }
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.11", default-features = false }
pin-project = "1"
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.31"
quoted_printable = "0.5"
rand = "0.8"
regex = "1.10"
reqwest = { version = "0.12.2", features = ["json"] }
rusqlite = { version = "0.31", features = ["sqlcipher"] }
regex = { workspace = true }
reqwest = { version = "0.11.27", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = "0.5"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.1"
thiserror = "1"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
@@ -101,25 +104,16 @@ toml = "0.8"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
[dev-dependencies]
ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
futures-lite = "2.3.0"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.9.0"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
[workspace]
@@ -132,6 +126,7 @@ members = [
"deltachat-repl",
"deltachat-time",
"format-flowed",
"deltachat-contact-tools",
]
[[bench]]
@@ -162,6 +157,13 @@ harness = false
name = "send_events"
harness = false
[workspace.dependencies]
anyhow = "1"
once_cell = "1.18.0"
regex = "1.10"
rusqlite = "0.31"
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
[features]
default = ["vendored"]
internals = []

View File

@@ -6,6 +6,9 @@
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
</a>
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
</a>
</p>
<p align="center">
@@ -192,6 +195,7 @@ or its language bindings:
- [Desktop](https://github.com/deltachat/deltachat-desktop)
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
- several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

View File

@@ -4,21 +4,18 @@ For example, to release version 1.116.0 of the core, do the following steps.
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
2. Run `npm run build:core:constants` in the root of the repository
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
4. add a link to compare previous with current version to the end of CHANGELOG.md:
3. add a link to compare previous with current version to the end of CHANGELOG.md:
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
6. Commit the changes as `chore(release): prepare for 1.116.0`.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
7. Tag the release: `git tag -a v1.116.0`.
6. Tag the release: `git tag -a v1.116.0`.
8. Push the release tag: `git push origin v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact;
use deltachat::context::Context;

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;

View File

@@ -0,0 +1,18 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
chrono = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -0,0 +1,671 @@
//! Contact-related tools, like parsing vcards and sanitizing name and address
#![forbid(unsafe_code)]
#![warn(
unused,
clippy::correctness,
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
clippy::unused_async,
clippy::explicit_iter_loop,
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string,
clippy::bool_to_int_with_if
)]
use std::fmt;
use std::ops::Deref;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use chrono::{DateTime, NaiveDateTime};
use once_cell::sync::Lazy;
use regex::Regex;
// TODOs to clean up:
// - Check if sanitizing is done correctly everywhere
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:{addr}\n\
FN:{display_name}\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\n");
}
res += "END:VCARD\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
let remainder = remove_prefix(s, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// TODO this doesn't handle the case where there are quotes around a colon
let (params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
Some(value)
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for line in lines.by_ref() {
if let Some(email) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some(name) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
{
key.get_or_insert(k);
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
{
photo.get_or_insert(p);
} else if let Some(rev) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
break;
}
}
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
}
contacts
}
/// Valid contact address.
#[derive(Debug, Clone)]
pub struct ContactAddress(String);
impl Deref for ContactAddress {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for ContactAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContactAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ContactAddress {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr.to_string()))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Make the name and address
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
} else {
strip_rtlo_characters(name)
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(
strip_rtlo_characters(&normalize_name(name)),
addr.to_string(),
)
};
let mut name = normalize_name(&name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();
}
(name, addr)
}
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
/// - Trims the resulting string
///
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
pub fn normalize_name(full_name: &str) -> String {
let full_name = full_name.trim();
if full_name.is_empty() {
return full_name.into();
}
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().to_string()),
_ => full_name.to_string(),
}
}
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
/// This method strips all occurrences of the RTLO Unicode character.
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
pub fn strip_rtlo_characters(input_str: &str) -> String {
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
}
/// Returns false if addr is an invalid address, otherwise true.
pub fn may_be_valid_addr(addr: &str) -> bool {
let res = EmailAddress::new(addr);
res.is_ok()
}
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(&norm).to_string()
} else {
norm
}
}
/// Compares two email addresses, normalizing them beforehand.
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
norm1 == norm2
}
///
/// Represents an email address, right now just the `name@domain` portion.
///
/// # Example
///
/// ```
/// use deltachat_contact_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
/// assert_eq!(&email.local, "someone");
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
/// Local part of the email address.
pub local: String,
/// Email address domain.
pub domain: String,
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
}
}
impl EmailAddress {
/// Performs a dead-simple parse of an email address.
pub fn new(input: &str) -> Result<EmailAddress> {
if input.is_empty() {
bail!("empty string is not valid");
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
if input
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
bail!("empty string is not valid for local part in {:?}", input);
}
if domain.is_empty() {
bail!("missing domain after '@' in {:?}", input);
}
if domain.ends_with('.') {
bail!("Domain {domain:?} should not contain the dot in the end");
}
Ok(EmailAddress {
local: (*local).to_string(),
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
}
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:alice@example.org\n\
FN:Alice Wonderland\n\
KEY:data:application/pgp-keys;base64,[base64-data]\n\
PHOTO:data:image/jpeg;base64,image in Base64\n\
REV:20240418T184242Z\n\
END:VCARD\n",
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:bob@example.com\n\
FN:bob@example.com\n\
REV:19700101T000000Z\n\
END:VCARD\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
let contact_address = ContactAddress::new(alice_addr)?;
assert_eq!(contact_address.as_ref(), alice_addr);
let invalid_addr = "<> foobar";
assert!(ContactAddress::new(invalid_addr).is_err());
Ok(())
}
#[test]
fn test_emailaddress_parse() {
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!(
EmailAddress::new("user@domain.tld").unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(
EmailAddress::new("user@localhost").unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert!(EmailAddress::new("tt.dd@uu").is_ok());
assert!(EmailAddress::new("u@d").is_ok());
assert!(EmailAddress::new("u@d.").is_err());
assert!(EmailAddress::new("u@d.t").is_ok());
assert_eq!(
EmailAddress::new("u@d.tt").unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert!(EmailAddress::new("u@tt").is_ok());
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing.
// This one is Android-like.
let vcard0 = "BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
";
// This one is DOS-like.
let vcard1 = vcard0.replace('\n', "\r\n");
for vcard in [vcard0, vcard1.as_str()] {
let contacts = parse_vcard(vcard);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.137.2"
version = "1.140.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -17,10 +17,10 @@ crate-type = ["cdylib", "staticlib"]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = { version = "1", default-features = false }
human-panic = { version = "2", default-features = false }
num-traits = "0.2"
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread"] }
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
anyhow = "1"
thiserror = "1"
rand = "0.8"

View File

@@ -17,7 +17,6 @@ typedef struct _dc_array dc_array_t;
typedef struct _dc_chatlist dc_chatlist_t;
typedef struct _dc_chat dc_chat_t;
typedef struct _dc_msg dc_msg_t;
typedef struct _dc_reactions dc_reactions_t;
typedef struct _dc_contact dc_contact_t;
typedef struct _dc_lot dc_lot_t;
typedef struct _dc_provider dc_provider_t;
@@ -363,8 +362,12 @@ uint32_t dc_get_id (dc_context_t* context);
* Must be freed using dc_event_emitter_unref() after usage.
*
* Note: Use only one event emitter per context.
* Having more than one event emitter running at the same time on the same context
* will result in events being randomly delivered to one of the emitters.
* The result of having multiple event emitters is unspecified.
* Currently events are broadcasted to all existing event emitters,
* but previous versions delivered events to only one event emitter
* and this behavior may change again in the future.
* Events emitted before creation of event emitter
* may or may not be available to event emitter.
*/
dc_event_emitter_t* dc_get_event_emitter(dc_context_t* context);
@@ -514,11 +517,15 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=Nothing else happens when the key changes.
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
* The prefix should be followed by the system and maybe subsystem,
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
* however, are not handled by the core otherwise.
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
* 1 = WebXDC realtime API is enabled.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -1117,36 +1124,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Send a reaction to message.
*
* Reaction is a string of emojis separated by spaces. Reaction to a
* single message can be sent multiple times. The last reaction
* received overrides all previously received reactions. It is
* possible to remove all reactions by sending an empty string.
*
* @deprecated 2023-11-27, use jsonrpc method `send_reaction` instead
* @memberof dc_context_t
* @param context The context object.
* @param msg_id ID of the message you react to.
* @param reaction A string consisting of emojis separated by spaces.
* @return The ID of the message sent out or 0 for errors.
*/
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
/**
* Get a structure with reactions to the message.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The message ID to get reactions for.
* @return A structure with all reactions to the message.
*/
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -1209,6 +1186,65 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
*/
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
/**
* Set Webxdc file as integration.
* see dc_init_webxdc_integration() for more details about Webxdc integrations.
*
* @warning This is an experimental API which may change in the future
*
* @memberof dc_context_t
* @param context The context object.
* @param file The .xdc file to use as Webxdc integration.
*/
void dc_set_webxdc_integration (dc_context_t* context, const char* file);
/**
* Init a Webxdc integration.
*
* A Webxdc integration is
* a Webxdc showing a map, getting locations via setUpdateListener(), setting POIs via sendUpdate();
* core takes eg. care of feeding locations to the Webxdc or sending the data out.
*
* @warning This is an experimental API, esp. support of integration types (eg. image editor, tools) is left out for simplicity
*
* Currently, Webxdc integrations are .xdc files shipped together with the main app.
* Before dc_init_webxdc_integration() can be called,
* UI has to call dc_set_webxdc_integration() to define a .xdc file to be used as integration.
*
* dc_init_webxdc_integration() returns a Webxdc message ID that
* UI can open and use mostly as usual.
*
* Concrete behaviour and status updates depend on the integration, driven by UI needs.
*
* There is no need to de-initialize the integration,
* however, unless documented otherwise,
* the integration is valid only as long as not re-initialized
* In other words, UI must not have a Webxdc with the same integration open twice.
*
* Example:
*
* ~~~
* // Define a .xdc file to be used as maps integration
* dc_set_webxdc_integration(context, path_to_maps_xdc);
*
* // Integrate the map to a chat, the map will show locations for this chat then:
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, any_chat_id);
*
* // Or use the Webxdc as a global map, showing locations of all chats:
* uint32_t webxdc_instance = dc_init_webxdc_integration(context, 0);
* ~~~
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to get the integration for.
* @return ID of the message that refers to the Webxdc instance.
* UI can open a Webxdc as usual with this instance.
*/
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
/**
* Save a draft for a chat in the database.
*
@@ -2576,7 +2612,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://securejoin.readthedocs.io/en/latest/new.html
* See https://securejoin.delta.chat/
* for details about both protocols.
* @return The text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
@@ -2612,8 +2648,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
*
* See https://securejoin.readthedocs.io/en/latest/new.html
* for details about both protocols.
* See https://securejoin.delta.chat/ for details about both protocols.
*
* @memberof dc_context_t
* @param context The context object.
@@ -4074,6 +4109,19 @@ char* dc_msg_get_subject (const dc_msg_t* msg);
char* dc_msg_get_file (const dc_msg_t* msg);
/**
* Save file copy at the user-provided path.
*
* Fails if file already exists at the provided path.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param path Destination file path with filename and extension.
* @return 0 on failure, 1 on success.
*/
int dc_msg_save_file (const dc_msg_t* msg, const char* path);
/**
* Get an original attachment filename, with extension but without the path. To get the full path,
* use dc_msg_get_file().
@@ -4138,7 +4186,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
* true if the Webxdc should get full internet access, including Webrtc.
* currently, this is only true for encrypted Webxdc's in the self chat
* that have requested internet access in the manifest.
* this is useful for development and maybe for internal integrations at some point.
*
* @memberof dc_msg_t
* @param msg The webxdc instance.
@@ -4347,9 +4394,9 @@ int dc_msg_has_deviating_timestamp(const dc_msg_t* msg);
/**
* Check if a message has a location bound to it.
* These messages are also returned by dc_get_locations()
* and the UI may decide to display a special icon beside such messages,
* Check if a message has a POI location bound to it.
* These locations are also returned by dc_get_locations()
* The UI may decide to display a special icon beside such messages.
*
* @memberof dc_msg_t
* @param msg The message object.
@@ -5320,52 +5367,6 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* @class dc_reactions_t
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
*
* An object representing all reactions for a single message.
*/
/**
* Returns array of contacts which reacted to the given message.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
*/
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
/**
* Returns a string containing space-separated reactions of a single contact.
*
* @deprecated 2023-11-27, use jsonrpc method `get_message_reactions` instead
* @memberof dc_reactions_t
* @param reactions The object containing message reactions.
* @param contact_id ID of the contact.
* @return Space-separated list of emoji sequences, which could be empty.
* Returned string should not be modified and should be freed
* with dc_str_unref() after usage.
*/
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
/**
* Frees an object containing message reactions.
*
* Reactions objects are created by dc_get_msg_reactions().
*
* @deprecated 2023-11-27
* @memberof dc_reactions_t
* @param reactions The object to free.
* If NULL is given, nothing is done.
*/
void dc_reactions_unref (dc_reactions_t* reactions);
/**
* @defgroup DC_MSG DC_MSG
*
@@ -5482,6 +5483,11 @@ void dc_reactions_unref (dc_reactions_t* reactions);
*/
#define DC_MSG_WEBXDC 80
/**
* Message containing shared contacts represented as a vCard (virtual contact file)
* with email addresses and possibly other fields.
*/
#define DC_MSG_VCARD 90
/**
* @}
@@ -6281,6 +6287,18 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
* Data received over an ephemeral peer channel.
*
* @param data1 (int) msg_id
* @param data2 (int) + (char*) binary data.
* length is returned as integer with dc_event_get_data2_int()
* and binary data is returned as dc_event_get_data2_str().
* Binary data must be passed to dc_str_unref() afterwards.
*/
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
/**
* Tells that the Background fetch was completed (or timed out).
*
@@ -6290,7 +6308,32 @@ void dc_event_unref(dc_event_t* event);
* This event is only emitted by the account manager
*/
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
#define DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE 2200
/**
* Inform that set of chats or the order of the chats in the chatlist has changed.
*
* Sometimes this is emitted together with `DC_EVENT_CHATLIST_ITEM_CHANGED`.
*/
#define DC_EVENT_CHATLIST_CHANGED 2300
/**
* Inform that all or a single chat list item changed and needs to be rerendered
* If `chat_id` is set to 0, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
*
* @param data1 (int) chat_id chat id of chatlist item to be rerendered, if chat_id = 0 all (cached & visible) items need to be rerendered
*/
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
/**
* Inform that some events have been skipped due to event channel overflow.
*
* @param data1 (int) number of events that have been skipped
*/
#define DC_EVENT_CHANNEL_OVERFLOW 2400
/**
* @}
@@ -7313,6 +7356,19 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/// "Establishing guaranteed end-to-end encryption, please wait…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "Contact"
#define DC_STR_CONTACT 200
/**
* @}
*/

View File

@@ -1,10 +1,9 @@
#![recursion_limit = "256"]
#![warn(unused, clippy::all)]
#![allow(
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
non_upper_case_globals,
non_camel_case_types,
clippy::missing_safety_doc,
clippy::expect_fun_call
)]
@@ -32,7 +31,6 @@ use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
@@ -68,8 +66,6 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
/// Struct representing the deltachat context.
pub type dc_context_t = Context;
pub type dc_reactions_t = Reactions;
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
fn block_on<T>(fut: T) -> T::Output
@@ -566,7 +562,11 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::ConfigSynced { .. } => 2111,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::WebxdcRealtimeData { .. } => 2150,
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
EventType::EventChannelOverflow { .. } => 2400,
}
}
@@ -596,6 +596,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -618,8 +619,13 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
contact_id.to_u32() as libc::c_int
}
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
EventType::EventChannelOverflow { n } => *n as libc::c_int,
}
}
@@ -656,8 +662,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ConfigSynced { .. } => 0,
EventType::ChatModified(_) => 0,
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::ConfigSynced { .. }
| EventType::ChatModified(_)
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingMsg { msg_id, .. }
@@ -672,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
status_update_serial,
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
}
}
@@ -720,7 +730,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::WebxdcInstanceDeleted { .. }
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. } => ptr::null_mut(),
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -736,6 +749,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::WebxdcRealtimeData { data, .. } => {
let ptr = libc::malloc(data.len());
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
ptr as *mut libc::c_char
}
}
}
@@ -1015,49 +1033,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_reaction(
context: *mut dc_context_t,
msg_id: u32,
reaction: *const libc::c_char,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_reaction()");
return 0;
}
let ctx = &*context;
block_on(async move {
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send reaction")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_msg_reactions(
context: *mut dc_context_t,
msg_id: u32,
) -> *mut dc_reactions_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_msg_reactions()");
return ptr::null_mut();
}
let ctx = &*context;
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
.context("failed dc_get_msg_reactions() call")
.log_err(ctx)
{
reactions
} else {
return ptr::null_mut();
};
Box::into_raw(Box::new(reactions))
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -1101,6 +1076,43 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_webxdc_integration(
context: *mut dc_context_t,
file: *const libc::c_char,
) {
if context.is_null() || file.is_null() {
eprintln!("ignoring careless call to dc_set_webxdc_integration()");
return;
}
let ctx = &*context;
block_on(ctx.set_webxdc_integration(&to_string_lossy(file)))
.log_err(ctx)
.unwrap_or_default();
}
#[no_mangle]
pub unsafe extern "C" fn dc_init_webxdc_integration(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_init_webxdc_integration()");
return 0;
}
let ctx = &*context;
let chat_id = if chat_id == 0 {
None
} else {
Some(ChatId::new(chat_id))
};
block_on(ctx.init_webxdc_integration(chat_id))
.log_err(ctx)
.map(|msg_id| msg_id.map(|id| id.to_u32()).unwrap_or_default())
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -2046,7 +2058,7 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
);
message::Message::default()
} else {
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
return ptr::null_mut();
}
}
@@ -3369,6 +3381,34 @@ pub unsafe extern "C" fn dc_msg_get_file(msg: *mut dc_msg_t) -> *mut libc::c_cha
.unwrap_or_else(|| "".strdup())
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_save_file(
msg: *mut dc_msg_t,
path: *const libc::c_char,
) -> libc::c_int {
if msg.is_null() || path.is_null() {
eprintln!("ignoring careless call to dc_msg_save_file()");
return 0;
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
let path = to_string_lossy(path);
let r = block_on(
ffi_msg
.message
.save_file(ctx, &std::path::PathBuf::from(path)),
);
match r {
Ok(()) => 1,
Err(_) => {
r.context("Failed to save file from message")
.log_err(ctx)
.unwrap_or_default();
0
}
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
@@ -4244,45 +4284,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
lot.get_timestamp()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_contacts(
reactions: *mut dc_reactions_t,
) -> *mut dc_array::dc_array_t {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
return ptr::null_mut();
}
let reactions = &*reactions;
let array: dc_array_t = reactions.contacts().into();
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
reactions: *mut dc_reactions_t,
contact_id: u32,
) -> *mut libc::c_char {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
return ptr::null_mut();
}
let reactions = &*reactions;
reactions.get(ContactId::new(contact_id)).as_str().strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
if reactions.is_null() {
eprintln!("ignoring careless call to dc_reactions_unref()");
return;
}
drop(Box::from_raw(reactions));
}
#[no_mangle]
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
@@ -4483,19 +4484,6 @@ where
}
}
trait ResultNullableExt<T> {
fn into_raw(self) -> *mut T;
}
impl<T, E> ResultNullableExt<T> for Result<T, E> {
fn into_raw(self) -> *mut T {
match self {
Ok(t) => Box::into_raw(Box::new(t)),
Err(_) => ptr::null_mut(),
}
}
}
fn convert_and_prune_message_ids(msg_ids: *const u32, msg_cnt: libc::c_int) -> Vec<MsgId> {
let ids = unsafe { std::slice::from_raw_parts(msg_ids, msg_cnt as usize) };
let msg_ids: Vec<MsgId> = ids
@@ -4975,7 +4963,9 @@ mod jsonrpc {
}
let account_manager = &*account_manager;
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let cmd_api = block_on(deltachat_jsonrpc::api::CommandApi::from_arc(
account_manager.inner.clone(),
));
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle, cmd_api);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.137.2"
version = "1.140.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -15,12 +15,13 @@ required-features = ["webserver"]
[dependencies]
anyhow = "1"
deltachat = { path = ".." }
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
num-traits = "0.2"
schemars = "0.8.13"
schemars = "0.8.19"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.10.1"
log = "0.4"
async-channel = { version = "2.0.0" }
async-channel = { version = "2.2.1" }
futures = { version = "0.3.30" }
serde_json = "1"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
@@ -28,11 +29,11 @@ typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.37.0" }
sanitize-filename = "0.5"
walkdir = "2.5.0"
base64 = "0.21"
base64 = "0.22"
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
env_logger = { version = "0.11.3", optional = true }
[dev-dependencies]
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }

View File

@@ -1,5 +1,8 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::str;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
@@ -15,12 +18,14 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
};
use deltachat::provider::get_provider_info;
use deltachat::qr::{self, Qr};
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
@@ -28,6 +33,8 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -39,7 +46,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::chat::FullChat;
use types::contact::ContactObject;
use types::contact::{ContactObject, VcardContact};
use types::events::Event;
use types::http::HttpResponse;
use types::message::{MessageData, MessageObject, MessageReadReceipt};
@@ -62,14 +69,14 @@ use crate::api::types::qr::QrObject;
struct AccountState {
/// The Qr code for current [`CommandApi::provide_backup`] call.
///
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
/// `Pending` or `Ready`, otherwise `NoProvider`.
backup_provider_qr: watch::Sender<ProviderQr>,
/// If there is currently is a call to [`CommandApi::provide_backup`] this will be
/// `Some`, otherwise `None`.
backup_provider_qr: watch::Sender<Option<Qr>>,
}
impl Default for AccountState {
fn default() -> Self {
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
let tx = watch::Sender::new(None);
Self {
backup_provider_qr: tx,
}
@@ -80,21 +87,30 @@ impl Default for AccountState {
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
/// Receiver side of the event channel.
///
/// Events from it can be received by calling `get_next_event` method.
event_emitter: Arc<EventEmitter>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
let event_emitter = Arc::new(accounts.get_event_emitter());
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
#[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
pub async fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
let event_emitter = Arc::new(accounts.read().await.get_event_emitter());
CommandApi {
accounts,
event_emitter,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
@@ -123,21 +139,13 @@ impl CommandApi {
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
.await;
let val: ProviderQr = receiver.borrow_and_update().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => loop {
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
let val: ProviderQr = receiver.borrow().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => continue,
ProviderQr::Ready(qr) => break Ok(qr),
};
},
ProviderQr::Ready(qr) => Ok(qr),
loop {
if let Some(qr) = receiver.borrow_and_update().clone() {
return Ok(qr);
}
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
}
}
}
@@ -165,8 +173,7 @@ impl CommandApi {
/// Get the next event.
async fn get_next_event(&self) -> Result<Event> {
let event_emitter = self.accounts.read().await.get_event_emitter();
event_emitter
self.event_emitter
.recv()
.await
.map(|event| event.into())
@@ -181,6 +188,16 @@ impl CommandApi {
self.accounts.write().await.add_account().await
}
/// Imports/migrated an existing account from a database path into this account manager.
/// Returns the ID of new account.
async fn migrate_account(&self, path_to_db: String) -> Result<u32> {
self.accounts
.write()
.await
.migrate_account(std::path::PathBuf::from(path_to_db))
.await
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts
.write()
@@ -325,6 +342,11 @@ impl CommandApi {
ctx.get_info().await
}
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
async fn draft_self_report(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.draft_self_report().await?.to_u32())
@@ -697,8 +719,7 @@ impl CommandApi {
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// return format: `[code, svg]`
async fn get_chat_securejoin_qr_code_svg(
@@ -726,8 +747,7 @@ impl CommandApi {
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// for details about both protocols.
/// See https://securejoin.delta.chat/ for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given
/// to `check_qr()`.
@@ -1425,6 +1445,37 @@ impl CommandApi {
Ok(contact_id.map(|id| id.to_u32()))
}
/// Parses a vCard file located at the given path. Returns contacts in their original order.
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
let vcard = fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat_contact_tools::parse_vcard(vcard)
.into_iter()
.map(|c| c.into())
.collect())
}
/// Imports contacts from a vCard file located at the given path.
///
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let vcard = tokio::fs::read(Path::new(&path)).await?;
let vcard = str::from_utf8(&vcard)?;
Ok(deltachat::contact::import_vcard(&ctx, vcard)
.await?
.into_iter()
.map(|c| c.to_u32())
.collect())
}
/// Returns a vCard containing contacts with the given ids.
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
deltachat::contact::make_vcard(&ctx, &contacts).await
}
// ---------------------------------------------
// chat
// ---------------------------------------------
@@ -1569,20 +1620,21 @@ impl CommandApi {
/// Returns once a remote device has retrieved the backup, or is cancelled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(ProviderQr::Pending);
})
.await;
let provider = imex::BackupProvider::prepare(&ctx).await?;
self.with_state(account_id, |state| {
state
.backup_provider_qr
.send_replace(ProviderQr::Ready(provider.qr()));
state.backup_provider_qr.send_replace(Some(provider.qr()));
})
.await;
provider.await
let res = provider.await;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(None);
})
.await;
res
}
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
@@ -1590,11 +1642,17 @@ impl CommandApi {
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = self.inner_get_backup_qr(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
qr::format_backup(&qr)
}
@@ -1603,14 +1661,20 @@ impl CommandApi {
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = self.inner_get_backup_qr(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(10),
self.inner_get_backup_qr(account_id),
)
.await
.context("Backup provider did not start in time")?
.context("Failed to get backup QR code")?;
generate_backup_qr(&ctx, &qr).await
}
@@ -1620,6 +1684,9 @@ impl CommandApi {
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
///
/// Do not forget to call start_io on the account after a successful import,
/// otherwise it will not connect to the email server.
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_text).await?;
@@ -1713,6 +1780,37 @@ impl CommandApi {
.await
}
async fn send_webxdc_realtime_data(
&self,
account_id: u32,
instance_msg_id: u32,
data: Vec<u8>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
send_webxdc_realtime_data(&ctx, MsgId::new(instance_msg_id), data).await
}
async fn send_webxdc_realtime_advertisement(
&self,
account_id: u32,
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
if let Some(fut) = fut {
tokio::spawn(async move {
fut.await.ok();
info!(ctx, "send_webxdc_realtime_advertisement done")
});
}
Ok(())
}
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
}
async fn get_webxdc_status_updates(
&self,
account_id: u32,
@@ -1754,6 +1852,29 @@ impl CommandApi {
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Sets Webxdc file as integration.
/// `file` is the .xdc to use as Webxdc integration.
async fn set_webxdc_integration(&self, account_id: u32, file_path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.set_webxdc_integration(&file_path).await
}
/// Returns Webxdc instance used for optional integrations.
/// UI can open the Webxdc as usual.
/// Returns `None` if there is no integration; the caller can add one using `set_webxdc_integration` then.
/// `integrate_for` is the chat to get the integration for.
async fn init_webxdc_integration(
&self,
account_id: u32,
chat_id: Option<u32>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx
.init_webxdc_integration(chat_id.map(ChatId::new))
.await?
.map(|msg_id| msg_id.to_u32()))
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
@@ -1862,6 +1983,15 @@ impl CommandApi {
Ok(can_send)
}
/// Saves a file copy at the user-provided path.
///
/// Fails if file already exists at the provided path.
async fn save_msg_file(&self, account_id: u32, msg_id: u32, path: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
message.save_file(&ctx, Path::new(&path)).await
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1934,19 +2064,21 @@ impl CommandApi {
);
let destination_path = account_folder.join("stickers").join(collection);
fs::create_dir_all(&destination_path).await?;
let file = message.get_file(&ctx).context("no file")?;
fs::copy(
&file,
destination_path.join(format!(
"{}.{}",
msg_id,
file.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
let file = message.get_filename().context("no file?")?;
message
.save_file(
&ctx,
&destination_path.join(format!(
"{}.{}",
msg_id,
Path::new(&file)
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
)),
)
.await?;
Ok(())
}
@@ -2141,15 +2273,3 @@ async fn get_config(
.await
}
}
/// Whether a QR code for a BackupProvider is currently available.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum ProviderQr {
/// There is no provider, asking for a QR is an error.
NoProvider,
/// There is a provider, the QR code is pending.
Pending,
/// There is a provider and QR code.
Ready(Qr),
}

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use deltachat::color;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -87,3 +88,35 @@ impl ContactObject {
})
}
}
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VcardContact {
/// Email address.
addr: String,
/// The contact's name, or the email address if no name was given.
display_name: String,
/// Public PGP key in Base64.
key: Option<String>,
/// Profile image in Base64.
profile_image: Option<String>,
/// Contact color as hex string.
color: String,
/// Last update timestamp.
timestamp: Option<i64>,
}
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,
key: vc.key,
profile_image: vc.profile_image,
color: color_int_to_hex_string(color),
timestamp: vc.timestamp.ok(),
}
}
}

View File

@@ -240,6 +240,10 @@ pub enum EventType {
status_update_serial: u32,
},
/// Data received over an ephemeral peer channel.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted { msg_id: u32 },
@@ -250,6 +254,18 @@ pub enum EventType {
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
ChatlistChanged,
/// Inform that a single chat list item changed and needs to be rerendered.
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
/// Inform than some events have been skipped due to event channel overflow.
EventChannelOverflow { n: u64 },
}
impl From<CoreEventType> for EventType {
@@ -353,10 +369,19 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(),
},
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
msg_id: msg_id.to_u32(),
data,
},
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
},
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
chat_id: chat_id.map(|id| id.to_u32()),
},
CoreEventType::ChatlistChanged => ChatlistChanged,
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::api::VcardContact;
use anyhow::{Context as _, Result};
use deltachat::chat::Chat;
use deltachat::chat::ChatItem;
@@ -35,6 +36,10 @@ pub struct MessageObject {
parent_id: Option<u32>,
text: String,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -83,6 +88,8 @@ pub struct MessageObject {
download_state: DownloadState,
reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
@@ -169,6 +176,13 @@ impl MessageObject {
Some(reactions.into())
};
let vcard_contacts: Vec<VcardContact> = message
.vcard_contacts(context)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(MessageObject {
id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(),
@@ -228,6 +242,8 @@ impl MessageObject {
download_state,
reactions,
vcard_contact: vcard_contacts.first().cloned(),
})
}
}
@@ -270,6 +286,11 @@ pub enum MessageViewtype {
/// Message is an webxdc instance.
Webxdc,
/// Message containing shared contacts represented as a vCard (virtual contact file)
/// with email addresses and possibly other fields.
/// Use `parse_vcard()` to retrieve them.
Vcard,
}
impl From<Viewtype> for MessageViewtype {
@@ -286,6 +307,7 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
}
}
}
@@ -304,6 +326,7 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,
}
}
}
@@ -342,6 +365,14 @@ pub enum SystemMessageType {
LocationOnly,
InvalidUnencryptedMail,
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
/// to complete.
SecurejoinWait,
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
/// send messages.
SecurejoinWaitTimeout,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
@@ -360,6 +391,9 @@ pub enum SystemMessageType {
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage,
/// This message contains a users iroh node address.
IrohNodeAddr,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -382,6 +416,9 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
}
}
}
@@ -631,7 +668,7 @@ impl MessageInfo {
#[derive(
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
)]
#[serde(rename_all = "camelCase", tag = "variant")]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum EphemeralTimer {
/// Timer is disabled.
Disabled,

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
pub mod api;
pub use yerpc;

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
use std::net::SocketAddr;
use std::path::PathBuf;

View File

@@ -25,12 +25,17 @@
"exports": {
".": {
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs"
"require": "./dist/deltachat.cjs",
"types": "./dist/deltachat.d.ts"
}
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
@@ -53,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.137.2"
"version": "1.140.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.137.2"
version = "1.140.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
@@ -11,10 +11,10 @@ anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.31"
rustyline = "14"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[features]
default = ["vendored"]

View File

@@ -19,6 +19,7 @@ use deltachat::location;
use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::reaction::send_reaction;
@@ -642,6 +643,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
let msg_id = MsgId::new(arg1.parse()?);
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
if let Some(res) = res {
println!("waiting for peer channel join");
res.await?;
}
println!("joined peer channel");
}
"send-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
if arg2.is_empty() {
bail!("no message");
}
let msg_id = MsgId::new(arg1.parse()?);
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
println!("sent realtime message");
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
bail!("Argument [chat-id] is missing.");

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
//! This is a CLI program and a little testing frame. This file must not be
//! included when using Delta Chat Core as a library.
//!
@@ -31,6 +32,7 @@ use rustyline::{
};
use tokio::fs;
use tokio::runtime::Handle;
use tracing_subscriber::EnvFilter;
mod cmdline;
use self::cmdline::*;
@@ -482,9 +484,10 @@ async fn handle_cmd(
#[tokio::main]
async fn main() -> Result<(), Error> {
pretty_env_logger::formatted_timed_builder()
.parse_default_env()
.filter_module("deltachat_repl", log::LevelFilter::Info)
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
)
.init();
let args = std::env::args().collect();

View File

@@ -4,6 +4,7 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options.
"""
from deltachat_rpc_client import events, run_bot_cli
hooks = events.HookCollection()

View File

@@ -3,6 +3,7 @@
it will echo back any message that has non-empty text and also supports the /help command.
"""
import logging
import sys
from threading import Thread

View File

@@ -2,6 +2,7 @@
"""
Example echo bot without using hooks
"""
import logging
import sys

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.137.2"
version = "1.140.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -17,10 +17,15 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]
readme = "README.md"
dependencies = [
"imap-tools",
]
[tool.setuptools.package-data]
deltachat_rpc_client = [
@@ -34,7 +39,7 @@ deltachat_rpc_client = [
line-length = 120
[tool.ruff]
select = [
lint.select = [
"E", "W", # pycodestyle
"F", # Pyflakes
"N", # pep8-naming

View File

@@ -104,7 +104,11 @@ def _run_cli(
if not client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
configure_thread = Thread(
target=client.configure,
daemon=True,
kwargs={"email": args.email, "password": args.password},
)
configure_thread.start()
client.run_forever()

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
from ._utils import AttrDict, futuremethod
@@ -28,6 +30,10 @@ class Account:
"""Wait until the next event and return it."""
return AttrDict(self._rpc.wait_for_event(self.id))
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
"""Remove the account."""
self._rpc.remove_account(self.id)
@@ -76,11 +82,25 @@ class Account:
"""Get self avatar."""
return self.get_config("selfavatar")
def check_qr(self, qr):
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
def configure(self):
"""Configure an account."""
yield self._rpc.configure.future(self.id)
def bring_online(self):
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
while True:
event = self.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one.
@@ -98,6 +118,11 @@ class Account:
obj = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
def create_chat(self, account: "Account") -> Chat:
addr = account.get_config("addr")
contact = self.create_contact(addr)
return contact.create_chat()
def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID."""
return Contact(self, contact_id)
@@ -107,7 +132,7 @@ class Account:
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
def get_blocked_contacts(self) -> List[AttrDict]:
def get_blocked_contacts(self) -> list[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
@@ -132,7 +157,7 @@ class Account:
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[List[Contact], List[AttrDict]]:
) -> Union[list[Contact], list[AttrDict]]:
"""Get a filtered list of contacts.
:param query: if a string is specified, only return contacts
@@ -167,7 +192,7 @@ class Account:
no_specials: bool = False,
alldone_hint: bool = False,
snapshot: bool = False,
) -> Union[List[Chat], List[AttrDict]]:
) -> Union[list[Chat], list[AttrDict]]:
"""Return list of chats.
:param query: if a string is specified only chats matching this query are returned.
@@ -219,13 +244,13 @@ class Account:
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
See https://securejoin.delta.chat/ for protocol details.
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> Tuple[str, str]:
def get_qr_code(self) -> tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account
@@ -237,15 +262,15 @@ class Account:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
def mark_seen_messages(self, messages: List[Message]) -> None:
def mark_seen_messages(self, messages: list[Message]) -> None:
"""Mark the given set of messages as seen."""
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
def delete_messages(self, messages: List[Message]) -> None:
def delete_messages(self, messages: list[Message]) -> None:
"""Delete messages (local and remote)."""
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
def get_fresh_messages(self) -> List[Message]:
def get_fresh_messages(self) -> list[Message]:
"""Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications.
@@ -255,12 +280,12 @@ class Account:
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def get_next_messages(self) -> List[Message]:
def get_next_messages(self) -> list[Message]:
"""Return a list of next messages."""
next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_next_messages(self) -> List[Message]:
def wait_next_messages(self) -> list[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
@@ -272,6 +297,12 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event."""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
@@ -284,7 +315,13 @@ class Account:
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
def wait_for_reactions_changed(self):
while True:
event = self.wait_for_event()
if event.kind == EventType.REACTIONS_CHANGED:
return event
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.",

View File

@@ -1,6 +1,9 @@
from __future__ import annotations
import calendar
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from tempfile import NamedTemporaryFile
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
from .const import ChatVisibility, ViewType
@@ -93,7 +96,7 @@ class Chat:
"""Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> Tuple[str, str]:
def get_qr_code(self) -> tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
@@ -117,7 +120,7 @@ class Chat:
html: Optional[str] = None,
viewtype: Optional[ViewType] = None,
file: Optional[str] = None,
location: Optional[Tuple[float, float]] = None,
location: Optional[tuple[float, float]] = None,
override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None,
) -> Message:
@@ -142,6 +145,10 @@ class Chat:
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
def send_file(self, path):
"""Send a file and return the resulting Message instance."""
return self.send_message(file=path)
def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
@@ -152,7 +159,7 @@ class Chat:
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def forward_messages(self, messages: List[Message]) -> None:
def forward_messages(self, messages: list[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
@@ -184,7 +191,7 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
"""get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
@@ -219,7 +226,7 @@ class Chat:
contact_id = cnt
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
def get_contacts(self) -> List[Contact]:
def get_contacts(self) -> list[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
@@ -243,7 +250,7 @@ class Chat:
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
timestamp_to: Optional["datetime"] = None,
) -> List[AttrDict]:
) -> list[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan."""
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
@@ -251,7 +258,7 @@ class Chat:
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
contacts: dict[int, Contact] = {}
for loc in result:
location = AttrDict(loc)
location["chat"] = self
@@ -259,3 +266,11 @@ class Chat:
location["message"] = Message(self.account, location.msg_id)
locations.append(location)
return locations
def send_contact(self, contact: Contact):
"""Send contact to the chat."""
vcard = contact.make_vcard()
with NamedTemporaryFile(suffix=".vcard") as f:
f.write(vcard.encode())
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})

View File

@@ -1,14 +1,13 @@
"""Event loop implementations offering high level event handling/hooking."""
from __future__ import annotations
import logging
from typing import (
TYPE_CHECKING,
Callable,
Dict,
Iterable,
Optional,
Set,
Tuple,
Type,
Union,
)
@@ -39,16 +38,16 @@ class Client:
def __init__(
self,
account: "Account",
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None,
) -> None:
self.account = account
self.logger = logger or logging
self._hooks: Dict[type, Set[tuple]] = {}
self._hooks: dict[type, set[tuple]] = {}
self._should_process_messages = 0
self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
for hook, event in hooks:
self.add_hook(hook, event)

View File

@@ -59,6 +59,10 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
class ChatId(IntEnum):
@@ -111,6 +115,7 @@ class ViewType(str, Enum):
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
class SystemMessageType(str, Enum):

View File

@@ -60,3 +60,6 @@ class Contact:
self.account,
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
)
def make_vcard(self) -> str:
return self._rpc.make_vcard(self.account.id, [self.id])

View File

@@ -1,4 +1,6 @@
from typing import TYPE_CHECKING, Dict, List
from __future__ import annotations
from typing import TYPE_CHECKING
from ._utils import AttrDict
from .account import Account
@@ -21,7 +23,7 @@ class DeltaChat:
account_id = self.rpc.add_account()
return Account(self, account_id)
def get_all_accounts(self) -> List[Account]:
def get_all_accounts(self) -> list[Account]:
"""Return a list of all available accounts."""
account_ids = self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids]
@@ -44,6 +46,6 @@ class DeltaChat:
"""Get information about the Delta Chat core in this system."""
return AttrDict(self.rpc.get_system_info())
def set_translations(self, translations: Dict[str, str]) -> None:
def set_translations(self, translations: dict[str, str]) -> None:
"""Set stock translation strings."""
self.rpc.set_stock_strings(translations)

View File

@@ -0,0 +1,226 @@
"""
Internal Python-level IMAP handling used by the tests.
"""
from __future__ import annotations
import imaplib
import io
import pathlib
import ssl
from contextlib import contextmanager
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
from . import Account, const
FLAGS = b"FLAGS"
FETCH = b"FETCH"
ALL = "1:*"
class DirectImap:
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
def connect(self):
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
user = self.account.get_config("addr")
pw = self.account.get_config("mail_pw")
if security == const.SocketSecurity.PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.SocketSecurity.STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
def shutdown(self):
try:
self.conn.logout()
except (OSError, imaplib.IMAP4.abort):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.folder.create(foldername)
except errors.MailboxFolderCreateError as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername: str) -> tuple:
assert not self._idling
return self.conn.folder.set(foldername)
def select_config_folder(self, config_name: str):
"""Return info about selected folder if it is
configured, otherwise None.
"""
if "_" not in config_name:
config_name = f"configured_{config_name}_folder"
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
return None
def list_folders(self) -> list[str]:
"""return list of all existing folder names."""
assert not self._idling
return [folder.name for folder in self.conn.folder.list()]
def delete(self, uid_list: str, expunge=True):
"""delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not
just flagged as deleted.
"""
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
if expunge:
self.conn.expunge()
def get_all_messages(self) -> list[MailMessage]:
assert not self._idling
return list(self.conn.fetch())
def get_unread_messages(self) -> list[str]:
assert not self._idling
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
def mark_all_read(self):
messages = self.get_unread_messages()
if messages:
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
print("marked seen:", messages, res)
def get_unread_cnt(self) -> int:
return len(self.get_unread_messages())
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()
def log(*args, **kwargs):
kwargs["file"] = stream
print(*args, **kwargs)
empty_folders = []
for imapfolder in self.list_folders():
self.select_folder(imapfolder)
messages = list(self.get_all_messages())
if not messages:
empty_folders.append(imapfolder)
continue
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
for msg in self.conn.fetch(mark_seen=False):
body = getattr(msg.obj, "text", None)
if not body:
body = getattr(msg.obj, "html", None)
if not body:
log("Message", msg.uid, "has empty body")
continue
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
fn = path.joinpath(str(msg.uid))
fn.write_bytes(body)
log("Message", msg.uid, fn)
log(
"Message",
msg.uid,
msg.flags,
"Message-Id:",
msg.obj.get("Message-Id"),
)
if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders)
print(stream.getvalue(), file=logfile)
@contextmanager
def idle(self):
"""return Idle ContextManager."""
idle_manager = IdleManager(self)
try:
yield idle_manager
finally:
idle_manager.done()
def append(self, folder: str, msg: str):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> list[bytes]:
"""(blocking) wait for next idle message from server."""
self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log(f"imap-direct: idle_check returned {res!r}")
return res
def wait_for_new_message(self, timeout=None) -> bytes:
while True:
for item in self.check(timeout=timeout):
if b"EXISTS" in item or b"RECENT" in item:
return item
def wait_for_seen(self, timeout=None) -> int:
"""Return first message with SEEN flag from a running idle-stream."""
while True:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))
if FLAGS in item and rb"\Seen" in item:
return int(item.split(b" ")[1])
def done(self):
"""send idle-done to server if we are currently in idle mode."""
return self.direct_imap.conn.idle.stop()

View File

@@ -1,8 +1,10 @@
"""High-level classes for event processing and filtering."""
from __future__ import annotations
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
from .const import EventType
@@ -263,9 +265,9 @@ class HookCollection:
"""
def __init__(self) -> None:
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
return iter(self._hooks)
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa

View File

@@ -2,7 +2,8 @@ import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
from ._utils import AttrDict, futuremethod
from .const import EventType
from .contact import Contact
if TYPE_CHECKING:
@@ -21,9 +22,10 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
def send_reaction(self, *reaction: str):
def send_reaction(self, *reaction: str) -> "Message":
"""Send a reaction to this message."""
self._rpc.send_reaction(self.account.id, self.id, reaction)
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction)
return Message(self.account, msg_id)
def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
@@ -61,3 +63,18 @@ class Message:
def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
"""Consume events until the message is delivered."""
while True:
event = self.account.wait_for_event()
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break
@futuremethod
def send_webxdc_realtime_advertisement(self):
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
def send_webxdc_realtime_data(self, data) -> None:
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))

View File

@@ -1,10 +1,12 @@
from __future__ import annotations
import os
import random
from typing import AsyncGenerator, List, Optional
from typing import AsyncGenerator, Optional
import pytest
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
from ._utils import futuremethod
from .rpc import Rpc
@@ -54,14 +56,10 @@ class ACFactory:
@futuremethod
def get_online_account(self):
account = yield self.new_configured_account.future()
account.start_io()
while True:
event = account.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
account.bring_online()
return account
def get_online_accounts(self, num: int) -> List[Account]:
def get_online_accounts(self, num: int) -> list[Account]:
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
@@ -75,6 +73,10 @@ class ACFactory:
ac_clone.configure()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def send_message(
self,
to_account: Account,

View File

@@ -1,12 +1,14 @@
from __future__ import annotations
import itertools
import json
import logging
import os
import subprocess
import sys
from queue import Queue
from queue import Empty, Queue
from threading import Event, Thread
from typing import Any, Dict, Iterator, Optional
from typing import Any, Iterator, Optional
class JsonRpcError(Exception):
@@ -67,11 +69,11 @@ class Rpc:
self._kwargs = kwargs
self.process: subprocess.Popen
self.id_iterator: Iterator[int]
self.event_queues: Dict[int, Queue]
self.event_queues: dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: Dict[int, Event]
self.request_events: dict[int, Event]
# Map from request ID to the result.
self.request_results: Dict[int, Any]
self.request_results: dict[int, Any]
self.request_queue: Queue[Any]
self.closing: bool
self.reader_thread: Thread
@@ -186,5 +188,14 @@ class Rpc:
queue = self.get_queue(account_id)
return queue.get()
def clear_all_events(self, account_id: int):
"""Removes all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:
queue.get_nowait()
except Empty:
pass
def __getattr__(self, attr: str):
return RpcMethod(self, attr)

View File

@@ -0,0 +1,217 @@
from __future__ import annotations
import base64
import os
from typing import TYPE_CHECKING
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
first_event = ""
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
first_event = "change"
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
first_event = "item_change"
break
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
break
def wait_for_chatlist_specific_item(account, chat_id):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
break
def wait_for_chatlist(account):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
break
def test_delivery_status(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice.clear_all_events()
bob.stop_io()
alice.stop_io()
alice_chat_bob.send_text("hi")
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
alice.clear_all_events()
alice.start_io()
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
bob.clear_all_events()
bob.start_io()
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
msg.get_snapshot().chat.accept()
msg.mark_seen()
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
alice.clear_all_events()
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_READ:
break
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acfactory.get_online_accounts(1)
invalid_contact = alice.create_contact("example@example.com", "invalid address")
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
alice.clear_all_events()
failing_message = invalid_chat.send_text("test")
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_FAILED:
break
wait_for_chatlist_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
alice.set_config("download_limit", "1")
msg = bob.wait_for_incoming_msg()
chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
)
message = alice.wait_for_incoming_msg()
snapshot = message.get_snapshot()
assert snapshot.download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
snapshot = message.get_snapshot()
chat_id = snapshot.chat_id
alice._rpc.download_full_message(alice.id, message.id)
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
bob.wait_for_incoming_msg_event()
alice_second_device: Account = acfactory.get_unconfigured_account()
alice._rpc.provide_backup.future(alice.id)
backup_code = alice._rpc.get_backup_qr(alice.id)
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
alice_second_device.start_io()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.clear_all_events()
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.send_text("hello")
msg = bob.wait_for_incoming_msg()
bob_chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.get_chat_by_id(bob_chat_id).send_text("hello")
# make sure alice_second_device already received the message
alice_second_device.wait_for_incoming_msg_event()
msg = alice.wait_for_incoming_msg()
alice_second_device.clear_all_events()
msg.mark_seen()
wait_for_chatlist_specific_item(bob, bob_chat_id)
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
alice_second_device.clear_all_events()
alice_chat_bob.pin()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
alice_second_device.clear_all_events()
alice_chat_bob.mute()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted

View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""
Testing webxdc iroh connectivity
If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import sys
import threading
import time
import pytest
from deltachat_rpc_client import EventType
@pytest.fixture()
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
assert p.exists()
return str(p)
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
assert ac1.get_config("webxdc_realtime_enabled") == "1"
assert ac2.get_config("webxdc_realtime_enabled") == "1"
ac1_ac2_chat = ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
assert ac2_webxdc_msg.get_snapshot().text == "play"
# send iroh announcements simultaneously
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
return ac1_webxdc_msg, ac2_webxdc_msg
def setup_thread_send_realtime_data(msg, data):
def thread_run():
for _i in range(10):
msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
def wait_receive_realtime_data(msg_data_list):
account = msg_data_list[0][0].account
msg_data_list = msg_data_list[:]
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
while msg_data_list:
event = account.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
for i, (msg, data) in enumerate(msg_data_list):
if msg.id == event.msg_id:
assert data == event.data
log(f"msg {msg.id}: got correct realtime data {data}")
del msg_data_list[i]
break
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1.create_chat(ac2)
ac2.create_chat(ac1)
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping1"
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == list(b"foo")
break
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
def test_no_duplicate_messages(acfactory, path_to_webxdc):
"""Test that messages are received only once."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("webxdc_realtime_enabled", "1")
ac2.set_config("webxdc_realtime_enabled", "1")
ac1_ac2_chat = ac1.create_chat(ac2)
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
ac2_webxdc_msg.get_snapshot().chat.accept()
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
# Issue a "send" call in parallel with sending advertisement.
# Previously due to a bug this caused subscribing to the channel twice.
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
def thread_run():
for i in range(10):
data = str(i).encode()
ac1_webxdc_msg.send_webxdc_realtime_data(data)
time.sleep(1)
threading.Thread(target=thread_run, daemon=True).start()
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
n = int(bytes(event.data).decode())
break
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert int(bytes(event.data).decode()) > n
break

View File

@@ -1,7 +1,7 @@
import logging
import pytest
from deltachat_rpc_client import Chat, SpecialContactId
from deltachat_rpc_client import Chat, EventType, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
@@ -579,3 +579,40 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
logging.info("Alice withdraws QR code.")
qr = alice.check_qr(qr_code)
assert qr["kind"] == "withdrawVerifyGroup"
alice.set_config_from_qr(qr_code)
logging.info("Bob scans withdrawn QR code.")
bob_chat = bob.secure_join(qr_code)
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

@@ -1,10 +1,15 @@
import concurrent.futures
import json
import logging
import os
import subprocess
import time
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client.rpc import JsonRpcError
@@ -439,3 +444,172 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
message = alice.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.show_padlock
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
logging.info("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
for m in msgs:
m.wait_until_delivered()
logging.info("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
msgs[-1].wait_until_delivered()
ac2.start_io()
logging.info("wait for ac2 to receive a reaction")
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1_addr
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
ac2.set_config("mvbox_move", "1")
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
for account in others:
chat = account.create_chat(alice)
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
contact_addr = account.get_config("addr")
contact = alice.create_contact(contact_addr, "")
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "hi"
bob_group = snapshot.chat
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
for i in range(10):
logging.info("Sending message %s", i)
alice_group.send_file(str(path))
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory, tmp_path):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
bob2 = acfactory.get_unconfigured_account()
bob2.import_backup(files[0])
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
while True:
event = bob2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN

View File

@@ -0,0 +1,15 @@
def test_vcard(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_contact(alice_contact_charlie)
event = bob.wait_for_incoming_msg_event()
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.vcard_contact
assert snapshot.vcard_contact.addr == "charlie@example.org"

View File

@@ -22,12 +22,11 @@ skipsdist = True
skip_install = True
deps =
ruff
black
commands =
black --quiet --check --diff src/ examples/ tests/
ruff src/ examples/ tests/
ruff format --quiet --diff src/ examples/ tests/
ruff check src/ examples/ tests/
[pytest]
timeout = 300
log_cli = true
#log_cli = true
log_level = debug

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.137.2"
version = "1.140.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -14,13 +14,13 @@ deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "2.3.0"
log = "0.4"
serde_json = "1"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.37.0", features = ["io-std"] }
tokio-util = "0.7.9"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
[features]

View File

@@ -0,0 +1,3 @@
platform_package
*.tgz
package-lock.json

View File

@@ -0,0 +1,3 @@
platform_package/*
scripts/
*.tgz

View File

@@ -0,0 +1,78 @@
## npm package for deltachat-rpc-server
This is the successor of `deltachat-node`,
it does not use NAPI bindings but instead uses stdio executables
to let you talk to core over jsonrpc over stdio.
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
## Usage
> The **minimum** nodejs version for this package is `20.11`
```
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
```
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
dc.close()
}
main()
```
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
## How to use on an unsupported platform
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
## How does it work when you install it
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
references:
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
When you import this package it searches for the rpc server in the following locations and order:
1. `DELTA_CHAT_RPC_SERVER` environment variable
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
3. prebuilds in npm packages
so by default it uses the prebuilds.
## How do you built this package in CI
- To build platform packages, run the `build_platform_package.py` script:
```
python3 build_platform_package.py <cargo-target>
# example
python3 build_platform_package.py x86_64-apple-darwin
```
- Then pass it as an artifact to the last CI action that publishes the main package.
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
- then publish `deltachat-rpc-server/npm-package`,
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
## How to build a version you can use localy on your host machine for development
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
- If you just need your host platform run `python scripts/make_local_dev_version.py`
- note: this clears the `platform_package` folder
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
## Thanks to nlnet
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.

View File

@@ -0,0 +1,42 @@
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
export interface SearchOptions {
/** whether take deltachat-rpc-server inside of $PATH*/
takeVersionFromPATH: boolean;
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
disableEnvPath: boolean;
}
/**
*
* @returns absolute path to deltachat-rpc-server binary
* @throws when it is not found
*/
export function getRPCServerPath(
options?: Partial<SearchOptions>
): Promise<string>;
export type DeltaChatOverJsonRpcServer = StdioDeltaChat & {
readonly pathToServerBinary: string;
};
export interface StartOptions {
/** whether to disable outputting stderr to the parent process's stderr */
muteStdErr: boolean;
}
/**
*
* @param directory directory for accounts folder
* @param options
*/
export function startDeltaChat(directory: string, options?: Partial<SearchOptions & StartOptions> ): Promise<DeltaChatOverJsonRpcServer>
export namespace FnTypes {
export type getRPCServerPath = typeof getRPCServerPath
export type startDeltaChat = typeof startDeltaChat
}

View File

@@ -0,0 +1,106 @@
//@ts-check
import { spawn } from "node:child_process";
import { stat } from "node:fs/promises";
import os from "node:os";
import process from "node:process";
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
import {
ENV_VAR_LOCATION_NOT_FOUND,
FAILED_TO_START_SERVER_EXECUTABLE,
NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR,
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
} from "./src/errors.js";
// Because this is not compiled by typescript, esm needs this stuff (` with { type: "json" };`,
// nodejs still complains about it being experimental, but deno also uses it, so treefit bets taht it will become standard)
import package_json from "./package.json" with { type: "json" };
import { createRequire } from "node:module";
function findRPCServerInNodeModules() {
const arch = os.arch();
const operating_system = process.platform;
const package_name = `@deltachat/stdio-rpc-server-${operating_system}-${arch}`;
try {
const { resolve } = createRequire(import.meta.url);
return resolve(package_name);
} catch (error) {
console.debug("findRpcServerInNodeModules", error);
if (Object.keys(package_json.optionalDependencies).includes(package_name)) {
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
} else {
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
}
}
}
/** @type {import("./index").FnTypes.getRPCServerPath} */
export async function getRPCServerPath(options = {}) {
const { takeVersionFromPATH, disableEnvPath } = {
takeVersionFromPATH: false,
disableEnvPath: false,
...options,
};
// 1. check if it is set as env var
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
try {
if (!(await stat(process.env[ENV_VAR_NAME])).isFile()) {
throw new Error(
`expected ${ENV_VAR_NAME} to point to the deltachat-rpc-server executable`
);
}
} catch (error) {
throw new Error(ENV_VAR_LOCATION_NOT_FOUND());
}
return process.env[ENV_VAR_NAME];
}
// 2. check if PATH should be used
if (takeVersionFromPATH) {
return PATH_EXECUTABLE_NAME;
}
// 3. check for prebuilds
return findRPCServerInNodeModules();
}
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
/** @type {import("./index").FnTypes.startDeltaChat} */
export async function startDeltaChat(directory, options = {}) {
const pathToServerBinary = await getRPCServerPath(options);
const server = spawn(pathToServerBinary, {
env: {
RUST_LOG: process.env.RUST_LOG,
DC_ACCOUNTS_PATH: directory,
},
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
});
server.on("error", (err) => {
throw new Error(FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, err));
});
let shouldClose = false;
server.on("exit", () => {
if (shouldClose) {
return;
}
throw new Error("Server quit");
});
/** @type {import('./index').DeltaChatOverJsonRpcServer} */
//@ts-expect-error
const dc = new StdioDeltaChat(server.stdin, server.stdout, true);
dc.close = () => {
shouldClose = true;
if (!server.kill()) {
console.log("server termination failed");
}
};
//@ts-expect-error
dc.pathToServerBinary = pathToServerBinary;
return dc;
}

View File

@@ -0,0 +1,19 @@
{
"license": "MPL-2.0",
"main": "index.js",
"name": "@deltachat/stdio-rpc-server",
"optionalDependencies": {},
"peerDependencies": {
"@deltachat/jsonrpc-client": "*"
},
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git"
},
"scripts": {
"prepack": "node scripts/update_optional_dependencies_and_version.js"
},
"type": "module",
"types": "index.d.ts",
"version": "1.140.0"
}

View File

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import subprocess
from sys import argv
from os import path, makedirs, chdir
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 2:
print("First argument should be target architecture as required by cargo")
exit(1)
target = argv[1].strip()
subprocess.run(
["cargo", "build", "--release", "-p", "deltachat-rpc-server", "--target", target],
check=True,
)
newpath = "platform_package"
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = "platform_package/" + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
def binary_path(binary_name):
return "../../target/" + target + "/release/" + binary_name
my_binary_name = "deltachat-rpc-server"
if not path.isfile(binary_path("deltachat-rpc-server")):
my_binary_name = "deltachat-rpc-server.exe"
if not path.isfile(binary_path("deltachat-rpc-server.exe")):
print("Did not find the build")
exit(1)
my_binary_path = binary_path(my_binary_name)
copy(my_binary_path, platform_path + "/" + my_binary_name)
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -0,0 +1,34 @@
# This script is for making a version of the npm packet that you can install locally
import subprocess
from sys import argv
from os import path, makedirs, chdir
import re
import json
import tomllib
from shutil import copy, rmtree
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
# get host target with "rustc -vV"
output = subprocess.run(["rustc", "-vV"], capture_output=True)
host_target = re.search('host: ([-\\w]*)', output.stdout.decode("utf-8")).group(1)
print("host target to build for is:", host_target)
# clean platform_package folder
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
else:
rmtree(path.join(path.dirname(path.abspath(__file__)), "../platform_package/"))
makedirs(newpath)
# run build_platform_package.py with the host's target to build it
subprocess.run(["python", "scripts/build_platform_package.py", host_target], capture_output=False, check=True)
# run update_optional_dependencies_and_version.js to adjust the package / make it installable locally
subprocess.run(["node", "scripts/update_optional_dependencies_and_version.js", "--local"], capture_output=False, check=True)
# typescript / npm local package installing/linking needs that this package has it's own node_modules folder
subprocess.run(["npm", "i"], capture_output=False, check=True)

View File

@@ -0,0 +1,46 @@
import subprocess
from sys import argv
from os import path, makedirs, chdir, chmod, stat
import json
from shutil import copy
from src.make_package import write_package_json
# ensure correct working directory
chdir(path.join(path.dirname(path.abspath(__file__)), "../"))
if len(argv) < 3:
print("First argument should be target architecture as required by cargo")
print("Second argument should be the location of th built binary (binary_path)")
exit(1)
target = argv[1].strip()
binary_path = argv[2].strip()
output = subprocess.run(["rustc","--print","target-list"], capture_output=True, check=True)
available_targets = output.stdout.decode("utf-8")
if available_targets.find(target) == -1:
print("target", target, "is not known / not valid")
exit(1)
newpath = r'platform_package'
if not path.exists(newpath):
makedirs(newpath)
# make new folder
platform_path = 'platform_package/' + target
if not path.exists(platform_path):
makedirs(platform_path)
# copy binary it over
my_binary_name = path.basename(binary_path)
new_binary_path = platform_path + "/" + my_binary_name
copy(binary_path, new_binary_path)
chmod(new_binary_path, 0o555) # everyone can read & execute, nobody can write
# make a package.json for it
write_package_json(platform_path, target, my_binary_name)

View File

@@ -0,0 +1,21 @@
def convert_cpu_arch_to_npm_cpu_arch(arch):
if arch == "x86_64":
return "x64"
if arch == "i686":
return "ia32"
if arch == "aarch64":
return "arm64"
if arch == "armv7" or arch == "arm":
return "arm"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.arch':", arch)
return arch
def convert_os_to_npm_os(os):
if os == "windows":
return "win32"
if os == "darwin" or os == "linux":
return os
if os.startswith("android"):
return "android"
print("architecture might not be known by nodejs, please make sure it can be returned by 'process.platform':", os)
return os

View File

@@ -0,0 +1,34 @@
import tomllib
import json
from .convert_platform import convert_cpu_arch_to_npm_cpu_arch, convert_os_to_npm_os
def write_package_json(platform_path, rust_target, my_binary_name):
if len(rust_target.split("-")) == 3:
[cpu_arch, vendor, os] = rust_target.split("-")
else:
[cpu_arch, vendor, os, _env] = rust_target.split("-")
# read version
tomlfile = open("../../Cargo.toml", 'rb')
version = tomllib.load(tomlfile)['package']['version']
package_json = {
"name": "@deltachat/stdio-rpc-server-"
+ convert_os_to_npm_os(os)
+ "-"
+ convert_cpu_arch_to_npm_cpu_arch(cpu_arch),
"version": version,
"os": [convert_os_to_npm_os(os)],
"cpu": [convert_cpu_arch_to_npm_cpu_arch(cpu_arch)],
"main": my_binary_name,
"license": "MPL-2.0",
"repository": {
"type": "git",
"url": "https://github.com/deltachat/deltachat-core-rust.git",
},
}
file = open(platform_path + "/package.json", 'w')
file.write(json.dumps(package_json, indent=4))

View File

@@ -0,0 +1,63 @@
import fs from "node:fs/promises";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const expected_cwd = join(dirname(fileURLToPath(import.meta.url)), "..");
if (process.cwd() !== expected_cwd) {
console.error(
"CWD missmatch: this script needs to be run from " + expected_cwd,
{ actual: process.cwd(), expected: expected_cwd }
);
process.exit(1);
}
// whether to use local paths instead of npm registry version number for the prebuilds in optionalDependencies
// useful for local development
const is_local = process.argv.includes("--local");
const package_json = JSON.parse(await fs.readFile("./package.json", "utf8"));
const cargo_toml = await fs.readFile("../Cargo.toml", "utf8");
const version = cargo_toml
.split("\n")
.find((line) => line.includes("version"))
.split('"')[1];
const platform_packages_dir = "./platform_package";
const platform_package_names = await Promise.all(
(await fs.readdir(platform_packages_dir)).map(async (name) => {
const p = JSON.parse(
await fs.readFile(
join(platform_packages_dir, name, "package.json"),
"utf8"
)
);
if (p.version !== version) {
console.error(
name,
"has a different version than the version of the rpc server.",
{ rpc_server: version, platform_package: p.version }
);
throw new Error("version missmatch");
}
return { folder_name: name, package_name: p.name };
})
);
package_json.version = version;
package_json.optionalDependencies = {};
for (const { folder_name, package_name } of platform_package_names) {
package_json.optionalDependencies[package_name] = is_local
? `file:${expected_cwd}/platform_package/${folder_name}` // npm seems to work better with an absolute path here
: version;
}
if (is_local) {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = 'file:../../deltachat-jsonrpc/typescript'
} else {
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*"
}
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));

View File

@@ -0,0 +1,5 @@
//@ts-check
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"

View File

@@ -0,0 +1,41 @@
//@ts-check
import { ENV_VAR_NAME } from "./const.js";
const cargoInstallCommand =
"cargo install --git https://github.com/deltachat/deltachat-core-rust deltachat-rpc-server";
export function NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name) {
return `deltachat-rpc-server not found:
- Install it with "npm i ${package_name}"
- or download/compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR() {
return `deltachat-rpc-server not found:
Unfortunately no prebuild is available for your system, so you need to provide deltachat-rpc-server yourself.
- Download or Compile deltachat-rpc-server for your platform and
- either put it into your PATH (for example with "${cargoInstallCommand}")
- or set the "${ENV_VAR_NAME}" env var to the path to deltachat-rpc-server"`;
}
export function ENV_VAR_LOCATION_NOT_FOUND(error) {
return `deltachat-rpc-server not found in ${ENV_VAR_NAME}:
Error: ${error}
Content of ${ENV_VAR_NAME}: "${process.env[ENV_VAR_NAME]}"`;
}
export function FAILED_TO_START_SERVER_EXECUTABLE(pathToServerBinary, error) {
return `Failed to start server executable at '${pathToServerBinary}',
Error: ${error}
Make sure the deltachat-rpc-server binary exists at this location
and you can start it with \`${pathToServerBinary} --version\``;
}

View File

@@ -1,3 +1,4 @@
#![recursion_limit = "256"]
//! Delta Chat core RPC server.
//!
//! It speaks JSON Lines over stdio.
@@ -10,6 +11,7 @@ use deltachat::constants::DC_VERSION_STR;
use deltachat_jsonrpc::api::{Accounts, CommandApi};
use futures_lite::stream::StreamExt;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tracing_subscriber::EnvFilter;
use yerpc::RpcServer as _;
#[cfg(target_family = "unix")]
@@ -27,6 +29,9 @@ async fn main() {
// "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."
if let Err(error) = &r {
log::error!("Fatal error: {error:#}.")
}
std::process::exit(if r.is_ok() { 0 } else { 1 });
}
@@ -59,7 +64,13 @@ async fn main_impl() -> Result<()> {
#[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();
// Logs from `log` crate and traces from `tracing` crate
// are configurable with `RUST_LOG` environment variable
// and go to stderr to avoid interferring with JSON-RPC using stdout.
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
@@ -68,7 +79,7 @@ async fn main_impl() -> Result<()> {
log::info!("Creating JSON-RPC API.");
let accounts = Arc::new(RwLock::new(accounts));
let state = CommandApi::from_arc(accounts.clone());
let state = CommandApi::from_arc(accounts.clone()).await;
let (client, mut out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), state.clone());

View File

@@ -23,9 +23,13 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "asn1-rs-derive", version = "0.4.0" },
{ name = "asn1-rs-impl", version = "0.1.0" },
{ name = "asn1-rs", version = "0.5.2" },
{ name = "async-channel", version = "1.9.0" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "base64", version = "0.21.7" },
{ name = "bitflags", version = "1.3.2" },
{ name = "block-buffer", version = "<0.10" },
{ name = "convert_case", version = "0.4.0" },
@@ -33,45 +37,77 @@ skip = [
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "darling", version = "<0.14" },
{ name = "der_derive", version = "0.6.1" },
{ name = "derive_more", version = "0.99.17" },
{ name = "der-parser", version = "8.2.0" },
{ name = "der", version = "0.6.1" },
{ name = "digest", version = "<0.10" },
{ name = "dlopen2", version = "0.4.1" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "event-listener", version = "2.5.3" },
{ name = "event-listener", version = "4.0.3" },
{ name = "fastrand", version = "1.9.0" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "hyper-rustls", version = "0.24.2" },
{ name = "hyper", version = "0.14.28" },
{ name = "idna", version = "0.4.0" },
{ name = "netlink-packet-core", version = "0.5.0" },
{ name = "netlink-packet-route", version = "0.15.0" },
{ name = "nix", version = "0.26.4" },
{ name = "oid-registry", version = "0.6.1" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pem", version = "1.1.1" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "rand", version = "<0.8" },
{ name = "rcgen", version = "<0.12.1" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "reqwest", version = "0.11.27" },
{ name = "ring", version = "0.16.20" },
{ name = "rustls-pemfile", version = "1.0.4" },
{ name = "rustls", version = "0.21.11" },
{ name = "rustls-webpki", version = "0.101.7" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "ssh-encoding", version = "0.1.0" },
{ name = "ssh-key", version = "0.5.1" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "synstructure", version = "0.12.6" },
{ name = "syn", version = "1.0.109" },
{ name = "system-configuration-sys", version = "0.5.0" },
{ name = "system-configuration", version = "0.5.1" },
{ name = "time", version = "<0.3" },
{ name = "tokio-rustls", version = "0.24.1" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "webpki-roots", version ="0.25.4" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
{ name = "windows-core", version = "<0.54.0" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "0.32.0" },
{ name = "windows", version = "<0.54.0" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
{ name = "winnow", version = "0.5.40" },
{ name = "winreg", version = "0.50.0" },
{ name = "x509-parser", version = "<0.16.0" },
]

65
flake.lock generated
View File

@@ -7,11 +7,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1710633978,
"narHash": "sha256-yemnwSvW7cdWtXGpivFA5jDO35rGPs6fqxlQ4l6ODXs=",
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "e91fb3d8517538e5ad9b422c9a4f604b56008a9e",
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1708939976,
"narHash": "sha256-O5+nFozxz2Vubpdl1YZtPrilcIXPcRAjqNdNE8oCRoA=",
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"owner": "numtide",
"repo": "devshell",
"rev": "5ddecd67edbd568ebe0a55905273e56cc82aabe3",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"type": "github"
},
"original": {
@@ -48,11 +48,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1710742993,
"narHash": "sha256-W0PQCe0bW3hKF5lHawXrKynBcdSP18Qa4sb8DcUfOqI=",
"lastModified": 1714112748,
"narHash": "sha256-jq6Cpf/pQH85p+uTwPPrGG8Ky/zUOTwMJ7mcqc5M4So=",
"owner": "nix-community",
"repo": "fenix",
"rev": "6f2fec850f569d61562d3a47dc263f19e9c7d825",
"rev": "3ae4b908a795b6a3824d401a0702e11a7157d7e1",
"type": "github"
},
"original": {
@@ -84,11 +84,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
@@ -120,11 +120,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
@@ -150,11 +150,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
@@ -166,11 +166,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"lastModified": 1713895582,
"narHash": "sha256-cfh1hi+6muQMbi9acOlju3V1gl8BEaZBXBR9jQfQi4U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"rev": "572af610f6151fd41c212f897c71f7056e3fb518",
"type": "github"
},
"original": {
@@ -182,12 +182,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1710765496,
"narHash": "sha256-p7ryWEeQfMwTB6E0wIUd5V2cFTgq+DRRBz2hYGnJZyA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e367f7a1fb93137af22a3908f00b9a35e2d286a7",
"type": "github"
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"path": "/nix/store/9fpv0kjq9a80isa1wkkvrdqsh9dpcn05-source",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "path"
},
"original": {
"id": "nixpkgs",
@@ -196,11 +195,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1710631334,
"narHash": "sha256-rL5LSYd85kplL5othxK5lmAtjyMOBg390sGBTb3LRMM=",
"lastModified": 1714076141,
"narHash": "sha256-Drmja/f5MRHZCskS6mvzFqxEaZMeciScCTFxWVLqWEY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c75037bbf9093a2acb617804ee46320d6d1fea5a",
"rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
"type": "github"
},
"original": {
@@ -223,11 +222,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1710708100,
"narHash": "sha256-Jd6pmXlwKk5uYcjyO/8BfbUVmx8g31Qfk7auI2IG66A=",
"lastModified": 1714031783,
"narHash": "sha256-xS/niQsq1CQPOe4M4jvVPO2cnXS/EIeRG5gIopUbk+Q=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "b6d1887bc4f9543b6c6bf098179a62446f34a6c3",
"rev": "56bee2ddafa6177b19c631eedc88d43366553223",
"type": "github"
},
"original": {

View File

@@ -35,6 +35,7 @@
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-contact-tools
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
@@ -482,6 +483,7 @@
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
@@ -523,9 +525,15 @@
};
};
devShells.default = pkgs.mkShell {
devShells.default = let
pkgs = import nixpkgs {
system = system;
overlays = [ fenix.overlays.default ];
};
in pkgs.mkShell {
buildInputs = with pkgs; [
(fenixPkgs.complete.withComponents [
(fenix.packages.${system}.complete.withComponents [
"cargo"
"clippy"
"rust-src"
@@ -533,8 +541,10 @@
"rustfmt"
])
cargo-deny
fenixPkgs.rust-analyzer
rust-analyzer-nightly
cargo-nextest
perl # needed to build vendored OpenSSL
git-cliff
];
};
}

251
fuzz/Cargo.lock generated
View File

@@ -94,6 +94,12 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -339,6 +345,12 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
[[package]]
name = "base64"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.5.3"
@@ -515,9 +527,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "3.4.0"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -526,9 +538,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.5.1"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -633,17 +645,16 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.23"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-integer",
"num-traits",
"time 0.1.45",
"wasm-bindgen",
"winapi",
"windows-targets 0.52.0",
]
[[package]]
@@ -953,7 +964,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.136.0"
version = "1.137.3"
dependencies = [
"anyhow",
"async-channel 2.1.1",
@@ -962,9 +973,10 @@ dependencies = [
"async-smtp",
"async_zip",
"backtrace",
"base64 0.21.0",
"base64 0.22.0",
"brotli",
"chrono",
"deltachat-contact-tools",
"deltachat-time",
"deltachat_derive",
"email",
@@ -989,6 +1001,7 @@ dependencies = [
"num-traits",
"num_cpus",
"once_cell",
"openssl-src",
"parking_lot",
"percent-encoding",
"pgp",
@@ -1023,6 +1036,16 @@ dependencies = [
"uuid 1.2.2",
]
[[package]]
name = "deltachat-contact-tools"
version = "0.1.0"
dependencies = [
"anyhow",
"once_cell",
"regex",
"rusqlite",
]
[[package]]
name = "deltachat-fuzz"
version = "0.0.0"
@@ -1785,9 +1808,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
dependencies = [
"fastrand 2.0.1",
"futures-core",
@@ -1912,9 +1935,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.24"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
dependencies = [
"bytes",
"fnv",
@@ -2051,9 +2074,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
@@ -2062,12 +2085,24 @@ dependencies = [
[[package]]
name = "http-body"
version = "0.4.5"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http",
]
[[package]]
name = "http-body-util"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
]
@@ -2077,12 +2112,6 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humansize"
version = "2.1.2"
@@ -2094,39 +2123,58 @@ dependencies = [
[[package]]
name = "hyper"
version = "0.14.23"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2 0.4.7",
"smallvec",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http",
"http-body",
"hyper",
"pin-project-lite",
"socket2 0.5.6",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
@@ -2180,17 +2228,29 @@ dependencies = [
[[package]]
name = "image"
version = "0.24.9"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"gif",
"jpeg-decoder",
"image-webp",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c"
dependencies = [
"byteorder",
"thiserror",
]
[[package]]
@@ -2314,12 +2374,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
[[package]]
name = "jpeg-decoder"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -3301,7 +3355,7 @@ checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7"
dependencies = [
"bytes",
"libc",
"socket2 0.5.4",
"socket2 0.5.6",
"tracing",
"windows-sys 0.48.0",
]
@@ -3480,11 +3534,11 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.11.24"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251"
checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19"
dependencies = [
"base64 0.21.0",
"base64 0.22.0",
"bytes",
"encoding_rs",
"futures-core",
@@ -3492,8 +3546,10 @@ dependencies = [
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
@@ -3502,7 +3558,7 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile",
"rustls-pemfile 2.1.1",
"serde",
"serde_json",
"serde_urlencoded",
@@ -3515,7 +3571,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg 0.50.0",
"winreg 0.52.0",
]
[[package]]
@@ -3708,9 +3764,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.9"
version = "0.21.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9"
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
dependencies = [
"ring 0.17.6",
"rustls-webpki",
@@ -3724,7 +3780,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pemfile 1.0.2",
"schannel",
"security-framework",
]
@@ -3738,6 +3794,22 @@ dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustls-pemfile"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab"
dependencies = [
"base64 0.21.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@@ -4055,9 +4127,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.10.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "smawk"
@@ -4077,12 +4149,12 @@ dependencies = [
[[package]]
name = "socket2"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e"
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
dependencies = [
"libc",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4294,22 +4366,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.38"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.38"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.107",
"syn 2.0.52",
]
[[package]]
@@ -4377,9 +4449,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.32.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@@ -4389,7 +4461,7 @@ dependencies = [
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.4",
"socket2 0.5.6",
"tokio-macros",
"windows-sys 0.48.0",
]
@@ -4406,9 +4478,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
dependencies = [
"proc-macro2",
"quote",
@@ -4442,9 +4514,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -4514,6 +4586,28 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -5169,9 +5263,9 @@ dependencies = [
[[package]]
name = "winreg"
version = "0.50.0"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
@@ -5265,3 +5359,18 @@ dependencies = [
"syn 1.0.107",
"synstructure",
]
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448"
dependencies = [
"zune-core",
]

View File

@@ -30,6 +30,9 @@ module.exports = {
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
DC_EVENT_CHANNEL_OVERFLOW: 2400,
DC_EVENT_CHATLIST_CHANGED: 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
DC_EVENT_CHAT_MODIFIED: 2020,
DC_EVENT_CONFIGURE_PROGRESS: 2041,
@@ -64,6 +67,7 @@ module.exports = {
DC_EVENT_SMTP_MESSAGE_SENT: 103,
DC_EVENT_WARNING: 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
DC_GCL_ADD_ALLDONE_HINT: 4,
DC_GCL_ADD_SELF: 2,
@@ -108,6 +112,7 @@ module.exports = {
DC_MSG_IMAGE: 20,
DC_MSG_STICKER: 23,
DC_MSG_TEXT: 10,
DC_MSG_VCARD: 90,
DC_MSG_VIDEO: 50,
DC_MSG_VIDEOCHAT_INVITATION: 70,
DC_MSG_VOICE: 41,
@@ -171,6 +176,7 @@ module.exports = {
DC_STR_CONFIGURATION_FAILED: 84,
DC_STR_CONNECTED: 107,
DC_STR_CONNTECTING: 108,
DC_STR_CONTACT: 200,
DC_STR_CONTACT_NOT_VERIFIED: 36,
DC_STR_CONTACT_SETUP_CHANGED: 37,
DC_STR_CONTACT_VERIFIED: 35,
@@ -264,6 +270,8 @@ module.exports = {
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,
DC_STR_SAVED_MESSAGES: 69,
DC_STR_SECUREJOIN_WAIT: 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT: 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
DC_STR_SECURE_JOIN_REPLIES: 118,
DC_STR_SECURE_JOIN_STARTED: 117,

View File

@@ -37,5 +37,9 @@ module.exports = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE'
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
}

View File

@@ -30,6 +30,9 @@ export enum C {
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
DC_EVENT_CHANNEL_OVERFLOW = 2400,
DC_EVENT_CHATLIST_CHANGED = 2300,
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
DC_EVENT_CHAT_MODIFIED = 2020,
DC_EVENT_CONFIGURE_PROGRESS = 2041,
@@ -64,6 +67,7 @@ export enum C {
DC_EVENT_SMTP_MESSAGE_SENT = 103,
DC_EVENT_WARNING = 300,
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
DC_GCL_ADD_ALLDONE_HINT = 4,
DC_GCL_ADD_SELF = 2,
@@ -108,6 +112,7 @@ export enum C {
DC_MSG_IMAGE = 20,
DC_MSG_STICKER = 23,
DC_MSG_TEXT = 10,
DC_MSG_VCARD = 90,
DC_MSG_VIDEO = 50,
DC_MSG_VIDEOCHAT_INVITATION = 70,
DC_MSG_VOICE = 41,
@@ -171,6 +176,7 @@ export enum C {
DC_STR_CONFIGURATION_FAILED = 84,
DC_STR_CONNECTED = 107,
DC_STR_CONNTECTING = 108,
DC_STR_CONTACT = 200,
DC_STR_CONTACT_NOT_VERIFIED = 36,
DC_STR_CONTACT_SETUP_CHANGED = 37,
DC_STR_CONTACT_VERIFIED = 35,
@@ -264,6 +270,8 @@ export enum C {
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,
DC_STR_SAVED_MESSAGES = 69,
DC_STR_SECUREJOIN_WAIT = 190,
DC_STR_SECUREJOIN_WAIT_TIMEOUT = 191,
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
DC_STR_SECURE_JOIN_REPLIES = 118,
DC_STR_SECURE_JOIN_STARTED = 117,
@@ -332,5 +340,9 @@ export const EventId2EventName: { [key: number]: string } = {
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
}

View File

@@ -1,5 +1,4 @@
#define NAPI_VERSION 4
#define NAPI_EXPERIMENTAL
#include <stdlib.h>
#include <stdio.h>

View File

@@ -26,6 +26,8 @@ function createTempUser(chatmailDomain) {
}
describe('static tests', function () {
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
it('reverse lookup of events', function () {
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
const eventValues = Object.values(EventId2EventName)
@@ -701,7 +703,7 @@ describe('Offline Tests with unconfigured account', function () {
})
describe('Integration tests', function () {
this.timeout(60 * 3000) // increase timeout to 1min
this.timeout(60 * 5 * 1000) // increase timeout to 5 min
let [dc, context, accountId, directory, account] = [
null,

View File

@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.137.2"
"version": "1.140.0"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.137.2"
version = "1.140.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"
@@ -46,7 +46,7 @@ deltachat = [
line-length = 120
[tool.ruff]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
line-length = 120
[tool.isort]

View File

@@ -328,10 +328,12 @@ class EventThread(threading.Thread):
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_reactions_changed", {"message": msg}
if msg is not None:
yield "ac_reactions_changed", {"message": msg}
elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", {"message": msg}
if msg is not None:
yield "ac_message_delivered", {"message": msg}
elif name == "DC_EVENT_CHAT_MODIFIED":
chat = account.get_chat_by_id(ffi_event.data1)
yield "ac_chat_modified", {"chat": chat}

View File

@@ -364,6 +364,9 @@ class Message:
else:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
# Message could be trashed, use the cached object if so.
if dc_msg == ffi.NULL:
dc_msg = self._dc_msg
return lib.dc_msg_get_state(dc_msg)
def is_in_fresh(self):
@@ -392,7 +395,7 @@ class Message:
def is_outgoing(self):
"""Return True if Message is outgoing."""
return self._msgstate in (
return lib.dc_msg_get_state(self._dc_msg) in (
const.DC_STATE_OUT_PREPARING,
const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_FAILED,
@@ -484,6 +487,9 @@ class Message:
# load message from db to get a fresh/current state
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
# Message could be trashed, use the cached object if so.
if dc_msg == ffi.NULL:
dc_msg = self._dc_msg
return lib.dc_msg_get_download_state(dc_msg)
def download_full(self) -> None:

View File

@@ -275,6 +275,7 @@ class ACSetup:
def __init__(self, testprocess, init_time) -> None:
self._configured_events = Queue()
self._account2state: Dict[Account, str] = {}
self._account2config: Dict[Account, Dict[str, str]] = {}
self._imap_cleaned: Set[str] = set()
self.testprocess = testprocess
self.init_time = init_time
@@ -336,6 +337,8 @@ class ACSetup:
if not success:
pytest.fail(f"configuring online account {acc} failed: {comment}")
self._account2state[acc] = self.CONFIGURED
if acc in self._account2config:
acc.update_config(self._account2config[acc])
return acc
def _onconfigure_start_io(self, acc):
@@ -523,6 +526,7 @@ class ACFactory:
configdict.setdefault("sentbox_watch", False)
configdict.setdefault("sync_msgs", False)
ac.update_config(configdict)
self._acsetup._account2config[ac] = configdict
self._preconfigure_key(ac, configdict["addr"])
return ac

View File

@@ -18,14 +18,14 @@ def test_db_busy_error(acfactory):
# make a number of accounts
accounts = acfactory.get_many_online_accounts(3)
log("created %s accounts" % len(accounts))
log(f"created {len(accounts)} accounts")
# put a bigfile into each account
for acc in accounts:
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
with open(acc.bigfile, "wb") as f:
f.write(b"01234567890" * 1000_000)
log("created %s bigfiles" % len(accounts))
log(f"created {len(accounts)} bigfiles")
contact_addrs = [acc.get_self_contact().addr for acc in accounts]
chat = accounts[0].create_group_chat("stress-group")

View File

@@ -1,7 +1,6 @@
import os
import queue
import sys
import time
import base64
from datetime import datetime, timezone
@@ -1344,7 +1343,6 @@ def test_quote_encrypted(acfactory, lp):
for quoted_msg in msg1, msg3:
# Save the draft with a quote.
# It should be encrypted if quoted message is encrypted.
msg_draft = Message.new_empty(ac1, "text")
msg_draft.set_text("message reply")
msg_draft.quote = quoted_msg
@@ -1358,10 +1356,14 @@ def test_quote_encrypted(acfactory, lp):
chat.set_draft(None)
assert chat.get_draft() is None
# Quote should be replaced with "..." if quoted message is encrypted.
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message reply"
assert msg_in.quoted_text == quoted_msg.text
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
assert not msg_in.is_encrypted()
if quoted_msg.is_encrypted():
assert msg_in.quoted_text == "..."
else:
assert msg_in.quoted_text == quoted_msg.text
def test_quote_attachment(tmp_path, acfactory, lp):
@@ -1493,107 +1495,6 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_in
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
reactions_queue = queue.Queue()
class InPlugin:
@account_hookimpl
def ac_reactions_changed(self, message):
reactions_queue.put(message)
ac2.add_account_plugin(InPlugin())
lp.sec("sending small+large messages from ac1 to ac2")
msgs = []
msgs.append(chat.send_text("hi"))
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
msgs.append(chat.send_file(str(path)))
for m in msgs:
ac1._evtracker.wait_msg_delivered(m)
lp.sec("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
ac1._evtracker.wait_msg_delivered(msgs[-1])
ac2.start_io()
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1_addr
assert reactions.get_by_contact(contacts[0]) == react_str
def test_reactions_for_a_reordering_move(acfactory, lp):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
lp.sec("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
ac1._evtracker.wait_msg_delivered(msg1)
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str))
lp.sec("moving messages to ac2's DeltaChat folder in the reverse order")
ac2.direct_imap.connect()
for uid in sorted([m.uid for m in ac2.direct_imap.get_all_messages()], reverse=True):
ac2.direct_imap.conn.move(uid, "DeltaChat")
lp.sec("receiving messages by ac2")
ac2.start_io()
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.text == msg1.text
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
assert len(contacts) == 1
assert contacts[0].addr == ac1.get_config("addr")
assert reactions.get_by_contact(contacts[0]) == react_str
def test_import_export_online_all(acfactory, tmp_path, data, lp):
(ac1, some1) = acfactory.get_online_accounts(2)
@@ -2135,14 +2036,15 @@ def test_send_receive_locations(acfactory, lp):
assert chat1.is_sending_locations()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
# Wait for "enabled location streaming" message.
ac2._evtracker.wait_next_incoming_message()
# First location is sent immediately as a location-only message.
ac1.set_location(latitude=2.0, longitude=3.0, accuracy=0.5)
ac1._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
chat1.send_text("🍞")
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
lp.sec("ac2: wait for incoming location message")
# currently core emits location changed before event_incoming message
ac2._evtracker.get_matching("DC_EVENT_LOCATION_CHANGED")
locations = chat2.get_locations()
@@ -2151,7 +2053,7 @@ def test_send_receive_locations(acfactory, lp):
assert locations[0].longitude == 3.0
assert locations[0].accuracy == 0.5
assert locations[0].timestamp > now
assert locations[0].marker == "🍞"
assert locations[0].marker is None
contact = ac2.create_contact(ac1)
locations2 = chat2.get_locations(contact=contact)
@@ -2230,17 +2132,21 @@ def test_delete_multiple_messages(acfactory, lp):
def test_trash_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.stop_io()
# Create the Trash folder on IMAP server
# and recreate the account so Trash folder is configured.
# Create the Trash folder on IMAP server and configure deletion to it. There was a bug that if
# Trash wasn't configured initially, it can't be configured later, let's check this.
lp.sec("Creating trash folder")
ac2.direct_imap.create_folder("Trash")
lp.sec("Creating new accounts")
ac2 = acfactory.new_online_configuring_account(cloned_from=ac2)
acfactory.bring_accounts_online()
ac2.set_config("delete_to_trash", "1")
assert ac2.get_config("configured_trash_folder")
lp.sec("Check that Trash can be configured initially as well")
ac3 = acfactory.new_online_configuring_account(cloned_from=ac2)
acfactory.bring_accounts_online()
assert ac3.get_config("configured_trash_folder")
ac3.stop_io()
ac2.start_io()
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending 3 messages")
@@ -2255,6 +2161,9 @@ def test_trash_multiple_messages(acfactory, lp):
assert msg.text in texts
if text != "second":
to_delete.append(msg)
# ac2 has received some messages, this is impossible w/o the trash folder configured, let's
# check the configuration.
assert ac2.get_config("configured_trash_folder") == "Trash"
lp.sec("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1

View File

@@ -41,13 +41,12 @@ skipsdist = True
skip_install = True
deps =
ruff
black
# pygments required by rst-lint
pygments
restructuredtext_lint
commands =
black --quiet --check --diff setup.py src/deltachat examples/ tests/
ruff src/deltachat tests/ examples/
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
@@ -62,7 +61,8 @@ commands =
[testenv:doc]
changedir=doc
deps =
sphinx
# Pinned version, workaround for <https://github.com/breathe-doc/breathe/issues/981>
sphinx<7.3
breathe
sphinx_rtd_theme
commands =

View File

@@ -1 +1 @@
2024-04-05
2024-06-04

View File

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

View File

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

View File

@@ -66,7 +66,11 @@ def main():
parser = ArgumentParser(prog="set_core_version")
parser.add_argument("newversion")
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
json_list = [
"package.json",
"deltachat-jsonrpc/typescript/package.json",
"deltachat-rpc-server/npm-package/package.json",
]
toml_list = [
"Cargo.toml",
"deltachat-ffi/Cargo.toml",

View File

@@ -485,10 +485,6 @@ impl Config {
/// Read a configuration from the given file into memory.
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
let dir = file
.parent()
.context("Cannot get config file directory")?
.to_path_buf();
let mut config = Self::new_nosync(file, writable).await?;
let bytes = fs::read(&config.file)
.await
@@ -500,9 +496,13 @@ impl Config {
// Convert them to relative paths.
let mut modified = false;
for account in &mut config.inner.accounts {
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
account.dir = new_dir.to_path_buf();
modified = true;
if account.dir.is_absolute() {
if let Some(old_path_parent) = account.dir.parent() {
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
account.dir = new_path.to_path_buf();
modified = true;
}
}
}
}
if modified && writable {

View File

@@ -6,6 +6,7 @@ use std::collections::BTreeSet;
use std::fmt;
use anyhow::Result;
use deltachat_contact_tools::EmailAddress;
use mailparse::MailHeaderMap;
use mailparse::ParsedMail;
use once_cell::sync::Lazy;
@@ -13,8 +14,6 @@ use once_cell::sync::Lazy;
use crate::config::Config;
use crate::context::Context;
use crate::headerdef::HeaderDef;
use crate::tools::time;
use crate::tools::EmailAddress;
/// `authres` is short for the Authentication-Results header, defined in
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
@@ -29,45 +28,28 @@ pub(crate) async fn handle_authres(
context: &Context,
mail: &ParsedMail<'_>,
from: &str,
message_time: i64,
) -> Result<DkimResults> {
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
// This email is invalid, but don't return an error, we still want to
// add a stub to the database so that it's not downloaded again
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
}
};
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
update_authservid_candidates(context, &authres).await?;
compute_dkim_results(context, authres, &from_domain, message_time).await
compute_dkim_results(context, authres).await
}
#[derive(Debug)]
pub(crate) struct DkimResults {
/// Whether DKIM passed for this particular e-mail.
pub dkim_passed: bool,
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
/// i.e. whether we expect DKIM to work.
pub dkim_should_work: bool,
/// Whether changing the public Autocrypt key should be allowed.
/// This is false if we expected DKIM to work (dkim_works=true),
/// but it failed now (dkim_passed=false).
pub allow_keychange: bool,
}
impl fmt::Display for DkimResults {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(
fmt,
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
self.dkim_passed, self.dkim_should_work, self.allow_keychange
)?;
if !self.allow_keychange {
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
}
write!(fmt, "DKIM Results: Passed={}", self.dkim_passed)?;
Ok(())
}
}
@@ -218,10 +200,6 @@ async fn update_authservid_candidates(
context
.set_config_internal(Config::AuthservIdCandidates, Some(&new_config))
.await?;
// Updating the authservid candidates may mean that we now consider
// emails as "failed" which "passed" previously, so we need to
// reset our expectation which DKIMs work.
clear_dkim_works(context).await?
}
Ok(())
}
@@ -238,8 +216,6 @@ async fn update_authservid_candidates(
async fn compute_dkim_results(
context: &Context,
mut authres: ParsedAuthresHeaders,
from_domain: &str,
message_time: i64,
) -> Result<DkimResults> {
let mut dkim_passed = false;
@@ -272,71 +248,7 @@ async fn compute_dkim_results(
}
}
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
if message_time > last_working_timestamp && dkim_passed {
set_dkim_works_timestamp(context, from_domain, message_time).await?;
dkim_should_work = true;
}
Ok(DkimResults {
dkim_passed,
dkim_should_work,
allow_keychange: dkim_passed || !dkim_should_work,
})
}
/// Whether DKIM in emails from this domain should be considered to work.
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
// When we get an email with valid DKIM-Authentication-Results,
// then we assume that DKIM works for 30 days from this time on.
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
let dkim_ever_worked = last_working_timestamp > 0;
// We're using time() here and not the time when the message
// claims to have been sent (passed around as `message_time`)
// because otherwise an attacker could just put a time way
// in the future into the `Date` header and then we would
// assume that DKIM doesn't have to be valid anymore.
let dkim_should_work_now = should_work_until > time();
Ok(dkim_ever_worked && dkim_should_work_now)
}
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
let last_working_timestamp: i64 = context
.sql
.query_get_value(
"SELECT dkim_works FROM sending_domains WHERE domain=?",
(from_domain,),
)
.await?
.unwrap_or(0);
Ok(last_working_timestamp)
}
async fn set_dkim_works_timestamp(
context: &Context,
from_domain: &str,
timestamp: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
(from_domain, timestamp),
)
.await?;
Ok(())
}
async fn clear_dkim_works(context: &Context) -> Result<()> {
context
.sql
.execute("DELETE FROM sending_domains", ())
.await?;
Ok(())
Ok(DkimResults { dkim_passed })
}
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
@@ -349,19 +261,12 @@ fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str>
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]
use std::time::Duration;
use tokio::fs;
use tokio::io::AsyncReadExt;
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
use crate::securejoin::join_securejoin;
use crate::test_utils;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
@@ -574,33 +479,8 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from, time()).await?;
assert!(res.allow_keychange);
}
for entry in &dir {
let mut file = fs::File::open(entry.path()).await?;
bytes.clear();
file.read_to_end(&mut bytes).await.unwrap();
let mail = mailparse::parse_mail(&bytes)?;
let from = &mimeparser::get_from(&mail.headers).unwrap().addr;
let res = handle_authres(&t, &mail, from, time()).await?;
if !res.allow_keychange {
println!(
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
entry.path()
);
test_failed = true;
}
let res = handle_authres(&t, &mail, from).await?;
let from_domain = EmailAddress::new(from).unwrap().domain;
assert_eq!(
res.dkim_should_work,
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
);
assert_eq!(res.dkim_passed, res.dkim_should_work);
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
@@ -613,9 +493,8 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
if res.dkim_passed != expected_result {
if authres_parsing_works {
println!(
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
"!!!!!! FAILURE Receiving {:?} wrong result: !!!!!!",
entry.path(),
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
);
test_failed = true;
}
@@ -638,116 +517,7 @@ Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@
let bytes = b"From: invalid@from.com
Authentication-Results: dkim=";
let mail = mailparse::parse_mail(bytes).unwrap();
handle_authres(&t, &mail, "invalid@rom.com", time())
.await
.unwrap();
}
#[ignore = "Disallowing keychanges is disabled for now"]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_handle_authres_fails() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Bob sends Alice a message, so she gets his key
tcm.send_recv_accept(&bob, &alice, "Hi").await;
// We don't need bob anymore, let's make sure it's not accidentally used
drop(bob);
// Assume Alice receives an email from bob@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
// And Alice knows her server's authserv-id
alice
.set_config(Config::AuthservIdCandidates, Some("example.org"))
.await?;
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
// Sleep to make sure key reset is ignored because of DKIM failure
// and not because reordering is suspected.
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let bob2 = tcm.unconfigured().await;
bob2.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob2).await?;
let chat = bob2.create_chat(&alice).await;
let mut sent = bob2
.send_text(chat.id, "Please send me lots of money")
.await;
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
let received = alice.recv_msg(&sent).await;
// Assert that the error tells the user about the problem
assert!(received.error.unwrap().contains("DKIM failed"));
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
.await?
.unwrap();
// Encryption preference is still mutual.
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
// Also check that the keypair was not changed
assert_eq!(
bob_state.public_key.unwrap(),
test_utils::bob_keypair().public
);
// Since Alice didn't change the key, Bob can't read her message
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
join_securejoin(&bob2.ctx, &qr).await.unwrap();
loop {
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
sent.payload
.insert_str(0, "Authentication-Results: example.org; dkim=fail\n");
alice.recv_msg(&sent).await;
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
bob2.recv_msg(&sent).await;
} else {
break;
}
}
// Unfortunately, securejoin currently doesn't work with authres-checking,
// so these checks would fail:
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
// assert_eq!(
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
// assert_eq!(
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
// VerifiedStatus::BidirectVerified
// );
// // Bob can read Alice's messages again
// let received = tcm
// .try_send_recv(&alice, &bob2, "Can you read this again?")
// .await;
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
// assert!(received.error.is_none());
Ok(())
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -796,10 +566,7 @@ Authentication-Results: dkim=";
let alice = tcm.alice().await;
let bob = tcm.bob().await;
// Assume Bob received an email from something@example.net with
// correct DKIM -> `set_dkim_works()` was called
set_dkim_works_timestamp(&bob, "example.org", time()).await?;
// And Bob knows his server's authserv-id
// Bob knows his server's authserv-id
bob.set_config(Config::AuthservIdCandidates, Some("example.net"))
.await?;
@@ -821,15 +588,13 @@ Authentication-Results: dkim=";
.insert_str(0, "Authentication-Results: example.net; dkim=fail\n");
let rcvd = bob.recv_msg(&sent).await;
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(rcvd
.id
.get_info(&bob)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));
.contains("DKIM Results: Passed=false"));
Ok(())
}

View File

@@ -3,7 +3,7 @@
use core::cmp::max;
use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::io::{Cursor, Seek};
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
@@ -12,6 +12,7 @@ use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::codecs::jpeg::JpegEncoder;
use image::io::Reader as ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
@@ -426,9 +427,25 @@ impl<'a> BlobObject<'a> {
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
let (nr_bytes, exif) = self.metadata()?;
let mut file = std::fs::File::open(self.to_abs_path())?;
let (nr_bytes, exif) = image_metadata(&file)?;
*no_exif_ref = exif.is_none();
let mut img = image::open(&blob_abs).context("image decode failure")?;
// It's strange that BufReader modifies a file position while it takes a non-mut
// reference. Ok, just rewind it.
file.rewind()?;
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
let imgreader = match imgreader {
Ok(ir) => ir,
_ => {
file.rewind()?;
ImageReader::with_format(
std::io::BufReader::new(&file),
ImageFormat::from_path(&blob_abs)?,
)
}
};
let fmt = imgreader.format().context("No format??")?;
let mut img = imgreader.decode().context("image decode failure")?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
@@ -457,10 +474,9 @@ impl<'a> BlobObject<'a> {
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
let jpeg_quality = 75;
let fmt = ImageFormat::from_path(&blob_abs);
let ofmt = match fmt {
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
Ok(ImageFormat::Jpeg) => {
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
ImageFormat::Jpeg => {
add_white_bg = false;
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
@@ -497,7 +513,7 @@ impl<'a> BlobObject<'a> {
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() {
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
@@ -538,7 +554,7 @@ impl<'a> BlobObject<'a> {
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))
if !matches!(fmt, ImageFormat::Jpeg)
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
{
blob_abs = blob_abs.with_extension("jpg");
@@ -575,15 +591,14 @@ impl<'a> BlobObject<'a> {
}
}
}
}
/// 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 exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
/// Returns image file size and Exif.
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
let len = file.metadata()?.len();
let mut bufreader = std::io::BufReader::new(file);
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
Ok((len, exif))
}
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
@@ -698,7 +713,10 @@ fn encode_img(
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
ImageOutputFormat::Jpeg { quality } => {
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
img.write_with_encoder(encoder)?;
// Convert image into RGB8 to avoid the error
// "The encoder or decoder for Jpeg does not support the color type Rgba8"
// (<https://github.com/image-rs/image/issues/2211>).
img.clone().into_rgb8().write_with_encoder(encoder)?;
}
}
Ok(())
@@ -740,7 +758,6 @@ fn add_white_bg(img: &mut DynamicImage) {
#[cfg(test)]
mod tests {
use fs::File;
use image::Pixel;
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
@@ -1189,6 +1206,21 @@ mod tests {
.await
.unwrap();
send_image_check_mediaquality(
Viewtype::File,
Some("1"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
send_image_check_mediaquality(
Viewtype::Sticker,
@@ -1206,6 +1238,28 @@ mod tests {
.unwrap();
}
/// Tests that RGBA PNG can be recoded into JPEG
/// by dropping alpha channel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_rgba_png_to_jpeg() {
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
send_image_check_mediaquality(
Viewtype::Image,
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");
@@ -1267,8 +1321,7 @@ mod tests {
.context("failed to write file")?;
check_image_size(&file, original_width, original_height);
let blob = BlobObject::new_from_path(&alice, &file).await?;
let (_, exif) = blob.metadata()?;
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
if has_exif {
let exif = exif.unwrap();
assert_eq!(exif_orientation(&exif, &alice), orientation);
@@ -1283,26 +1336,68 @@ mod tests {
let alice_msg = alice.get_last_msg().await;
assert_eq!(alice_msg.get_width() as u32, compressed_width);
assert_eq!(alice_msg.get_height() as u32, compressed_height);
check_image_size(
alice_msg.get_file(&alice).unwrap(),
compressed_width,
compressed_height,
);
let file_saved = alice
.get_blobdir()
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
alice_msg.save_file(&alice, &file_saved).await?;
check_image_size(file_saved, compressed_width, compressed_height);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();
let file_saved = bob
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
if viewtype == Viewtype::File {
assert_eq!(file_saved.extension().unwrap(), extension);
let bytes1 = fs::read(&file_saved).await?;
assert_eq!(&bytes1, bytes);
}
let blob = BlobObject::new_from_path(&bob, &file).await?;
let (_, exif) = blob.metadata()?;
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert!(exif.is_none());
let img = check_image_size(file, compressed_width, compressed_height);
let img = check_image_size(file_saved, compressed_width, compressed_height);
Ok(img)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_big_gif_as_image() -> Result<()> {
let bytes = include_bytes!("../test-data/image/screenshot.gif");
let (width, height) = (1920u32, 1080u32);
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
alice
.set_config(
Config::MediaQuality,
Some(&(MediaQuality::Worse as i32).to_string()),
)
.await?;
let file = alice.get_blobdir().join("file").with_extension("gif");
fs::write(&file, &bytes)
.await
.context("failed to write file")?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
let bob_msg = bob.recv_msg(&sent).await;
// DC must detect the image as GIF and send it w/o reencoding.
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
assert_eq!(bob_msg.get_width() as u32, width);
assert_eq!(bob_msg.get_height() as u32, height);
let file_saved = bob
.get_blobdir()
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
bob_msg.save_file(&bob, &file_saved).await?;
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert_eq!(file_size, bytes.len() as u64);
check_image_size(file_saved, width, height);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_increation_in_blobdir() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -8,20 +8,23 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::{anyhow, bail, ensure, Context as _, Result};
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
use deltachat_derive::{FromSql, ToSql};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
use tokio::task;
use crate::aheader::EncryptPreference;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
self, Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK,
DC_CHAT_ID_LAST_SPECIAL, DC_CHAT_ID_TRASH, DC_RESEND_USER_AVATAR_DAYS,
};
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
use crate::contact::{self, Contact, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
@@ -36,6 +39,7 @@ use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::securejoin::BobState;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
use crate::stock_str;
@@ -43,7 +47,7 @@ use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
smeared_time, strip_rtlo_characters, time, IsNoneOrEmpty, SystemTime,
smeared_time, time, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::WEBXDC_SUFFIX;
@@ -124,6 +128,10 @@ pub(crate) enum CantSendReason {
/// Not a member of the chat.
NotAMember,
/// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending
/// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed.
SecurejoinWait,
}
impl fmt::Display for CantSendReason {
@@ -143,6 +151,7 @@ impl fmt::Display for CantSendReason {
write!(f, "mailing list does not have a know post address")
}
Self::NotAMember => write!(f, "not a member of the chat"),
Self::SecurejoinWait => write!(f, "awaiting SecureJoin for 1:1 chat"),
}
}
}
@@ -209,24 +218,11 @@ impl ChatId {
}
/// Returns [`ChatId`] of a chat that `msg` belongs to.
///
/// Checks that `msg` is assigned to the right chat.
pub(crate) fn lookup_by_message(msg: &Message) -> Option<Self> {
if msg.chat_id == DC_CHAT_ID_TRASH {
return None;
}
if msg.download_state != DownloadState::Done
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
// `DownloadState::Undecipherable`. Remove eventually with the comment in
// `MimeMessage::from_bytes()`.
|| msg
.error
.as_ref()
.filter(|e| e.starts_with("Decrypting failed:"))
.is_some()
{
// If `msg` is not fully downloaded or undecipherable, it may have been assigned to the
// wrong chat (they often get assigned to the 1:1 chat with the sender).
if msg.download_state == DownloadState::Undecipherable {
return None;
}
Some(msg.chat_id)
@@ -296,7 +292,7 @@ impl ChatId {
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
.await
.map(|chat| chat.id)?;
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
chat_id
} else {
warn!(
@@ -308,6 +304,8 @@ impl ChatId {
}
};
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(chat_id)
}
@@ -424,6 +422,7 @@ impl ChatId {
}
}
}
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
// NB: For a 1:1 chat this currently triggers `Contact::block()` on other devices.
@@ -446,6 +445,8 @@ impl ChatId {
pub(crate) async fn unblock_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
self.set_blocked(context, Blocked::Not).await?;
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
// TODO: For a 1:1 chat this currently triggers `Contact::unblock()` on other devices.
@@ -456,6 +457,7 @@ impl ChatId {
.log_err(context)
.ok();
}
Ok(())
}
@@ -487,7 +489,7 @@ impl ChatId {
// went to "contact requests" list rather than normal chatlist.
for contact_id in get_chat_contacts(context, self).await? {
if contact_id != ContactId::SELF {
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
.await?;
}
}
@@ -499,6 +501,7 @@ impl ChatId {
if self.set_blocked(context, Blocked::Not).await? {
context.emit_event(EventType::ChatModified(self));
chatlist_events::emit_chatlist_item_changed(context, self);
}
if sync.into() {
@@ -541,6 +544,7 @@ impl ChatId {
.await?;
context.emit_event(EventType::ChatModified(self));
chatlist_events::emit_chatlist_item_changed(context, self);
// make sure, the receivers will get all keys
self.reset_gossiped_timestamp(context).await?;
@@ -589,6 +593,7 @@ impl ChatId {
if protection_status_modified {
self.add_protection_msg(context, protect, contact_id, timestamp_sort)
.await?;
chatlist_events::emit_chatlist_item_changed(context, self);
}
Ok(())
}
@@ -612,7 +617,10 @@ impl ChatId {
let sort_to_bottom = true;
let ts = self
.calc_sort_timestamp(context, timestamp_sent, sort_to_bottom, false)
.await?;
.await?
// Always sort protection messages below `SystemMessage::SecurejoinWait{,Timeout}` ones
// in case of race conditions.
.saturating_add(1);
self.set_protection_for_timestamp_sort(context, protect, ts, contact_id)
.await
}
@@ -675,6 +683,8 @@ impl ChatId {
.await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, self);
if sync.into() {
let chat = Chat::load_from_db(context, self).await?;
@@ -781,6 +791,7 @@ impl ChatId {
.await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
context
.set_config_internal(Config::LastHousekeeping, None)
@@ -792,6 +803,7 @@ impl ChatId {
msg.text = stock_str::self_deleted_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
chatlist_events::emit_chatlist_changed(context);
Ok(())
}
@@ -1172,6 +1184,15 @@ impl ChatId {
Ok(self.get_param(context).await?.exists(Param::Devicetalk))
}
/// Returns chat member list timestamp.
pub(crate) async fn get_member_list_timestamp(self, context: &Context) -> Result<i64> {
Ok(self
.get_param(context)
.await?
.get_i64(Param::MemberListTimestamp)
.unwrap_or_default())
}
async fn parent_query<T, F>(
self,
context: &Context,
@@ -1396,6 +1417,18 @@ impl ChatId {
Ok(sort_timestamp)
}
/// Spawns a task checking after a timeout whether the SecureJoin has finished for the 1:1 chat
/// and otherwise notifying the user accordingly.
pub(crate) fn spawn_securejoin_wait(self, context: &Context, timeout: u64) {
let context = context.clone();
task::spawn(async move {
tokio::time::sleep(Duration::from_secs(timeout)).await;
let chat = Chat::load_from_db(&context, self).await?;
chat.check_securejoin_wait(&context, 0).await?;
Result::<()>::Ok(())
});
}
}
impl std::fmt::Display for ChatId {
@@ -1430,7 +1463,7 @@ impl rusqlite::types::ToSql for ChatId {
impl rusqlite::types::FromSql for ChatId {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
i64::column_result(value).and_then(|val| {
if 0 <= val && val <= i64::from(std::u32::MAX) {
if 0 <= val && val <= i64::from(u32::MAX) {
Ok(ChatId::new(val as u32))
} else {
Err(rusqlite::types::FromSqlError::OutOfRange(val))
@@ -1518,7 +1551,7 @@ impl Chat {
Ok(contacts) => {
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
chat_name = contact.get_display_name().to_owned();
contact.get_display_name().clone_into(&mut chat_name);
}
}
}
@@ -1575,6 +1608,12 @@ impl Chat {
Some(ReadOnlyMailingList)
} else if !self.is_self_in_chat(context).await? {
Some(NotAMember)
} else if self
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?
> 0
{
Some(SecurejoinWait)
} else {
None
};
@@ -1588,6 +1627,69 @@ impl Chat {
Ok(self.why_cant_send(context).await?.is_none())
}
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
///
/// If the timeout has expired, notifies the user that sending messages is possible. See also
/// [`CantSendReason::SecurejoinWait`].
pub(crate) async fn check_securejoin_wait(
&self,
context: &Context,
timeout: u64,
) -> Result<u64> {
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
return Ok(0);
}
let (mut param0, mut param1) = (Params::new(), Params::new());
param0.set_cmd(SystemMessage::SecurejoinWait);
param1.set_cmd(SystemMessage::SecurejoinWaitTimeout);
let (param0, param1) = (param0.to_string(), param1.to_string());
let Some((param, ts_sort, ts_start)) = context
.sql
.query_row_optional(
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
(self.id, &param0, &param1),
|row| {
let param: String = row.get(0)?;
let ts_sort: i64 = row.get(1)?;
let ts_start: i64 = row.get(2)?;
Ok((param, ts_sort, ts_start))
},
)
.await?
else {
return Ok(0);
};
if param == param1 {
return Ok(0);
}
let now = time();
// Don't await SecureJoin if the clock was set back.
if ts_start <= now {
let timeout = ts_start
.saturating_add(timeout.try_into()?)
.saturating_sub(now);
if timeout > 0 {
return Ok(timeout as u64);
}
}
add_info_msg_with_cmd(
context,
self.id,
&stock_str::securejoin_wait_timeout(context).await,
SystemMessage::SecurejoinWaitTimeout,
// Use the sort timestamp of the "please wait" message, this way the added message is
// never sorted below the protection message if the SecureJoin finishes in parallel.
ts_sort,
Some(now),
None,
None,
)
.await?;
context.emit_event(EventType::ChatModified(self.id));
Ok(0)
}
/// Checks if the user is part of a chat
/// and has basically the permissions to edit the chat therefore.
/// The function does not check if the chat type allows editing of concrete elements.
@@ -1830,6 +1932,10 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
let is_bot = context.get_config_bool(Config::Bot).await?;
msg.param
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
// Set "References:" to identify the "thread" of the conversation.
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
@@ -2038,6 +2144,7 @@ impl Chat {
msg.id = MsgId::new(u32::try_from(raw_id)?);
maybe_set_logging_xdc(context, msg, self.id).await?;
context.update_webxdc_integration_database(msg).await?;
}
context.scheduler.interrupt_ephemeral_task().await;
Ok(msg.id)
@@ -2223,8 +2330,9 @@ pub struct ChatInfo {
}
pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()> {
// if there is no saved-messages chat, there is nothing to update. this is no error.
if let Some(chat_id) = ChatId::lookup_by_contact(context, ContactId::SELF).await? {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::SELF).await?
{
let icon = include_bytes!("../assets/icon-saved-messages.png");
let blob = BlobObject::create(context, "icon-saved-messages.png", icon).await?;
let icon = blob.as_name().to_string();
@@ -2237,8 +2345,9 @@ pub(crate) async fn update_saved_messages_icon(context: &Context) -> Result<()>
}
pub(crate) async fn update_device_icon(context: &Context) -> Result<()> {
// if there is no device-chat, there is nothing to update. this is no error.
if let Some(chat_id) = ChatId::lookup_by_contact(context, ContactId::DEVICE).await? {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, ContactId::DEVICE).await?
{
let icon = include_bytes!("../assets/icon-device.png");
let blob = BlobObject::create(context, "icon-device.png", icon).await?;
let icon = blob.as_name().to_string();
@@ -2289,7 +2398,9 @@ async fn update_special_chat_name(
contact_id: ContactId,
name: String,
) -> Result<()> {
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
if let Some(ChatIdBlocked { id: chat_id, .. }) =
ChatIdBlocked::lookup_by_contact(context, contact_id).await?
{
// the `!= name` condition avoids unneeded writes
context
.sql
@@ -2318,6 +2429,26 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<()> {
Ok(())
}
/// Checks if there is a 1:1 chat in-progress SecureJoin for Bob and, if necessary, schedules a task
/// unblocking the chat and notifying the user accordingly.
pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
let Some(bobstate) = BobState::from_db(&context.sql).await? else {
return Ok(());
};
if !bobstate.in_progress() {
return Ok(());
}
let chat_id = bobstate.alice_chat();
let chat = Chat::load_from_db(context, chat_id).await?;
let timeout = chat
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
.await?;
if timeout > 0 {
chat_id.spawn_securejoin_wait(context, timeout);
}
Ok(())
}
/// Handle a [`ChatId`] and its [`Blocked`] status at once.
///
/// This struct is an optimisation to read a [`ChatId`] and its [`Blocked`] status at once
@@ -2492,10 +2623,36 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.get_blob(Param::File, context, !msg.is_increation())
.await?
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
let send_as_is = msg.viewtype == Viewtype::File;
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
// formats as GIF or VOICE.
//
// Typical conversions:
// - from FILE to AUDIO/VIDEO/IMAGE
// - from FILE/IMAGE to GIF */
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(&blob.to_abs_path())
{
if better_type != Viewtype::Webxdc
|| context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await
.is_ok()
{
msg.viewtype = better_type;
}
}
} else if msg.viewtype == Viewtype::Webxdc {
context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await?;
}
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
if msg.viewtype == Viewtype::Image
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
if !send_as_is
&& (msg.viewtype == Viewtype::Image
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
{
blob.recode_to_image_size(context, &mut maybe_sticker)
.await?;
@@ -2514,34 +2671,6 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
.set(Param::Filename, stem.to_string() + "." + blob_ext);
}
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
// Correct the type, take care not to correct already very special
// formats as GIF or VOICE.
//
// Typical conversions:
// - from FILE to AUDIO/VIDEO/IMAGE
// - from FILE/IMAGE to GIF */
if let Some((better_type, better_mime)) =
message::guess_msgtype_from_suffix(&blob.to_abs_path())
{
if better_type != Viewtype::Webxdc
|| context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await
.is_ok()
{
msg.viewtype = better_type;
if !msg.param.exists(Param::MimeType) {
msg.param.set(Param::MimeType, better_mime);
}
}
}
} else if msg.viewtype == Viewtype::Webxdc {
context
.ensure_sendable_webxdc_file(&blob.to_abs_path())
.await?;
}
if !msg.param.exists(Param::MimeType) {
if let Some((_, mime)) = message::guess_msgtype_from_suffix(&blob.to_abs_path()) {
msg.param.set(Param::MimeType, mime);
@@ -2575,7 +2704,9 @@ async fn prepare_msg_common(
if let Some(reason) = chat.why_cant_send(context).await? {
if matches!(
reason,
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest
CantSendReason::ProtectionBroken
| CantSendReason::ContactRequest
| CantSendReason::SecurejoinWait
) && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification.
@@ -2585,6 +2716,18 @@ async fn prepare_msg_common(
}
}
// Check a quote reply is not leaking data from other chats.
// This is meant as a last line of defence, the UI should check that before as well.
// (We allow Chattype::Single in general for "Reply Privately";
// checking for exact contact_id will produce false positives when ppl just left the group)
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
if let Some(quoted_message) = msg.quoted_message(context).await? {
if quoted_message.chat_id != chat_id {
bail!("Bad quote reply");
}
}
}
// check current MessageState for drafts (to keep msg_id) ...
let update_msg_id = if msg.state == MessageState::OutDraft {
msg.hidden = false;
@@ -2698,10 +2841,12 @@ async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -
}
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
context.emit_msgs_changed(msg.chat_id, msg.id);
if !msg.hidden {
context.emit_msgs_changed(msg.chat_id, msg.id);
}
if msg.param.exists(Param::SetLatitude) {
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
context.emit_location_changed(Some(ContactId::SELF)).await?;
}
context.scheduler.interrupt_smtp().await;
@@ -2768,6 +2913,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
recipients.push(from);
}
// Webxdc integrations are messages, however, shipped with main app and must not be sent out
if msg.param.get_int(Param::WebxdcIntegration).is_some() {
recipients.clear();
}
if recipients.is_empty() {
// may happen eg. for groups with only SELF and bcc_self disabled
info!(
@@ -2802,29 +2952,28 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
);
}
let now = time();
let now = smeared_time(context);
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
if rendered_msg.is_group {
if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
// Reject member list synchronisation from older messages. See also
// `receive_imf::apply_group_changes()`.
msg.chat_id
.update_timestamp(context, Param::MemberListTimestamp, now)
.update_timestamp(
context,
Param::MemberListTimestamp,
now.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
)
.await?;
}
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if !msg.hidden {
if let Err(err) =
location::set_msg_location_id(context, msg.id, last_added_location_id).await
{
error!(context, "Failed to set msg_location_id: {err:#}.");
}
}
}
if let Some(sync_ids) = rendered_msg.sync_ids_to_delete {
@@ -2844,13 +2993,9 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
msg.update_param(context).await?;
}
msg.subject = rendered_msg.subject.clone();
msg.subject.clone_from(&rendered_msg.subject);
msg.update_subject(context).await?;
let chunk_size = context
.get_configured_provider()
.await?
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
let chunk_size = context.get_max_smtp_rcpt_to().await?;
let trans_fn = |t: &mut rusqlite::Transaction| {
let mut row_ids = Vec::<i64>::new();
for recipients_chunk in recipients.chunks(chunk_size) {
@@ -3099,7 +3244,9 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
.await?;
for chat_id_in_archive in chat_ids_in_archive {
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
}
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
} else {
let exists = context
.sql
@@ -3126,6 +3273,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
}
context.emit_event(EventType::MsgsNoticed(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -3193,6 +3341,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
for c in changed_chats {
context.emit_event(EventType::MsgsNoticed(c));
chatlist_events::emit_chatlist_item_changed(context, c);
}
Ok(())
@@ -3355,6 +3504,8 @@ pub async fn create_group_chat(
}
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if protect == ProtectionStatus::Protected {
chat_id
@@ -3442,11 +3593,14 @@ pub(crate) async fn create_broadcast_list_ex(
let chat_id = ChatId::new(u32::try_from(row_id)?);
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
if sync.into() {
let id = SyncId::Grpid(grpid);
let action = SyncAction::CreateBroadcast(chat_name);
self::sync(context, id, action).await.log_err(context).ok();
}
Ok(chat_id)
}
@@ -3717,6 +3871,7 @@ pub(crate) async fn set_muted_ex(
.await
.context(format!("Failed to set mute duration for {chat_id}"))?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
if sync.into() {
let chat = Chat::load_from_db(context, chat_id).await?;
chat.sync(context, SyncAction::SetMuted(duration))
@@ -3877,6 +4032,7 @@ async fn rename_ex(
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
success = true;
}
}
@@ -3937,6 +4093,7 @@ pub async fn set_chat_profile_image(
context.emit_msgs_changed(chat_id, msg.id);
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
Ok(())
}
@@ -4083,6 +4240,8 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msg_id: msg.id,
});
msg.timestamp_sort = create_smeared_timestamp(context);
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}
@@ -4436,9 +4595,10 @@ impl Context {
}
_ => (),
}
ChatId::lookup_by_contact(self, contact_id)
ChatIdBlocked::lookup_by_contact(self, contact_id)
.await?
.with_context(|| format!("No chat for addr '{addr}'"))?
.id
}
SyncId::Grpid(grpid) => {
if let SyncAction::CreateBroadcast(name) = action {
@@ -4685,6 +4845,59 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quote_replies() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let grp_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
let grp_msg_id = send_text_msg(&alice, grp_chat_id, "bar".to_string()).await?;
let grp_msg = Message::load_from_db(&alice, grp_msg_id).await?;
let one2one_chat_id = alice.create_chat(&bob).await.id;
let one2one_msg_id = send_text_msg(&alice, one2one_chat_id, "foo".to_string()).await?;
let one2one_msg = Message::load_from_db(&alice, one2one_msg_id).await?;
// quoting messages in same chat is okay
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
let one2one_quote_reply_msg_id = result.unwrap();
// quoting messages from groups to one-to-ones is okay ("reply privately")
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&grp_msg)).await?;
let result = send_msg(&alice, one2one_chat_id, &mut msg).await;
assert!(result.is_ok());
// quoting messages from one-to-one chats in groups is an error; usually this is also not allowed by UI at all ...
let mut msg = Message::new(Viewtype::Text);
msg.set_text("baz".to_string());
msg.set_quote(&alice, Some(&one2one_msg)).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_err());
// ... but forwarding messages with quotes is allowed
let result = forward_msgs(&alice, &[one2one_quote_reply_msg_id], grp_chat_id).await;
assert!(result.is_ok());
// ... and bots are not restricted
alice.set_config(Config::Bot, Some("1")).await?;
let result = send_msg(&alice, grp_chat_id, &mut msg).await;
assert!(result.is_ok());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_contact_to_chat_ex_add_self() {
// Adding self to a contact should succeed, even though it's pointless.
@@ -4775,9 +4988,9 @@ mod tests {
Ok(())
}
/// Test simultaneous removal of user from the chat and leaving the group.
/// Test parallel removal of user from the chat and leaving the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_simultaneous_member_remove() -> Result<()> {
async fn test_parallel_member_remove() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -4808,20 +5021,25 @@ mod tests {
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
let alice_sent_add_msg = alice.pop_sent_msg().await;
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let alice_sent_remove_msg = alice.pop_sent_msg().await;
// Bob leaves the chat.
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
// Bob receives a msg about Alice adding Claire to the group.
bob.recv_msg(&alice_sent_add_msg).await;
SystemTime::shift(Duration::from_secs(3600));
// This adds Bob because they left quite long ago.
let alice_sent_msg = alice.send_text(alice_chat_id, "What a silence!").await;
bob.recv_msg(&alice_sent_msg).await;
// Test that add message is rewritten.
bob.golden_test_chat(bob_chat_id, "chat_test_simultaneous_member_remove")
bob.golden_test_chat(bob_chat_id, "chat_test_parallel_member_remove")
.await;
// Alice removes Bob from the chat.
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let alice_sent_remove_msg = alice.pop_sent_msg().await;
// Bob receives a msg about Alice removing him from the group.
let bob_received_remove_msg = bob.recv_msg(&alice_sent_remove_msg).await;
@@ -4858,8 +5076,13 @@ mod tests {
bob.recv_msg(&sent_msg).await;
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
// This doesn't add Fiona back because Bob just removed them.
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
bob.recv_msg(&sent_msg).await;
SystemTime::shift(Duration::from_secs(3600));
let sent_msg = alice.send_text(alice_chat_id, "Welcome back, Fiona!").await;
bob.recv_msg(&sent_msg).await;
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
.await;
Ok(())
@@ -4989,6 +5212,32 @@ mod tests {
Ok(())
}
/// Tests that if member added message is completely lost,
/// member is eventually added.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lost_member_added() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "Group", &[bob])
.await;
let alice_sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&alice_sent).await.chat_id;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
// Attempt to add member, but message is lost.
let claire_id = Contact::create(alice, "", "claire@foo.de").await?;
add_contact_to_chat(alice, alice_chat_id, claire_id).await?;
alice.pop_sent_msg().await;
let alice_sent = alice.send_text(alice_chat_id, "Hi again!").await;
bob.recv_msg(&alice_sent).await;
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
Ok(())
}
/// Test that group updates are robust to lost messages and eventual out of order arrival.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_lost() -> Result<()> {
@@ -7279,4 +7528,27 @@ mod tests {
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
Ok(())
}
/// Tests sending JPEG image with .png extension.
///
/// This is a regression test, previously sending failed
/// because image was passed to PNG decoder
/// and it failed to decode image.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_jpeg_with_png_ext() -> Result<()> {
let alice = TestContext::new_alice().await;
let bob = TestContext::new_bob().await;
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
let file = alice.get_blobdir().join("screenshot.png");
tokio::fs::write(&file, bytes).await?;
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let alice_chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
let _msg = bob.recv_msg(&sent_msg).await;
Ok(())
}
}

View File

@@ -416,7 +416,7 @@ impl Chatlist {
if chat.id.is_archived_link() {
Ok(Default::default())
} else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await
Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
} else {
Ok(Summary {
text: stock_str::no_messages(context).await,

View File

@@ -31,10 +31,11 @@ fn rgb_to_u32((r, g, b): (f64, f64, f64)) -> u32 {
///
/// Saturation is set to maximum (100.0) to make colors distinguishable, and lightness is set to
/// half (50.0) to make colors suitable both for light and dark theme.
pub(crate) fn str_to_color(s: &str) -> u32 {
pub fn str_to_color(s: &str) -> u32 {
rgb_to_u32(hsluv_to_rgb((str_to_angle(s), 100.0, 50.0)))
}
/// Returns color as a "#RRGGBB" `String` where R, G, B are hex digits.
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{color:#08x}").replace("0x", "#")
}

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