Compare commits

...

409 Commits

Author SHA1 Message Date
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
B. Petersen
e43ffb20a1 chore(release): prepare for 1.137.2 2024-04-05 14:21:15 +00:00
link2xt
2f0f247e70 refactor: use Rust 1.77.0 support for recursion in async functions 2024-04-04 17:01:15 +00:00
Simon Laux
5bda4f0c26 update node constants for #5387 (#5429) 2024-04-04 15:31:02 +02:00
iequidoo
d39c8a3a19 refactor: is_probably_private_reply: Remove reaction-specific code
Instead, look up the 1:1 chat in `receive_imf::add_parts()`. This is a more generic approach to fix
assigning outgoing reactions to 1:1 chats in the multi-device setup. Although currently both
approaches give the same result, this way we can even implement a "react privately"
functionality. Maybe it sounds useless, but it seems better to have less reaction-specific code.
2024-04-03 21:29:27 -03:00
link2xt
e465415039 fix: do not ignore Message::load_from_db errors 2024-04-04 01:44:50 +02:00
B. Petersen
5cef77b8e6 fix: do not show empty summary if message reacted to is deleted
we checked for tombstones already using `is_trash()`,
however, we've overseen that tombstones get deleted at some point :)

therefore, just do not treat loading failures of the weak msg_id as errors -
usually, they are not - and if, just the normal summary is shown.
in theory, we could check for existance explicitly before tryong load_from_db,
however, that would be additional code (and maybe another database call)
and not worth the effort.

anyways, this commit also adds an explicit test
for physical deletion after housekeeping.
2024-04-04 01:44:50 +02:00
dependabot[bot]
60e733c30c chore(cargo): bump fast-socks5 from 0.9.5 to 0.9.6
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.9.5 to 0.9.6.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/commits/v0.9.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 21:51:57 +00:00
dependabot[bot]
8b98816eb9 Merge pull request #5404 from deltachat/dependabot/cargo/rustyline-14.0.0 2024-04-03 21:19:38 +00:00
link2xt
50165b3e35 chore: upgrade image from 0.24.9 to 0.25.1 2024-04-03 20:59:06 +00:00
link2xt
0be8b5a5c4 chore: upgrade h2 from 0.4.3 to 0.4.4
This upgrade contains a fix for RUSTSEC-2024-0332
2024-04-03 20:34:14 +00:00
B. Petersen
451bb6e9db add tests for get_summary_text_without_prefix() 2024-04-03 20:55:22 +02:00
B. Petersen
83196d4cb5 add get_summary_text_without_prefix()
use get_summary_text_without_prefix() to get raw summaries without prefixes,
this is needed for reaction summaries,
where we also do not show the name, so we do not want to show "Forwarded" as well.
2024-04-03 20:55:22 +02:00
B. Petersen
0003e55ad5 test reactions for forwarded messages 2024-04-03 20:55:22 +02:00
link2xt
02014eda6c chore: update from brotli 3.4.0 to brotli 4.0.0 2024-04-03 17:25:28 +00:00
link2xt
f1c6cd69e9 chore: update chrono from 0.4.34 to 0.4.37 2024-04-03 16:49:19 +00:00
bjoern
ace281ff6c feat: show reactions in summaries (#5387)
shows the last reaction in chatlist's summaries if there is no
newer message.

the reason to show reactions in the summary, is to make them a _little_
more visible when one is not in the chat. esp. in not-so-chatty or in
one-to-ones chats this becomes handy: imaging a question and someone
"answers" with "thumbs up" ... 

otoh, reactions are still tuned down on purpose: no notifications, chats
are opend as usual, the chatlist is not sorted by reactions and also the
date in the summary refer to the last message - i thought quite a bit
about that, this seems to be good compromise and will raise the fewest
questions. it is somehow clear to the users that reactions are not the
same as a real message. also, it is comparable easy to implement - no
UI changes required :)

all that is very close to what whatsapp is doing (figured that out by
quite some testing ... to cite @adbenitez: if in doubt, we can blame
whatsapp :)

technically, i first wanted to go for the "big solution" and add two
more columns, chat_id and timestamp, however, it seemed a bit bloated if
we really only need the last one. therefore, i just added the last
reaction information to the chat's param, which seems more performant
but also easier to code :)
2024-04-03 08:50:05 +00:00
dependabot[bot]
c9edd525e0 chore(cargo): bump rustyline from 13.0.0 to 14.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 13.0.0 to 14.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v13.0.0...v14.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-03 06:12:01 +00:00
link2xt
3f35b442c3 chore(release): prepare for 1.137.1 2024-04-03 01:28:13 +00:00
link2xt
87e9365016 ci: remove android builds for x86 and x86_64
They are failing to build.
2024-04-03 01:17:21 +00:00
link2xt
9806509f4a chore(release): prepare for 1.137.0 2024-04-02 21:22:58 +00:00
link2xt
91600a34b6 chore(cargo): update aho-corasick from 1.1.2 to 1.1.3 2024-04-02 21:11:04 +00:00
dependabot[bot]
d16351d207 chore(cargo): bump regex from 1.10.3 to 1.10.4
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.3 to 1.10.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.3...1.10.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 21:03:12 +00:00
dependabot[bot]
4caf638201 chore(cargo): bump backtrace from 0.3.69 to 0.3.71
Bumps [backtrace](https://github.com/rust-lang/backtrace-rs) from 0.3.69 to 0.3.71.
- [Release notes](https://github.com/rust-lang/backtrace-rs/releases)
- [Commits](https://github.com/rust-lang/backtrace-rs/compare/0.3.69...0.3.71)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 20:59:21 +00:00
B. Petersen
375fcbd63c reactions are not a 'probably private reply'
`is_probably_private_reply()` checks
if a message should better go to the one to one chat
instead of the chat already identified by the `In-Reply-To`.
this functionality is needed to make "Reply Privately" work.

however, this functionality is never true for reactions.
if we would return `true` here, own reactions seen by a second device
would not get the correct chat assiged.
2024-04-02 22:33:36 +02:00
B. Petersen
6ff3a2cf7c add failing test for #5418 (wrong DC_EVENT_REACTIONS_CHANGED) 2024-04-02 22:33:36 +02:00
link2xt
a890fe3a9a chore(cargo): bump reqwest from 0.11.24 to 0.12.2
This also removes duplicate dependencies
for `http-body`, `http` and `hyper`.
2024-04-02 18:58:13 +00:00
link2xt
2b8bf29fce api(deltachat-rpc-client): add futures
futures allow to call multiple methods in parallel
without threads.

This introduces RpcFuture class and futuremethod decorator.
2024-04-02 16:54:25 +00:00
link2xt
26400a9e4e chore: update deny.toml 2024-04-02 16:39:45 +00:00
dependabot[bot]
f8b9bb9083 chore(cargo): bump axum from 0.7.4 to 0.7.5
Bumps [axum](https://github.com/tokio-rs/axum) from 0.7.4 to 0.7.5.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.7.4...axum-v0.7.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 16:35:51 +00:00
dependabot[bot]
42f9047a54 chore(cargo): bump futures-lite from 2.2.0 to 2.3.0
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v2.2.0...v2.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 16:33:31 +00:00
dependabot[bot]
6433a3a5f3 chore(cargo): bump syn from 2.0.52 to 2.0.57
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.52 to 2.0.57.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.52...2.0.57)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 16:32:58 +00:00
dependabot[bot]
4b6a03c904 chore(cargo): bump anyhow from 1.0.80 to 1.0.81
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.80...1.0.81)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 16:31:50 +00:00
dependabot[bot]
ff3df01d98 chore(cargo): bump serde_json from 1.0.114 to 1.0.115
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.114 to 1.0.115.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.114...v1.0.115)

---
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-04-02 16:30:56 +00:00
dependabot[bot]
cdc99854b2 chore(cargo): bump strum_macros from 0.26.1 to 0.26.2
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.26.1 to 0.26.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.26.1...v0.26.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 16:29:31 +00:00
dependabot[bot]
e7072bcb75 chore(cargo): bump async-smtp from 0.9.0 to 0.9.1
Bumps [async-smtp](https://github.com/async-email/async-smtp) from 0.9.0 to 0.9.1.
- [Commits](https://github.com/async-email/async-smtp/compare/v0.9.0...v0.9.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:46:20 +00:00
dependabot[bot]
7950bde3c6 chore(cargo): bump smallvec from 1.13.1 to 1.13.2
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.13.1 to 1.13.2.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.13.1...v1.13.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 13:18:17 +00:00
dependabot[bot]
a259669c98 chore(cargo): bump tokio-stream from 0.1.14 to 0.1.15
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.14 to 0.1.15.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.14...tokio-stream-0.1.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 13:11:11 +00:00
dependabot[bot]
603e6be9b4 chore(cargo): bump toml from 0.8.10 to 0.8.12
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.10 to 0.8.12.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.10...toml-v0.8.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 13:00:15 +00:00
dependabot[bot]
a78c484467 chore(cargo): bump uuid from 1.7.0 to 1.8.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.7.0...1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 12:58:52 +00:00
dependabot[bot]
e78f07b343 chore(cargo): bump strum from 0.26.1 to 0.26.2
Bumps [strum](https://github.com/Peternator7/strum) from 0.26.1 to 0.26.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.26.1...v0.26.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 12:55:39 +00:00
dependabot[bot]
8abf10aacb chore(cargo): bump pin-project from 1.1.4 to 1.1.5
Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.1.4 to 1.1.5.
- [Release notes](https://github.com/taiki-e/pin-project/releases)
- [Changelog](https://github.com/taiki-e/pin-project/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/pin-project/compare/v1.1.4...v1.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 12:53:42 +00:00
dependabot[bot]
2fef4acdd6 chore(cargo): bump tokio from 1.36.0 to 1.37.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.36.0...tokio-1.37.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 12:49:13 +00:00
Sebastian Klähn
de27be3a36 build: add development shell (#5390)
Add nix development shell to flake.nix.

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2024-04-02 11:20:34 +02:00
dependabot[bot]
c62e8539a1 chore(cargo): bump thiserror from 1.0.57 to 1.0.58
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.57 to 1.0.58.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.57...1.0.58)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 04:49:18 +00:00
link2xt
22c0aef9c0 build(python): remove setuptools_scm dependency
We update version in several .toml and .json files
on every release anyway, so updating pyproject.toml is easy.

setuptools_scm makes it more difficult to build
python packages for software distributions
because it requires full git checkout
with tags rather than just a worktree.
It is also possible to remove or move tags
after the release, so git revision no longer
pins python package version if setuptools_scm is used.
2024-04-02 04:43:39 +00:00
link2xt
87805bc36d build: add repository to Cargo.toml 2024-03-31 22:40:01 +00:00
B. Petersen
99c4d24eab cleanup jobs and Params relicts
- the `jobs` table is no longer in use,
  no need to track files on housekeeping,
  no need to clear it from repl tool

- some `Params` were used for jobs table only,
  they can be used freely for other purposes on other tables.
  param 'protection settings timestamp' was never used in practise,
  its code is removed as well, so we can free the Param as well.
2024-03-30 08:10:57 +01:00
bjoern
7bf9c4a2d9 api: remove unused dc_accounts_all_work_done() (#5384)
it was used by iOS to know when a background fetch was complete;
meanwhile the superiour `dc_accounts_background_fetch()` is used for
that.

there is still the corresponding context function `dc_all_work_done()`,
this not used by any UI as well, however, it is in use by a python
tests.

not sure, what to do with it, at a first glance, the test still seems
useful.
2024-03-30 01:18:17 +01:00
iequidoo
304e902fce fix: Don't send selfavatar in SecureJoin messages before contact verification (#5354)
Don't attach selfavatar in "v{c,g}-request" and "v{c,g}-auth-required" messages:
- These messages are deleted right after processing, so other devices won't see the avatar.
- It's also good for privacy because the contact isn't yet verified and these messages are auto-sent
  unlike usual unencrypted messages.
2024-03-28 21:29:05 -03:00
B. Petersen
0155d93622 test: Remove flaky time check from test_list_from()
the rendered time output seems different on different systems and timezomes,
eg. on my local machine, it is
`Sent: 2024.03.20 10:00:01 ` and not `Sent: 2024.03.20 09:00:01`,
maybe because of winter/summer time, idk.

as the gist of the test is to check the name,
however, i just removed the whole time check.
2024-03-28 16:40:41 +01:00
link2xt
ebd097bdbe ci: shorter names for deltachat-rpc-server jobs 2024-03-28 06:57:44 +00:00
link2xt
a11d01f8a3 ci: build deltachat-rpc-server for Android 2024-03-28 06:57:44 +00:00
link2xt
38491b694b build(nix): add outputs for Android binaries
Using NDK 24 because NDK 23 does not have getauxval() function.
2024-03-28 06:57:44 +00:00
link2xt
e702c1a8ca feat: include more entries into DNS fallback cache 2024-03-25 10:30:08 +00:00
link2xt
1adea3c678 fix: put overridden sender name into message info 2024-03-25 05:19:04 +00:00
link2xt
9af812a3e7 refactor(jsonrpc): add msg_id and account_id to get_message() errors 2024-03-24 18:18:34 +00:00
link2xt
36bdf8a67e fix: do not ignore Contact::get_by_id() error in from_field_to_contact_id() 2024-03-24 18:18:34 +00:00
link2xt
20b30fc70a refactor: remove MessageObject::from_message_id()
It accepted u32, not Message-ID.
There is a properly typed from_msg_id function
that accepts MsgId next to it.
2024-03-24 18:18:34 +00:00
link2xt
e59ff6ca74 feat: include 3 recent Message-IDs in References header
Do not include oldest reference, because chat members
which have been added later and have not seen the first message
do not have referenced message in the database.

Instead, include up to 3 recent Message-IDs.
2024-03-23 02:06:24 +00:00
iequidoo
0e5db36205 fix: Rescan folders after changing Config::SentboxWatch
If `Config::SentboxWatch` changes, the sentbox needs to be [un]configured which is done by
`Imap::scan_folders()`.
2024-03-22 19:08:05 -03:00
iequidoo
7960944b14 test: test_mvbox_sentbox_threads: Check that sentbox gets configured after setting sentbox_watch (#5105) 2024-03-22 19:08:05 -03:00
link2xt
71c2383cbe ci: update to Rust 1.77.0 2024-03-22 19:44:15 +00:00
link2xt
5f5b272726 chore: add result to .gitignore 2024-03-22 18:42:08 +00:00
link2xt
b34fe8f118 feat: do not include provider hostname in Message-ID
It is leaked by anonymous mailing lists,
making it possible to tell which provider the sender is using.
Use `localhost` as the hostname instead.
2024-03-20 00:36:15 +00:00
bjoern
810be4f6c7 fix: preserve upper-/lowercase of links parsed by dehtml() (#5362)
this PR fixes a bug that lowercases all links handleld by `dehtml()`,
which is wrong.

closes #5361
2024-03-19 16:38:23 +01:00
link2xt
1ebbe26ebb api!: remove data from DC_EVENT_INCOMING_MSG_BUNCH
It is not used by existing clients
and incorrectly included all downloaded messages,
including outgoing messages and MDNs.
2024-03-19 14:21:35 +00:00
link2xt
0f5d5dd2b2 build(cmake): build outside the source tree
If user specifies build directory, respect this
instead of building inside the target/ directory.

Also remove outdated comment about Rust 1.50.0 support.
Current MSRV is 1.70.0 and we use 2021 edition
which implies version 2 resolver.
2024-03-19 12:14:33 +00:00
link2xt
473dbe01af chore(release): prepare for 1.136.6 2024-03-19 03:57:44 +00:00
link2xt
069ed7afa6 chore: nix flake update 2024-03-19 03:56:11 +00:00
link2xt
9313ece3cd ci: automate publishing of deltachat-rpc-server to PyPI 2024-03-19 03:25:39 +00:00
link2xt
900168c68c build: read version from Cargo.toml in wheel-rpc-server.py 2024-03-19 03:25:39 +00:00
link2xt
0bd137b4e5 build: add description to deltachat-rpc-server wheels
This is required to pass `twine check`.
2024-03-19 03:04:40 +00:00
link2xt
75da205ff6 docs(deltachat-rpc-server): update deltachat-rpc-client URL 2024-03-19 03:04:40 +00:00
link2xt
67e5fbbfe3 ci: update actions/cache from v3 to v4 2024-03-19 02:17:04 +00:00
link2xt
570daf42ec chore(release): prepare for 1.136.5 2024-03-19 00:01:25 +00:00
link2xt
fcbbb91cde docs(deltachat-rpc-client): document that 0 is a special value of set_ephemeral_timer() 2024-03-18 20:15:54 +00:00
link2xt
c3a7fc4c8d test: test that reordering of Member added message results in square bracket error
This is a test reproducing the problem
in <https://github.com/deltachat/deltachat-core-rust/issues/5339>.
Fix would be to avoid reordering on the server side,
so the test checks that the unverified message
is replaced with a square bracket error
as expected if messages arrive in the wrong order.
2024-03-18 18:14:06 +00:00
iequidoo
4b4c57a480 fix: Add white background to recoded avatars (#3787)
Add white background instead of the default black one to avatars when recoding to JPEG. But not for
"usual" images to spare the CPU. The motivation is to handle correctly
"black-on-transparent-background" avatars which are quite common.
2024-03-17 22:14:08 -03:00
bjoern
b95d58208c add save_mime_headers to debug info (#5350)
as turned out on recent researches wrt a slow database, setting
`save_mime_headers` will result in 25x larger databases.

this is definetely something we want to know at least in the debug info.

(the option cannot be enabled in current UIs,
however, esp. devs find options to set this.
apart from that, save_mime_headers is only used in some python tests)

ftr: the huge database was more than 10 times slower, leading to missing
notifications on ios (as things do not finish within the few seconds iOS
gave us).
moreover, the hugeness also avoids exporting; this is fixed by
https://github.com/deltachat/deltachat-core-rust/pull/5349
2024-03-17 08:27:08 +01:00
bjoern
c468eb088e fix: on iOS, use FILE (default) instead of MEMORY (#5349)
this PR fixes one of the issues we had with an (honestly accidentally)
huge database of >2gb.

this database could not be exported on iOS, as ram memory is limited
there, leading to the app just crashing.

it is unclear if that would be better on eg. Android, however,
temp_store=FILE is not working as well there, this is the smaller
drawback there.

before merging, this PR should be tested on time with export/import on
iOS (export/import uses VACUUM which uses /tmp) EDIT: did so, works on
iphone7 with ios15
2024-03-17 08:25:55 +01:00
B. Petersen
de37135ed6 nicer summaries: prefer emoji over names 2024-03-15 19:20:33 +01:00
iequidoo
33777d8759 fix: Update MemberListTimestamp when sending a group message
`Param::MemberListTimestamp` was updated only from `receive_imf::apply_group_changes()` i.e. for
received messages. If we sent a message, that timestamp wasn't updated, so remote group membership
changes always overrode local ones. Especially that was a problem when a message is sent offline so
that it doesn't incorporate recent group membership changes.
2024-03-15 06:14:38 -03:00
link2xt
8cc348bfa4 fix: terminate ephemeral and location loop immediately on channel close
When scheduler is destroyed, e.g. during a key import,
there is some time between destroying the interrupt channel
and the loop task.

To avoid busy looping, tasks should terminate if
receiving from the interrupt loop fails
instead of treating it as the interrupt.
2024-03-15 01:26:23 +00:00
link2xt
76bbd5fd72 build: add README to deltachat-rpc-client Python packages 2024-03-11 14:42:32 +01:00
link2xt
eaed2381e7 chore(release): prepare for 1.136.4 2024-03-11 12:52:46 +00:00
link2xt
6198ed0ef5 ci: add workflow for automatic publishing of deltachat-rpc-client 2024-03-11 11:45:02 +00:00
link2xt
9f4af679a3 build: build deltachat-rpc-server wheels with nix 2024-03-10 20:22:47 +00:00
B. Petersen
e158b889c9 fix: remove duplicate CHANGELOG entries for 1.135.1 2024-03-10 01:49:58 +01:00
link2xt
9f7defa8da build(nix): make .#libdeltachat buildable on macOS 2024-03-09 19:10:11 +00:00
link2xt
e9d7fe0561 chore(release): prepare for 1.136.3 2024-03-09 16:07:15 +00:00
iequidoo
7d7289bd51 feat: Start IMAP loop for sentbox only if it is configured (#5105) 2024-03-09 15:06:49 +01:00
iequidoo
ebdc52247c chore: RPC client: Add missing constants (#5110) 2024-03-08 19:23:47 -03:00
Simon Laux
36bb4a7a32 ci: remove artefacts from npm package 2024-03-08 08:08:39 +00:00
iequidoo
c0832af634 refactor: Remove deduplicate_peerstates()
There's the `UNIQUE (acpeerstates.addr)` constraint since db v94.
2024-03-08 00:42:39 -03:00
iequidoo
b6db0152b0 fix: Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address
An unencrypted message with already known Autocrypt key, but sent from another address, means that
it's rather a new contact sharing the same key than the existing one changed its address, otherwise
it would already have our key to encrypt.
2024-03-08 00:42:39 -03:00
iequidoo
bc7fd4495b fix: Remove leading whitespace from Subject (#5106)
If Subject is multiline-formatted, `mailparse` adds the leading whitespace to it. The solution is to
always remove the leading whitespace, because if Subject isn't multiline-formatted, it never
contains the leading whitespace anyway. But as for the trailing whitespace -- i checked -- it's
never removed, so let's keep this as is.
2024-03-08 00:08:52 -03:00
dependabot[bot]
e67e86422f chore(deps): bump mio from 0.8.8 to 0.8.11 in /fuzz
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.8 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.8...v0.8.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-08 02:54:57 +00:00
link2xt
2030de11d9 chore: fix 2024-03-05 nightly clippy warnings 2024-03-08 02:53:47 +00:00
link2xt
2c5a0cac5f build(nix): include SystemConfiguration framework on darwin systems 2024-03-08 00:28:51 +00:00
link2xt
251917e602 build(nix): cleanup cross-compilation code 2024-03-08 00:28:51 +00:00
link2xt
273719ae7c ci: wait for build_windows task before trying to publish it 2024-03-07 23:45:06 +00:00
iequidoo
e639b58c6f refactor: Don't even parse Autocrypt header for outgoing messages (#5259)
Accordingly, there's no need in `Peerstate` for self addresses (and in the db too).
2024-03-06 19:32:37 -03:00
link2xt
5addfa8d1d chore(release): prepare for 1.136.2 2024-03-05 22:42:36 +00:00
link2xt
02d68332c7 build: downgrade cc to 1.0.83
1.0.84 and 1.0.85 are yanked.
With 1.0.86 and 1.0.89 Delta Chat for Android fails to build.

Fixes <https://github.com/deltachat/deltachat-android/issues/2972>.
2024-03-05 22:21:28 +00:00
link2xt
97abb9a0a9 ci: update setup-node action 2024-03-05 03:19:46 +00:00
link2xt
d0e0cfafef chore(release): prepare for 1.136.1 2024-03-05 01:23:12 +00:00
link2xt
f630b5fb39 chore: update node constants 2024-03-05 01:18:22 +00:00
link2xt
d9bab938d5 build: restore MSRV 1.70.0 2024-03-05 01:04:49 +00:00
link2xt
215ec14b20 build: revert to OpenSSL 3.1
OpenSSL 3.2 build currently fails under Nix
so we cannot build deltachat-rpc-server releases:
<https://github.com/alexcrichton/openssl-src-rs/issues/235>
2024-03-05 00:14:52 +00:00
link2xt
ea728e9b62 docs: add missing 1.136.0 link to changelog 2024-03-04 21:33:20 +00:00
link2xt
2af9ff1d01 chore(release): prepare for 1.136.0 2024-03-04 21:10:04 +00:00
link2xt
7502234686 api: dc_accounts_set_push_device_token and dc_get_push_state APIs 2024-03-04 21:10:04 +00:00
link2xt
863a386d0f test: test that ASM "encrypted" with plaintext algorithm is not accepted 2024-03-04 21:10:04 +00:00
link2xt
e4b49dfdef fix: validate Group IDs and SecureJoin tokens 2024-03-04 21:10:04 +00:00
iequidoo
612aa1431e fix: Check that peer SecureJoin messages (except vc/vg-request) gossip our addr+pubkey
This fixes the following identity-misbinding attack:

It appears that Bob’s messages in the SecureJoin protocol do not properly “bind” to Alice’s public
key or fingerprint. Even though Bob’s messages carry Alice’s public key and address as a gossip in
the protected payload, Alice does not reject the message if the gossiped key is different from her
own key. As a result, Mallory could perform an identity-misbinding attack. If Mallory obtained
Alice’s QR invite code, she could change her own QR code to contain the same tokens as in Alice’s QR
code, and convince Bob to scan the modified QR code, possibly as an insider attacker. Mallory would
forward messages from Bob to Alice and craft appropriate responses for Bob on his own. In the end,
Bob would believe he is talking to Mallory, but Alice would believe she is talking to Bob.
2024-03-04 21:10:04 +00:00
link2xt
781d3abdb9 fix: make should_do_gossip() return true even if we send securejoin only to Alice 2024-03-04 21:10:04 +00:00
link2xt
78d01933ad fix: don't leak Group-ID in Message-ID
Chat assignment based on In-Reply-To and References works good enough
even if the message cannot be decrypted.
2024-03-04 21:07:10 +00:00
iequidoo
1a1467f7cf fix: Remove unsigned Chat-Group-* headers from Autocrypt-encrypted messages
These headers are opportunistically protected, so if they appear in the unencrypted part, they are
probably added by a malicious server.
2024-03-04 21:07:10 +00:00
link2xt
8d09291d1e fix: do not send Secure-Join-Group in vg-request
Secure-Join-Group is only expected by old core in vg-request-with-auth.
There is no reason to leak group ID in unencrypted vg-request.
Besides that, Secure-Join-Group is deprecated
as Alice knows Group ID corresponding to the auth code,
so the header can be removed completely eventually.
2024-03-04 21:07:10 +00:00
link2xt
4ccd2b8d02 fix: require that Autocrypt Setup Message is self-sent 2024-03-04 21:07:10 +00:00
iequidoo
794596ec69 fix: Don't log SecureJoin QRs
Delta Chat mustn't write sensitive information to unencrypted log files in local storage.
2024-03-04 21:07:10 +00:00
link2xt
3a787519b3 test: test that encrypted Message-ID overwrites X-Microsoft-Original-Message-ID 2024-03-04 21:07:10 +00:00
link2xt
c03e163ed2 fix: reject messages with protected From not corresponding to outer From 2024-03-04 21:07:10 +00:00
iequidoo
6cee295a5d fix: Don't treat forged outgoing messages as Autocrypt-encrypted 2024-03-04 21:07:10 +00:00
iequidoo
f0be7daae9 test: Add failing test on outgoing message forgery
If a message is sent from SELF, but signed with a foreign key, it mustn't be considered
Autocrypt-encrypted and shown with a padlock. Currently this is broken.
2024-03-04 21:07:10 +00:00
link2xt
0b279ec84e fix: make protected Message-ID take precedence over X-Microsoft-Original-Message-ID 2024-03-04 21:07:10 +00:00
link2xt
e919de78a3 fix: do not take Secure-Join-Auth from unprotected headers 2024-03-04 21:07:10 +00:00
link2xt
6ea675a12f fix: do not use Secure-Join-Group header
Alice already knows which auth token corresponds to which group.
There is no need to trust Bob on sending the correct group ID.
2024-03-04 21:07:10 +00:00
link2xt
b970ebe67a fix: do not compress SecureJoin messages 2024-03-04 21:07:10 +00:00
link2xt
3c4c701f9b fix: protect Secure-Join header
Secure-Join header must come from protected headers
unless it is a "vc-request" or "vg-request".
2024-03-04 21:07:10 +00:00
link2xt
01ac9c8b90 fix: ensure Autocrypt-Gossip is not taken from insecure headers 2024-03-04 21:07:10 +00:00
link2xt
f6de23738d build: increase MSRV to 1.74.0
This is what updated dependencies require.

Also update Rust used to build manylinux wheels
from 1.72.0 to 1.76.0.
2024-03-04 21:07:10 +00:00
link2xt
ddc2704278 chore: cargo update 2024-03-04 21:07:10 +00:00
link2xt
3d2b164c05 chore(deps): update mio to fix RUSTSEC-2024-0019 2024-03-04 21:07:09 +00:00
link2xt
2094bc3135 chore(deps): update buffer-redux to remove unmaintained safemem 2024-03-04 21:07:09 +00:00
holger krekel
acff8205e2 test: fix pytest compat (#5317)
seems pytest_report_header has changed with pytest incompatible but we
don't need it anyway so we can just leave it out.
2024-03-04 21:07:09 +00:00
link2xt
255400028a build: do not vendor OpenSSL when cross-compiling (#5316)
Compilation of vendored OpenSSL inside Nix is broken since
<https://github.com/alexcrichton/openssl-src-rs/pull/229> due to build
script changes.

There is anyway no need to compile vendored OpenSSL as nixpkgs already
contains OpenSSL package.

This fixes `nix build .#deltachat-rpc-server-x86_64-linux` and similar
commands which are used during releases.
2024-03-04 21:07:09 +00:00
Hocuri
d7615b223f feat(Self-Reporting): Report number of protected/encrypted/unencrypted chats (#5292) 2024-03-04 21:07:09 +00:00
dependabot[bot]
00fbf540c4 chore(cargo): bump tempfile from 3.10.0 to 3.10.1
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.10.0 to 3.10.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.10.0...v3.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:07:09 +00:00
dependabot[bot]
288eccf722 chore(cargo): bump image from 0.24.8 to 0.24.9
Bumps [image](https://github.com/image-rs/image) from 0.24.8 to 0.24.9.
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.8...v0.24.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 14:46:53 +00:00
dependabot[bot]
99ee769580 chore(cargo): bump textwrap from 0.16.0 to 0.16.1
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.16.0...0.16.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 08:01:51 +00:00
dependabot[bot]
345759d653 chore(cargo): bump syn from 2.0.48 to 2.0.52
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.48 to 2.0.52.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.48...2.0.52)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-03 04:15:51 +00:00
link2xt
db0143f01a build: remove deprecated unmaintained field from deny.toml 2024-03-03 03:49:27 +00:00
link2xt
4da0c19766 test: fixup tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db 2024-03-03 03:28:47 +00:00
link2xt
08247a5d37 refactor: build contexts using ContextBuilder 2024-03-02 17:19:50 +00:00
link2xt
ceadd8928e api: add ContextBuilder.build() to build Context without opening 2024-03-02 17:19:50 +00:00
dependabot[bot]
c3d96814ca chore(cargo): bump walkdir from 2.4.0 to 2.5.0
Bumps [walkdir](https://github.com/BurntSushi/walkdir) from 2.4.0 to 2.5.0.
- [Commits](https://github.com/BurntSushi/walkdir/compare/2.4.0...2.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 17:08:47 +00:00
dependabot[bot]
c2953623b9 chore(cargo): bump serde from 1.0.196 to 1.0.197
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.196 to 1.0.197.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.196...v1.0.197)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 17:07:36 +00:00
dependabot[bot]
1907d1859e chore(cargo): bump anyhow from 1.0.79 to 1.0.80
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.79 to 1.0.80.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.79...1.0.80)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:00:57 +00:00
dependabot[bot]
a1970e998f chore(cargo): bump log from 0.4.20 to 0.4.21
Bumps [log](https://github.com/rust-lang/log) from 0.4.20 to 0.4.21.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.20...0.4.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-02 16:00:33 +00:00
dependabot[bot]
1e9baefca0 chore(cargo): bump serde_json from 1.0.113 to 1.0.114
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114)

---
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-03-02 15:59:55 +00:00
iequidoo
e16322d99d test: get_protected_chat(): Use FFIEventTracker instead of dc_wait_next_msgs() (#5207)
The way it was implemented it threw out all remaining messages after finding the next incoming
message. Better use FFIEventTracker functions, they are used in all the tests anyway.
2024-03-02 12:12:26 -03:00
dependabot[bot]
ecfe3898c6 Merge pull request #5311 from deltachat/dependabot/cargo/rusqlite-0.31.0 2024-03-02 03:12:56 +00:00
link2xt
5499ca52bf refactor: get rid of ImapActionResult 2024-03-02 01:31:29 +00:00
link2xt
4e8979f7c8 refactor: merge ImapConfig into Imap 2024-03-01 21:12:21 +00:00
dependabot[bot]
417db31098 chore(cargo): bump rusqlite from 0.30.0 to 0.31.0
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.30.0...v0.31.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-01 21:11:09 +00:00
link2xt
cd9f6c3d5b ci: build c.delta.chat docs with nix 2024-03-01 20:28:20 +00:00
link2xt
07870a6d69 refactor(imap): remove Session from Imap structure
Connection establishment now happens only in one place in each IMAP loop.
Now all connection establishment happens in one place
and is limited by the ratelimit.

Backoff was removed from fake_idle
as it does not establish connections anymore.
If connection fails, fake_idle will return an error.
We then drop the connection and get back to the beginning of IMAP
loop.

Backoff may be still nice to have to delay retries
in case of constant connection failures
so we don't immediately hit ratelimit if the network is unusable
and returns immediate error on each connection attempt
(e.g. ICMP network unreachable error),
but adding backoff for connection failures is out of scope for this change.
2024-03-01 18:36:03 +00:00
link2xt
b08a4d6fcf ci: upload cffi docs without GH actions 2024-03-01 02:30:55 +00:00
link2xt
b3a82b416f ci: upload python docs without GH actions 2024-03-01 02:21:35 +00:00
link2xt
4e5d7fb821 ci: build Python docs with Nix 2024-03-01 02:00:53 +00:00
link2xt
1d73f97ef3 nix: add deltachat-time to sources 2024-03-01 01:58:11 +00:00
link2xt
f5601e7683 Merge pull request #5296 from deltachat/link2xt/imap-session
refactor: move more methods from Imap into Session
2024-02-29 02:29:52 +00:00
link2xt
0000c09ad3 fix(imap): allow maybe_network to interrupt connection ratelimit
ratelimit can be exhausted quickly if the network is not available,
i.e. if every connection attempt returns "network unreachable" error.
When the network becomes available, we want to retry connecting
as soon as maybe_network is called without waiting for ratelimiter.
2024-02-29 02:29:18 +00:00
link2xt
a83884d7e9 refactor(imap): require watch_folder for fake_idle() 2024-02-28 23:18:30 +00:00
link2xt
9e00e8627f refactor(imap): pass Session to add_all_recipients_as_contacts() 2024-02-28 22:51:07 +00:00
link2xt
85c9622675 refactor(imap): move fetch_many_msgs() into Session 2024-02-28 22:48:23 +00:00
link2xt
30432d8fa5 refactor(imap): move fetch_metadata() to Session 2024-02-28 22:42:54 +00:00
link2xt
8b9f19be70 refactor(imap): move get_all_recipients() to Session 2024-02-28 22:40:37 +00:00
link2xt
39c317e211 refator(imap): move sync_seen_flags() to Session 2024-02-28 22:39:06 +00:00
link2xt
36ab7bdf47 refactor(imap): do not get Session twice in fetch_new_messages() 2024-02-28 22:32:38 +00:00
link2xt
f8f0ca08da refactor(imap): pass Imap Session to update_recent_quota() 2024-02-28 22:30:19 +00:00
link2xt
2a0a05d03c refactor(imap): move resync_folders() to Session 2024-02-28 22:26:25 +00:00
link2xt
7bc2f0cb6b refactor(imap): move select_with_uidvalidity() to Session 2024-02-28 22:15:33 +00:00
link2xt
4355bd77a9 refactor(imap): move resync_folder_uids() to Session 2024-02-28 22:02:36 +00:00
link2xt
f0091696c2 refactor(imap): move prefetch() to Session 2024-02-28 21:56:28 +00:00
link2xt
d2e86c5852 refactor(imap): move prefetch_existing_msgs to Session 2024-02-28 21:49:44 +00:00
link2xt
d4a505b52e refactor(imap): move list_folders() to Session 2024-02-28 21:43:25 +00:00
iequidoo
08a30031eb fix: Don't send sync messages on self-{status,avatar} update from self-sent messages (#5289)
Sync messages should only be sent in response to user actions.
2024-02-26 12:28:03 -03:00
link2xt
44686d6caa ci: update to Rust 1.76 and fix clippy warnings 2024-02-25 10:43:01 +00:00
iequidoo
9862d40f89 feat: Send Chat-Group-Avatar as inline base64 (#5253)
Before group avatar was sent as an attachment. Let's do the same as with user avatar and send group
avatar as base64. Receiver code uses the same functions for user and chat avatars, so base64 avatars
are supported for most receivers already.
2024-02-24 20:24:04 -03:00
link2xt
256c8c13f1 build: unpin OpenSSL
`deltachat-rpc-server` releases are built
with Nix and LLVM/clang toolchains now
that fully support atomics.
Zig toolchain that required disabling atomics
and resulted in problems with OpenSSL 3.2 releases
is not used anymore.
2024-02-24 13:32:42 +00:00
link2xt
0b3a56c3c4 api: make store_self_keypair private
It is not useful as public API because input argument types
are not public.
Use `imex` instead.
2024-02-23 19:29:45 +00:00
iequidoo
89024bbf37 test: Fix test_verified_oneonone_chat_broken_by_device_change() (#5280)
It was broken completely and before "fix: apply Autocrypt headers if timestamp is unchanged" that
didn't show up because the message from the second Bob's device never had "Date" greater than one
from the message sent before from the first device.
2024-02-23 15:23:02 -03:00
link2xt
cf16671d8d fix(imap): set connectivity to "connecting" only after ratelimit 2024-02-22 13:08:44 +00:00
link2xt
671feb68a4 fix: do not fake idle after trigger_reconnect()
In this case connection failure
may be a connection timeout (currently 1 minute),
so it does not make sense to fake idle for another minute immediately after.

However, failure may be immediate if the port is closed
and the server refuses connection every time.
To prevent busy loop in this case
we apply ratelimit to connection attempts rather than login attempts.
This partially reverts ccec26ffa7
2024-02-22 13:08:44 +00:00
link2xt
ccd5158109 ci: upgrade setup-python GitHub Action 2024-02-22 06:18:34 +00:00
link2xt
0a18e32d62 chore(cargo): update rpgp to 0.11
<https://github.com/rpgp/rpgp/releases/tag/v0.11.0>
2024-02-22 05:08:00 +00:00
iequidoo
e9fadc0785 feat: Recognise Trash folder by name (#5275)
If a folder is named "Trash" or like this, it should be recognised as such even if it does not have
a \Trash attribute.
2024-02-20 18:29:04 -03:00
link2xt
cfa13f0669 build: tag armv6 wheels with tags accepted by PyPI
See
<https://github.com/pypi/warehouse/blob/main/warehouse/forklift/legacy.py>
for the tag checking code.

linux_armv6l and linux_armv7l are accepted,
but {many,musl}linux_*_armv6l are not.
2024-02-20 18:10:52 +00:00
link2xt
89e43c6678 chore(release): prepare for 1.135.1 2024-02-20 17:21:21 +00:00
link2xt
8a67797cb1 build: add footer template for git-cliff 2024-02-20 17:05:36 +00:00
link2xt
b29bc19ef4 ci: try to upload deltachat-rpc-server only on release 2024-02-20 15:59:53 +00:00
link2xt
e765066f05 ci: build deltachat-rpc-server with nix 2024-02-20 15:59:53 +00:00
link2xt
67aa785a9e ci: build deltachat-repl for Windows with nix 2024-02-20 15:59:53 +00:00
link2xt
c88c26426d build: add flake.nix 2024-02-20 15:59:53 +00:00
link2xt
b4e9a9764f fix: apply Autocrypt headers if timestamp is unchanged
If two messages arrive with the same timestamp,
the one that arrived later should be preferred.
2024-02-20 12:46:41 +00:00
gerryfrancis
06e79e8926 Correct typo in imap.rs 2024-02-20 12:46:30 +00:00
link2xt
9427f7b587 fix: never encrypt {vc,vg}-request
Even if 1:1 chat with alice is protected,
we should send vc-request unencrypted.
This happens if Alice changed the key
and QR-code Bob scans contains fingerprint
that is different from the verified fingerprint.
Sending vc-request encrypted to the old key
does not help because Alice is not able
to decrypt it in this case.
2024-02-19 15:32:50 +00:00
iequidoo
bce22edfe3 feat: Sync Config::Selfstatus across devices (#4893)
Use sync messages for that as it is done for e.g. Config::Displayname. Maybe we need to remove
self-status synchronisation via usual messages then, but let's think of it a bit.
2024-02-19 12:18:13 -03:00
iequidoo
656d4ed506 feat: Sync self-avatar across devices (#4893)
Use sync messages for that as it is done for e.g. Config::Displayname. Maybe we need to remove
avatar synchronisation via usual messages then, but let's think of it a bit.
2024-02-19 12:18:13 -03:00
iequidoo
5e3fcafb3a fix: Context::get_info: Report displayname as "displayname" (w/o underscore)
Otherwise the user thinks that the config key is "display_name" and can't change it using `set
display_name` command.
2024-02-19 12:18:13 -03:00
link2xt
660cfd4f01 refactor: rename incorrectly named variables in create_keypair
Encryption subkey is incorrectly referred to as public key
in variable names.
This is incorrect because generated encryption key
is secret too just as the signing primary key.
Generated OpenPGP secret key consists of primary signing key
and encryption subkey.
Then OpenPGP public key consisting of
the primary signing public key
and encryption public key is generated.
Keypair consists of the secret OpenPGP key and public OpenPGP key,
each of them has a primary key and subkey inside.
2024-02-17 17:50:33 +00:00
link2xt
7a1270f861 refactor: return error with a cause when failing to export keys 2024-02-17 17:50:33 +00:00
link2xt
b35b893351 refactor(create_keypair): remove unnecessary map_err 2024-02-17 17:50:33 +00:00
link2xt
f45f9263db ci: fixup node-package.yml after artifact actions upgrade 2024-02-17 13:49:40 +00:00
link2xt
8289dc92ed ci: update to actions/checkout@v4
Also disable --progress.
It is not disabled by default for backward compatibility,
but solves the problem of lots of progress lines
in the downloadable raw output:
https://github.com/actions/checkout/pull/1067
2024-02-17 13:49:01 +00:00
link2xt
862107c708 feat: remove webxdc sending limit
The limit is better enforced by webxdc distributors,
e.g. xdc store bots or actually email providers
to allow for experimentation with large frameworks
or porting existing apps and testing them
before reducing their size.

Besides that, the comment on WEBXDC_SENDING_LIMIT was outdated,
it was not updated when the limit was increased to 640 kB.
2024-02-17 00:09:21 +00:00
iequidoo
778660a8c9 test: Add a test on protection message sort timestamp (#5088)
Even if `vc-request-with-auth` is received with a delay, the protection message must have the sort
timestamp equal to the Sent timestamp of `vc-request-with-auth`, otherwise subsequent chat messages
would also have greater sort timestamps and while it doesn't affect the chat itself (because Sent
timestamps are shown to a user), it affects the chat position in the chatlist because chats there
are sorted by sort timestamps of the last messages, so the user sees chats sorted out of
order. That's what happened in #5088 where a user restores the backup made before setting up a
verified chat with their contact and fetches new messages, including `vc-request-with-auth` and also
messages from other chats, after that.
2024-02-15 14:24:46 -03:00
iequidoo
6e55f0c6e3 feat: Mock SystemTime::now() for the tests
Add a new crate `deltachat_time` with a fake `struct SystemTimeTools` for mocking
`SystemTime::now()` for test purposes. One still needs to use `std::time::SystemTime` as a struct
representing a system time. I think such a minimalistic approach is ok -- even if somebody uses the
original `SystemTime::now()` instead of the mock by mistake, that could break only tests but not the
program itself. The worst thing that can happen is that tests using `SystemTime::shift()` and
checking messages timestamps f.e. wouldn't catch the corresponding bugs, but now we don't have such
tests at all which is much worse.
2024-02-15 14:24:46 -03:00
link2xt
3b0e740c17 ci: replace download-artifact v1 with v4 2024-02-15 11:31:58 +00:00
link2xt
2dd87b6b5e ci: use actions/download-artifact@v4
download-artifact@v3 does not download artifacts uploaded with upload-artifact@v4
2024-02-15 04:32:14 +00:00
link2xt
cdcacf2f83 ci: update actions/upload-artifact
There are no breaking changes listed in the README that affect our usage.
2024-02-15 02:41:49 +00:00
bjoern
51aaaf2e8d cleanup changelog (#5265)
somehow a whole issue sneaked in :)
2024-02-14 18:02:16 +01:00
bjoern
e6438f9981 chore(release): prepare for 1.135.0 (#5264)
after merging:

6. Tag the release: `git tag -a v1.135.0`.

7. Push the release tag: `git push origin v1.135.0`.

8. Create a GitHub release: `gh release create v1.135.0 -n ''`.

---------

Co-authored-by: link2xt <link2xt@testrun.org>
2024-02-14 11:04:02 +01:00
bjoern
9135cffaa4 do not change db schema in an incompatible way (#5254)
PR #5099 removed some columns in the database that were actually in use.

usually, to not worsen UX unnecessarily
(releases take time - in between, "Add Second Device", "Backup" etc.
would fail), we try to avoid such schema changes (checking for
db-version would avoid import etc. but would still worse UX),
see discussion at #2294.

these are the errors, the user will be confronted with otherwise:

<img width=400
src=https://github.com/deltachat/deltachat-core-rust/assets/9800740/e3f0fd6e-a7a9-43f6-9023-0ae003985425>

it is not great to maintain the old columns, but well :)

as no official releases with newer cores are rolled out yet, i think, it
is fine to change the "107" migration
and not copy things a second time in a newer migration.

(this issue happens to me during testing, and is probably also the issue
reported by @lk108 for ubuntu-touch)
2024-02-13 23:00:47 +01:00
link2xt
73492ca4bc chore(scripts): do not install deltachat-rpc-client twice 2024-02-13 11:20:11 +00:00
iequidoo
fe3c1f69c3 feat: Cache system time instead of looking at the clock several times in a row
The system clock may be adjusted and even go back, so caching system time in code sections where
it's not supposed to change may even protect from races/bugs.
2024-02-12 21:13:36 -03:00
iequidoo
31ee3feb57 fix: Use SystemTime instead of Instant everywhere
If a time value doesn't need to be sent to another host, saved to the db or otherwise used across
program restarts, a monotonically nondecreasing clock (`Instant`) should be used. But as `Instant`
may use `libc::clock_gettime(CLOCK_MONOTONIC)`, e.g. on Android, and does not advance while being in
deep sleep mode, get rid of `Instant` in favor of using `SystemTime`, but add `tools::Time` as an
alias for it with the appropriate comment so that it's clear why `Instant` isn't used in those
places and to protect from unwanted usages of `Instant` in the future. Also this can help to switch
to another clock impl if we find any.
2024-02-12 21:13:36 -03:00
link2xt
f4ed63c54c chore: cargo update 2024-02-12 23:16:39 +00:00
link2xt
8f88cdd826 chore(cargo): update to strum 0.26 2024-02-12 18:54:14 +00:00
iequidoo
9933a4268f fix: Create mvbox on setting mvbox_move 2024-02-12 15:41:11 -03:00
iequidoo
8a54c228fd feat: server_sent_unsolicited_exists(): Log folder name 2024-02-12 15:41:11 -03:00
iequidoo
b5f2c747e0 feat: Context::set_config(): Restart IO scheduler if needed (#5111)
Restart the IO scheduler if needed to make the new config value effective (for `MvboxMove,
OnlyFetchMvbox, SentboxWatch` currently). Also add `set_config_internal()` which doesn't affect
running the IO scheduler. The reason is that `Scheduler::start()` itself calls `set_config()`,
although not for the mentioned keys, but still, and also Rust complains about recursive async calls.
2024-02-12 15:41:11 -03:00
iequidoo
ba35e83db2 feat: Add device message about outgoing undecryptable messages (#5164)
Currently when a user sets up another device by logging in, a new key is created. If a message is
sent from either device outside, it cannot be decrypted by the other device.

The message is replaced with square bracket error like this:
```
<string name="systemmsg_cannot_decrypt">This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose "Add as second device" or import a backup.</string>
```
(taken from Android repo `res/values/strings.xml`)

If the message is outgoing, it does not help to "simply reply to this message". Instead, we should
add a translatable device message of a special type so UI can link to the FAQ entry about second
device. But let's limit such notifications to 1 per day. And as for the undecryptable message
itself, let it go to Trash if it can't be assigned to a chat by its references.
2024-02-11 23:22:52 -03:00
dependabot[bot]
61a2c551fc chore(cargo): bump toml from 0.8.8 to 0.8.10
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.8 to 0.8.10.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.8...toml-v0.8.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-11 03:48:58 +00:00
dependabot[bot]
20c91ba2fa chore(cargo): bump serde from 1.0.194 to 1.0.196
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.194 to 1.0.196.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.194...v1.0.196)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 21:39:05 +00:00
dependabot[bot]
969f8b916b chore(cargo): bump syn from 2.0.46 to 2.0.48
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.46 to 2.0.48.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.46...2.0.48)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 21:38:55 +00:00
dependabot[bot]
b7b7a7e95d chore(cargo): bump serde_json from 1.0.111 to 1.0.113
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.111 to 1.0.113.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.111...v1.0.113)

---
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-02-09 18:50:12 +00:00
dependabot[bot]
455b108a6c chore(cargo): bump fast-socks5 from 0.9.2 to 0.9.5
Bumps [fast-socks5](https://github.com/dizda/fast-socks5) from 0.9.2 to 0.9.5.
- [Release notes](https://github.com/dizda/fast-socks5/releases)
- [Commits](https://github.com/dizda/fast-socks5/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 18:50:01 +00:00
dependabot[bot]
645ca7741b chore(cargo): bump uuid from 1.6.1 to 1.7.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.6.1...1.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 06:13:42 +00:00
dependabot[bot]
36643c551d chore(cargo): bump axum from 0.7.3 to 0.7.4
Bumps [axum](https://github.com/tokio-rs/axum) from 0.7.3 to 0.7.4.
- [Release notes](https://github.com/tokio-rs/axum/releases)
- [Changelog](https://github.com/tokio-rs/axum/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/axum/compare/axum-v0.7.3...axum-v0.7.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 04:36:49 +00:00
link2xt
0fcdee8857 refactor: resultify token::exists 2024-02-09 04:36:32 +00:00
dependabot[bot]
26ae686687 chore(cargo): bump reqwest from 0.11.23 to 0.11.24
Bumps [reqwest](https://github.com/seanmonstar/reqwest) from 0.11.23 to 0.11.24.
- [Release notes](https://github.com/seanmonstar/reqwest/releases)
- [Changelog](https://github.com/seanmonstar/reqwest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/reqwest/compare/v0.11.23...v0.11.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 04:02:42 +00:00
iequidoo
b94bd9a659 fix: Emit ConfigSynced event on the second device
Before it was emitted only on the source device and `test_sync()` didn't catch the bug because of
the interference of the previous call to `set_config_bool()`. Now there's a separate test that does
its job well.
2024-02-09 00:41:01 -03:00
iequidoo
f15e7d43e3 fix: ImapSession::select_or_create_folder(): Don't fail if folder is created in parallel 2024-02-08 23:39:09 -03:00
iequidoo
05c256dd5b fix: Imap::configure_mvbox: Do select_with_uidvalidity() before return
Before moving emails to the mvbox we need to remember its UIDVALIDITY, otherwise emails moved before
that wouldn't be fetched but considered "old" instead.

Also:
- Don't use `session.create()` to create mvbox as `select_with_uidvalidity()` already creates mvbox
  on its own.
- Don't try to create compat folders like "INBOX.DeltaChat", but only look for them.
2024-02-08 23:39:09 -03:00
dependabot[bot]
37295f6967 chore(cargo): bump libc from 0.2.151 to 0.2.153
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.151 to 0.2.153.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.151...0.2.153)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 02:34:41 +00:00
dependabot[bot]
dfdbb91f0a chore(cargo): bump regex from 1.10.2 to 1.10.3
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.2 to 1.10.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.2...1.10.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 02:32:50 +00:00
dependabot[bot]
72f93dca7a chore(cargo): bump base64 from 0.21.5 to 0.21.7
Bumps [base64](https://github.com/marshallpierce/rust-base64) from 0.21.5 to 0.21.7.
- [Changelog](https://github.com/marshallpierce/rust-base64/blob/master/RELEASE-NOTES.md)
- [Commits](https://github.com/marshallpierce/rust-base64/compare/v0.21.5...v0.21.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 02:31:48 +00:00
dependabot[bot]
ec2cf31cfa chore(cargo): bump smallvec from 1.11.2 to 1.13.1
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.11.2 to 1.13.1.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.11.2...v1.13.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 02:31:24 +00:00
iequidoo
ecd4d2afe0 test: delete_server_after="1" should cause immediate message deletion (#5201) 2024-02-08 12:18:11 -03:00
Hocuri
ec9d104cf3 Basic self-reporting, core part (#5129)
Part of https://github.com/deltachat/deltachat-android/issues/2909

For now, this is only sending a few basic metrics.
2024-02-07 20:23:11 +01:00
Hocuri
11214c7d1f fix: Never allow a message timestamp to be a lot in the future (#5249)
We must finish what was started in
https://github.com/deltachat/deltachat-core-rust/issues/5088.
2024-02-07 20:17:46 +01:00
iequidoo
fba27ff884 fix: Guarantee immediate message deletion if delete_server_after == 0 (#5201) 2024-02-07 11:43:11 -03:00
link2xt
f8907e3c83 feat: add wildcard pattern support to provider database 2024-02-06 21:02:28 +00:00
Hocuri
f1688d2b3f fix: Mark the gossip keys from the message as verified, not the ones from the db (#5247) 2024-02-06 10:22:38 +01:00
iequidoo
693045b542 chore(cargo): bump iana-time-zone from yanked 0.1.59 to 0.1.60 2024-02-04 10:42:13 +01:00
dependabot[bot]
14dfb9abec Merge pull request #5235 from deltachat/dependabot/cargo/image-0.24.8 2024-02-03 17:09:30 +00:00
dependabot[bot]
c8ed3ed73b Merge pull request #5236 from deltachat/dependabot/cargo/chrono-0.4.33 2024-02-03 17:04:12 +00:00
dependabot[bot]
bce5203eeb Merge pull request #5241 from deltachat/dependabot/cargo/futures-lite-2.2.0 2024-02-03 16:53:21 +00:00
dependabot[bot]
74c0c2cc38 Merge pull request #5243 from deltachat/dependabot/cargo/pin-project-1.1.4 2024-02-03 16:51:59 +00:00
iequidoo
4f25072352 fix: dehtml: Don't just truncate text when trying to decode (#5223)
If `escaper::decode_html_buf_sloppy()` just truncates the text (which happens when it fails to
html-decode it at some position), then it's probably not HTML at all and should be left as
is. That's what happens with hyperlinks f.e. and there was even a test on this wrong behaviour which
is fixed now. So, now hyperlinks are not truncated in messages and should work as expected.
2024-02-02 14:55:52 -03:00
B. Petersen
91c3a39134 update welcome image, thanks @paulaluap 2024-02-01 23:16:47 +01:00
dependabot[bot]
ae94b2a7b3 chore(cargo): bump pin-project from 1.1.3 to 1.1.4
Bumps [pin-project](https://github.com/taiki-e/pin-project) from 1.1.3 to 1.1.4.
- [Release notes](https://github.com/taiki-e/pin-project/releases)
- [Changelog](https://github.com/taiki-e/pin-project/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/pin-project/compare/v1.1.3...v1.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 21:27:59 +00:00
dependabot[bot]
3b013a1017 chore(cargo): bump futures-lite from 2.1.0 to 2.2.0
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v2.1.0...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 21:27:11 +00:00
dependabot[bot]
80aab220b6 chore(cargo): bump chrono from 0.4.31 to 0.4.33
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.31 to 0.4.33.
- [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.31...v0.4.33)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 21:25:01 +00:00
dependabot[bot]
34c3e44b9d chore(cargo): bump image from 0.24.7 to 0.24.8
Bumps [image](https://github.com/image-rs/image) from 0.24.7 to 0.24.8.
- [Changelog](https://github.com/image-rs/image/blob/master/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.24.7...v0.24.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-01 21:24:34 +00:00
B. Petersen
78d304443a chore(release): prepare for 1.134.0 2024-01-31 14:52:40 +00:00
link2xt
d6c24eb9f6 Make Accounts::background_fetch() not return Result 2024-01-31 14:04:03 +01:00
link2xt
f7fd1ef2bf Emit DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE even on timeout
Otherwise if there is a timeout,
UI will wait for DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE forever.
2024-01-31 14:04:03 +01:00
link2xt
af7bf5bd2b s/forgeting/forgetting/ 2024-01-31 14:04:03 +01:00
link2xt
ea666f1098 Hide background_fetch_without_timeout from public API 2024-01-31 14:04:03 +01:00
Simon Laux
5bb80f94c7 Apply suggestions from code review
Co-authored-by: bjoern <r10s@b44t.com>
2024-01-31 14:04:03 +01:00
link2xt
2f29c56a36 fix: do not log error if watched folder is not configured
This may happen if Sent folder does not exist
but configuration option to watch it is enabled.
2024-01-31 14:04:03 +01:00
Simon Laux
de86b8a96e rename event also in core 2024-01-31 14:04:03 +01:00
Simon Laux
060c9c8aa1 cargo fmt 2024-01-31 14:04:03 +01:00
Simon Laux
727428a965 rename event and mention event in method documentation 2024-01-31 14:04:03 +01:00
Simon Laux
df455bbcf5 BackgroundFetchCompletedForAllAccounts event 2024-01-31 14:04:03 +01:00
Simon Laux
946eea4c9e add rate limit for quota check in background fetch (12h for now) 2024-01-31 14:04:03 +01:00
Simon Laux
5cbc87369e rename cffi function 2024-01-31 14:04:03 +01:00
Simon Laux
5cdd5e0564 Apply suggestions from code review
Co-authored-by: link2xt <link2xt@testrun.org>
2024-01-31 14:04:03 +01:00
Simon Laux
f493d6bb40 don't hold write lock in cffi (this blocked events) 2024-01-31 14:04:03 +01:00
Simon Laux
8e073b9c3e log time that the function took 2024-01-31 14:04:03 +01:00
Simon Laux
ea2a692d18 fix iOS build issue 2024-01-31 14:04:03 +01:00
Simon Laux
1b7c5be9c5 Test server has no sentbox folder 2024-01-31 14:04:03 +01:00
Simon Laux
f7903df805 api: cffi: add dc_accounts_background_fetch_with_timeout 2024-01-31 14:04:03 +01:00
Simon Laux
d2c61dc90e api: jsonrpc: add background_fetch_for_all_accounts 2024-01-31 14:04:03 +01:00
Simon Laux
7b68098785 feat: add background fetch method 2024-01-31 14:04:03 +01:00
Simon Laux
48f2ea717e refactor: move convert folder meaning logic in own method
also unify the error handling for the cases where it can go wrong.
2024-01-31 14:04:03 +01:00
link2xt
cb3f03fd39 feat: add support for IMAP METADATA 2024-01-31 04:16:04 +00:00
iequidoo
06f1fe18d6 fix: Delete resent messages on receiver side (#5155)
If a Delta Chat message has the Message-ID already existing in the db, but a greater "Date", it's a
resent message that can be deleted. Messages having the same "Date" mustn't be deleted because they
can be already seen messages moved back to INBOX. Also don't delete messages having lesser "Date" to
avoid deleting both messages in a multi-device setting.
2024-01-30 22:01:45 -03:00
iequidoo
1dbf924c6a feat: chat::resend_msgs: Guarantee strictly increasing time in the Date header
Use `create_smeared_timestamp()` for this. This allows to dedup messages on the receiver -- if it
sees the same Message-ID, but a different timestamp, then it's a resent message that can be deleted.
2024-01-30 22:01:45 -03:00
link2xt
3f6814f421 chore: remove unnecessary exception from deny.toml 2024-01-30 20:27:07 +00:00
link2xt
782828ac4f chore(deps): update imap-proto 2024-01-30 20:12:07 +00:00
iequidoo
bd3759d55e test: test_import_export_online_all: Send the message to the existing address to avoid errors (#5220) 2024-01-29 20:34:03 -03:00
iequidoo
672993e69e feat: qr::check_qr(): Accept i.delta.chat invite links (#5217)
Accepts invite links with `https://i.delta.chat[/]#FINGERPRINT&a=ADDR[&OPTIONAL_PARAMS]`
scheme. Only `i.delta.chat` domain is supported now not to intersect with the common HTTPS scheme.
2024-01-28 20:33:56 -03:00
iequidoo
987bdaf237 ci: Add/remove necessary newlines to fix Python lint 2024-01-26 14:46:46 -03:00
iequidoo
7cf382a3b8 fix: Treat only "Auto-Submitted: auto-generated" messages as bot-sent (#5213)
"Auto-Submitted: auto-replied" messages mustn't be considered as sent by either bots or non-bots,
e.g. MDNs have this header value and it's the same for bots and non-bots.
2024-01-26 13:03:19 -03:00
Simon Laux
19dce9ddfa api!: jsonrpc: device message api now requires Option<MessageData> instead of String for the message (#5211)
api: jsonrpc: device message api now setting empty device messages to
block adding a message for a specific label

required for https://github.com/deltachat/deltachat-desktop/pull/3639

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2024-01-25 11:26:11 +01:00
Simon Laux
0afc0dd65a fix: add tolerance for macOS and iOS changing # to %23
fixes #1969

Bug description:
macOS and iOS sometimes replace the # with %23 (uri encode it), we should be able to parse this wrong format too, see issue https://github.com/deltachat/deltachat-core-rust/issues/1969 for more info
2024-01-24 23:58:49 +01:00
link2xt
73d612a07d feat: do not drop unknown report attachments
In particular TLSRPT reports
contain files that may be interesting for admins.
Currently Delta Chat drops the attachment
so message appears as a text message without actual payload.
2024-01-24 16:13:20 +00:00
link2xt
3b1529ef81 chore(release): prepare for 1.133.2 2024-01-24 01:57:45 +00:00
link2xt
15187c0adb fix: downgrade OpenSSL from 3.2.0 to 3.1.4 2024-01-24 01:48:23 +00:00
Sebastian Klähn
c5f31c3d03 fix: No new chats for MDNs with alias (#5196) (#5199)
close #5196
2024-01-22 16:51:37 +01:00
link2xt
2c17e78347 chore(release): prepare for 1.133.1 2024-01-21 04:18:13 +00:00
Sebastian Klähn
4ee646ce0b feat(api): Add is_bot to cffi and jsonrpc (#5197)
@adbenitez wants this feature on Deltalab to display a bot tag. 
Other UIs might also want to adopt this feature :)

---------
Co-authored-by: link2xt <link2xt@testrun.org>
2024-01-20 15:00:10 +00:00
B. Petersen
1f7b4a74fa add missing 'unencrypted message' defines
in #5161, it was forgotten to adapt deltachat.h;
moreover, this PR tweaks some other minor things
2024-01-20 15:00:23 +01:00
Sebastian Klähn
4bc90701cc feat: Add system message when provider does not allow unencrypted messages (#5161) (#5195)
close #5161

![Screenshot from 2024-01-19
19-56-09](https://github.com/deltachat/deltachat-core-rust/assets/39526136/27ecdd9b-1739-410b-bb26-80d5bdbbc39a)

---------

Co-authored-by: bjoern <r10s@b44t.com>
2024-01-20 11:47:23 +00:00
dependabot[bot]
490deb9347 chore(deps): bump h2 from 0.3.17 to 0.3.24 in /fuzz
Bumps [h2](https://github.com/hyperium/h2) from 0.3.17 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.17...v0.3.24)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 23:58:52 +00:00
Sebastian Klähn
28d9484a13 fix(node): run tests with native ESM modules instead of esm (#5194)
close #5156

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2024-01-19 18:09:19 +01:00
link2xt
e67e684ee0 test: wait for joiner success in test_verified_group_[member_added]_recovery
If we wait for inviter success,
vg-member-added message may be still in flight
and reach ac2 after device resetup.

Making ac2 wait for joining the group ensures that old
device receives vg-member-added message
and new device will not receive it and fail to decrypt.

Other instances of wait_for_securejoin_inviter_success()
in the same tests are also replaced for reliability.
2024-01-18 17:00:21 +00:00
link2xt
6cfe3e6a97 chore(deps): update h2 0.4.0 -> 0.4.2 2024-01-18 13:40:38 +00:00
Sebastian Klähn
99ac524905 chore(deps): update h2 from 0.3.22 -> 0.3.24 2024-01-18 14:21:58 +01:00
link2xt
2faf7fdb78 fix: BCC-to-self even if server deletion is set to "at once" 2024-01-18 10:20:01 +00:00
link2xt
6a8ea8a083 fix: set message download state to Failure on IMAP errors
Previously the message was removed from `download` table,
but message bubble was stuck in InProgress state.

Now download state is updated by the caller,
so it cannot be accidentally skipped.
2024-01-18 10:18:57 +00:00
link2xt
e0e56cd831 chore: update quoted_printable to 0.5
And update mailparse to 0.14.1 so there is no duplicate dependency.
2024-01-18 09:35:05 +00:00
missytake
bbc6febb72 test: no timeout in SetupPlugin 2024-01-17 14:20:29 +01:00
missytake
7f7f42d721 test: Ensure that member is added before yielding chat 2024-01-17 14:20:29 +01:00
iequidoo
589236c27b fix: chat::send_msg: Remove encryption-related params from already sent message
This allows to send existing messages (incoming and outgoing) taken from encrypted chats, to
unencrypted ones. `Param::ForcePlaintext` is removed as well -- if a message can be sent encrypted
this time, nothing bad with this.
2024-01-17 14:20:29 +01:00
iequidoo
c16c5e0802 test: Bring test_forward_encrypted_to_unencrypted into line with current API
Currently `Chat.send_msg()` modifies the source message and returns another message object
equivalent to the source one. That's how it works in the core and thus in Python bindings too.
2024-01-17 14:20:29 +01:00
missytake
36cab40ac1 test: add get_protected_chat to testplugin.py 2024-01-17 14:20:29 +01:00
missytake
4186d78305 test: add python test for message forwarding from encrypted to unencrypted chat 2024-01-17 14:20:29 +01:00
iequidoo
06cccb77f8 feat: Use Quoted-Printable for the text part (#3986)
This is needed to protect from ESPs (such as gmx.at) doing their own Quoted-Printable encoding and
thus breaking messages and signatures. It's unlikely that the reader uses a MUA not supporting
Quoted-Printable encoding. And RFC 2646 "4.6" also recommends it for encrypted messages.
2024-01-16 23:46:24 -03:00
link2xt
1895f4c556 chore(release): prepare for 1.133.0 2024-01-15 22:55:26 +00:00
link2xt
849a873e61 feat: only try to configure non-strict TLS checks if explicitly set
Trying non-strict TLS checks is not necessary
for most servers with proper TLS setup,
but doubles the time needed to fail configuration
when the server is not responding, e.g.
when all connection attempts time out.

There is also a risk of accidentally
configuring non-strict TLS checks in a rare case
that strict TLS check configuration spuriously failed,
e.g. on a bad network.

If the server has a known broken TLS setup,
it can still be added to the provider database
or configured with non-strict TLS check manually.
User can also configure another email provider,
such as chatmail servers, instead of using the server
with invalid TLS hostname.

This change does not affect exising setups.
2024-01-15 22:54:31 +00:00
link2xt
b5c0372c99 docs: restore "Constants" page in Doxygen >=1.9.8
deltachat.h uses `@defgroup` commands to create topics
for groups of constants. Prior to Doxygen 1.9.8
defining a group created a "module"
and all constants were visible from the modules.html page.
In Doxygen 1.9.8 "modules" were renamed into "topics"
as C++20 modules have taken their place,
so Delta Chat documentation does not have modules
in Doxygen sense anymore.

The change is to replace "modules.html" with "topics.html"
in the DoxygenLayout.xml.

See <https://www.doxygen.nl/manual/grouping.html> for
Doxygen documentation about groups and their relation to topics.
2024-01-14 12:17:54 +00:00
link2xt
1ba9b69849 chore: npm run build:core:constants 2024-01-13 22:51:24 +00:00
holger krekel
6345a4f5b3 fix link for securejoin 2024-01-13 12:50:16 +01:00
Sebastian Klähn
382fc75b1e Add more docs (#5174)
Add some docs to smtp functions
2024-01-12 11:14:05 +01:00
Sebastian Klähn
92fc9ea971 feat: Encrypt MDNs #5168 (#5175)
This PR stops MDNs from being forced to be sent unencrypted. 
If no encryption is possible (by `should_encrypt`), the fix #5152 still
applies.

close #5168
2024-01-12 10:54:54 +01:00
Sebastian Klähn
de7ac2a240 fix: emit events more reliable when starting and stopping io #5097 (#5101)
Send `EventType::ConnectivityChanged` when using the context methods
`start_io` and `stop_io`.

close #5097

---------

Co-authored-by: Septias <scoreplayer2000@gmail.comclear>
2024-01-12 09:45:34 +01:00
link2xt
7b0e5adaee chore(deps): update rustyline from 12 to 13 2024-01-12 02:45:53 +00:00
iequidoo
406b59501b chore: deltachat-jsonrpc/src/api/types/events.rs: Apply rustfmt 2024-01-11 21:53:26 -03:00
iequidoo
d5da2bed75 feat: Add ConfigSynced event
Add an event for a case if a multi-device synced config value changed. Maybe the app needs to
refresh smth on such an event. For uniformity it is emitted on the source device too. The value is
omitted, otherwise it would be logged which might not be good for privacy.
2024-01-11 21:53:26 -03:00
173 changed files with 12704 additions and 5627 deletions

View File

@@ -24,9 +24,11 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.75.0
RUSTUP_TOOLCHAIN: 1.77.1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install rustfmt and clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
- name: Cache rust cargo artifacts
@@ -42,7 +44,9 @@ jobs:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: EmbarkStudios/cargo-deny-action@v1
with:
arguments: --all-features --workspace
@@ -53,7 +57,9 @@ jobs:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Check provider database
run: scripts/update-provider-database.sh
@@ -63,8 +69,9 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Rustdoc
@@ -76,18 +83,20 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.75.0
rust: 1.77.1
- os: windows-latest
rust: 1.75.0
rust: 1.77.1
- os: macos-latest
rust: 1.75.0
rust: 1.77.1
# Minimum Supported Rust Version = 1.70.0
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest
rust: 1.70.0
rust: 1.77.0
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
@@ -96,10 +105,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
@@ -111,7 +130,9 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
@@ -120,7 +141,7 @@ jobs:
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Upload C library
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug/libdeltachat.a
@@ -133,7 +154,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
@@ -142,7 +165,7 @@ jobs:
run: cargo build -p deltachat-rpc-server
- name: Upload deltachat-rpc-server
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
@@ -152,8 +175,9 @@ jobs:
name: Python lint
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install tox
run: pip install tox
@@ -193,16 +217,18 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
@@ -243,10 +269,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
@@ -254,7 +282,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -21,69 +21,65 @@ jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Cross-compile deltachat-rpc-server for x86_64, i686, aarch64 and armv7 Linux
runs-on: ubuntu-22.04
name: Linux
strategy:
fail-fast: false
matrix:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install ziglang
run: pip install wheel ziglang==0.11.0
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: sh scripts/zig-rpc-server.sh
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
- name: Upload dist directory with Linux binaries
uses: actions/upload-artifact@v3
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: linux
path: dist/
name: deltachat-rpc-server-${{ matrix.arch }}-linux
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_windows:
name: Build deltachat-rpc-server for Windows
name: Windows
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
artifact: win32.exe
path: deltachat-rpc-server.exe
target: i686-pc-windows-msvc
- os: windows-latest
artifact: win64.exe
path: deltachat-rpc-server.exe
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.artifact }}
path: target/${{ matrix.target}}/release/${{ matrix.path }}
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
if-no-files-found: error
build_macos:
name: Build deltachat-rpc-server for macOS
name: macOS
strategy:
fail-fast: false
matrix:
include:
- arch: x86_64
- arch: aarch64
arch: [x86_64, aarch64]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Setup rust target
run: rustup target add ${{ matrix.arch }}-apple-darwin
@@ -92,61 +88,140 @@ jobs:
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
- name: Upload binary
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
if-no-files-found: error
build_android:
name: Android
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
if-no-files-found: error
publish:
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
permissions:
id-token: write
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
- name: Download Linux binaries
uses: actions/download-artifact@v3
- uses: actions/checkout@v4
with:
name: linux
path: dist/
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Download win32 binary
uses: actions/download-artifact@v3
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32.exe
path: deltachat-rpc-server-win32.exe.d
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download win64 binary
uses: actions/download-artifact@v3
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64.exe
path: deltachat-rpc-server-win64.exe.d
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@v3
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@v3
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Flatten dist/ directory
- 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: Create bin/ directory
run: |
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
mkdir -p bin
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
- name: List binaries
run: ls -l bin/
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: 3.12
@@ -154,15 +229,40 @@ jobs:
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: scripts/wheel-rpc-server.py
run: |
mkdir -p dist
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
- name: List downloaded artifacts
- name: List artifacts
run: ls -l dist/
- name: Upload binaries 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 }} \
dist/*
bin/* dist/*
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -13,9 +13,10 @@ jobs:
steps:
- name: Install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: Get tag
@@ -49,7 +50,7 @@ jobs:
ls -lah
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}

View File

@@ -14,9 +14,11 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Add Rust cache

View File

@@ -14,10 +14,12 @@ jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Use Node.js 18.x
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x

View File

@@ -14,9 +14,10 @@ jobs:
matrix:
os: [macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: System info
@@ -28,7 +29,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -36,7 +37,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/
@@ -56,7 +57,7 @@ jobs:
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz
@@ -74,9 +75,10 @@ jobs:
- name: Change working directory owner
run: chown root:root .
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- run: apt-get update
@@ -97,7 +99,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -105,7 +107,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/
@@ -125,7 +127,7 @@ jobs:
tar -zcvf "linux.tar.gz" -C prebuilds .
- name: Upload Prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: linux
path: node/linux.tar.gz
@@ -137,9 +139,10 @@ jobs:
steps:
- name: Install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: Get tag
@@ -165,25 +168,25 @@ jobs:
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download Linux prebuild
uses: actions/download-artifact@v1
uses: actions/download-artifact@v4
with:
name: linux
- name: Download macOS prebuild
uses: actions/download-artifact@v1
uses: actions/download-artifact@v4
with:
name: macos-latest
- name: Download Windows prebuild
uses: actions/download-artifact@v1
uses: actions/download-artifact@v4
with:
name: windows-latest
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf linux/linux.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tar -xvzf linux.tar.gz -C node/prebuilds
tar -xvzf macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -rf linux macos-latest windows-latest
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
- name: Install dependencies without running scripts
run: |
npm install --ignore-scripts
@@ -201,7 +204,7 @@ jobs:
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload prebuild
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}

View File

@@ -23,9 +23,10 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: System info
@@ -37,7 +38,7 @@ jobs:
node --version
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
${{ env.APPDATA }}/npm-cache
@@ -45,7 +46,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/

View File

@@ -0,0 +1,47 @@
name: Publish deltachat-rpc-client to PyPI
on:
workflow_dispatch:
release:
types: [published]
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install pypa/build
run: python3 -m pip install build
- name: Build a binary wheel and a source tarball
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
publish-to-pypi:
name: Publish Python distribution to PyPI
if: github.event_name == 'release'
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-client
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -10,15 +10,17 @@ on:
jobs:
build_repl:
name: Build REPL example
runs-on: windows-latest
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build
run: cargo build -p deltachat-repl --features vendored
run: nix build .#deltachat-repl-win64
- name: Upload binary
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: repl.exe
path: "target/debug/deltachat-repl.exe"
path: "result/bin/deltachat-repl.exe"

View File

@@ -1,4 +1,4 @@
name: Build & Deploy Documentation on rs.delta.chat
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
on:
push:
@@ -6,11 +6,13 @@ on:
- main
jobs:
build:
build-rs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps --document-private-items
@@ -22,3 +24,41 @@ jobs:
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
build-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.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@py.delta.chat:/home/delta/build/master"
build-c:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- 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"

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
@@ -14,15 +14,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
show-progress: false
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.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/cffi/"
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 }}@delta.chat:/var/www/html/cffi/"

View File

@@ -1,25 +0,0 @@
name: Build & Deploy Documentation on py.delta.chat
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Build Python documentation
run: scripts/build-python-docs.sh
- name: Upload to py.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: delta
KEY: ${{ secrets.CODESPEAK_KEY }}
HOST: "lists.codespeak.net"
SOURCE: "dist/html/"
TARGET: "/home/delta/build/master"

7
.gitignore vendored
View File

@@ -44,3 +44,10 @@ node/build/
node/dist/
node/prebuilds/
node/.nyc_output/
# Nix symlink.
result
# direnv
.envrc
.direnv

View File

@@ -1,5 +1,613 @@
# Changelog
## [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
- [**breaking**] Increase Minimum Supported Rust Version to 1.77.0.
### Features / Changes
- Show reactions in summaries ([#5387](https://github.com/deltachat/deltachat-core-rust/pull/5387)).
### Tests
- Test reactions for forwarded messages
### Refactor
- `is_probably_private_reply`: Remove reaction-specific code.
- Use Rust 1.77.0 support for recursion in async functions.
### Miscellaneous Tasks
- cargo: Bump rustyline from 13.0.0 to 14.0.0.
- Update chrono from 0.4.34 to 0.4.37.
- Update from brotli 3.4.0 to brotli 4.0.0.
- Upgrade `h2` from 0.4.3 to 0.4.4.
- Upgrade `image` from 0.24.9 to 0.25.1.
- cargo: Bump fast-socks5 from 0.9.5 to 0.9.6.
## [1.137.1] - 2024-04-03
### CI
- Remove android builds for `x86` and `x86_64`.
## [1.137.0] - 2024-04-02
### API-Changes
- [**breaking**] Remove data from `DC_EVENT_INCOMING_MSG_BUNCH`.
- [**breaking**] Remove unused `dc_accounts_all_work_done()` ([#5384](https://github.com/deltachat/deltachat-core-rust/pull/5384)).
- deltachat-rpc-client: Add futures.
### Build system
- cmake: Build outside the source tree.
- nix: Add outputs for Android binaries.
- Add `repository` to Cargo.toml.
- python: Remove `setuptools_scm` dependency.
- Add development shell ([#5390](https://github.com/deltachat/deltachat-core-rust/pull/5390)).
### CI
- Update to Rust 1.77.0.
- Build deltachat-rpc-server for Android.
- Shorter names for deltachat-rpc-server jobs.
### Features / Changes
- Do not include provider hostname in `Message-ID`.
- Include 3 recent Message-IDs in `References` header.
- Include more entries into DNS fallback cache.
### Fixes
- Preserve upper-/lowercase of links parsed by `dehtml()` ([#5362](https://github.com/deltachat/deltachat-core-rust/pull/5362)).
- Rescan folders after changing `Config::SentboxWatch`.
- Do not ignore `Contact::get_by_id()` error in `from_field_to_contact_id()`.
- Put overridden sender name into message info.
- Don't send selfavatar in `SecureJoin` messages before contact verification ([#5354](https://github.com/deltachat/deltachat-core-rust/pull/5354)).
- Always set correct `chat_id` for `DC_EVENT_REACTIONS_CHANGED` ([#5419](https://github.com/deltachat/deltachat-core-rust/pull/5419)).
### Refactor
- Remove `MessageObject::from_message_id()`.
- jsonrpc: Add `msg_id` and `account_id` to `get_message()` errors.
- Cleanup `jobs` and `Params` relicts.
### Tests
- `Test_mvbox_sentbox_threads`: Check that sentbox gets configured after setting `sentbox_watch` ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
- Remove flaky time check from `test_list_from()`.
- Add failing test for #5418 (wrong `DC_EVENT_REACTIONS_CHANGED`)
### Miscellaneous Tasks
- Add `result` to .gitignore.
- cargo: Bump thiserror from 1.0.57 to 1.0.58.
- cargo: Bump tokio from 1.36.0 to 1.37.0.
- cargo: Bump pin-project from 1.1.4 to 1.1.5.
- cargo: Bump strum from 0.26.1 to 0.26.2.
- cargo: Bump uuid from 1.7.0 to 1.8.0.
- cargo: Bump toml from 0.8.10 to 0.8.12.
- cargo: Bump tokio-stream from 0.1.14 to 0.1.15.
- cargo: Bump smallvec from 1.13.1 to 1.13.2.
- cargo: Bump async-smtp from 0.9.0 to 0.9.1.
- cargo: Bump strum_macros from 0.26.1 to 0.26.2.
- cargo: Bump serde_json from 1.0.114 to 1.0.115.
- cargo: Bump anyhow from 1.0.80 to 1.0.81.
- cargo: Bump syn from 2.0.52 to 2.0.57.
- cargo: Bump futures-lite from 2.2.0 to 2.3.0.
- cargo: Bump axum from 0.7.4 to 0.7.5.
- cargo: Bump reqwest from 0.11.24 to 0.12.2.
- cargo: Bump backtrace from 0.3.69 to 0.3.71.
- cargo: Bump regex from 1.10.3 to 1.10.4.
- cargo: Update aho-corasick from 1.1.2 to 1.1.3.
- Update deny.toml.
## [1.136.6] - 2024-03-19
### Build system
- Add description to deltachat-rpc-server wheels.
- Read version from Cargo.toml in wheel-rpc-server.py.
### CI
- Update actions/cache from v3 to v4.
- Automate publishing of deltachat-rpc-server to PyPI.
### Documentation
- deltachat-rpc-server: Update deltachat-rpc-client URL.
### Miscellaneous Tasks
- Nix flake update.
## [1.136.5] - 2024-03-18
### Features / Changes
- Nicer summaries: prefer emoji over names
- Add `save_mime_headers` to debug info ([#5350](https://github.com/deltachat/deltachat-core-rust/pull/5350))
### Fixes
- Terminate ephemeral and location loop immediately on channel close.
- Update MemberListTimestamp when sending a group message.
- On iOS, use FILE (default) instead of MEMORY ([#5349](https://github.com/deltachat/deltachat-core-rust/pull/5349)).
- Add white background to recoded avatars ([#3787](https://github.com/deltachat/deltachat-core-rust/pull/3787)).
### Build system
- Add README to deltachat-rpc-client Python packages.
### Documentation
- deltachat-rpc-client: Document that 0 is a special value of `set_ephemeral_timer()`.
### Tests
- Test that reordering of Member added message results in square bracket error.
## [1.136.4] - 2024-03-11
### Build system
- nix: Make .#libdeltachat buildable on macOS.
- Build deltachat-rpc-server wheels with nix.
### CI
- Add workflow for automatic publishing of deltachat-rpc-client.
### Fixes
- Remove duplicate CHANGELOG entries for 1.135.1.
## [1.136.3] - 2024-03-09
### Features / Changes
- Start IMAP loop for sentbox only if it is configured ([#5105](https://github.com/deltachat/deltachat-core-rust/pull/5105)).
### Fixes
- Remove leading whitespace from Subject ([#5106](https://github.com/deltachat/deltachat-core-rust/pull/5106)).
- Create new Peerstate for unencrypted message with already known Autocrypt key, but a new address.
### Build system
- nix: Cleanup cross-compilation code.
- nix: Include SystemConfiguration framework on darwin systems.
### CI
- Wait for `build_windows` task before trying to publish it.
- Remove artifacts from npm package.
### Refactor
- Don't parse Autocrypt header for outgoing messages ([#5259](https://github.com/deltachat/deltachat-core-rust/pull/5259)).
- Remove `deduplicate_peerstates()`.
- Fix 2024-03-05 nightly clippy warnings.
### Miscellaneous Tasks
- deps: Bump mio from 0.8.8 to 0.8.11 in /fuzz.
- RPC client: Add missing constants ([#5110](https://github.com/deltachat/deltachat-core-rust/pull/5110)).
## [1.136.2] - 2024-03-05
### Build system
- Downgrade `cc` to 1.0.83 to fix build for Android.
### CI
- Update setup-node action.
## [1.136.1] - 2024-03-05
### Build system
- Revert to OpenSSL 3.1.
- Restore MSRV 1.70.0.
### Miscellaneous Tasks
- Update node constants.
## [1.136.0] - 2024-03-04
### Features / Changes
- Recognise Trash folder by name ([#5275](https://github.com/deltachat/deltachat-core-rust/pull/5275)).
- Send Chat-Group-Avatar as inline base64 ([#5253](https://github.com/deltachat/deltachat-core-rust/pull/5253)).
- Self-Reporting: Report number of protected/encrypted/unencrypted chats ([#5292](https://github.com/deltachat/deltachat-core-rust/pull/5292)).
### Fixes
- Don't send sync messages on self-{status,avatar} update from self-sent messages ([#5289](https://github.com/deltachat/deltachat-core-rust/pull/5289)).
- imap: Allow `maybe_network` to interrupt connection ratelimit.
- imap: Set connectivity to "connecting" only after ratelimit.
- Remove `Group-ID` from `Message-ID`.
- Prioritize protected `Message-ID` over `X-Microsoft-Original-Message-ID`.
### API-Changes
- Make `store_self_keypair` private.
- Add `ContextBuilder.build()` to build Context without opening.
- `dc_accounts_set_push_device_token` and `dc_get_push_state` APIs for iOS push notifications.
### Build system
- Tag armv6 wheels with tags accepted by PyPI.
- Unpin OpenSSL.
- Remove deprecated `unmaintained` field from deny.toml.
- Do not vendor OpenSSL when cross-compiling ([#5316](https://github.com/deltachat/deltachat-core-rust/pull/5316)).
- Increase MSRV to 1.74.0.
### CI
- Upgrade setup-python GitHub Action.
- Update to Rust 1.76 and fix clippy warnings.
- Build Python docs with Nix.
- Upload python docs without GH actions.
- Upload cffi docs without GH actions.
- Build c.delta.chat docs with nix.
### Other
- refactor: move more methods from Imap into Session.
- Add deltachat-time to sources.
### Refactor
- Remove Session from Imap structure.
- Merge ImapConfig into Imap.
- Get rid of ImapActionResult.
- Build contexts using ContextBuilder.
- Do not send `Secure-Join-Group` in `vg-request`.
### Tests
- Fix `test_verified_oneonone_chat_broken_by_device_change()` ([#5280](https://github.com/deltachat/deltachat-core-rust/pull/5280)).
- `get_protected_chat()`: Use FFIEventTracker instead of `dc_wait_next_msgs()` ([#5207](https://github.com/deltachat/deltachat-core-rust/pull/5207)).
- Fixup `tests/test_3_offline.py::TestOfflineAccountBasic::test_wrong_db`.
- Fix pytest compat ([#5317](https://github.com/deltachat/deltachat-core-rust/pull/5317)).
## [1.135.1] - 2024-02-20
### Features / Changes
- Sync self-avatar across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- Sync Config::Selfstatus across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- Remove webxdc sending limit.
### Fixes
- Never encrypt `{vc,vg}-request` SecureJoin messages.
- Apply Autocrypt headers if timestamp is unchanged.
- `Context::get_info`: Report displayname as "displayname" (w/o underscore).
### Tests
- Mock `SystemTime::now()` for the tests.
- Add a test on protection message sort timestamp ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
### Build system
- Add flake.nix.
- Add footer template for git-cliff.
### CI
- Update GitHub Actions `actions/upload-artifact`, `actions/download-artifact`, `actions/checkout`.
- Build deltachat-repl for Windows with nix.
- Build deltachat-rpc-server with nix.
- Try to upload deltachat-rpc-server only on release.
- Fixup node-package.yml after artifact actions upgrade.
- Update to actions/checkout@v4.
- Replace download-artifact v1 with v4.
### Refactor
- `create_keypair`: Remove unnecessary `map_err`.
- Return error with a cause when failing to export keys.
- Rename incorrectly named variables in `create_keypair`.
## [1.135.0] - 2024-02-13
### Features / Changes
- Add wildcard pattern support to provider database.
- Add device message about outgoing undecryptable messages ([#5164](https://github.com/deltachat/deltachat-core-rust/pull/5164)).
- Context::set_config(): Restart IO scheduler if needed ([#5111](https://github.com/deltachat/deltachat-core-rust/pull/5111)).
- Server_sent_unsolicited_exists(): Log folder name.
- Cache system time instead of looking at the clock several times in a row.
- Basic self-reporting ([#5129](https://github.com/deltachat/deltachat-core-rust/pull/5129)).
### Fixes
- Dehtml: Don't just truncate text when trying to decode ([#5223](https://github.com/deltachat/deltachat-core-rust/pull/5223)).
- Mark the gossip keys from the message as verified, not the ones from the db ([#5247](https://github.com/deltachat/deltachat-core-rust/pull/5247)).
- Guarantee immediate message deletion if delete_server_after == 0 ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
- Never allow a message timestamp to be a lot in the future ([#5249](https://github.com/deltachat/deltachat-core-rust/pull/5249)).
- Imap::configure_mvbox: Do select_with_uidvalidity() before return.
- ImapSession::select_or_create_folder(): Don't fail if folder is created in parallel.
- Emit ConfigSynced event on the second device.
- Create mvbox on setting mvbox_move.
- Use SystemTime instead of Instant everywhere.
- Restore database rows removed in previous release; this ensures compatibility when adding second device or importing backup and not all devices run the new core ([#5254](https://github.com/deltachat/deltachat-core-rust/pull/5254))
### Miscellaneous Tasks
- cargo: Bump image from 0.24.7 to 0.24.8.
- cargo: Bump chrono from 0.4.31 to 0.4.33.
- cargo: Bump futures-lite from 2.1.0 to 2.2.0.
- cargo: Bump pin-project from 1.1.3 to 1.1.4.
- cargo: Bump iana-time-zone from yanked 0.1.59 to 0.1.60.
- cargo: Bump smallvec from 1.11.2 to 1.13.1.
- cargo: Bump base64 from 0.21.5 to 0.21.7.
- cargo: Bump regex from 1.10.2 to 1.10.3.
- cargo: Bump libc from 0.2.151 to 0.2.153.
- cargo: Bump reqwest from 0.11.23 to 0.11.24.
- cargo: Bump axum from 0.7.3 to 0.7.4.
- cargo: Bump uuid from 1.6.1 to 1.7.0.
- cargo: Bump fast-socks5 from 0.9.2 to 0.9.5.
- cargo: Bump serde_json from 1.0.111 to 1.0.113.
- cargo: Bump syn from 2.0.46 to 2.0.48.
- cargo: Bump serde from 1.0.194 to 1.0.196.
- cargo: Bump toml from 0.8.8 to 0.8.10.
- cargo: Update to strum 0.26.
- Cargo update.
- scripts: Do not install deltachat-rpc-client twice.
### Other
- Update welcome image, thanks @paulaluap
- Merge pull request #5243 from deltachat/dependabot/cargo/pin-project-1.1.4
- Merge pull request #5241 from deltachat/dependabot/cargo/futures-lite-2.2.0
- Merge pull request #5236 from deltachat/dependabot/cargo/chrono-0.4.33
- Merge pull request #5235 from deltachat/dependabot/cargo/image-0.24.8
### Refactor
- Resultify token::exists.
### Tests
- Delete_server_after="1" should cause immediate message deletion ([#5201](https://github.com/deltachat/deltachat-core-rust/pull/5201)).
## [1.134.0] - 2024-01-31
### API-Changes
- [**breaking**] JSON-RPC: device message api now requires `Option<MessageData>` instead of `String` for the message ([#5211](https://github.com/deltachat/deltachat-core-rust/pull/5211)).
- CFFI: add `dc_accounts_background_fetch` and event `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE`.
- JSON-RPC: add `accounts_background_fetch`.
### Features / Changes
- `Qr::check_qr()`: Accept i.delta.chat invite links ([#5217](https://github.com/deltachat/deltachat-core-rust/pull/5217)).
- Add support for IMAP METADATA, fetching `/shared/comment` and `/shared/admin` and displaying it in account info.
### Fixes
- Add tolerance for macOS and iOS changing `#` to `%23`.
- Do not drop unknown report attachments, such as TLS reports.
- Treat only "Auto-Submitted: auto-generated" messages as bot-sent ([#5213](https://github.com/deltachat/deltachat-core-rust/pull/5213)).
- `Chat::resend_msgs`: Guarantee strictly increasing time in the `Date` header.
- Delete resent messages on receiver side ([#5155](https://github.com/deltachat/deltachat-core-rust/pull/5155)).
- Fix iOS build issue.
### CI
- Add/remove necessary newlines to fix Python lint.
### Tests
- `test_import_export_online_all`: Send the message to the existing address to avoid errors ([#5220](https://github.com/deltachat/deltachat-core-rust/pull/5220)).
## [1.133.2] - 2024-01-24
### Fixes
- Downgrade OpenSSL from 3.2.0 to 3.1.4 ([#5206](https://github.com/deltachat/deltachat-core-rust/issues/5206))
- No new chats for MDNs with alias ([#5196](https://github.com/deltachat/deltachat-core-rust/issues/5196)) ([#5199](https://github.com/deltachat/deltachat-core-rust/pull/5199)).
## [1.133.1] - 2024-01-21
### API-Changes
- Add `is_bot` to cffi and jsonrpc ([#5197](https://github.com/deltachat/deltachat-core-rust/pull/5197)).
### Features / Changes
- Add system message when provider does not allow unencrypted messages ([#5195](https://github.com/deltachat/deltachat-core-rust/pull/5195)).
### Fixes
- `Chat::send_msg`: Remove encryption-related params from already sent message. This allows to send received encrypted `dc_msg_t` object to unencrypted chat, e.g. in a Python bot.
- Set message download state to Failure on IMAP errors. This avoids partially downloaded messages getting stuck in "Downloading..." state without actually being in a download queue.
- BCC-to-self even if server deletion is set to "at once". This is a workaround for SMTP servers which do not return response in time, BCC-self works as a confirmation that message was sent out successfully and does not need more retries.
- node: Run tests with native ESM modules instead of `esm` ([#5194](https://github.com/deltachat/deltachat-core-rust/pull/5194)).
- Use Quoted-Printable MIME encoding for the text part ([#3986](https://github.com/deltachat/deltachat-core-rust/pull/3986)).
### Tests
- python: Add `get_protected_chat` to testplugin.py.
## [1.133.0] - 2024-01-14
### Features / Changes
- Securejoin protocol implementation refinements
- Track forward and backward verification separately ([#5089](https://github.com/deltachat/deltachat-core-rust/pull/5089)) to avoid inconsistent states.
- Mark 1:1 chat as verified for Bob early. 1:1 chat with Alice is verified as soon as Alice's key is verified rather than at the end of the protocol.
- Put Message-ID into hidden headers and take it from there on receiver ([#4798](https://github.com/deltachat/deltachat-core-rust/pull/4798)). This works around servers which generate their own Message-ID and overwrite the one generated by Delta Chat.
- deltachat-repl: Enable INFO logging by default and add timestamps.
- Add `ConfigSynced` (`DC_EVENT_CONFIG_SYNCED`) event which is emitted when configuration is changed via synchronization message or synchronization message for configuration is sent. UI may refresh elments based on the configuration key which is a part of the event.
- Sync contact creation/rename across devices ([#5163](https://github.com/deltachat/deltachat-core-rust/pull/5163)).
- Encrypt MDNs ([#5175](https://github.com/deltachat/deltachat-core-rust/pull/5175)).
- Only try to configure non-strict TLS checks if explicitly set ([#5181](https://github.com/deltachat/deltachat-core-rust/pull/5181)).
### Build system
- Use released version of iroh 0.4.2 for "setup second device" feature.
### CI
- Update to Rust 1.75.0.
- Downgrade `chai` from 4.4.0 to 4.3.10.
### Documentation
- Add a link <https://www.ietf.org/archive/id/draft-bucksch-autoconfig-00.html> to autoconfig RFC draft.
- Update securejoin link in `standards.md` from <https://countermitm.readthedocs.io/> to <https://securejoin.readthedocs.io>.
- Restore "Constants" page in Doxygen >=1.9.8
### Fixes
- imap: Limit the rate of LOGIN attempts rather than connection attempts. This is to avoid having to wait for rate limiter right after switching from a bad or offline network to a working network while still guarding against reconnection loop.
- Do not ignore `peerstate.save_to_db()` errors.
- securejoin: Mark 1:1s as protected regardless of the Config::VerifiedOneOnOneChats.
- Delete received outgoing messages from SMTP queue ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
- imap: Fail fast on `LIST` errors to avoid busy loop when connection is lost.
- Split SMTP jobs already in `chat::create_send_msg_jobs()` ([#5115](https://github.com/deltachat/deltachat-core-rust/pull/5115)).
- Do not remove contents from unencrypted [Schleuder](https://schleuder.org/) mailing lists messages.
- Reset message error when scheduling resending ([#5119](https://github.com/deltachat/deltachat-core-rust/pull/5119)).
- Emit events more reliably when starting and stopping I/O ([#5101](https://github.com/deltachat/deltachat-core-rust/pull/5101)).
- Fix timestamp of chat protection info message for correct message ordering after restoring a backup ([#5088](https://github.com/deltachat/deltachat-core-rust/pull/5088)).
### Refactor
- sql: Recreate `config` table with UNIQUE constraint.
- sql: Recreate `keypairs` table to remove unused `addr` and `created` fields and move `is_default` flag to `config` table.
- Send `Secure-Join-Fingerprint` only in `*-request-with-auth`.
### Tests
- Test joining non-protected group.
- Test that read receipts don't degrade encryption.
- Test that changing default private key breaks backward verification.
- Test recovery from lost vc-contact-confirm.
- Use `wait_for_incoming_msg_event()` more.
## [1.132.1] - 2023-12-12
### Features / Changes
@@ -3357,3 +3965,21 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9
[1.132.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.9...v1.132.0
[1.132.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.0...v1.132.1
[1.133.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.132.1...v1.133.0
[1.133.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.0...v1.133.1
[1.133.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.1...v1.133.2
[1.134.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.133.2...v1.134.0
[1.135.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.134.0...v1.135.0
[1.135.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.0...v1.135.1
[1.136.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.135.1...v1.136.0
[1.136.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.0...v1.136.1
[1.136.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.1...v1.136.2
[1.136.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.2...v1.136.3
[1.136.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.3...v1.136.4
[1.136.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.4...v1.136.5
[1.136.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.136.5...v1.136.6
[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

View File

@@ -12,26 +12,22 @@ 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
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --release --no-default-features --features jsonrpc
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version
# "1" bug which makes `--no-default-features` affect only
# `deltachat`, but not `deltachat-ffi` package.
#
# We can't enable version "2" resolver [1] because it is not
# stable yet on rust 1.50.0.
#
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
)
@@ -39,12 +35,12 @@ add_custom_target(
lib_deltachat
ALL
DEPENDS
"target/release/libdeltachat.a"
"target/release/libdeltachat.${DYNAMIC_EXT}"
"target/release/pkgconfig/deltachat.pc"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc"
)
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "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)

1215
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
[package]
name = "deltachat"
version = "1.132.1"
version = "1.137.4"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
rust-version = "1.77"
repository = "https://github.com/deltachat/deltachat-core-rust"
[profile.dev]
debug = 0
@@ -30,37 +31,37 @@ opt-level = "z"
codegen-units = 1
strip = true
[patch.crates-io]
imap-proto = { git = "https://github.com/djc/tokio-imap.git", rev = "01ff256a7e42a9f7d2732706f8b71a16ce93427e" }
[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"
anyhow = { workspace = true }
async-broadcast = "0.7.0"
async-channel = "2.0.0"
async-imap = { version = "0.9.5", default-features = false, features = ["runtime-tokio"] }
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 = "3.4", default-features=false, features = ["std"] }
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
base64 = "0.22"
brotli = { version = "5", default-features=false, features = ["std"] }
chrono = { version = "0.4.37", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.9"
fd-lock = "4"
futures = "0.3"
futures-lite = "2.0.0"
futures-lite = "2.3.0"
hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
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"
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
mailparse = "0.14"
@@ -68,50 +69,58 @@ 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.10", default-features = false }
pin-project = "1"
pgp = { version = "0.11", default-features = false }
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.9"
reqwest = { version = "0.11.23", features = ["json"] }
rusqlite = { version = "0.30", features = ["sqlcipher"] }
regex = { workspace = true }
reqwest = { version = "0.12.2", 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"
strum = "0.25"
strum_macros = "0.25"
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"
tagger = "4.3.4"
textwrap = "0.16.0"
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.14", features = ["fs"] }
tokio-stream = { version = "0.1.15", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9"
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.0.0"
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]
@@ -122,7 +131,9 @@ members = [
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
"deltachat-time",
"format-flowed",
"deltachat-contact-tools",
]
[[bench]]
@@ -153,6 +164,12 @@ harness = false
name = "send_events"
harness = false
[workspace.dependencies]
anyhow = "1"
once_cell = "1.18.0"
regex = "1.10"
rusqlite = { version = "0.31" }
[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

@@ -9,13 +9,16 @@ For example, to release version 1.116.0 of the core, do the following steps.
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. Update the version by running `scripts/set_core_version.py 1.116.0`.
4. 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. Commit the changes as `chore(release): prepare for 1.116.0`.
5. 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`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
6. Tag the release: `git tag -a v1.116.0`.
7. Tag the release: `git tag -a v1.116.0`.
7. Push the release tag: `git push origin v1.116.0`.
8. Push the release tag: `git push origin v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -77,3 +77,17 @@ body = """
"""
# remove the leading and trailing whitespace from the template
trim = true
footer = """
{% for release in releases -%}
{% if release.version -%}
{% if release.previous.version -%}
[{{ release.version | trim_start_matches(pat="v") }}]: \
https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..{{ release.version }}
{% endif -%}
{% else -%}
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..HEAD
{% endif -%}
{% endfor %}
"""

View File

@@ -0,0 +1,18 @@
[package]
name = "deltachat-contact-tools"
version = "0.1.0"
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address"
license = "MPL-2.0"
# TODO maybe it should be called "deltachat-text-utils" or similar?
# 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.
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -0,0 +1,280 @@
//! 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::Result;
use once_cell::sync::Lazy;
use regex::Regex;
/// 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());
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(
&captures
.get(1)
.map_or("".to_string(), |m| normalize_name(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(name), addr.to_string())
}
}
/// 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 super::*;
#[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);
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.132.1"
version = "1.137.4"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -20,7 +20,7 @@ libc = "0.2"
human-panic = { version = "1", 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

@@ -9,7 +9,7 @@
<tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/>
</tab>
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>

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);
@@ -423,19 +426,16 @@ char* dc_get_blobdir (const dc_context_t* context);
* Sending messages to self is needed for a proper multi-account setup,
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
* 0=do not watch the `Sent`-folder (default),
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* 0=do not watch the `Sent`-folder (default).
* - `mvbox_move` = 1=detect chat messages,
* move them to the `DeltaChat` folder,
* and watch the `DeltaChat` folder for updates (default),
* 0=do not move chat-messages
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `only_fetch_mvbox` = 1=Do not fetch messages from folders other than the
* `DeltaChat` folder. Messages will still be fetched from the
* spam folder and `sendbox_watch` will also still be respected
* if enabled.
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
@@ -689,8 +689,25 @@ int dc_get_connectivity (dc_context_t* context);
char* dc_get_connectivity_html (dc_context_t* context);
#define DC_PUSH_NOT_CONNECTED 0
#define DC_PUSH_HEARTBEAT 1
#define DC_PUSH_CONNECTED 2
/**
* Get the current push notification state.
* One of:
* - DC_PUSH_NOT_CONNECTED
* - DC_PUSH_HEARTBEAT
* - DC_PUSH_CONNECTED
*
* @memberof dc_context_t
* @param context The context object.
* @return Push notification state.
*/
int dc_get_push_state (dc_context_t* context);
/**
* Standalone version of dc_accounts_all_work_done().
* Only used by the python tests.
*/
int dc_all_work_done (dc_context_t* context);
@@ -1103,36 +1120,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.
*
@@ -1195,6 +1182,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.
*
@@ -2562,7 +2608,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.
@@ -2598,8 +2644,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.
@@ -3084,23 +3129,6 @@ dc_context_t* dc_accounts_get_selected_account (dc_accounts_t* accounts);
int dc_accounts_select_account (dc_accounts_t* accounts, uint32_t account_id);
/**
* This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
*
* iOS can:
* - call dc_start_io() (in case IO was not running)
* - call dc_maybe_network()
* - while dc_accounts_all_work_done() returns false:
* - Wait for #DC_EVENT_CONNECTIVITY_CHANGED
*
* @memberof dc_accounts_t
* @param accounts The account manager as created by dc_accounts_new().
* @return Whether all accounts finished their background work.
* #DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
*/
int dc_accounts_all_work_done (dc_accounts_t* accounts);
/**
* Start job and IMAP/SMTP tasks for all accounts managed by the account manager.
* If IO is already running, nothing happens.
@@ -3151,6 +3179,33 @@ void dc_accounts_maybe_network (dc_accounts_t* accounts);
void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
/**
* Perform a background fetch for all accounts in parallel with a timeout.
* Pauses the scheduler, fetches messages from imap and then resumes the scheduler.
*
* dc_accounts_background_fetch() was created for the iOS Background fetch.
*
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end
* even in case of timeout, unless the function fails and returns 0.
* Process all events until you get this one and you can safely return to the background
* without forgetting to create notifications caused by timing race conditions.
*
* @memberof dc_accounts_t
* @param timeout The timeout in seconds
* @return Return 1 if DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE was emitted and 0 otherwise.
*/
int dc_accounts_background_fetch (dc_accounts_t* accounts, uint64_t timeout);
/**
* Sets device token for Apple Push Notification service.
* Returns immediately.
*
* @memberof dc_accounts_t
* @param token Hexadecimal device token
*/
void dc_accounts_set_push_device_token (dc_accounts_t* accounts, const char *token);
/**
* Create the event emitter that is used to receive events.
*
@@ -4114,7 +4169,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.
@@ -4397,6 +4451,9 @@ int dc_msg_is_info (const dc_msg_t* msg);
* Currently, the following types are defined:
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
*
* Even when you display an icon,
* you should still display the text of the informational message using dc_msg_get_text()
@@ -4423,6 +4480,7 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
@@ -5068,6 +5126,15 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
*/
int dc_contact_is_verified (dc_contact_t* contact);
/**
* Returns whether contact is a bot.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0 if the contact is not a bot, 1 otherwise.
*/
int dc_contact_is_bot (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
@@ -5283,52 +5350,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
*
@@ -6019,10 +6040,12 @@ void dc_event_unref(dc_event_t* event);
* Downloading a bunch of messages just finished. This is an
* event to allow the UI to only show one notification per message bunch,
* instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
* UI may store #DC_EVENT_INCOMING_MSG events
* and display notifications for all messages at once
* when this event arrives.
*
* @param data1 0
* @param data2 (char*) msg_ids, a json object with the message ids.
* @param data2 0
*/
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
@@ -6206,6 +6229,18 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_SELFAVATAR_CHANGED 2110
/**
* A multi-device synced config value changed. Maybe the app needs to refresh smth. For uniformity
* this is emitted on the source device too. The value isn't reported, otherwise it would be logged
* which might not be good for privacy. You can get the new value with
* `dc_get_config(context, data2)`.
*
* @param data1 0
* @param data2 (char*) Configuration key.
*/
#define DC_EVENT_CONFIG_SYNCED 2111
/**
* webxdc status update received.
* To get the received status update, use dc_get_webxdc_status_updates() with
@@ -6230,6 +6265,33 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
/**
* Tells that the Background fetch was completed (or timed out).
*
* This event acts as a marker, when you reach this event you can be sure
* that all events emitted during the background fetch were processed.
*
* This event is only emitted by the account manager
*/
#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
/**
* @}
@@ -6998,6 +7060,8 @@ void dc_event_unref(dc_event_t* event);
/// "You added member %1$s."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the added member's name.
#define DC_STR_ADD_MEMBER_BY_YOU 128
/// "Member %1$s added by %2$s."
@@ -7219,6 +7283,37 @@ void dc_event_unref(dc_event_t* event);
/// Used as the first info messages in newly created groups.
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
/// "Member %1$s added."
///
/// Used as info messages.
///
/// `%1$s` will be replaced by the added member's name.
#define DC_STR_MESSAGE_ADD_MEMBER 173
/// "Your email provider %1$s requires end-to-end encryption which is not setup yet."
///
/// Used as info messages when a message cannot be sent because it cannot be encrypted.
///
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/// "You reacted %1$s to '%2$s'"
///
/// `%1$s` will be replaced by the reaction, usually an emoji
/// `%2$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_YOU_REACTED 176
/// "%1$s reacted %2$s to '%3$s'"
///
/// `%1$s` will be replaced by the name the contact who reacted
/// `%2$s` will be replaced by the reaction, usually an emoji
/// `%3$s` will be replaced by the summary of the message the reaction refers to
///
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/**
* @}
*/

View File

@@ -3,8 +3,6 @@
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
)]
@@ -26,15 +24,13 @@ use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::context::{Context, ContextBuilder};
use deltachat::ephemeral::Timer as EphemeralTimer;
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::stock_str::StockStrings;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::*;
use deltachat::{accounts::Accounts, log::LogExt};
@@ -69,8 +65,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
@@ -104,12 +98,11 @@ pub unsafe extern "C" fn dc_context_new(
let ctx = if blobdir.is_null() || *blobdir == 0 {
// generate random ID as this functionality is not yet available on the C-api.
let id = rand::thread_rng().gen();
block_on(Context::new(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
))
block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.open(),
)
} else {
eprintln!("blobdir can not be defined explicitly anymore");
return ptr::null_mut();
@@ -133,12 +126,11 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
}
let id = rand::thread_rng().gen();
match block_on(Context::new_closed(
as_path(dbfile),
id,
Events::new(),
StockStrings::new(),
)) {
match block_on(
ContextBuilder::new(as_path(dbfile).to_path_buf())
.with_id(id)
.build(),
) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {err:#}");
@@ -387,7 +379,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
block_on(async move { ctx.get_connectivity().await as u32 as libc::c_int })
block_on(ctx.get_connectivity()) as u32 as libc::c_int
}
#[no_mangle]
@@ -410,6 +402,16 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_push_state(context: *const dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_push_state()");
return 0;
}
let ctx = &*context;
block_on(ctx.push_state()) as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_all_work_done(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -556,8 +558,12 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::SecurejoinJoinerProgress { .. } => 2061,
EventType::ConnectivityChanged => 2100,
EventType::SelfavatarChanged => 2110,
EventType::ConfigSynced { .. } => 2111,
EventType::WebxdcStatusUpdate { .. } => 2120,
EventType::WebxdcInstanceDeleted { .. } => 2121,
EventType::AccountsBackgroundFetchDone => 2200,
EventType::ChatlistChanged => 2300,
EventType::ChatlistItemChanged { .. } => 2301,
}
}
@@ -583,8 +589,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::Error(_)
| EventType::ConnectivityChanged
| EventType::SelfavatarChanged
| EventType::ConfigSynced { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ErrorSelfNotInGroup(_) => 0,
| EventType::ErrorSelfNotInGroup(_)
| EventType::AccountsBackgroundFetchDone => 0,
EventType::ChatlistChanged => 0,
EventType::MsgsChanged { chat_id, .. }
| EventType::ReactionsChanged { chat_id, .. }
| EventType::IncomingMsg { chat_id, .. }
@@ -609,6 +618,9 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
}
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::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
}
}
@@ -643,7 +655,11 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ConnectivityChanged
| EventType::WebxdcInstanceDeleted { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::SelfavatarChanged => 0,
| EventType::SelfavatarChanged
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::ChatlistItemChanged { .. }
| EventType::ConfigSynced { .. } => 0,
EventType::ChatModified(_) => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
@@ -705,7 +721,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::SelfavatarChanged
| EventType::WebxdcStatusUpdate { .. }
| EventType::WebxdcInstanceDeleted { .. }
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
| EventType::AccountsBackgroundFetchDone
| EventType::ChatEphemeralTimerModified { .. }
| EventType::IncomingMsgBunch { .. }
| EventType::ChatlistItemChanged { .. }
| EventType::ChatlistChanged => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -717,11 +737,10 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
let data2 = file.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
.unwrap_or_default()
.to_c_string()
.unwrap_or_default()
.into_raw(),
EventType::ConfigSynced { key } => {
let data2 = key.to_string().to_c_string().unwrap_or_default();
data2.into_raw()
}
}
}
@@ -1001,49 +1020,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,
@@ -1087,6 +1063,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,
@@ -2032,7 +2045,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();
}
}
@@ -4125,6 +4138,15 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_is_bot()");
return 0;
}
(*contact).contact.is_bot() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
if contact.is_null() {
@@ -4221,45 +4243,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 _)
@@ -4460,19 +4443,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
@@ -4828,16 +4798,6 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
Box::into_raw(Box::new(array))
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_all_work_done(accounts: *mut dc_accounts_t) -> libc::c_int {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_all_work_done()");
return 0;
}
let accounts = &*accounts;
block_on(async move { accounts.read().await.all_work_done().await as libc::c_int })
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
if accounts.is_null() {
@@ -4882,6 +4842,49 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
block_on(async move { accounts.write().await.maybe_network_lost().await });
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_background_fetch(
accounts: *mut dc_accounts_t,
timeout_in_seconds: u64,
) -> libc::c_int {
if accounts.is_null() || timeout_in_seconds <= 2 {
eprintln!("ignoring careless call to dc_accounts_background_fetch()");
return 0;
}
let accounts = &*accounts;
block_on(async move {
let accounts = accounts.read().await;
accounts
.background_fetch(Duration::from_secs(timeout_in_seconds))
.await;
});
1
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_set_push_device_token(
accounts: *mut dc_accounts_t,
token: *const libc::c_char,
) {
if accounts.is_null() {
eprintln!("ignoring careless call to dc_accounts_set_push_device_token()");
return;
}
let accounts = &*accounts;
let token = to_string_lossy(token);
block_on(async move {
let mut accounts = accounts.write().await;
if let Err(err) = accounts.set_push_device_token(&token).await {
accounts.emit_event(EventType::Error(format!(
"Failed to set notify token: {err:#}."
)));
}
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
accounts: *mut dc_accounts_t,
@@ -4919,7 +4922,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,10 +1,11 @@
[package]
name = "deltachat-jsonrpc"
version = "1.132.1"
version = "1.137.4"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0"
repository = "https://github.com/deltachat/deltachat-core-rust"
[[bin]]
name = "deltachat-jsonrpc-server"
@@ -17,24 +18,24 @@ deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.13"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.9.0"
tempfile = "3.10.1"
log = "0.4"
async-channel = { version = "2.0.0" }
futures = { version = "0.3.30" }
serde_json = "1"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
tokio = { version = "1.33.0" }
tokio = { version = "1.37.0" }
sanitize-filename = "0.5"
walkdir = "2.3.3"
base64 = "0.21"
walkdir = "2.5.0"
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.33.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
@@ -28,6 +29,7 @@ use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -62,14 +64,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 +82,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 +134,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 +168,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())
@@ -231,13 +233,27 @@ impl CommandApi {
Ok(())
}
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout.
/// Process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
self.accounts
.write()
.await
.background_fetch(std::time::Duration::from_secs_f64(timeout_in_seconds))
.await;
Ok(())
}
// ---------------------------------------------
// Methods that work on individual accounts
// ---------------------------------------------
/// Starts background tasks for a single account.
async fn start_io(&self, account_id: u32) -> Result<()> {
let mut ctx = self.get_context(account_id).await?;
let ctx = self.get_context(account_id).await?;
ctx.start_io().await;
Ok(())
}
@@ -311,6 +327,11 @@ impl CommandApi {
ctx.get_info().await
}
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())
}
/// Sets the given configuration key.
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -383,7 +404,7 @@ impl CommandApi {
/// Configures this account with the currently set parameters.
/// Setup the credential config before calling this.
async fn configure(&self, account_id: u32) -> Result<()> {
let mut ctx = self.get_context(account_id).await?;
let ctx = self.get_context(account_id).await?;
ctx.stop_io().await;
let result = ctx.configure().await;
if result.is_err() {
@@ -678,8 +699,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(
@@ -707,8 +727,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()`.
@@ -896,19 +915,35 @@ impl CommandApi {
.to_u32())
}
// for now only text messages, because we only used text messages in desktop thusfar
/// Add a message to the device-chat.
/// Device-messages usually contain update information
/// and some hints that are added during the program runs, multi-device etc.
/// The device-message may be defined by a label;
/// if a message with the same label was added or skipped before,
/// the message is not added again, even if the message was deleted in between.
/// If needed, the device-chat is created before.
///
/// Sends the `MsgsChanged` event on success.
///
/// Setting msg to None will prevent the device message with this label from being added in the future.
async fn add_device_message(
&self,
account_id: u32,
label: String,
text: String,
) -> Result<u32> {
msg: Option<MessageData>,
) -> Result<Option<u32>> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
if let Some(msg) = msg {
let mut message = msg.create_message(&ctx).await?;
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?;
if !message_id.is_unset() {
return Ok(Some(message_id.to_u32()));
}
} else {
deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?;
}
Ok(None)
}
/// Mark all messages in a chat as _noticed_.
@@ -1062,9 +1097,12 @@ impl CommandApi {
.collect::<Vec<JSONRPCMessageListItem>>())
}
async fn get_message(&self, account_id: u32, message_id: u32) -> Result<MessageObject> {
async fn get_message(&self, account_id: u32, msg_id: u32) -> Result<MessageObject> {
let ctx = self.get_context(account_id).await?;
MessageObject::from_message_id(&ctx, message_id).await
let msg_id = MsgId::new(msg_id);
MessageObject::from_msg_id(&ctx, msg_id)
.await
.with_context(|| format!("Failed to load message {msg_id} for account {account_id}"))
}
async fn get_message_html(&self, account_id: u32, message_id: u32) -> Result<Option<String>> {
@@ -1084,7 +1122,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
let message_result = MessageObject::from_msg_id(&ctx, MsgId::new(message_id)).await;
messages.insert(
message_id,
match message_result {
@@ -1531,20 +1569,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`].
@@ -1552,11 +1591,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)
}
@@ -1565,14 +1610,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
}
@@ -1582,6 +1633,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?;
@@ -1808,38 +1862,7 @@ impl CommandApi {
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
viewtype.into()
} else if data.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(data.text.unwrap_or_default());
if data.html.is_some() {
message.set_html(data.html);
}
if data.override_sender_name.is_some() {
message.set_override_sender_name(data.override_sender_name);
}
if let Some(file) = data.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = data.location {
message.set_location(latitude, longitude);
}
if let Some(id) = data.quoted_message_id {
message
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
let mut message = data.create_message(&ctx).await?;
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
@@ -2038,11 +2061,9 @@ impl CommandApi {
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
let message = MessageObject::from_message_id(&ctx, msg_id).await?;
Ok((msg_id, message))
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message).await?;
let message = MessageObject::from_msg_id(&ctx, msg_id).await?;
Ok((msg_id.to_u32(), message))
}
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
@@ -2121,13 +2142,6 @@ async fn set_config(
value,
)
.await?;
match key {
"sentbox_watch" | "mvbox_move" | "only_fetch_mvbox" => {
ctx.restart_io_if_running().await;
}
_ => {}
}
}
Ok(())
}
@@ -2143,15 +2157,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

@@ -45,6 +45,9 @@ pub struct ContactObject {
/// the contact's last seen timestamp
last_seen: i64,
was_seen_recently: bool,
/// If the contact is a bot.
is_bot: bool,
}
impl ContactObject {
@@ -80,6 +83,7 @@ impl ContactObject {
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),
is_bot: contact.is_bot(),
})
}
}

View File

@@ -28,55 +28,37 @@ pub enum EventType {
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Info {
msg: String,
},
Info { msg: String },
/// Emitted when SMTP connection is established and login was successful.
SmtpConnected {
msg: String,
},
SmtpConnected { msg: String },
/// Emitted when IMAP connection is established and login was successful.
ImapConnected {
msg: String,
},
ImapConnected { msg: String },
/// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent {
msg: String,
},
SmtpMessageSent { msg: String },
/// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted {
msg: String,
},
ImapMessageDeleted { msg: String },
/// Emitted when an IMAP message has been moved
ImapMessageMoved {
msg: String,
},
ImapMessageMoved { msg: String },
/// Emitted before going into IDLE on the Inbox folder.
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created
NewBlobFile {
file: String,
},
NewBlobFile { file: String },
/// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile {
file: String,
},
DeletedBlobFile { file: String },
/// The library-user should write a warning string to the log.
///
/// This event should *not* be reported to the end-user using a popup or something like
/// that.
Warning {
msg: String,
},
Warning { msg: String },
/// The library-user should report an error to the end-user.
///
@@ -88,18 +70,14 @@ pub enum EventType {
/// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then.
Error {
msg: String,
},
Error { msg: String },
/// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to
/// setChatName(), setChatProfileImage(),
/// addContactToChat(), removeContactFromChat(),
/// and messages sending functions.
ErrorSelfNotInGroup {
msg: String,
},
ErrorSelfNotInGroup { msg: String },
/// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database:
@@ -110,10 +88,7 @@ pub enum EventType {
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
#[serde(rename_all = "camelCase")]
MsgsChanged {
chat_id: u32,
msg_id: u32,
},
MsgsChanged { chat_id: u32, msg_id: u32 },
/// Reactions for the message changed.
#[serde(rename_all = "camelCase")]
@@ -126,60 +101,39 @@ pub enum EventType {
/// There is a fresh message. Typically, the user will show an notification
/// when receiving this message.
///
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
#[serde(rename_all = "camelCase")]
IncomingMsg {
chat_id: u32,
msg_id: u32,
},
IncomingMsg { chat_id: u32, msg_id: u32 },
/// Downloading a bunch of messages just finished. This is an experimental
/// Downloading a bunch of messages just finished. This is an
/// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")]
IncomingMsgBunch {
msg_ids: Vec<u32>,
},
IncomingMsgBunch,
/// Messages were seen or noticed.
/// chat id is always set.
#[serde(rename_all = "camelCase")]
MsgsNoticed {
chat_id: u32,
},
MsgsNoticed { chat_id: u32 },
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgDelivered {
chat_id: u32,
msg_id: u32,
},
MsgDelivered { chat_id: u32, msg_id: u32 },
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgFailed {
chat_id: u32,
msg_id: u32,
},
MsgFailed { chat_id: u32, msg_id: u32 },
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
#[serde(rename_all = "camelCase")]
MsgRead {
chat_id: u32,
msg_id: u32,
},
MsgRead { chat_id: u32, msg_id: u32 },
/// A single message is deleted.
#[serde(rename_all = "camelCase")]
MsgDeleted {
chat_id: u32,
msg_id: u32,
},
MsgDeleted { chat_id: u32, msg_id: u32 },
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
@@ -189,24 +143,17 @@ pub enum EventType {
/// This event does not include ephemeral timer modification, which
/// is a separate event.
#[serde(rename_all = "camelCase")]
ChatModified {
chat_id: u32,
},
ChatModified { chat_id: u32 },
/// Chat ephemeral timer changed.
#[serde(rename_all = "camelCase")]
ChatEphemeralTimerModified {
chat_id: u32,
timer: u32,
},
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
/// Contact(s) created, renamed, blocked or deleted.
///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[serde(rename_all = "camelCase")]
ContactsChanged {
contact_id: Option<u32>,
},
ContactsChanged { contact_id: Option<u32> },
/// Location of one or more contact has changed.
///
@@ -214,9 +161,7 @@ pub enum EventType {
/// If the locations of several contacts have been changed,
/// this parameter is set to `None`.
#[serde(rename_all = "camelCase")]
LocationChanged {
contact_id: Option<u32>,
},
LocationChanged { contact_id: Option<u32> },
/// Inform about the configuration progress started by configure().
ConfigureProgress {
@@ -234,9 +179,7 @@ pub enum EventType {
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexProgress {
progress: usize,
},
ImexProgress { progress: usize },
/// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex().
@@ -246,9 +189,7 @@ pub enum EventType {
///
/// @param data2 0
#[serde(rename_all = "camelCase")]
ImexFileWritten {
path: String,
},
ImexFileWritten { path: String },
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
@@ -263,10 +204,7 @@ pub enum EventType {
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
#[serde(rename_all = "camelCase")]
SecurejoinInviterProgress {
contact_id: u32,
progress: usize,
},
SecurejoinInviterProgress { contact_id: u32, progress: usize },
/// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code).
@@ -277,10 +215,7 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him)
#[serde(rename_all = "camelCase")]
SecurejoinJoinerProgress {
contact_id: u32,
progress: usize,
},
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
/// The connectivity to the server changed.
/// This means that you should refresh the connectivity view
@@ -288,8 +223,17 @@ pub enum EventType {
/// getConnectivityHtml() for details.
ConnectivityChanged,
/// Deprecated by `ConfigSynced`.
SelfavatarChanged,
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
/// would be logged which might not be good for privacy.
ConfigSynced {
/// Configuration key.
key: String,
},
#[serde(rename_all = "camelCase")]
WebxdcStatusUpdate {
msg_id: u32,
@@ -298,9 +242,23 @@ pub enum EventType {
/// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted {
msg_id: u32,
},
WebxdcInstanceDeleted { msg_id: u32 },
/// Tells that the Background fetch was completed (or timed out).
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// 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> },
}
impl From<CoreEventType> for EventType {
@@ -336,9 +294,7 @@ impl From<CoreEventType> for EventType {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
CoreEventType::IncomingMsgBunch => IncomingMsgBunch,
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(),
},
@@ -396,6 +352,9 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::ConnectivityChanged => ConnectivityChanged,
CoreEventType::SelfavatarChanged => SelfavatarChanged,
CoreEventType::ConfigSynced { key } => ConfigSynced {
key: key.to_string(),
},
CoreEventType::WebxdcStatusUpdate {
msg_id,
status_update_serial,
@@ -406,6 +365,11 @@ impl From<CoreEventType> for EventType {
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,
}
}
}

View File

@@ -105,11 +105,6 @@ enum MessageQuote {
}
impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
@@ -345,6 +340,7 @@ pub enum SystemMessageType {
SecurejoinMessage,
LocationStreamingEnabled,
LocationOnly,
InvalidUnencryptedMail,
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
@@ -385,6 +381,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
}
}
}
@@ -546,6 +543,44 @@ pub struct MessageData {
pub quoted_message_id: Option<u32>,
}
impl MessageData {
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
viewtype.into()
} else if self.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(self.text.unwrap_or_default());
if self.html.is_some() {
message.set_html(self.html);
}
if self.override_sender_name.is_some() {
message.set_override_sender_name(self.override_sender_name);
}
if let Some(file) = self.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = self.location {
message.set_location(latitude, longitude);
}
if let Some(id) = self.quoted_message_id {
message
.set_quote(
context,
Some(
&Message::load_from_db(context, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
Ok(message)
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {

View File

@@ -53,5 +53,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.132.1"
"version": "1.137.4"
}

View File

@@ -1,19 +1,20 @@
[package]
name = "deltachat-repl"
version = "1.132.1"
version = "1.137.4"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.20"
log = "0.4.21"
pretty_env_logger = "0.5"
rusqlite = "0.30"
rustyline = "12"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
rusqlite = "0.31"
rustyline = "14"
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
[features]
default = ["vendored"]

View File

@@ -3,7 +3,7 @@ extern crate dirs;
use std::path::Path;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use std::time::Duration;
use anyhow::{bail, ensure, Result};
use deltachat::chat::{
@@ -33,14 +33,6 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", ())
.await
.unwrap();
println!("(1) Jobs reset.");
}
if 0 != bits & 2 {
context
.sql()

View File

@@ -9,7 +9,6 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write};
use std::path::Path;
use std::process::Command;
use ansi_term::Color;
@@ -20,8 +19,7 @@ use deltachat::context::*;
use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*;
use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use deltachat::EventType;
use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError;
@@ -299,8 +297,8 @@ impl Highlighter for DcHelper {
self.highlighter.highlight(line, pos)
}
fn highlight_char(&self, line: &str, pos: usize) -> bool {
self.highlighter.highlight_char(line, pos)
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
self.highlighter.highlight_char(line, pos, forced)
}
}
@@ -312,7 +310,10 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
let context = ContextBuilder::new(args[1].clone().into())
.with_id(1)
.open()
.await?;
let events = context.get_event_emitter();
tokio::task::spawn(async move {
@@ -401,7 +402,7 @@ enum ExitResult {
async fn handle_cmd(
line: &str,
mut ctx: Context,
ctx: Context,
selected_chat: &mut ChatId,
) -> Result<ExitResult, Error> {
let mut args = line.splitn(2, ' ');

View File

@@ -1,9 +1,10 @@
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
requires = ["setuptools>=45"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.137.4"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -19,8 +20,9 @@ classifiers = [
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]
dynamic = [
"version"
readme = "README.md"
dependencies = [
"imap-tools",
]
[tool.setuptools.package-data]
@@ -31,9 +33,6 @@ deltachat_rpc_client = [
[project.entry-points.pytest11]
"deltachat_rpc_client.pytestplugin" = "deltachat_rpc_client.pytestplugin"
[tool.setuptools_scm]
root = ".."
[tool.black]
line-length = 120

View File

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

View File

@@ -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()
@@ -168,3 +172,33 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
return "removed", addr, addr
return None
class futuremethod: # noqa: N801
"""Decorator for async methods."""
def __init__(self, func):
self._func = func
def __get__(self, instance, owner=None):
if instance is None:
return self
def future(*args):
generator = self._func(instance, *args)
res = next(generator)
def f():
try:
generator.send(res())
except StopIteration as e:
return e.value
return f
def wrapper(*args):
f = future(*args)
return f()
wrapper.future = future
return wrapper

View File

@@ -1,8 +1,10 @@
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
from ._utils import AttrDict, futuremethod
from .chat import Chat
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
from .contact import Contact
@@ -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,9 +82,24 @@ class Account:
"""Get self avatar."""
return self.get_config("selfavatar")
def configure(self) -> None:
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."""
self._rpc.configure(self.id)
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.
@@ -97,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)
@@ -106,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]
@@ -131,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
@@ -166,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.
@@ -218,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
@@ -236,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.
@@ -254,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]
@@ -283,7 +309,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,8 @@
from __future__ import annotations
import calendar
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
from .const import ChatVisibility, ViewType
@@ -84,14 +86,16 @@ class Chat:
self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
"""Set ephemeral timer of this chat in seconds.
0 means the timer is disabled, use 1 for immediate deletion."""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
"""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)
@@ -115,7 +119,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:
@@ -140,6 +144,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)
@@ -150,7 +158,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)
@@ -182,7 +190,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]
@@ -217,7 +225,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.
@@ -241,7 +249,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
@@ -249,7 +257,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

View File

@@ -1,13 +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,
)
@@ -38,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,17 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
class ChatId(IntEnum):
"""Special chat ids"""
TRASH = 3
ARCHIVED_LINK = 6
ALLDONE_HINT = 7
LAST_SPECIAL = 9
class ChatType(IntEnum):
@@ -122,3 +133,107 @@ class SystemMessageType(str, Enum):
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
class MessageState(IntEnum):
"""State of the message."""
UNDEFINED = 0
IN_FRESH = 10
IN_NOTICED = 13
IN_SEEN = 16
OUT_PREPARING = 18
OUT_DRAFT = 19
OUT_PENDING = 20
OUT_FAILED = 24
OUT_DELIVERED = 26
OUT_MDN_RCVD = 28
class MessageId(IntEnum):
"""Special message ids"""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
AUTOMATIC = 0
STRICT = 1
ACCEPT_INVALID_CERTIFICATES = 3
class Connectivity(IntEnum):
"""Connectivity states"""
NOT_CONNECTED = 1000
CONNECTING = 2000
WORKING = 3000
CONNECTED = 4000
class KeyGenType(IntEnum):
"""Type of the key to generate"""
DEFAULT = 0
RSA2048 = 1
ED25519 = 2
RSA4096 = 3
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
OK = 1
PREPARATION = 2
BROKEN = 3
class PushNotifyState(IntEnum):
"""Push notifications state"""
NOT_CONNECTED = 0
HEARTBEAT = 1
CONNECTED = 2
class ShowEmails(IntEnum):
"""Show emails mode"""
OFF = 0
ACCEPTED_CONTACTS = 1
ALL = 2
class SocketSecurity(IntEnum):
"""Socket security"""
AUTOMATIC = 0
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

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,7 +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
@@ -262,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

@@ -3,6 +3,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from ._utils import AttrDict
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,10 @@ 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

View File

@@ -1,10 +1,13 @@
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
@@ -37,9 +40,10 @@ class ACFactory:
assert not account.is_configured()
return account
def new_configured_account(self) -> Account:
@futuremethod
def new_configured_account(self):
account = self.new_preconfigured_account()
account.configure()
yield account.configure.future()
assert account.is_configured()
return account
@@ -49,17 +53,15 @@ class ACFactory:
bot.configure(credentials["email"], credentials["password"])
return bot
def get_online_account(self) -> Account:
account = self.new_configured_account()
account.start_io()
while True:
event = account.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
@futuremethod
def get_online_account(self):
account = yield self.new_configured_account.future()
account.bring_online()
return account
def get_online_accounts(self, num: int) -> List[Account]:
return [self.get_online_account() for _ in range(num)]
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]
def resetup_account(self, ac: Account) -> Account:
"""Resetup account from scratch, losing the encryption key."""
@@ -71,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,18 +1,62 @@
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):
pass
class RpcFuture:
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
class RpcMethod:
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Synchronously calls JSON-RPC method."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Asynchronously calls JSON-RPC method."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
"method": self.name,
"params": args,
"id": request_id,
}
event = Event()
self.rpc.request_events[request_id] = event
self.rpc.request_queue.put(request)
return RpcFuture(self.rpc, request_id, event)
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
@@ -25,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
@@ -144,25 +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):
def method(*args) -> Any:
request_id = next(self.id_iterator)
request = {
"jsonrpc": "2.0",
"method": attr,
"params": args,
"id": request_id,
}
event = Event()
self.request_events[request_id] = event
self.request_queue.put(request)
event.wait()
response = self.request_results.pop(request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
return method
return RpcMethod(self, attr)

View File

@@ -0,0 +1,218 @@
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")
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
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"),
)
msg_id = alice.wait_for_incoming_msg_event().msg_id
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
alice._rpc.download_full_message(alice.id, msg_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")
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
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()
event = alice.wait_for_incoming_msg_event()
msg = alice.get_message_by_id(event.msg_id)
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

@@ -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:
@@ -164,6 +164,21 @@ def test_qr_readreceipt(acfactory) -> None:
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
@@ -175,7 +190,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -183,7 +198,8 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -191,8 +207,7 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -239,7 +254,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
@@ -247,7 +262,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -255,8 +271,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
@@ -564,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.set_config("mvbox_move", "1")
ac2.configure()
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

@@ -25,7 +25,7 @@ deps =
black
commands =
black --quiet --check --diff src/ examples/ tests/
ruff src/ examples/ tests/
ruff check src/ examples/ tests/
[pytest]
timeout = 300

View File

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

View File

@@ -30,7 +30,7 @@ deltachat-rpc-server
The common use case for this program is to create bindings to use Delta Chat core from programming
languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
1. Python: https://pypi.org/project/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.

View File

@@ -68,7 +68,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

@@ -0,0 +1,8 @@
[package]
name = "deltachat-time"
version = "1.0.0"
description = "Time-related tools"
edition = "2021"
license = "MPL-2.0"
[dependencies]

35
deltachat-time/src/lib.rs Normal file
View File

@@ -0,0 +1,35 @@
#![allow(missing_docs)]
use std::sync::RwLock;
use std::time::{Duration, SystemTime};
static SYSTEM_TIME_SHIFT: RwLock<Duration> = RwLock::new(Duration::new(0, 0));
/// Fake struct for mocking `SystemTime::now()` for test purposes. You still need to use
/// `SystemTime` as a struct representing a system time.
pub struct SystemTimeTools();
impl SystemTimeTools {
pub const UNIX_EPOCH: SystemTime = SystemTime::UNIX_EPOCH;
pub fn now() -> SystemTime {
return SystemTime::now() + *SYSTEM_TIME_SHIFT.read().unwrap();
}
/// Simulates a system clock forward adjustment by `duration`.
pub fn shift(duration: Duration) {
*SYSTEM_TIME_SHIFT.write().unwrap() += duration;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
SystemTimeTools::shift(Duration::from_secs(60));
let t = SystemTimeTools::now();
assert!(t > SystemTime::now());
}
}

View File

@@ -1,5 +1,4 @@
[advisories]
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
@@ -10,6 +9,12 @@ ignore = [
# There is no fix at the time of writing this (2023-11-28).
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
# Unmaintained ansi_term
"RUSTSEC-2021-0139",
# Unmaintained encoding
"RUSTSEC-2021-0153",
]
[bans]
@@ -21,6 +26,7 @@ skip = [
{ 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" },
@@ -32,13 +38,9 @@ skip = [
{ name = "digest", version = "<0.10" },
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "env_logger", version = "0.10.2" },
{ name = "event-listener", version = "2.5.3" },
{ name = "fd-lock", version = "3.0.13" },
{ name = "getrandom", version = "<0.2" },
{ name = "h2", version = "0.3.22" },
{ name = "http-body", version = "0.4.5" },
{ name = "http", version = "0.2.11" },
{ name = "hyper", version = "0.14.27" },
{ name = "idna", version = "0.4.0" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
@@ -55,8 +57,10 @@ skip = [
{ name = "signature", version = "1.6.4" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "sync_wrapper", version = "0.1.2" },
{ name = "syn", version = "1.0.109" },
{ name = "time", version = "<0.3" },
{ name = "toml_edit", version = "0.21.1" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
@@ -69,6 +73,7 @@ skip = [
{ 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" },
]
@@ -100,5 +105,4 @@ license-files = [
github = [
"async-email",
"deltachat",
"djc",
]

288
flake.lock generated Normal file
View File

@@ -0,0 +1,288 @@
{
"nodes": {
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1712088936,
"narHash": "sha256-mVjeSWQiR/t4UZ9fUawY9OEPAhY1R3meYG+0oh8DUBs=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "2d8181caef279f19c4a33dc694723f89ffc195d4",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1711099426,
"narHash": "sha256-HzpgM/wc3aqpnHJJ2oDqPBkNsqWbW0WfWUO8lKu8nGk=",
"owner": "numtide",
"repo": "devshell",
"rev": "2d45b54ca4a183f2fdcf4b19c895b64fbf620ee8",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1713421495,
"narHash": "sha256-5vVF9W1tJT+WdfpWAEG76KywktKDAW/71mVmNHEHjac=",
"owner": "nix-community",
"repo": "fenix",
"rev": "fd47b1f9404fae02a4f38bd9f4b12bad7833c96b",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": "systems_3"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nix-filter": {
"locked": {
"lastModified": 1710156097,
"narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=",
"owner": "numtide",
"repo": "nix-filter",
"rev": "3342559a24e85fc164b295c3444e8a139924675b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-filter",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1711703276,
"narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713248628,
"narHash": "sha256-NLznXB5AOnniUtZsyy/aPWOk8ussTuePp2acb9U+ISA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5672bc9dbf9d88246ddab5ac454e82318d094bb8",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1713562564,
"narHash": "sha256-NQpYhgoy0M89g9whRixSwsHb8RFIbwlxeYiVSDwSXJg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "92d295f588631b0db2da509f381b4fb1e74173c5",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1713537308,
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"android": "android",
"fenix": "fenix",
"flake-utils": "flake-utils_3",
"naersk": "naersk",
"nix-filter": "nix-filter",
"nixpkgs": "nixpkgs_4"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1713373173,
"narHash": "sha256-octd9BFY9G/Gbr4KfwK4itZp4Lx+qvJeRRcYnN+dEH8=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "46702ffc1a02a2ac153f1d1ce619ec917af8f3a6",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

541
flake.nix Normal file
View File

@@ -0,0 +1,541 @@
{
description = "Delta Chat core";
inputs = {
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
nix-filter.url = "github:numtide/nix-filter";
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
android.url = "github:tadfisher/android-nixpkgs";
};
outputs = { self, nixpkgs, flake-utils, nix-filter, naersk, fenix, android }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
fenixPkgs = fenix.packages.${system};
naersk' = pkgs.callPackage naersk { };
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
inherit (sdkPkgs) ndk-24-0-8215888 cmdline-tools-latest;
});
androidNdkRoot = "${androidSdk}/share/android-sdk/ndk/24.0.8215888";
rustSrc = nix-filter.lib {
root = ./.;
# Include only necessary files
# to avoid rebuilds e.g. when README.md or flake.nix changes.
include = [
./benches
./assets
./Cargo.lock
./Cargo.toml
./CMakeLists.txt
./CONTRIBUTING.md
./deltachat_derive
./deltachat-contact-tools
./deltachat-ffi
./deltachat-jsonrpc
./deltachat-ratelimit
./deltachat-repl
./deltachat-rpc-client
./deltachat-time
./deltachat-rpc-server
./format-flowed
./release-date.in
./src
];
exclude = [
(nix-filter.lib.matchExt "nix")
"flake.lock"
];
};
# Map from architecture name to rust targets and nixpkgs targets.
arch2targets = {
"x86_64-linux" = {
rustTarget = "x86_64-unknown-linux-musl";
crossTarget = "x86_64-unknown-linux-musl";
};
"armv7l-linux" = {
rustTarget = "armv7-unknown-linux-musleabihf";
crossTarget = "armv7l-unknown-linux-musleabihf";
};
"armv6l-linux" = {
rustTarget = "arm-unknown-linux-musleabihf";
crossTarget = "armv6l-unknown-linux-musleabihf";
};
"aarch64-linux" = {
rustTarget = "aarch64-unknown-linux-musl";
crossTarget = "aarch64-unknown-linux-musl";
};
"i686-linux" = {
rustTarget = "i686-unknown-linux-musl";
crossTarget = "i686-unknown-linux-musl";
};
"x86_64-darwin" = {
rustTarget = "x86_64-apple-darwin";
crossTarget = "x86_64-darwin";
};
"aarch64-darwin" = {
rustTarget = "aarch64-apple-darwin";
crossTarget = "aarch64-darwin";
};
};
cargoLock = {
lockFile = ./Cargo.lock;
outputHashes = {
"email-0.0.20" = "sha256-rV4Uzqt2Qdrfi5Ti1r+Si1c2iW1kKyWLwOgLkQ5JGGw=";
"encoded-words-0.2.0" = "sha256-KK9st0hLFh4dsrnLd6D8lC6pRFFs8W+WpZSGMGJcosk=";
"lettre-0.9.2" = "sha256-+hU1cFacyyeC9UGVBpS14BWlJjHy90i/3ynMkKAzclk=";
};
};
mkRustPackage = packageName:
naersk'.buildPackage {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
};
pkgsWin64 = pkgs.pkgsCross.mingwW64;
mkWin64RustPackage = packageName:
let
rustTarget = "x86_64-pc-windows-gnu";
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
pkgsWin64.stdenv.cc
pkgsWin64.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
LD = "${pkgsWin64.stdenv.cc}/bin/${pkgsWin64.stdenv.cc.targetPrefix}cc";
};
pkgsWin32 = pkgs.pkgsCross.mingw32;
mkWin32RustPackage = packageName:
let
rustTarget = "i686-pc-windows-gnu";
in
let
toolchainWin = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naerskWin = pkgs.callPackage naersk {
cargo = toolchainWin;
rustc = toolchainWin;
};
# Get rid of MCF Gthread library.
# See <https://github.com/NixOS/nixpkgs/issues/156343>
# and <https://discourse.nixos.org/t/statically-linked-mingw-binaries/38395>
# for details.
#
# Use DWARF-2 instead of SJLJ for exception handling.
winCC = pkgsWin32.buildPackages.wrapCC (
(pkgsWin32.buildPackages.gcc-unwrapped.override
({
threadsCross = {
model = "win32";
package = null;
};
})).overrideAttrs (oldAttr: {
configureFlags = oldAttr.configureFlags ++ [
"--disable-sjlj-exceptions --with-dwarf2"
];
})
);
in
naerskWin.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
depsBuildBuild = [
winCC
pkgsWin32.windows.pthreads
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${winCC}/bin/${winCC.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${winCC}/bin/${winCC.targetPrefix}cc";
LD = "${winCC}/bin/${winCC.targetPrefix}cc";
};
mkCrossRustPackage = arch: packageName:
let
rustTarget = arch2targets."${arch}".rustTarget;
crossTarget = arch2targets."${arch}".crossTarget;
pkgsCross = import nixpkgs {
system = system;
crossSystem.config = crossTarget;
};
in
let
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
LD = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
};
androidAttrs = {
armeabi-v7a = {
cc = "armv7a-linux-androideabi19-clang";
rustTarget = "armv7-linux-androideabi";
};
arm64-v8a = {
cc = "aarch64-linux-android21-clang";
rustTarget = "aarch64-linux-android";
};
};
mkAndroidRustPackage = arch: packageName:
let
rustTarget = androidAttrs.${arch}.rustTarget;
toolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.targets.${rustTarget}.stable.rust-std
];
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
targetToolchain = "${androidNdkRoot}/toolchains/llvm/prebuilt/linux-x86_64";
targetCcName = androidAttrs.${arch}.cc;
targetCc = "${targetToolchain}/bin/${targetCcName}";
in
naersk-lib.buildPackage rec {
pname = packageName;
cargoBuildOptions = x: x ++ [ "--package" packageName ];
version = manifest.version;
strictDeps = true;
src = rustSrc;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
];
auditable = false; # Avoid cargo-auditable failures.
doCheck = false; # Disable test as it requires network access.
CARGO_BUILD_TARGET = rustTarget;
TARGET_CC = "${targetCc}";
CARGO_BUILD_RUSTFLAGS = [
"-C"
"linker=${TARGET_CC}"
];
CC = "${targetCc}";
LD = "${targetCc}";
};
mkAndroidPackages = arch: {
"deltachat-rpc-server-${arch}-android" = mkAndroidRustPackage arch "deltachat-rpc-server";
"deltachat-repl-${arch}-android" = mkAndroidRustPackage arch "deltachat-repl";
};
mkRustPackages = arch:
let
rpc-server = mkCrossRustPackage arch "deltachat-rpc-server";
in
{
"deltachat-repl-${arch}" = mkCrossRustPackage arch "deltachat-repl";
"deltachat-rpc-server-${arch}" = rpc-server;
"deltachat-rpc-server-${arch}-wheel" =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-${arch}-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
rpc-server
];
buildPhase = ''
mkdir tmp
cp ${rpc-server}/bin/deltachat-rpc-server tmp/deltachat-rpc-server
python3 scripts/wheel-rpc-server.py ${arch} tmp/deltachat-rpc-server
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
};
in
{
formatter = pkgs.nixpkgs-fmt;
packages =
mkRustPackages "aarch64-linux" //
mkRustPackages "i686-linux" //
mkRustPackages "x86_64-linux" //
mkRustPackages "armv7l-linux" //
mkRustPackages "armv6l-linux" //
mkAndroidPackages "armeabi-v7a" //
mkAndroidPackages "arm64-v8a" //
mkAndroidPackages "x86" //
mkAndroidPackages "x86_64" // rec {
# Run with `nix run .#deltachat-repl foo.db`.
deltachat-repl = mkRustPackage "deltachat-repl";
deltachat-rpc-server = mkRustPackage "deltachat-rpc-server";
deltachat-repl-win64 = mkWin64RustPackage "deltachat-repl";
deltachat-rpc-server-win64 = mkWin64RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win64-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win64-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win64
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win64}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win64 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
deltachat-repl-win32 = mkWin32RustPackage "deltachat-repl";
deltachat-rpc-server-win32 = mkWin32RustPackage "deltachat-rpc-server";
deltachat-rpc-server-win32-wheel =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-win32-wheel";
version = manifest.version;
src = nix-filter.lib {
root = ./.;
include = [
"scripts/wheel-rpc-server.py"
"deltachat-rpc-server/README.md"
"LICENSE"
"Cargo.toml"
];
};
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildInputs = [
deltachat-rpc-server-win32
];
buildPhase = ''
mkdir tmp
cp ${deltachat-rpc-server-win32}/bin/deltachat-rpc-server.exe tmp/deltachat-rpc-server.exe
python3 scripts/wheel-rpc-server.py win32 tmp/deltachat-rpc-server.exe
'';
installPhase = ''mkdir -p $out; cp -av deltachat_rpc_server-*.whl $out'';
};
# Run `nix build .#docs` to get C docs generated in `./result/`.
docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.doxygen ];
buildPhase = ''scripts/run-doxygen.sh'';
installPhase = ''mkdir -p $out; cp -av deltachat-ffi/html deltachat-ffi/xml $out'';
};
libdeltachat =
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
src = rustSrc;
cargoDeps = pkgs.rustPlatform.importCargoLock cargoLock;
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
];
buildInputs = pkgs.lib.optionals isDarwin [
pkgs.darwin.apple_sdk.frameworks.CoreFoundation
pkgs.darwin.apple_sdk.frameworks.Security
pkgs.darwin.apple_sdk.frameworks.SystemConfiguration
pkgs.libiconv
];
postInstall = ''
substituteInPlace $out/include/deltachat.h \
--replace __FILE__ '"${placeholder "out"}/include/deltachat.h"'
'';
};
# Source package for deltachat-rpc-server.
# Fake package that downloads Linux version,
# needed to install deltachat-rpc-server on Android with `pip`.
deltachat-rpc-server-source =
pkgs.stdenv.mkDerivation {
pname = "deltachat-rpc-server-source";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
nativeBuildInputs = [
pkgs.python3
pkgs.python3Packages.wheel
];
buildPhase = ''python3 scripts/wheel-rpc-server.py source deltachat-rpc-server-${manifest.version}.tar.gz'';
installPhase = ''mkdir -p $out; cp -av deltachat-rpc-server-${manifest.version}.tar.gz $out'';
};
deltachat-rpc-client =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-rpc-client";
version = manifest.version;
src = pkgs.lib.cleanSource ./deltachat-rpc-client;
format = "pyproject";
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.imap-tools
];
};
deltachat-python =
pkgs.python3Packages.buildPythonPackage {
pname = "deltachat-python";
version = manifest.version;
src = pkgs.lib.cleanSource ./python;
format = "pyproject";
buildInputs = [
libdeltachat
];
nativeBuildInputs = [
pkgs.pkg-config
];
propagatedBuildInputs = [
pkgs.python3Packages.setuptools
pkgs.python3Packages.pkgconfig
pkgs.python3Packages.cffi
pkgs.python3Packages.imap-tools
pkgs.python3Packages.pluggy
pkgs.python3Packages.requests
];
};
python-docs =
pkgs.stdenv.mkDerivation {
pname = "docs";
version = manifest.version;
src = pkgs.lib.cleanSource ./.;
buildInputs = [
deltachat-python
deltachat-rpc-client
pkgs.python3Packages.breathe
pkgs.python3Packages.sphinx_rtd_theme
];
nativeBuildInputs = [ pkgs.sphinx ];
buildPhase = ''sphinx-build -b html -a python/doc/ dist/html'';
installPhase = ''mkdir -p $out; cp -av dist/html $out'';
};
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
cargo
clippy
rustc
rustfmt
rust-analyzer
cargo-deny
perl # needed to build vendored OpenSSL
];
};
}
);
}

608
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,9 +29,13 @@ module.exports = {
DC_DOWNLOAD_FAILURE: 20,
DC_DOWNLOAD_IN_PROGRESS: 1000,
DC_DOWNLOAD_UNDECIPHERABLE: 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
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,
DC_EVENT_CONFIG_SYNCED: 2111,
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
DC_EVENT_CONTACTS_CHANGED: 2030,
DC_EVENT_DELETED_BLOB_FILE: 151,
@@ -79,6 +83,7 @@ module.exports = {
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
DC_INFO_GROUP_IMAGE_CHANGED: 3,
DC_INFO_GROUP_NAME_CHANGED: 2,
DC_INFO_INVALID_UNENCRYPTED_MAIL: 13,
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
DC_INFO_LOCATION_ONLY: 9,
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
@@ -112,6 +117,9 @@ module.exports = {
DC_PROVIDER_STATUS_BROKEN: 3,
DC_PROVIDER_STATUS_OK: 1,
DC_PROVIDER_STATUS_PREPARATION: 2,
DC_PUSH_CONNECTED: 2,
DC_PUSH_HEARTBEAT: 1,
DC_PUSH_NOT_CONNECTED: 0,
DC_QR_ACCOUNT: 250,
DC_QR_ADDR: 320,
DC_QR_ASK_VERIFYCONTACT: 200,
@@ -225,11 +233,13 @@ module.exports = {
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
DC_STR_IMAGE: 9,
DC_STR_INCOMING_MESSAGES: 103,
DC_STR_INVALID_UNENCRYPTED_MAIL: 174,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
DC_STR_LOCATION: 66,
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
DC_STR_MESSAGES: 114,
DC_STR_MESSAGE_ADD_MEMBER: 173,
DC_STR_MSGACTIONBYME: 63,
DC_STR_MSGACTIONBYUSER: 62,
DC_STR_MSGADDMEMBER: 17,
@@ -249,6 +259,7 @@ module.exports = {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
DC_STR_PART_OF_TOTAL_USED: 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
DC_STR_REACTED_BY: 177,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
@@ -276,6 +287,7 @@ module.exports = {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
DC_STR_VOICEMESSAGE: 7,
DC_STR_WELCOME_MESSAGE: 71,
DC_STR_YOU_REACTED: 176,
DC_TEXT1_DRAFT: 1,
DC_TEXT1_SELF: 3,
DC_TEXT1_USERNAME: 2,

View File

@@ -34,6 +34,10 @@ module.exports = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
}

View File

@@ -29,9 +29,13 @@ export enum C {
DC_DOWNLOAD_FAILURE = 20,
DC_DOWNLOAD_IN_PROGRESS = 1000,
DC_DOWNLOAD_UNDECIPHERABLE = 30,
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
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,
DC_EVENT_CONFIG_SYNCED = 2111,
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
DC_EVENT_CONTACTS_CHANGED = 2030,
DC_EVENT_DELETED_BLOB_FILE = 151,
@@ -79,6 +83,7 @@ export enum C {
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
DC_INFO_GROUP_IMAGE_CHANGED = 3,
DC_INFO_GROUP_NAME_CHANGED = 2,
DC_INFO_INVALID_UNENCRYPTED_MAIL = 13,
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
DC_INFO_LOCATION_ONLY = 9,
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
@@ -112,6 +117,9 @@ export enum C {
DC_PROVIDER_STATUS_BROKEN = 3,
DC_PROVIDER_STATUS_OK = 1,
DC_PROVIDER_STATUS_PREPARATION = 2,
DC_PUSH_CONNECTED = 2,
DC_PUSH_HEARTBEAT = 1,
DC_PUSH_NOT_CONNECTED = 0,
DC_QR_ACCOUNT = 250,
DC_QR_ADDR = 320,
DC_QR_ASK_VERIFYCONTACT = 200,
@@ -225,11 +233,13 @@ export enum C {
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
DC_STR_IMAGE = 9,
DC_STR_INCOMING_MESSAGES = 103,
DC_STR_INVALID_UNENCRYPTED_MAIL = 174,
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
DC_STR_LOCATION = 66,
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
DC_STR_MESSAGES = 114,
DC_STR_MESSAGE_ADD_MEMBER = 173,
DC_STR_MSGACTIONBYME = 63,
DC_STR_MSGACTIONBYUSER = 62,
DC_STR_MSGADDMEMBER = 17,
@@ -249,6 +259,7 @@ export enum C {
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
DC_STR_PART_OF_TOTAL_USED = 116,
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
DC_STR_REACTED_BY = 177,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
@@ -276,6 +287,7 @@ export enum C {
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
DC_STR_VOICEMESSAGE = 7,
DC_STR_WELCOME_MESSAGE = 71,
DC_STR_YOU_REACTED = 176,
DC_TEXT1_DRAFT = 1,
DC_TEXT1_SELF = 3,
DC_TEXT1_USERNAME = 2,
@@ -319,6 +331,10 @@ export const EventId2EventName: { [key: number]: string } = {
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
2111: 'DC_EVENT_CONFIG_SYNCED',
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
2300: 'DC_EVENT_CHATLIST_CHANGED',
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
}

View File

@@ -178,7 +178,7 @@ export class AccountManager extends EventEmitter {
static newTemporary() {
let directory = null
while (true) {
const randomString = Math.random().toString(36).substr(2, 5)
const randomString = Math.random().toString(36).substring(2, 5)
directory = join(tmpdir(), 'deltachat-' + randomString)
if (!existsSync(directory)) break
}

View File

@@ -1,5 +1,4 @@
#define NAPI_VERSION 4
#define NAPI_EXPERIMENTAL
#include <stdlib.h>
#include <stdio.h>
@@ -3048,14 +3047,6 @@ NAPI_METHOD(dcn_accounts_select_account) {
NAPI_RETURN_UINT32(result);
}
NAPI_METHOD(dcn_accounts_all_work_done) {
NAPI_ARGV(1);
NAPI_DCN_ACCOUNTS();
int result = dc_accounts_all_work_done(dcn_accounts->dc_accounts);
NAPI_RETURN_INT32(result);
}
NAPI_METHOD(dcn_accounts_start_io) {
NAPI_ARGV(1);
NAPI_DCN_ACCOUNTS();
@@ -3382,7 +3373,6 @@ NAPI_INIT() {
NAPI_EXPORT_FUNCTION(dcn_accounts_get_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_get_selected_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_select_account);
NAPI_EXPORT_FUNCTION(dcn_accounts_all_work_done);
NAPI_EXPORT_FUNCTION(dcn_accounts_start_io);
NAPI_EXPORT_FUNCTION(dcn_accounts_stop_io);
NAPI_EXPORT_FUNCTION(dcn_accounts_maybe_network);

View File

@@ -1,13 +1,17 @@
// @ts-check
import DeltaChat from '../dist'
import { DeltaChat } from '../dist/index.js'
import { deepStrictEqual, strictEqual } from 'assert'
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { EventId2EventName, C } from '../dist/constants'
import { EventId2EventName, C } from '../dist/constants.js'
import { join } from 'path'
import { statSync } from 'fs'
import { Context } from '../dist/context'
import { Context } from '../dist/context.js'
import {fileURLToPath} from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
chai.use(chaiAsPromised)
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
@@ -22,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)
@@ -235,7 +241,7 @@ describe('Basic offline Tests', function () {
'delete_device_after',
'delete_server_after',
'deltachat_core_version',
'display_name',
'displayname',
'download_limit',
'e2ee_enabled',
'entered_account_settings',
@@ -246,6 +252,7 @@ describe('Basic offline Tests', function () {
'journal_mode',
'key_gen_type',
'last_housekeeping',
'last_cant_decrypt_outgoing_msgs',
'level',
'mdns_enabled',
'media_quality',
@@ -696,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

@@ -10,7 +10,6 @@
"@types/node": "^20.8.10",
"chai": "~4.3.10",
"chai-as-promised": "^7.1.1",
"esm": "^3.2.25",
"mocha": "^8.2.1",
"node-gyp": "^10.0.0",
"prebuildify": "^5.0.1",
@@ -53,8 +52,8 @@
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
"test": "npm run test:lint && npm run test:mocha",
"test:lint": "npm run lint",
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.132.1"
"version": "1.137.4"
}

View File

@@ -1,9 +1,10 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.137.4"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.7"
@@ -26,9 +27,6 @@ dependencies = [
"pluggy",
"requests",
]
dynamic = [
"version"
]
[project.urls]
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
@@ -44,11 +42,6 @@ deltachat = [
"py.typed"
]
[tool.setuptools_scm]
root = ".."
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match v*.*"
[tool.black]
line-length = 120

View File

@@ -1,6 +1,5 @@
"""Account class implementation."""
import os
from array import array
from contextlib import contextmanager
@@ -376,22 +375,6 @@ class Account:
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
def _wait_next_message_ids(self) -> List[int]:
"""Return IDs of all next messages from all chats."""
dc_array = ffi.gc(lib.dc_wait_next_msgs(self._dc_context), lib.dc_array_unref)
return [lib.dc_array_get_id(dc_array, i) for i in range(lib.dc_array_get_cnt(dc_array))]
def wait_next_incoming_message(self) -> Message:
"""Waits until the next incoming message
with ID higher than given is received and returns it."""
while True:
message_ids = self._wait_next_message_ids()
for msg_id in message_ids:
message = Message.from_db(self, msg_id)
if message and not message.is_from_self() and not message.is_from_device():
self.set_config("last_msg_id", str(msg_id))
return message
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat()
@@ -478,6 +461,16 @@ class Account:
msg_ids = [msg.id for msg in messages]
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
def resend_messages(self, messages: List[Message]) -> None:
"""Resend list of messages.
:param messages: list of :class:`deltachat.message.Message` object.
:returns: None
"""
msg_ids = [msg.id for msg in messages]
if lib.dc_resend_msgs(self._dc_context, msg_ids, len(msg_ids)) != 1:
raise ValueError(f"could not resend messages {msg_ids}")
def delete_messages(self, messages: List[Message]) -> None:
"""delete messages (local and remote).

View File

@@ -182,6 +182,12 @@ class FFIEventTracker:
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
break
def wait_securejoin_joiner_progress(self, target):
while True:
event = self.get_matching("DC_EVENT_SECUREJOIN_JOINER_PROGRESS")
if event.data2 >= target:
break
def wait_idle_inbox_ready(self):
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
so that new messages are not mistaken for old ones:
@@ -322,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

@@ -115,7 +115,7 @@ def pytest_configure(config):
deltachat.register_global_plugin(la)
def pytest_report_header(config, startdir):
def pytest_report_header(config):
info = get_core_info()
summary = [
"Deltachat core={} sqlite={} journal_mode={}".format(
@@ -590,6 +590,20 @@ class ACFactory:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def get_protected_chat(self, ac1: Account, ac2: Account):
chat = ac1.create_group_chat("Protected Group", verified=True)
qr = chat.get_join_qr()
ac2.qr_join_chat(qr)
ac2._evtracker.wait_securejoin_joiner_progress(1000)
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text
return chat
def introduce_each_other(self, accounts, sending=True):
to_wait = []
for i, acc in enumerate(accounts):

View File

@@ -547,13 +547,14 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
assert msg_in.get_sender_contact().addr == ac2_addr
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group.
- The "member added" message is not a system message but a plain text message.
- Bob's second device doesn't display the Alice's avatar (bug #5354).
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
missing, cannot encrypt".
"""
@@ -568,6 +569,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
ac2_offl.import_self_keys(str(dir))
ac2_offl.stop_io()
lp.sec("ac1: set avatar")
avatar_path = data.get_path("d.png")
ac1.set_avatar(avatar_path)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
@@ -580,11 +585,13 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
ac2_offl.start_io()
# Receive "Member Me (<addr>) added by <addr>." message.
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
contact = msg_in.get_sender_contact()
assert msg_in.is_system_message()
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")

View File

@@ -1,7 +1,6 @@
import os
import queue
import sys
import time
import base64
from datetime import datetime, timezone
@@ -44,21 +43,21 @@ def test_configure_generate_key(acfactory, lp):
lp.sec("ac1: send unencrypted message to ac2")
chat.send_text("message1")
lp.sec("ac2: waiting for message from ac1")
msg_in = ac2.wait_next_incoming_message()
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message1"
assert not msg_in.is_encrypted()
lp.sec("ac2: send encrypted message to ac1")
msg_in.chat.send_text("message2")
lp.sec("ac1: waiting for message from ac2")
msg2_in = ac1.wait_next_incoming_message()
msg2_in = ac1._evtracker.wait_next_incoming_message()
assert msg2_in.text == "message2"
assert msg2_in.is_encrypted()
lp.sec("ac1: send encrypted message to ac2")
msg2_in.chat.send_text("message3")
lp.sec("ac2: waiting for message from ac1")
msg3_in = ac2.wait_next_incoming_message()
msg3_in = ac2._evtracker.wait_next_incoming_message()
assert msg3_in.text == "message3"
assert msg3_in.is_encrypted()
@@ -382,9 +381,24 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
assert msgs_changed_event.data2 == 0
def test_enable_mvbox_move(acfactory, lp):
(ac1,) = acfactory.get_online_accounts(1)
lp.sec("ac2: start without mvbox thread")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False)
acfactory.bring_accounts_online()
lp.sec("ac2: configuring mvbox")
ac2.set_config("mvbox_move", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=False)
lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False)
@@ -392,10 +406,18 @@ def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online()
lp.sec("ac1: create and configure sentbox")
ac1.direct_imap.create_folder("Sent")
ac1.set_config("sentbox_watch", "1")
lp.sec("ac1: send message and wait for ac2 to receive it")
acfactory.get_accepted_chat(ac1, ac2).send_text("message1")
assert ac2._evtracker.wait_next_incoming_message().text == "message1"
assert ac1.get_config("configured_mvbox_folder") == "DeltaChat"
while ac1.get_config("configured_sentbox_folder") != "Sent":
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def test_move_works(acfactory):
ac1 = acfactory.new_online_configuring_account()
@@ -498,6 +520,26 @@ def test_forward_messages(acfactory, lp):
assert not chat3.get_messages()
def test_forward_encrypted_to_unencrypted(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
chat = acfactory.get_protected_chat(ac1, ac2)
lp.sec("ac1: send encrypted message to ac2")
txt = "This should be encrypted"
chat.send_text(txt)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == txt
assert msg.is_encrypted()
lp.sec("ac2: forward message to ac3 unencrypted")
unencrypted_chat = ac2.create_chat(ac3)
msg_id = msg.id
msg2 = unencrypted_chat.send_msg(msg)
assert msg2 == msg
assert msg.id != msg_id
assert not msg.is_encrypted()
def test_forward_own_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -523,6 +565,27 @@ def test_forward_own_message(acfactory, lp):
assert msg_in.is_forwarded()
def test_resend_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat1 = ac1.create_chat(ac2)
lp.sec("ac1: send message to ac2")
chat1.send_text("message")
lp.sec("ac2: receive message")
msg_in = ac2._evtracker.wait_next_incoming_message()
assert msg_in.text == "message"
chat2 = msg_in.chat
chat2_msg_cnt = len(chat2.get_messages())
lp.sec("ac1: resend message")
ac1.resend_messages([msg_in])
lp.sec("ac2: check that message is deleted")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
assert len(chat2.get_messages()) == chat2_msg_cnt
def test_long_group_name(acfactory, lp):
"""See bug https://github.com/deltachat/deltachat-core-rust/issues/3650 "Space added before long
group names after MIME serialization/deserialization".
@@ -1429,112 +1492,12 @@ 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,) = acfactory.get_online_accounts(1)
(ac1, some1) = acfactory.get_online_accounts(2)
lp.sec("create some chat content")
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts(query="some1")) == 1
@@ -1551,7 +1514,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
contacts = ac.get_contacts(query="some1")
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3
@@ -2165,17 +2128,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")
@@ -2190,6 +2157,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

@@ -52,8 +52,8 @@ class TestOfflineAccountBasic:
def test_wrong_db(self, tmp_path):
p = tmp_path / "hello.db"
p.write_text("123")
account = Account(str(p))
assert not account.is_open()
with pytest.raises(ValueError):
_account = Account(str(p))
def test_os_name(self, tmp_path):
p = tmp_path / "hello.db"

View File

@@ -47,7 +47,7 @@ deps =
restructuredtext_lint
commands =
black --quiet --check --diff setup.py src/deltachat examples/ tests/
ruff src/deltachat tests/ examples/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
[testenv:mypy]
@@ -62,7 +62,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 @@
2023-12-12
2024-04-24

View File

@@ -35,12 +35,8 @@ and an own build machine.
- `run_all.sh` builds Python wheels
- `zig-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Zig toolchain statically linked against musl libc.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
- `build-python-docs.sh` builds Python documentation into `dist/html/`.
## Triggering runs on the build machine locally (fast!)
There is experimental support for triggering a remote Python or Rust test run

View File

@@ -1,13 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
python3 -m venv venv
venv/bin/pip install ./python
venv/bin/pip install ./deltachat-rpc-client
venv/bin/pip install sphinx breathe sphinx_rtd_theme
venv/bin/pip install ./deltachat-rpc-client
venv/bin/sphinx-build -b html -a python/doc/ dist/html

View File

@@ -15,55 +15,6 @@ resources:
tag_filter: "v*"
jobs:
- name: doxygen
plan:
- get: deltachat-core-rust
trigger: true
# Build Doxygen documentation
- task: build-doxygen
config:
inputs:
- name: deltachat-core-rust
outputs:
- name: c-docs
image_resource:
source:
repository: alpine
type: registry-image
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache doxygen git
cd deltachat-core-rust
scripts/run-doxygen.sh
cd ..
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
- task: upload-c-docs
config:
inputs:
- name: c-docs
image_resource:
type: registry-image
source:
repository: alpine
platform: linux
run:
path: sh
args:
- -ec
- |
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "(("c.delta.chat".private_key))" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
rsync -e "ssh -o StrictHostKeyChecking=no" -avz --delete c-docs/html/ delta@c.delta.chat:build-c/master
- name: python-x86_64
plan:
- get: deltachat-core-rust

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.72.0
RUST_VERSION=1.77.1
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -220,9 +220,9 @@ if __name__ == "__main__":
process_dir(Path(sys.argv[1]))
out_all += "pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
out_all += "pub(crate) static PROVIDER_DATA: [(&str, &Provider); " + str(len(domains_set)) + "] = [\n";
out_all += out_domains
out_all += "]));\n\n"
out_all += "];\n\n"
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
out_all += out_ids

View File

@@ -73,6 +73,8 @@ def main():
"deltachat-jsonrpc/Cargo.toml",
"deltachat-rpc-server/Cargo.toml",
"deltachat-repl/Cargo.toml",
"python/pyproject.toml",
"deltachat-rpc-client/pyproject.toml",
]
try:
opts = parser.parse_args()
@@ -102,7 +104,7 @@ def main():
found = True
if not found:
raise SystemExit(
f"{changelog_name} contains no entry for version: {newversion}"
f"CHANGELOG.md contains no entry for version: {newversion}"
)
for toml_filename in toml_list:

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -1,24 +1,26 @@
#!/usr/bin/env python3
"""Build Python wheels for deltachat-rpc-server.
Run scripts/zig-rpc-server.sh first."""
"""Build Python wheels for deltachat-rpc-server."""
from pathlib import Path
from wheel.wheelfile import WheelFile
import tomllib
import tarfile
import sys
from io import BytesIO
def metadata_contents(version):
readme_text = (Path("deltachat-rpc-server") / "README.md").read_text()
return f"""Metadata-Version: 2.1
Name: deltachat-rpc-server
Version: {version}
Summary: Delta Chat JSON-RPC server
Description-Content-Type: text/markdown
{readme_text}
"""
def build_source_package(version):
filename = f"dist/deltachat-rpc-server-{version}.tar.gz"
def build_source_package(version, filename):
with tarfile.open(filename, "w:gz") as pkg:
def pack(name, contents):
@@ -99,7 +101,7 @@ setup(
def build_wheel(version, binary, tag, windows=False):
filename = f"dist/deltachat_rpc_server-{version}-{tag}.whl"
filename = f"deltachat_rpc_server-{version}-{tag}.whl"
with WheelFile(filename, "w") as wheel:
wheel.write("LICENSE", "deltachat_rpc_server/LICENSE")
@@ -126,9 +128,11 @@ def main():
Path(binary).chmod(0o755)
wheel.write(
binary,
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server",
(
"deltachat_rpc_server/deltachat-rpc-server.exe"
if windows
else "deltachat_rpc_server/deltachat-rpc-server"
),
)
wheel.writestr(
f"deltachat_rpc_server-{version}.dist-info/METADATA",
@@ -144,54 +148,41 @@ def main():
)
def main():
with open("deltachat-rpc-server/Cargo.toml", "rb") as f:
cargo_toml = tomllib.load(f)
version = cargo_toml["package"]["version"]
Path("dist").mkdir(exist_ok=True)
build_source_package(version)
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-linux",
"py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-armv7-linux",
"py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-linux",
"py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-i686-linux",
"py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
)
arch2tags = {
"x86_64-linux": "manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64",
"armv7l-linux": "linux_armv7l.manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l",
"armv6l-linux": "linux_armv6l",
"aarch64-linux": "manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64",
"i686-linux": "manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686",
"win64": "win_amd64",
"win32": "win32",
# macOS versions for platform compatibility tags are taken from https://doc.rust-lang.org/rustc/platform-support.html
build_wheel(
version,
"dist/deltachat-rpc-server-x86_64-macos",
"py3-none-macosx_10_7_x86_64",
)
build_wheel(
version,
"dist/deltachat-rpc-server-aarch64-macos",
"py3-none-macosx_11_0_arm64",
)
"x86_64-darwin": "macosx_10_7_x86_64",
"aarch64-darwin": "macosx_11_0_arm64",
}
build_wheel(
version, "dist/deltachat-rpc-server-win32.exe", "py3-none-win32", windows=True
)
build_wheel(
version,
"dist/deltachat-rpc-server-win64.exe",
"py3-none-win_amd64",
windows=True,
)
def main():
with Path("Cargo.toml").open("rb") as fp:
cargo_manifest = tomllib.load(fp)
version = cargo_manifest["package"]["version"]
if sys.argv[1] == "source":
filename = f"deltachat-rpc-server-{version}.tar.gz"
build_source_package(version, filename)
else:
arch = sys.argv[1]
executable = sys.argv[2]
tags = arch2tags[arch]
if arch in ["win32", "win64"]:
build_wheel(
version,
executable,
f"py3-none-{tags}",
windows=True,
)
else:
build_wheel(version, executable, f"py3-none-{tags}")
main()

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env python
# /// pyproject
# [run]
# dependencies = [
# "ziglang==0.11.0"
# ]
# ///
import os
import subprocess
import sys
def flag_filter(flag: str) -> bool:
# Workaround for <https://github.com/sfackler/rust-openssl/issues/2043>.
if flag == "-latomic":
return False
if flag == "-lc":
return False
if flag == "-Wl,-melf_i386":
return False
if "self-contained" in flag and "crt" in flag:
return False
return True
def main():
args = [flag for flag in sys.argv[1:] if flag_filter(flag)]
zig_target = os.environ["ZIG_TARGET"]
zig_cpu = os.environ.get("ZIG_CPU")
if zig_cpu:
zig_cpu_args = ["-mcpu=" + zig_cpu]
args = [x for x in args if not x.startswith("-march")]
else:
zig_cpu_args = []
# Disable atomics and use locks instead in OpenSSL.
# Zig toolchains do not provide atomics.
# This is a workaround for <https://github.com/deltachat/deltachat-core-rust/issues/4799>
args += ["-DBROKEN_CLANG_ATOMICS"]
subprocess.run(
[
sys.executable,
"-m",
"ziglang",
"cc",
"-target",
zig_target,
*zig_cpu_args,
*args,
],
check=True,
)
main()

View File

@@ -1,50 +0,0 @@
#!/bin/sh
#
# Build statically linked deltachat-rpc-server using zig.
set -x
set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
rustup target add i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86-linux-musl" \
cargo build --release --target i686-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add armv7-unknown-linux-musleabihf
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="arm-linux-musleabihf" \
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
cargo build --release --target armv7-unknown-linux-musleabihf -p deltachat-rpc-server --features vendored
rustup target add x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
cargo build --release --target x86_64-unknown-linux-musl -p deltachat-rpc-server --features vendored
rustup target add aarch64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
cargo build --release --target aarch64-unknown-linux-musl -p deltachat-rpc-server --features vendored
mkdir -p dist
cp target/x86_64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-linux
cp target/i686-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-i686-linux
cp target/aarch64-unknown-linux-musl/release/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-linux
cp target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server dist/deltachat-rpc-server-armv7-linux

View File

@@ -119,8 +119,9 @@ All group members form the member list.
To allow different groups with the same members,
groups are identified by a group-id.
The group-id MUST be created only from the characters
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`
and MUST have a length of at least 11 characters.
`0`-`9`, `A`-`Z`, `a`-`z` `_` and `-`,
MUST have a length of at least 11 characters
and no more than 32 characters.
Groups MUST have a group-name.
The group-name is any non-zero-length UTF-8 string.

View File

@@ -5,6 +5,7 @@ use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context as _, Result};
use futures::future::join_all;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
@@ -16,8 +17,9 @@ use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};
use crate::context::Context;
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
/// Account manager, that can handle multiple accounts in a single place.
@@ -36,6 +38,9 @@ pub struct Accounts {
/// This way changing a translation for one context automatically
/// changes it for all other contexts.
pub(crate) stockstrings: StockStrings,
/// Push notification subscriber shared between accounts.
push_subscriber: PushSubscriber,
}
impl Accounts {
@@ -72,8 +77,9 @@ impl Accounts {
.context("failed to load accounts config")?;
let events = Events::new();
let stockstrings = StockStrings::new();
let push_subscriber = PushSubscriber::new();
let accounts = config
.load_accounts(&events, &stockstrings, &dir)
.load_accounts(&events, &stockstrings, push_subscriber.clone(), &dir)
.await
.context("failed to load accounts")?;
@@ -83,6 +89,7 @@ impl Accounts {
accounts,
events,
stockstrings,
push_subscriber,
})
}
@@ -119,13 +126,17 @@ impl Accounts {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = Context::new(
&dbfile,
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -134,14 +145,15 @@ impl Accounts {
/// Adds a new closed account.
pub async fn add_closed_account(&mut self) -> Result<u32> {
let account_config = self.config.new_account().await?;
let dbfile = account_config.dbfile(&self.dir);
let ctx = Context::new_closed(
&account_config.dbfile(&self.dir),
account_config.id,
self.events.clone(),
self.stockstrings.clone(),
)
.await?;
let ctx = ContextBuilder::new(dbfile)
.with_id(account_config.id)
.with_events(self.events.clone())
.with_stock_strings(self.stockstrings.clone())
.with_push_subscriber(self.push_subscriber.clone())
.build()
.await?;
self.accounts.insert(account_config.id, ctx);
Ok(account_config.id)
@@ -241,25 +253,6 @@ impl Accounts {
self.accounts.keys().copied().collect()
}
/// This is meant especially for iOS, because iOS needs to tell the system when its background work is done.
///
/// Returns whether all accounts finished their background work.
/// DC_EVENT_CONNECTIVITY_CHANGED will be sent when this turns to true.
///
/// iOS can:
/// - call dc_start_io() (in case IO was not running)
/// - call dc_maybe_network()
/// - while dc_accounts_all_work_done() returns false:
/// - Wait for DC_EVENT_CONNECTIVITY_CHANGED
pub async fn all_work_done(&self) -> bool {
for account in self.accounts.values() {
if !account.all_work_done().await {
return false;
}
}
true
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
pub async fn start_io(&mut self) {
for account in self.accounts.values_mut() {
@@ -291,6 +284,42 @@ impl Accounts {
}
}
/// Performs a background fetch for all accounts in parallel.
///
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_without_timeout(&self) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
join_all(
self.accounts
.values()
.cloned()
.map(background_fetch_and_log_error),
)
.await;
}
/// Performs a background fetch for all accounts in parallel with a timeout.
///
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
/// process all events until you get this one and you can safely return to the background
/// without forgetting to create notifications caused by timing race conditions.
pub async fn background_fetch(&self, timeout: std::time::Duration) {
if let Err(_err) =
tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await
{
self.emit_event(EventType::Warning(
"Background fetch timed out.".to_string(),
));
}
self.emit_event(EventType::AccountsBackgroundFetchDone);
}
/// Emits a single event.
pub fn emit_event(&self, event: EventType) {
self.events.emit(Event { id: 0, typ: event })
@@ -300,6 +329,12 @@ impl Accounts {
pub fn get_event_emitter(&self) -> EventEmitter {
self.events.get_emitter()
}
/// Sets notification token for Apple Push Notification service.
pub async fn set_push_device_token(&mut self, token: &str) -> Result<()> {
self.push_subscriber.set_device_token(token).await;
Ok(())
}
}
/// Configuration file name.
@@ -485,24 +520,24 @@ impl Config {
&self,
events: &Events,
stockstrings: &StockStrings,
push_subscriber: PushSubscriber,
dir: &Path,
) -> Result<BTreeMap<u32, Context>> {
let mut accounts = BTreeMap::new();
for account_config in &self.inner.accounts {
let ctx = Context::new(
&account_config.dbfile(dir),
account_config.id,
events.clone(),
stockstrings.clone(),
)
.await
.with_context(|| {
format!(
"failed to create context from file {:?}",
account_config.dbfile(dir)
)
})?;
let dbfile = account_config.dbfile(dir);
let ctx = ContextBuilder::new(dbfile.clone())
.with_id(account_config.id)
.with_events(events.clone())
.with_stock_strings(stockstrings.clone())
.with_push_subscriber(push_subscriber.clone())
.build()
.await
.with_context(|| format!("failed to create context from file {:?}", &dbfile))?;
// Try to open without a passphrase,
// but do not return an error if account is passphare-protected.
ctx.open("".to_string()).await?;
accounts.insert(account_config.id, ctx);
}

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;
@@ -14,7 +15,6 @@ 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
@@ -216,7 +216,7 @@ async fn update_authservid_candidates(
if old_ids != new_ids {
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
context
.set_config(Config::AuthservIdCandidates, Some(&new_config))
.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

View File

@@ -5,11 +5,14 @@ use std::ffi::OsStr;
use std::fmt;
use std::io::Cursor;
use std::iter::FusedIterator;
use std::mem;
use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use image::codecs::jpeg::JpegEncoder;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -33,6 +36,12 @@ pub struct BlobObject<'a> {
name: String,
}
#[derive(Debug, Clone)]
enum ImageOutputFormat {
Png,
Jpeg { quality: u8 },
}
impl<'a> BlobObject<'a> {
/// Creates a new blob object with a unique name.
///
@@ -312,6 +321,30 @@ impl<'a> BlobObject<'a> {
true
}
/// Returns path to the stored Base64-decoded blob.
///
/// If `data` represents an image of known format, this adds the corresponding extension to
/// `suggested_file_stem`.
pub(crate) async fn store_from_base64(
context: &Context,
data: &str,
suggested_file_stem: &str,
) -> Result<String> {
let buf = base64::engine::general_purpose::STANDARD.decode(data)?;
let ext = if let Ok(format) = image::guess_format(&buf) {
if let Some(ext) = format.extensions_str().first() {
format!(".{ext}")
} else {
String::new()
}
} else {
String::new()
};
let blob =
BlobObject::create(context, &format!("{suggested_file_stem}{ext}"), &buf).await?;
Ok(blob.as_name().to_string())
}
pub async fn recode_to_avatar_size(&mut self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
@@ -388,6 +421,8 @@ impl<'a> BlobObject<'a> {
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
// Add white background only to avatars to spare the CPU.
let mut add_white_bg = img_wh <= constants::BALANCED_AVATAR_SIZE;
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
@@ -421,13 +456,19 @@ impl<'a> BlobObject<'a> {
let exceeds_wh = img.width() > img_wh || img.height() > img_wh;
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,
_ => {
let jpeg_quality = 75;
ImageOutputFormat::Jpeg(jpeg_quality)
Ok(ImageFormat::Jpeg) => {
add_white_bg = false;
ImageOutputFormat::Jpeg {
quality: jpeg_quality,
}
}
_ => ImageOutputFormat::Jpeg {
quality: jpeg_quality,
},
};
// We need to rewrite images with Exif to remove metadata such as location,
// camera model, etc.
@@ -438,14 +479,18 @@ impl<'a> BlobObject<'a> {
let do_scale = exceeds_max_bytes
|| strict_limits
&& (exceeds_wh
|| exif.is_some()
&& encoded_img_exceeds_bytes(
|| exif.is_some() && {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encoded_img_exceeds_bytes(
context,
&img,
ofmt.clone(),
max_bytes,
&mut encoded,
)?);
)?
});
if do_scale {
if !exceeds_wh {
@@ -458,6 +503,9 @@ impl<'a> BlobObject<'a> {
}
loop {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
let new_img = img.thumbnail(img_wh, img_wh);
if encoded_img_exceeds_bytes(
@@ -491,7 +539,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))
&& matches!(ofmt, ImageOutputFormat::Jpeg(_))
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
{
blob_abs = blob_abs.with_extension("jpg");
let file_name = blob_abs.file_name().context("No image file name (???)")?;
@@ -500,6 +548,9 @@ impl<'a> BlobObject<'a> {
}
if encoded.is_empty() {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
}
encode_img(&img, ofmt, &mut encoded)?;
}
@@ -643,7 +694,16 @@ fn encode_img(
) -> anyhow::Result<()> {
encoded.clear();
let mut buf = Cursor::new(encoded);
img.write_to(&mut buf, fmt)?;
match fmt {
ImageOutputFormat::Png => img.write_to(&mut buf, ImageFormat::Png)?,
ImageOutputFormat::Jpeg { quality } => {
let encoder = JpegEncoder::new_with_quality(&mut buf, quality);
// 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(())
}
@@ -669,11 +729,20 @@ fn encoded_img_exceeds_bytes(
Ok(false)
}
/// Removes transparency from an image using a white background.
fn add_white_bg(img: &mut DynamicImage) {
for y in 0..img.height() {
for x in 0..img.width() {
let mut p = Rgba([255u8, 255, 255, 255]);
p.blend(&img.get_pixel(x, y));
img.put_pixel(x, y, p);
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use fs::File;
use image::{GenericImageView, Pixel};
use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
@@ -885,6 +954,40 @@ mod tests {
assert!(!stem.contains('?'));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_white_bg() {
let t = TestContext::new().await;
let bytes0 = include_bytes!("../test-data/image/logo.png").as_slice();
let bytes1 = include_bytes!("../test-data/image/avatar900x900.png").as_slice();
for (bytes, color) in [
(bytes0, [255u8, 255, 255, 255]),
(bytes1, [253u8, 198, 0, 255]),
] {
let avatar_src = t.dir.path().join("avatar.png");
fs::write(&avatar_src, bytes).await.unwrap();
let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap();
let img_wh = 128;
let maybe_sticker = &mut false;
let strict_limits = true;
blob.recode_to_size(
&t,
blob.to_abs_path(),
maybe_sticker,
img_wh,
20_000,
strict_limits,
)
.unwrap();
tokio::task::block_in_place(move || {
let img = image::open(blob.to_abs_path()).unwrap();
assert!(img.width() == img_wh);
assert!(img.height() == img_wh);
assert_eq!(img.get_pixel(0, 0), Rgba(color));
});
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_selfavatar_outside_blobdir() {
let t = TestContext::new().await;
@@ -1105,6 +1208,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");

View File

@@ -2,13 +2,13 @@
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::{Duration, SystemTime};
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;
@@ -16,13 +16,14 @@ use strum_macros::EnumIter;
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;
@@ -44,7 +45,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,
smeared_time, time, IsNoneOrEmpty, SystemTime,
};
use crate::webxdc::WEBXDC_SUFFIX;
@@ -210,24 +211,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)
@@ -309,6 +297,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)
}
@@ -425,6 +415,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.
@@ -447,6 +438,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.
@@ -457,6 +450,7 @@ impl ChatId {
.log_err(context)
.ok();
}
Ok(())
}
@@ -500,6 +494,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() {
@@ -542,6 +537,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?;
@@ -590,6 +586,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(())
}
@@ -676,6 +673,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?;
@@ -782,8 +781,11 @@ impl ChatId {
.await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
context.set_config(Config::LastHousekeeping, None).await?;
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;
context.scheduler.interrupt_inbox().await;
if chat.is_self_talk() {
@@ -791,6 +793,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(())
}
@@ -1171,6 +1174,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,
@@ -1429,7 +1441,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))
@@ -1517,7 +1529,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);
}
}
}
@@ -1788,18 +1800,10 @@ impl Chat {
update_msg_id: Option<MsgId>,
timestamp: i64,
) -> Result<MsgId> {
let mut new_references = "".into();
let mut to_id = 0;
let mut location_id = 0;
let from = context.get_primary_self_addr().await?;
let new_rfc724_mid = {
let grpid = match self.typ {
Chattype::Group => Some(self.grpid.as_str()),
_ => None,
};
create_outgoing_rfc724_mid(grpid, &from)
};
let new_rfc724_mid = create_outgoing_rfc724_mid();
if self.typ == Chattype::Single {
if let Some(id) = context
@@ -1837,58 +1841,72 @@ impl Chat {
// reset encrypt error state eg. for forwarding
msg.param.remove(Param::ErroneousE2ee);
// 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
//
// as self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
if !self.is_self_talk() {
if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
// the whole list of messages referenced may be huge;
// only use the oldest and the parent message
let parent_references = parent_references
.find(' ')
.and_then(|n| parent_references.get(..n))
.unwrap_or(&parent_references);
if !parent_references.is_empty() && !parent_rfc724_mid.is_empty() {
// angle brackets are added by the mimefactory later
new_references = format!("{parent_references} {parent_rfc724_mid}");
} else if !parent_references.is_empty() {
new_references = parent_references.to_string();
} else if !parent_in_reply_to.is_empty() && !parent_rfc724_mid.is_empty() {
new_references = format!("{parent_in_reply_to} {parent_rfc724_mid}");
} else if !parent_in_reply_to.is_empty() {
new_references = parent_in_reply_to;
} else {
// as a fallback, use our Message-ID, see reasoning below.
new_references = new_rfc724_mid.clone();
}
} else {
// this is a top-level message, add our Message-ID as first reference.
// as we always try to extract the grpid also from `References:`-header,
// this allows group conversations also if smtp-server as outlook change `Message-ID:`-header
// (MUAs usually keep the first Message-ID in `References:`-header unchanged).
new_references = new_rfc724_mid.clone();
// 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).
let new_references;
if self.is_self_talk() {
// As self-talks are mainly used to transfer data between devices,
// we do not set In-Reply-To/References in this case.
new_references = String::new();
} else if let Some((parent_rfc724_mid, parent_in_reply_to, parent_references)) =
// We don't filter `OutPending` and `OutFailed` messages because the new message for
// which `parent_query()` is done may assume that it will be received in a context
// affected by those messages, e.g. they could add new members to a group and the
// new message will contain them in "To:". Anyway recipients must be prepared to
// orphaned references.
self
.id
.get_parent_mime_headers(context, MessageState::OutPending)
.await?
{
// "In-Reply-To:" is not changed if it is set manually.
// This does not affect "References:" header, it will contain "default parent" (the
// latest message in the thread) anyway.
if msg.in_reply_to.is_none() && !parent_rfc724_mid.is_empty() {
msg.in_reply_to = Some(parent_rfc724_mid.clone());
}
// Use parent `In-Reply-To` as a fallback
// in case parent message has no `References` header
// as specified in RFC 5322:
// > If the parent message does not contain
// > a "References:" field but does have an "In-Reply-To:" field
// > containing a single message identifier, then the "References:" field
// > will contain the contents of the parent's "In-Reply-To:" field
// > followed by the contents of the parent's "Message-ID:" field (if
// > any).
let parent_references = if parent_references.is_empty() {
parent_in_reply_to
} else {
parent_references
};
// The whole list of messages referenced may be huge.
// Only take 2 recent references and add third from `In-Reply-To`.
let mut references_vec: Vec<&str> = parent_references.rsplit(' ').take(2).collect();
references_vec.reverse();
if !parent_rfc724_mid.is_empty()
&& !references_vec.contains(&parent_rfc724_mid.as_str())
{
references_vec.push(&parent_rfc724_mid)
}
if references_vec.is_empty() {
// As a fallback, use our Message-ID,
// same as in the case of top-level message.
new_references = new_rfc724_mid.clone();
} else {
new_references = references_vec.join(" ");
}
} else {
// This is a top-level message.
// Add our Message-ID as first references.
// This allows us to identify replies to our message even if
// email server such as Outlook changes `Message-ID:` header.
// MUAs usually keep the first Message-ID in `References:` header unchanged.
new_references = new_rfc724_mid.clone();
}
// add independent location to database
@@ -2031,6 +2049,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)
@@ -2657,6 +2676,11 @@ pub async fn send_msg(context: &Context, chat_id: ChatId, msg: &mut Message) ->
return send_msg_inner(context, chat_id, msg).await;
}
if msg.state != MessageState::Undefined && msg.state != MessageState::OutPreparing {
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.update_param(context).await?;
}
send_msg_inner(context, chat_id, msg).await
}
@@ -2686,10 +2710,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;
@@ -2730,26 +2756,25 @@ async fn prepare_send_msg(
/// The caller has to interrupt SMTP loop or otherwise process new rows.
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
let attach_selfavatar = match shall_attach_selfavatar(context, msg.chat_id).await {
Ok(attach_selfavatar) => attach_selfavatar,
Err(err) => {
warn!(context, "SMTP job cannot get selfavatar-state: {err:#}.");
false
}
};
let mimefactory = MimeFactory::from_msg(context, msg, attach_selfavatar).await?;
let mimefactory = MimeFactory::from_msg(context, msg).await?;
let attach_selfavatar = mimefactory.attach_selfavatar;
let mut recipients = mimefactory.recipients();
let from = context.get_primary_self_addr().await?;
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled and we are not going to
// delete it immediately. `from` must be the last addr, see `receive_imf_inner()` why.
// Send BCC to self if it is enabled.
//
// Previous versions of Delta Chat did not send BCC self
// if DeleteServerAfter was set to immediately delete messages
// from the server. This is not the case anymore
// because BCC-self messages are also used to detect
// that message was sent if SMTP server is slow to respond
// and connection is frequently lost
// before receiving status line.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
if context.get_config_bool(Config::BccSelf).await?
&& context.get_config_delete_server_after().await? != Some(0)
&& !recipients
.iter()
.any(|x| x.to_lowercase() == lowercase_from)
@@ -2757,6 +2782,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!(
@@ -2791,12 +2821,26 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
);
}
let now = smeared_time(context);
if rendered_msg.is_gossiped {
msg.chat_id.set_gossiped_timestamp(context, time()).await?;
msg.chat_id.set_gossiped_timestamp(context, now).await?;
}
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.saturating_add(constants::TIMESTAMP_SENT_TOLERANCE),
)
.await?;
}
if let Some(last_added_location_id) = rendered_msg.last_added_location_id {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await {
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 {
@@ -2815,7 +2859,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, time()).await {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
}
@@ -2825,7 +2869,7 @@ 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()
@@ -3080,7 +3124,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
@@ -3107,6 +3153,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(())
}
@@ -3174,6 +3221,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(())
@@ -3336,6 +3384,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
@@ -3423,11 +3473,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)
}
@@ -3639,7 +3692,7 @@ pub enum MuteDuration {
Forever,
/// Chat is muted for a limited period of time.
Until(SystemTime),
Until(std::time::SystemTime),
}
impl rusqlite::types::ToSql for MuteDuration {
@@ -3698,6 +3751,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))
@@ -3858,6 +3912,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;
}
}
@@ -3918,6 +3973,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(())
}
@@ -4063,6 +4119,9 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
chat_id: msg.chat_id,
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;
}
@@ -4133,7 +4192,7 @@ pub async fn add_device_msg_with_importance(
if let Some(msg) = msg {
chat_id = ChatId::get_for_contact(context, ContactId::DEVICE).await?;
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let rfc724_mid = create_outgoing_rfc724_mid();
prepare_msg_blob(context, msg).await?;
let timestamp_sent = create_smeared_timestamp(context);
@@ -4252,7 +4311,9 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
(),
)
.await?;
context.set_config(Config::QuotaExceeding, None).await?;
context
.set_config_internal(Config::QuotaExceeding, None)
.await?;
Ok(())
}
@@ -4271,7 +4332,7 @@ pub(crate) async fn add_info_msg_with_cmd(
parent: Option<&Message>,
from_id: Option<ContactId>,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid(None, "@device");
let rfc724_mid = create_outgoing_rfc724_mid();
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
let mut param = Params::new();
@@ -4454,9 +4515,8 @@ impl Context {
#[cfg(test)]
mod tests {
use super::*;
use crate::chatlist::{get_archived_cnt, Chatlist};
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{sync, TestContext, TestContextManager};
@@ -4754,9 +4814,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;
@@ -4787,20 +4847,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;
@@ -4837,8 +4902,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(())
@@ -5912,11 +5982,11 @@ mod tests {
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
let sent_msg = alice.pop_sent_msg().await;
let msg = sent_msg.payload();
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 2);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Gr.").count(), 0);
assert_eq!(msg.match_indices("References: <Gr.").count(), 1);
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 2);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
assert_eq!(msg.match_indices("Message-ID: <Mr.").count(), 0);
assert_eq!(msg.match_indices("References: <Mr.").count(), 1);
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
@@ -5933,7 +6003,7 @@ mod tests {
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
let sent_msg = bob.pop_sent_msg().await;
let msg = sent_msg.payload();
let msg = msg.replace("Message-ID: <Gr.", "Message-ID: <XXX");
let msg = msg.replace("Message-ID: <Mr.", "Message-ID: <XXX");
let msg = msg.replace("Chat-", "XXXX-");
assert_eq!(msg.match_indices("Chat-").count(), 0);
@@ -6489,6 +6559,7 @@ mod tests {
// Bob receives all messages
let bob = TestContext::new_bob().await;
let msg = bob.recv_msg(&sent1).await;
let sent1_ts_sent = msg.timestamp_sent;
assert_eq!(msg.get_text(), "alice->bob");
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 1);
@@ -6511,6 +6582,7 @@ mod tests {
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
assert_eq!(msg_from.get_addr(), "alice@example.org");
assert!(sent1_ts_sent < msg.timestamp_sent);
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) {
Ok(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

@@ -5,13 +5,15 @@ use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::addr_cmp;
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::constants::DC_VERSION_STR;
use crate::contact::addr_cmp;
use crate::constants::{self, DC_VERSION_STR};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -291,6 +293,9 @@ pub enum Config {
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
/// Timestamp of the last `CantDecryptOutgoingMsgs` notification.
LastCantDecryptOutgoingMsgs,
/// To how many seconds to debounce scan_all_folders. Used mainly in tests, to disable debouncing completely.
#[strum(props(default = "60"))]
ScanAllFoldersDebounceSecs,
@@ -347,6 +352,13 @@ pub enum Config {
/// Row ID of the key in the `keypairs` table
/// used for signatures, encryption to self and included in `Autocrypt` header.
KeyId,
/// This key is sent to the self_reporting bot so that the bot can recognize the user
/// without storing the email address
SelfReportingId,
/// MsgId of webxdc map integration.
WebxdcIntegration,
}
impl Config {
@@ -354,15 +366,31 @@ impl Config {
///
/// This must be checked on both sides so that if there are different client versions, the
/// synchronisation of a particular option is either done or not done in both directions.
/// Moreover, receivers of a config value need to check if a key can be synced because some
/// settings (e.g. Avatar path) could otherwise lead to exfiltration of files from a receiver's
/// Moreover, receivers of a config value need to check if a key can be synced because if it is
/// a file path, it could otherwise lead to exfiltration of files from a receiver's
/// device if we assume an attacker to have control of a device in a multi-device setting or if
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
/// mustn't be controlled by other devices.
pub(crate) fn is_synced(&self) -> bool {
// We don't restart IO from the synchronisation code, so this is to be on the safe side.
if self.needs_io_restart() {
return false;
}
matches!(
self,
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails
Self::Displayname
| Self::MdnsEnabled
| Self::ShowEmails
| Self::Selfavatar
| Self::Selfstatus,
)
}
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
)
}
}
@@ -450,6 +478,15 @@ impl Context {
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
pub(crate) async fn should_watch_sentbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::SentboxWatch).await?
&& self
.get_config(Config::ConfiguredSentboxFolder)
.await?
.is_some())
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -484,51 +521,25 @@ impl Context {
}
}
/// Set the given config key.
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
self.set_config_ex(key.is_synced().into(), key, value).await
/// Executes [`SyncData::Config`] item sent by other device.
pub(crate) async fn sync_config(&self, key: &Config, value: &str) -> Result<()> {
let config_value;
let value = match key {
Config::Selfavatar if value.is_empty() => None,
Config::Selfavatar => {
config_value = BlobObject::store_from_base64(self, value, "avatar").await?;
Some(config_value.as_str())
}
_ => Some(value),
};
match key.is_synced() {
true => self.set_config_ex(Nosync, *key, value).await,
false => Ok(()),
}
}
pub(crate) async fn set_config_ex(
&self,
sync: sync::Sync,
key: Config,
mut value: Option<&str>,
) -> Result<()> {
let better_value;
fn check_config(key: Config, value: Option<&str>) -> Result<()> {
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
.await?;
match value {
Some(value) => {
let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
}
None => {
self.sql.set_raw_config(key.as_ref(), None).await?;
}
}
self.emit_event(EventType::SelfavatarChanged);
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
// Interrupt ephemeral loop to delete old messages immediately.
self.scheduler.interrupt_ephemeral_task().await;
ret?
}
Config::Displayname => {
if let Some(v) = value {
better_value = improve_single_line_input(v);
value = Some(&better_value);
}
self.sql.set_raw_config(key.as_ref(), value).await?;
}
Config::Socks5Enabled
| Config::BccSelf
| Config::E2eeEnabled
@@ -549,6 +560,82 @@ impl Context {
matches!(value, None | Some("0") | Some("1")),
"Boolean value must be either 0 or 1"
);
}
_ => (),
}
Ok(())
}
/// Set the given config key and make it effective.
/// This may restart the IO scheduler. If `None` is passed as a value the value is cleared and
/// set to the default if there is one.
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?;
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self.clone()).await?,
_ => Default::default(),
};
self.set_config_internal(key, value).await?;
if key == Config::SentboxWatch {
self.last_full_folder_scan.lock().await.take();
}
Ok(())
}
pub(crate) async fn set_config_internal(&self, key: Config, value: Option<&str>) -> Result<()> {
self.set_config_ex(Sync, key, value).await
}
pub(crate) async fn set_config_ex(
&self,
sync: sync::Sync,
key: Config,
mut value: Option<&str>,
) -> Result<()> {
Self::check_config(key, value)?;
let sync = sync == Sync && key.is_synced();
let better_value;
match key {
Config::Selfavatar => {
self.sql
.execute("UPDATE contacts SET selfavatar_sent=0;", ())
.await?;
match value {
Some(path) => {
let mut blob = BlobObject::new_from_path(self, path.as_ref()).await?;
blob.recode_to_avatar_size(self).await?;
self.sql
.set_raw_config(key.as_ref(), Some(blob.as_name()))
.await?;
if sync {
let buf = fs::read(blob.to_abs_path()).await?;
better_value = base64::engine::general_purpose::STANDARD.encode(buf);
value = Some(&better_value);
}
}
None => {
self.sql.set_raw_config(key.as_ref(), None).await?;
if sync {
better_value = String::new();
value = Some(&better_value);
}
}
}
self.emit_event(EventType::SelfavatarChanged);
}
Config::DeleteDeviceAfter => {
let ret = self.sql.set_raw_config(key.as_ref(), value).await;
// Interrupt ephemeral loop to delete old messages immediately.
self.scheduler.interrupt_ephemeral_task().await;
ret?
}
Config::Displayname => {
if let Some(v) = value {
better_value = improve_single_line_input(v);
value = Some(&better_value);
}
self.sql.set_raw_config(key.as_ref(), value).await?;
}
Config::Addr => {
@@ -556,12 +643,20 @@ impl Context {
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
Config::MvboxMove => {
self.sql.set_raw_config(key.as_ref(), value).await?;
self.sql
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
.await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
if sync != Sync {
if key.is_synced() {
self.emit_event(EventType::ConfigSynced { key });
}
if !sync {
return Ok(());
}
let Some(val) = value else {
@@ -576,7 +671,7 @@ impl Context {
{
return Ok(());
}
self.send_sync_msg().await.log_err(self).ok();
Box::pin(self.send_sync_msg()).await.log_err(self).ok();
Ok(())
}
@@ -588,8 +683,7 @@ impl Context {
/// Set the given config to a boolean value.
pub async fn set_config_bool(&self, key: Config, value: bool) -> Result<()> {
self.set_config(key, if value { Some("1") } else { Some("0") })
.await?;
self.set_config(key, from_bool(value)).await?;
Ok(())
}
@@ -609,6 +703,11 @@ impl Context {
}
}
/// Returns a value for use in `Context::set_config_*()` for the given `bool`.
pub(crate) fn from_bool(val: bool) -> Option<&'static str> {
Some(if val { "1" } else { "0" })
}
// Separate impl block for self address handling
impl Context {
/// Determine whether the specified addr maps to the/a self addr.
@@ -635,13 +734,13 @@ impl Context {
let mut secondary_addrs = self.get_all_self_addrs().await?;
// never store a primary address also as a secondary
secondary_addrs.retain(|a| !addr_cmp(a, primary_new));
self.set_config(
self.set_config_internal(
Config::SecondaryAddrs,
Some(secondary_addrs.join(" ").as_str()),
)
.await?;
self.set_config(Config::ConfiguredAddr, Some(primary_new))
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
.await?;
Ok(())
@@ -689,13 +788,10 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use std::string::ToString;
use num_traits::FromPrimitive;
use super::*;
use crate::constants;
use crate::test_utils::{sync, TestContext};
use crate::test_utils::{sync, TestContext, TestContextManager};
#[test]
fn test_to_string() {
@@ -866,10 +962,10 @@ mod tests {
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
assert!(alice0.get_config_bool(Config::MdnsEnabled).await?);
assert_eq!(alice0.get_config_bool(Config::MdnsEnabled).await?, true);
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert!(!alice1.get_config_bool(Config::MdnsEnabled).await?);
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
alice0
@@ -889,14 +985,150 @@ mod tests {
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
// Usual sync scenario.
async fn test_config_str(
alice0: &TestContext,
alice1: &TestContext,
key: Config,
val: &str,
) -> Result<()> {
alice0.set_config(key, Some(val)).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_config(key).await?, Some(val.to_string()));
Ok(())
}
test_config_str(&alice0, &alice1, Config::Displayname, "Alice Sync").await?;
test_config_str(&alice0, &alice1, Config::Selfstatus, "My status").await?;
assert!(alice0.get_config(Config::Selfavatar).await?.is_none());
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
sync(&alice0, &alice1).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
alice0.set_config(Config::Selfavatar, None).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config(Config::Selfavatar).await?.is_none());
Ok(())
}
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice0 = &tcm.alice().await;
let alice1 = &tcm.alice().await;
for a in [alice0, alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.pop_sent_msg().await; // Sync message
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status.to_string())
);
sync(alice1, alice0).await;
assert_eq!(
alice0.get_config(Config::Selfstatus).await?,
Some(status1.to_string())
);
// Need a chat with another contact to send self-avatar.
let bob = &tcm.bob().await;
let a0b_chat_id = tcm.send_recv_accept(bob, alice0, "hi").await.chat_id;
let file = alice0.dir.path().join("avatar.png");
let bytes = include_bytes!("../test-data/image/avatar64x64.png");
tokio::fs::write(&file, bytes).await?;
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.pop_sent_msg().await; // Sync message
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;
alice1
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
let sent_msg = alice0.send_text(a0b_chat_id, "hi").await;
alice1.recv_msg(&sent_msg).await;
assert!(alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.is_some());
sync(alice1, alice0).await;
assert!(alice0
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".jpg"))
.is_some());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_event_config_synced() -> Result<()> {
let alice0 = TestContext::new_alice().await;
let alice1 = TestContext::new_alice().await;
for a in [&alice0, &alice1] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
alice0
.set_config(Config::Displayname, Some("Alice Sync"))
.await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some("Alice Sync".to_string())
);
alice1
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
alice0.set_config(Config::Displayname, None).await?;
alice0
.evtracker
.get_matching(|e| {
matches!(
e,
EventType::ConfigSynced {
key: Config::Displayname
}
)
})
.await;
Ok(())
}

View File

@@ -16,16 +16,16 @@ mod server_params;
use anyhow::{bail, ensure, Context as _, Result};
use auto_mozilla::moz_autoconfigure;
use auto_outlook::outlk_autodiscover;
use deltachat_contact_tools::EmailAddress;
use futures::FutureExt;
use futures_lite::FutureExt as _;
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use server_params::{expand_param_vector, ServerParams};
use tokio::task;
use crate::config::Config;
use crate::contact::addr_cmp;
use crate::config::{self, Config};
use crate::context::Context;
use crate::imap::Imap;
use crate::imap::{session::Session as ImapSession, Imap};
use crate::log::LogExt;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::message::{Message, Viewtype};
@@ -35,8 +35,9 @@ use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{time, EmailAddress};
use crate::tools::time;
use crate::{chat, e2ee, provider};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
@@ -112,12 +113,13 @@ impl Context {
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
let success = configure(self, &mut param).await;
self.set_config(Config::NotifyAboutWrongPw, None).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
on_configure_completed(self, param, old_addr).await?;
success?;
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
Ok(())
}
@@ -355,8 +357,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let mut smtp_configured = false;
let mut errors = Vec::new();
for smtp_server in smtp_servers {
smtp_param.user = smtp_server.username.clone();
smtp_param.server = smtp_server.hostname.clone();
smtp_param.user.clone_from(&smtp_server.username);
smtp_param.server.clone_from(&smtp_server.hostname);
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
@@ -394,7 +396,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Configure IMAP
let mut imap: Option<Imap> = None;
let mut imap: Option<(Imap, ImapSession)> = None;
let imap_servers: Vec<&ServerParams> = servers
.iter()
.filter(|params| params.protocol == Protocol::Imap)
@@ -402,8 +404,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let imap_servers_count = imap_servers.len();
let mut errors = Vec::new();
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
param.imap.user = imap_server.username.clone();
param.imap.server = imap_server.hostname.clone();
param.imap.user.clone_from(&imap_server.username);
param.imap.server.clone_from(&imap_server.hostname);
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
param.imap.certificate_checks = match imap_server.strict_tls {
@@ -432,7 +434,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
);
}
let mut imap = match imap {
let (mut imap, mut imap_session) = match imap {
Some(imap) => imap,
None => bail!(nicer_configuration_error(ctx, errors).await),
};
@@ -453,9 +455,11 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let create_mvbox = ctx.should_watch_mvbox().await?;
imap.configure_folders(ctx, create_mvbox).await?;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
imap.select_with_uidvalidity(ctx, "INBOX")
imap_session
.select_with_uidvalidity(ctx, "INBOX")
.await
.context("could not read INBOX status")?;
@@ -473,7 +477,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// the trailing underscore is correct
param.save_as_configured_params(ctx).await?;
ctx.set_config(Config::ConfiguredTimestamp, Some(&time().to_string()))
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;
progress!(ctx, 920);
@@ -481,7 +485,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
e2ee::ensure_secret_key_exists(ctx).await?;
info!(ctx, "key generation completed");
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
ctx.set_config_internal(Config::FetchedExistingMsgs, config::from_bool(false))
.await?;
ctx.scheduler.interrupt_inbox().await;
@@ -575,7 +579,7 @@ async fn try_imap_one_param(
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
) -> Result<Imap, ConfigurationError> {
) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
param.user,
@@ -613,9 +617,9 @@ async fn try_imap_one_param(
msg: format!("{err:#}"),
})
}
Ok(()) => {
Ok(session) => {
info!(context, "success: {}", inf);
Ok(imap)
Ok((imap, session))
}
}
}

View File

@@ -137,20 +137,11 @@ impl ServerParams {
}
fn expand_strict_tls(self) -> Vec<ServerParams> {
if self.strict_tls.is_none() {
vec![
Self {
strict_tls: Some(true), // Strict.
..self.clone()
},
Self {
strict_tls: None, // Automatic.
..self
},
]
} else {
vec![self]
}
vec![Self {
// Strict if not set by the user or provider database.
strict_tls: Some(self.strict_tls.unwrap_or(true)),
..self
}]
}
}
@@ -162,31 +153,10 @@ pub(crate) fn expand_param_vector(
domain: &str,
) -> Vec<ServerParams> {
v.into_iter()
.map(|params| {
if params.socket == Socket::Plain {
ServerParams {
// Avoid expanding plaintext configuration into configuration with and without
// `strict_tls` if `strict_tls` is set to `None` as `strict_tls` is not used for
// plaintext connections. Always setting it to "enabled", just in case.
strict_tls: Some(true),
..params
}
} else {
params
}
})
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first. Username is only changed if
// default value (address with domain) didn't work for all available hosts and ports.
//
// Strict TLS must be expanded first, so we try all configurations with strict TLS first
// and only then try again without strict TLS. Otherwise we may lock to wrong hostname
// without strict TLS when another hostname with strict TLS is available. For example, if
// both smtp.example.net and mail.example.net are running an SMTP server, but both use a
// certificate that is only valid for mail.example.net, we want to skip smtp.example.net
// and use mail.example.net with strict TLS instead of using smtp.example.net without
// strict TLS.
.flat_map(|params| params.expand_strict_tls().into_iter())
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
@@ -257,22 +227,6 @@ mod tests {
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: None,
},
ServerParams {
protocol: Protocol::Smtp,
hostname: "example.net".to_string(),
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: None
}
],
);
@@ -284,7 +238,7 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: None,
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",

View File

@@ -206,6 +206,8 @@ pub(crate) const WORSE_AVATAR_SIZE: u32 = 128;
pub const BALANCED_IMAGE_SIZE: u32 = 1280;
pub const WORSE_IMAGE_SIZE: u32 = 640;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
@@ -214,6 +216,13 @@ pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
// `max_smtp_rcpt_to` in the provider db.
pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50;
/// How far the last quota check needs to be in the past to be checked by the background function (in seconds).
pub(crate) const DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT: u64 = 12 * 60 * 60; // 12 hours
/// How far in the future the sender timestamp of a message is allowed to be, in seconds. Also used
/// in the group membership consistency algo to reject outdated membership changes.
pub(crate) const TIMESTAMP_SENT_TOLERANCE: i64 = 60;
#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;

View File

@@ -2,17 +2,18 @@
use std::cmp::Reverse;
use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use std::time::UNIX_EPOCH;
use anyhow::{bail, ensure, Context as _, Result};
use async_channel::{self as channel, Receiver, Sender};
pub use deltachat_contact_tools::may_be_valid_addr;
use deltachat_contact_tools::{
addr_cmp, addr_normalize, normalize_name, sanitize_name_and_addr, strip_rtlo_characters,
ContactAddress,
};
use deltachat_derive::{FromSql, ToSql};
use once_cell::sync::Lazy;
use regex::Regex;
use rusqlite::OptionalExtension;
use serde::{Deserialize, Serialize};
use tokio::task;
@@ -34,60 +35,12 @@ use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*};
use crate::tools::{
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
EmailAddress,
};
use crate::{chat, stock_str};
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, SystemTime};
use crate::{chat, chatlist_events, stock_str};
/// Time during which a contact is considered as seen recently.
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Valid contact address.
#[derive(Debug, Clone)]
pub(crate) 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)
}
}
/// Contact ID, including reserved IDs.
///
/// Some contact IDs are reserved to identify special contacts. This
@@ -761,6 +714,7 @@ impl Contact {
if count > 0 {
// Chat name updated
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
}
}
}
@@ -797,7 +751,9 @@ impl Contact {
Ok(row_id)
}).await?;
Ok((ContactId::new(row_id), sth_modified))
let contact_id = ContactId::new(row_id);
Ok((contact_id, sth_modified))
}
/// Add a number of contacts.
@@ -1052,55 +1008,52 @@ impl Contact {
"Can not provide encryption info for special contact"
);
let mut ret = String::new();
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
let contact = Contact::get_by_id(context, contact_id).await?;
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
if let Some(peerstate) =
peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
{
let stock_message = match peerstate.prefer_encrypt {
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
EncryptPreference::Reset => stock_str::encr_none(context).await,
};
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
else {
return Ok(stock_str::encr_none(context).await);
};
let finger_prints = stock_str::finger_prints(context).await;
ret += &format!("{stock_message}.\n{finger_prints}:");
let stock_message = match peerstate.prefer_encrypt {
EncryptPreference::Mutual => stock_str::e2e_preferred(context).await,
EncryptPreference::NoPreference => stock_str::e2e_available(context).await,
EncryptPreference::Reset => stock_str::encr_none(context).await,
};
let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
} else {
cat_fingerprint(
&mut ret,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
}
} else {
ret += &stock_str::encr_none(context).await;
}
let finger_prints = stock_str::finger_prints(context).await;
let mut ret = format!("{stock_message}.\n{finger_prints}:");
let fingerprint_self = load_self_public_key(context)
.await?
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(true)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
cat_fingerprint(
&mut ret,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
} else {
cat_fingerprint(
&mut ret,
&peerstate.addr,
&fingerprint_other_verified,
&fingerprint_other_unverified,
);
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
}
Ok(ret)
@@ -1416,46 +1369,6 @@ impl Contact {
}
}
/// 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
}
}
fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(
&captures
.get(1)
.map_or("".to_string(), |m| normalize_name(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(name), addr.to_string())
}
}
pub(crate) async fn set_blocked(
context: &Context,
sync: sync::Sync,
@@ -1528,6 +1441,7 @@ WHERE type=? AND id IN (
}
}
chatlist_events::emit_chatlist_changed(context);
Ok(())
}
@@ -1537,7 +1451,7 @@ WHERE type=? AND id IN (
/// as profile images can be set only by receiving messages, this should be always the case, however.
///
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
/// this typically happens if we see message with our own profile image, sent from another device.
/// this typically happens if we see message with our own profile image.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
@@ -1550,7 +1464,7 @@ pub(crate) async fn set_profile_image(
if contact_id == ContactId::SELF {
if was_encrypted {
context
.set_config(Config::Selfavatar, Some(profile_image))
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar.");
@@ -1563,7 +1477,9 @@ pub(crate) async fn set_profile_image(
AvatarAction::Delete => {
if contact_id == ContactId::SELF {
if was_encrypted {
context.set_config(Config::Selfavatar, None).await?;
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar deletion.");
}
@@ -1576,6 +1492,7 @@ pub(crate) async fn set_profile_image(
if changed {
contact.update_param(context).await?;
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(context, contact_id).await;
}
Ok(())
}
@@ -1595,7 +1512,7 @@ pub(crate) async fn set_status(
if contact_id == ContactId::SELF {
if encrypted && has_chat_version {
context
.set_config(Config::Selfstatus, Some(&status))
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.await?;
}
} else {
@@ -1631,6 +1548,7 @@ pub(crate) async fn update_last_seen(
> 0
&& timestamp > time() - SEEN_RECENTLY_SECONDS
{
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
context
.scheduler
.interrupt_recently_seen(contact_id, timestamp)
@@ -1639,26 +1557,6 @@ pub(crate) async fn update_last_seen(
Ok(())
}
/// 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(),
}
}
fn cat_fingerprint(
ret: &mut String,
addr: &str,
@@ -1682,14 +1580,6 @@ fn cat_fingerprint(
}
}
/// 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
}
fn split_address_book(book: &str) -> Vec<(&str, &str)> {
book.lines()
.collect::<Vec<&str>>()
@@ -1730,6 +1620,12 @@ impl RecentlySeenLoop {
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
type MyHeapElem = (Reverse<i64>, ContactId);
let now = SystemTime::now();
let now_ts = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
// Priority contains all recently seen sorted by the timestamp
// when they become not recently seen.
//
@@ -1740,7 +1636,7 @@ impl RecentlySeenLoop {
.query_map(
"SELECT id, last_seen FROM contacts
WHERE last_seen > ?",
(time() - SEEN_RECENTLY_SECONDS,),
(now_ts - SEEN_RECENTLY_SECONDS,),
|row| {
let contact_id: ContactId = row.get("id")?;
let last_seen: i64 = row.get("last_seen")?;
@@ -1756,7 +1652,6 @@ impl RecentlySeenLoop {
loop {
let now = SystemTime::now();
let (until, contact_id) =
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
(
@@ -1782,6 +1677,11 @@ impl RecentlySeenLoop {
// Timeout, notify about contact.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
unseen_queue.pop();
}
}
@@ -1799,7 +1699,10 @@ impl RecentlySeenLoop {
timestamp,
})) => {
// Received an interrupt.
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
if contact_id != ContactId::UNDEFINED {
unseen_queue
.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
}
}
}
} else {
@@ -1811,13 +1714,18 @@ impl RecentlySeenLoop {
// Event is already in the past.
if let Some(contact_id) = contact_id {
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
chatlist_events::emit_chatlist_item_changed_for_contact_chat(
&context,
*contact_id,
)
.await;
}
unseen_queue.pop();
}
}
}
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
pub(crate) fn try_interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.try_send(RecentlySeenInterrupt {
contact_id,
@@ -1826,6 +1734,17 @@ impl RecentlySeenLoop {
.ok();
}
#[cfg(test)]
pub(crate) async fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
self.interrupt_send
.send(RecentlySeenInterrupt {
contact_id,
timestamp,
})
.await
.unwrap();
}
pub(crate) fn abort(self) {
self.handle.abort();
}
@@ -1833,6 +1752,8 @@ impl RecentlySeenLoop {
#[cfg(test)]
mod tests {
use deltachat_contact_tools::may_be_valid_addr;
use super::*;
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
use crate::chatlist::Chatlist;
@@ -1972,18 +1893,6 @@ mod tests {
Ok(())
}
#[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(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_add_or_lookup() {
// add some contacts, this also tests add_address_book()
@@ -2807,6 +2716,44 @@ Hi."#;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_was_seen_recently_event() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let recently_seen_loop = RecentlySeenLoop::new(bob.ctx.clone());
let chat = bob.create_chat(&alice).await;
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
for _ in 0..2 {
let chat = alice.create_chat(&bob).await;
let sent_msg = alice.send_text(chat.id, "moin").await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(!contact.was_seen_recently());
bob.evtracker.clear_events();
bob.recv_msg(&sent_msg).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(contact.was_seen_recently());
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
.await;
recently_seen_loop
.interrupt(contact.id, contact.last_seen)
.await;
// Wait for `was_seen_recently()` to turn off.
bob.evtracker.clear_events();
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
assert!(!contact.was_seen_recently());
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
.await;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_verified_by_none() -> Result<()> {
let mut tcm = TestContextManager::new();

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