Compare commits

...

56 Commits

Author SHA1 Message Date
link2xt
dfffd90686 feat(sql): truncate WAL on stop_io() 2023-12-05 00:46:43 +00:00
link2xt
280f13b8cf fix: do not lock accounts.toml on iOS
This results in 0xdead10cc crashes on suspend.
iOS itself ensures that multiple instances of Delta Chat are not running.
2023-12-04 21:51:17 +00:00
link2xt
a96b44a482 fix: do not mark recipients as verified if there is no Chat-Verified header 2023-12-04 15:34:09 +00:00
link2xt
4286d248e9 feat: increase TCP timeouts from 30 to 60 seconds
GitHub Action tests sometimes fail with TCP connection
timeouts, especially for macOS.
2023-12-04 12:50:07 +00:00
dependabot[bot]
116537019b chore(deps): bump self_cell from 1.0.1 to 1.0.2 in /fuzz
Bumps [self_cell](https://github.com/Voultapher/self_cell) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/Voultapher/self_cell/releases)
- [Commits](https://github.com/Voultapher/self_cell/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-03 16:05:27 -03:00
iequidoo
8b37b8c1fd fix: Don't sort message creating a protected group over a protection message (#4963)
Otherwise it looks like the message creating a protected group is not verified. For this, use
`sent_timestamp` of the received message as an upper limit of the sort timestamp (`msgs.timestamp`)
of the protection message. As the protection message is added to the chat earlier, this way its
timestamp is always less or eq than the received message's timestamp.
2023-12-03 15:10:54 -03:00
iequidoo
63b4339ca0 test: Message order in a just created protected group on the second device (#4963)
Test that on the second device of a protected group creator the first message is
`SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group.
2023-12-03 15:10:54 -03:00
link2xt
fdd239f61f fix: narrow the scope of verification exception to 1:1 chats
Allowing outgoing unencrypted messages in groups with 2 members
breaks the test
`python/tests/test_0_complex_or_slow.py::test_verified_group_vs_delete_server_after`
2023-12-03 15:46:56 +00:00
link2xt
5ca5d95c5e refactor: call has_verified_encryption() in a single place
This centralizes all Securejoin/verification checks and updates in one
place right before add_parts() even before we assign the message to
the chat, so we can decouple chat logic from verification logic.
2023-12-03 15:46:56 +00:00
link2xt
3fcad50924 refactor: move to_ids.is_empty() check into mark_recepients_as_verified() 2023-12-03 15:46:56 +00:00
link2xt
8e40540d24 refactor: add debug assertion where we expect a 1:1 chat 2023-12-03 15:46:56 +00:00
link2xt
04d22bb84d refactor: remove chattype argument from has_verified_encryption() 2023-12-03 15:46:56 +00:00
link2xt
5415f1bfa1 docs: has_verified_encryption() does not check that all members are verified 2023-12-03 15:46:56 +00:00
link2xt
ff3bf4791a chore: update dependencies 2023-12-03 00:43:21 +00:00
dependabot[bot]
eebea216cb chore(cargo): bump testdir from 0.8.1 to 0.9.0
Bumps [testdir](https://github.com/flub/testdir) from 0.8.1 to 0.9.0.
- [Changelog](https://github.com/flub/testdir/blob/main/CHANGELOG.md)
- [Commits](https://github.com/flub/testdir/compare/v0.8.1...v0.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-02 15:50:51 +00:00
link2xt
fbcd7f46b8 chore(release): prepare for 1.131.9 2023-12-02 01:18:34 +00:00
iequidoo
846278b18e feat: chat::rename_ex: Don't send sync message if usual message is sent
It's not necessary and in other places like add_contact_to_chat_ex() sync messages are also sent
only if there are no system messages sent like MemberAddedToGroup.
2023-12-01 21:41:58 -03:00
iequidoo
2f2b1e18bf test: Split test_sync_alter_chat() into smaller tests 2023-12-01 21:41:58 -03:00
iequidoo
073c250fa4 refactor: Add test_utils::sync()
Add a function that pops a sync message from one Alice's device and receives it on another.
2023-12-01 21:41:58 -03:00
iequidoo
1f336f89a6 feat: Sync Config::Displayname across devices (#4893)
We already synchronise status/footer when we see a self-sent message with a Chat-Version
header. Would be nice to do the same for display name.

But let's do it the same way as for `Config::{MdnsEnabled,ShowEmails}`. Otherwise, if we sync the
display name using the "From" header, smth like `Param::StatusTimestamp` is needed then to reject
outdated display names. Also this timestamp needs to be updated when `Config::Displayname` is set
locally. Also this wouldn't work if system time isn't synchronised on devices. Also using multiple
approaches to sync different config values would lead to more code and bugs while having almost no
value -- using "From" only saves some bytes and allows to sync some things w/o the synchronisation
itself to be enabled. But the latter also can be a downside -- if it's usual synchonisation, you can
(potentially) disable it and share the same email account across people in some organisation
allowing them to have different display names. With using "From" for synchronisation such a
capability definitely requires a new config option.
2023-12-01 21:41:58 -03:00
iequidoo
a47fec7f6c feat: Sync Config::{MdnsEnabled,ShowEmails} across devices (#4954)
Motivation: Syncing these options will improve UX in very most cases and should be done. Other
candidates are less clear or are advanced options, we can reconsider that at some point later.

Approach:
- Sync options one-by-one when the corresponding option is set (even if to the same value).
- Don't sync when an option is reset to a default as defaults may differ across client versions.
- Check on both sides that the option should be synced so that if there are different client
  versions, the synchronisation of the 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 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.
- Don't sync `SyncMsgs` itself.
2023-12-01 21:41:58 -03:00
iequidoo
084434d3b4 feat: receive_imf_inner: Add missing initialisation of ReceivedMsg::from_is_signed 2023-12-01 21:41:58 -03:00
iequidoo
ebfbc11973 feat: Don't affect MimeMessage with "From" and secured headers from encrypted unsigned messages
If a message is encrypted, but unsigned:
- Don't set `MimeMessage::from_is_signed`.
- Remove "secure-join-fingerprint" and "chat-verified" headers from `MimeMessage`.
- Minor: Preserve "Subject" from the unencrypted top level if there's no "Subject" in the encrypted
  part, this message is displayed w/o a padlock anyway.

Apparently it didn't lead to any vulnerabilities because there are checks for
`MimeMessage::signatures.is_empty()` in all necessary places, but still the code looked dangerous,
especially because `from_is_singed` var name didn't correspond to its actual value (it was rather
`from_is_encrypted_maybe_signed`).
2023-12-01 19:06:11 -03:00
link2xt
9cc9579b2d feat: remove receiver limit on .xdc size
If we have downloaded the file anyway,
might as well allow to open it.
2023-12-01 15:53:38 +00:00
link2xt
7beccd9dbc refactor: better error context in send_webxdc_status_update_struct() 2023-12-01 15:19:23 +00:00
link2xt
0e195bc7a2 fix: lock the database when INSERTing a webxdc update
`query_row_optional` does not hold the write lock
and may fail with "database is locked" error
or cause the other task such as SMTP loop to fail.
2023-12-01 15:19:23 +00:00
link2xt
f89efd5fce test: test inserting lots of webxdc updates
Currently this leads to
DEBUG    root:rpc.py:136 account_id=1 got an event {'kind': 'Warning', 'msg': 'src/scheduler.rs:711: send_smtp_messages failed: failed to send message: failed to update retries count: database is locked: Error code 5: The database file is locked'}
and
FAILED tests/test_webxdc.py::test_webxdc_insert_lots_of_updates - deltachat_rpc_client.rpc.JsonRpcError: {'code': -1, 'message': 'database is locked\n\nCaused by:\n    Error code 5: The database file is locked'}
2023-12-01 15:19:23 +00:00
link2xt
48d278fca9 chore: update dependencies 2023-12-01 02:41:48 +00:00
link2xt
c84effdaa1 refactor: add more error context to send_webxdc_status_update()
This is a follow-up to b9fa05c3bb
2023-12-01 02:32:21 +00:00
link2xt
e9601ef138 test: make Result-returning tests produce a line number
Without this change
when the test returns a `Result`, `cargo test` does not show
the line number.

To make the tests as easy to debug as the panicking tests,
enable `backtrace` feature on `anyhow` and add debug information
to add source line numbers to backtraces.

Now running `RUST_BACKTRACE=1 cargo test` produces backtraces
with the line numbers. For example:

Error: near ",": syntax error in SELECT COUNT(,,*) FROM msgs_status_updates; at offset 13

Caused by:
    Error code 1: SQL error or missing database

Stack backtrace:
   0: <core::result::Result<T,F> as core::ops::try_trait::FromResidual<core::result::Result<core::convert::Infallible,E>>>::from_residual
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/result.rs:1962:27
   1: deltachat::sql::Sql::query_row::{{closure}}::{{closure}}
             at ./src/sql.rs:466:23
   2: deltachat::sql::Sql::call::{{closure}}::{{closure}}
             at ./src/sql.rs:379:55
   3: tokio::runtime::context::runtime_mt::exit_runtime
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/context/runtime_mt.rs:35:5
   4: tokio::runtime::scheduler::multi_thread::worker::block_in_place
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/scheduler/multi_thread/worker.rs:438:9
   5: tokio::runtime::scheduler::block_in_place::block_in_place
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/scheduler/block_in_place.rs:20:5
   6: tokio::task::blocking::block_in_place
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/task/blocking.rs:78:9
   7: deltachat::sql::Sql::call::{{closure}}
             at ./src/sql.rs:379:19
   8: deltachat::sql::Sql::query_row::{{closure}}
             at ./src/sql.rs:469:10
   9: deltachat::sql::Sql::count::{{closure}}
             at ./src/sql.rs:443:76
  10: deltachat::webxdc::tests::change_logging_webxdc::{{closure}}
             at ./src/webxdc.rs:2644:18
  11: <core::pin::Pin<P> as core::future::future::Future>::poll
             at /rustc/79e9716c980570bfd1f666e3b16ac583f0168962/library/core/src/future/future.rs:125:9
  12: tokio::runtime::park::CachedParkThread::block_on::{{closure}}
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/park.rs:282:63
  13: tokio::runtime::coop::with_budget
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/coop.rs:107:5
      tokio::runtime::coop::budget
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/coop.rs:73:5
      tokio::runtime::park::CachedParkThread::block_on
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/park.rs:282:31
  14: tokio::runtime::context::blocking::BlockingRegionGuard::block_on
             at /home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.34.0/src/runtime/context/blocking.rs:66:9
...

Line 10 of the backtrace contains the line number in the test (2644).
2023-11-30 22:27:38 +00:00
iequidoo
44c5cd5526 feat: Ratelimit IMAP connections (#4940)
Limit the number of IMAP connections to 1 per minute regardless of the reason of reconnection, but
allow one immediate retry. This is more reliable than ratelimiting only in error conditions because
ratelimiting can't be skipped by mistake. Anyway connections shouldn't be frequent in normal
operation mode.
2023-11-30 19:22:01 -03:00
link2xt
1c9662a8f2 refactor: rename min_verified into verified 2023-11-30 12:04:03 +00:00
link2xt
5d08b2ce33 refactor: remove unused PeerstateVerifiedStatus 2023-11-30 12:04:03 +00:00
link2xt
bb9d7d7ef3 feat: send Chat-Verified headers in 1:1 chats
Chat-Verified is going to be useful to upgrade one-way verification
to bidirectional verification.
2023-11-30 12:04:03 +00:00
link2xt
766bb5c8aa refactor: factor securejoin processing out of add_parts 2023-11-30 12:04:03 +00:00
link2xt
84144659cf refactor: remove {vc-contact-confirm,vg-member-added}-received steps 2023-11-30 12:04:03 +00:00
link2xt
1394137436 refactor: make min_verified a boolean
We either need a securejoin or autocrypt key,
there are no intermediate states.
2023-11-30 12:04:03 +00:00
link2xt
998614b923 api: make Contact.is_verified() return bool 2023-11-30 12:04:03 +00:00
B. Petersen
5b346397b8 api: deprecate CFFI APIs dc_send_reaction(), dc_get_msg_reactions(), dc_reactions_get_contacts(), dc_reactions_get_by_contact_id(), dc_reactions_unref and dc_reactions_t
this is now done with jsonrpc via
`dc_jsonrpc_request()` or `dc_jsonrpc_blocking_call()`
using the methods `send_reaction` and `get_message_reactions`
2023-11-29 11:29:29 +01:00
B. Petersen
1f99269002 api: remove dc_get_http_response(), dc_http_response_get_mimetype(), dc_http_response_get_encoding(), dc_http_response_get_blob(), dc_http_response_get_size(), dc_http_response_unref() and dc_http_response_t from cffi
this is now done with jsonrpc via
`dc_jsonrpc_request()` or `dc_jsonrpc_blocking_call()`
using the method `get_http_response`
2023-11-29 11:29:29 +01:00
iequidoo
160cbe8125 fix: Use keyring with all private keys when decrypting a message (#5046)
Before a keyring with the only default key was used, i.e. the key used for signing and encrypting to
self.
2023-11-29 02:20:19 -03:00
link2xt
b9fa05c3bb refactor: improve logging of send_webxdc_status_update errors
send_webxdc_status_update JSON-RPC call
and corresponding Rust call sometimes fail in CI with
---
database is locked

Caused by:
    Error code 5: The database file is locked
---

Adding more context to send_webxdc_status_update() errors
to better localize the error origin.
2023-11-28 22:48:55 +00:00
link2xt
4287a4d3ad refactor: factor out insert_tombstone 2023-11-28 21:57:41 +00:00
link2xt
37d2aafb26 fix: return correct MsgId for malformed message tombstone
.execute() returns the number of affected rows,
in this case it is always 1 and MsgId(1) is returned
instead of the actual tombstone row ID.
2023-11-28 21:57:41 +00:00
link2xt
4332170691 ci: add exception for RUSTSEC-2023-0071 to cargo-deny config
See
<https://rustsec.org/advisories/RUSTSEC-2023-0071>
and discussion at
<https://github.com/RustCrypto/RSA/issues/19>
for details.
2023-11-28 16:26:11 +00:00
link2xt
9a7c0f4737 chore: update OpenSSL 2023-11-28 15:05:34 +00:00
link2xt
9e7e172a7b build: switch from fork of iroh to iroh 0.4.2 pre-release 2023-11-28 02:59:42 +00:00
link2xt
71fbaf572a chore(release): prepare for 1.131.8 2023-11-28 00:01:17 +00:00
link2xt
2ab29e5bfa fix: allow IMAP servers not returning UIDNEXT on SELECT and STATUS 2023-11-27 23:50:43 +00:00
link2xt
85f8f910b9 chore: update wasm-bindgen from 0.2.88 to 0.2.89
0.2.88 is yanked: https://github.com/rustwasm/wasm-bindgen/issues/3685
2023-11-27 21:41:23 +00:00
link2xt
b779d08d7f test: check that receive_status_update has forward compatibility
This ensures old version of Delta Chat will be fine with a new "uid" field.
2023-11-27 13:49:41 +00:00
link2xt
3b5634f14b fix: do not emit events about webxdc update events logged into debug log webxdc 2023-11-27 13:49:41 +00:00
link2xt
f91ba357cf feat(webxdc): add unique IDs to status updates sent outside
This allows for deduplication
if status updates are sent over multiple transports.
2023-11-27 13:49:41 +00:00
Hocuri
616faff96b fix: Use the correct securejoin strings used in the UI, remove old TODO (#5047) 2023-11-26 15:54:11 +01:00
link2xt
5e6869403e chore(release): prepare for 1.131.7 2023-11-24 18:26:25 +00:00
link2xt
7ff7d82959 Revert "fix: check UIDNEXT with a STATUS command before going IDLE"
This reverts commit 2e50abedaa.

STATUS is broken on mail.163.com.
It returns `STATUS "INBOX" ()` reply
when `STATUS "INBOX" (UIDNEXT)` is requested.
2023-11-24 18:19:02 +00:00
51 changed files with 1861 additions and 1129 deletions

View File

@@ -1,5 +1,61 @@
# Changelog
## [1.131.9] - 2023-12-02
### API-Changes
- Remove `dc_get_http_response()`, `dc_http_response_get_mimetype()`, `dc_http_response_get_encoding()`, `dc_http_response_get_blob()`, `dc_http_response_get_size()`, `dc_http_response_unref()` and `dc_http_response_t` from cffi.
- Deprecate CFFI APIs `dc_send_reaction()`, `dc_get_msg_reactions()`, `dc_reactions_get_contacts()`, `dc_reactions_get_by_contact_id()`, `dc_reactions_unref` and `dc_reactions_t`.
- Make `Contact.is_verified()` return bool.
### Build system
- Switch from fork of iroh to iroh 0.4.2 pre-release.
### Features / Changes
- Send `Chat-Verified` headers in 1:1 chats.
- Ratelimit IMAP connections ([#4940](https://github.com/deltachat/deltachat-core-rust/pull/4940)).
- Remove receiver limit on `.xdc` size.
- Don't affect MimeMessage with "From" and secured headers from encrypted unsigned messages.
- Sync `Config::{MdnsEnabled,ShowEmails}` across devices ([#4954](https://github.com/deltachat/deltachat-core-rust/pull/4954)).
- Sync `Config::Displayname` across devices ([#4893](https://github.com/deltachat/deltachat-core-rust/pull/4893)).
- `Chat::rename_ex`: Don't send sync message if usual message is sent.
### Fixes
- Lock the database when INSERTing a webxdc update, avoid "Database is locked" errors.
- Use keyring with all private keys when decrypting a message ([#5046](https://github.com/deltachat/deltachat-core-rust/pull/5046)).
### Tests
- Make Result-returning tests produce a line number.
- Add `test_utils::sync()`.
- Test inserting lots of webxdc updates.
- Split `test_sync_alter_chat()` into smaller tests.
## [1.131.8] - 2023-11-27
### Features / Changes
- webxdc: Add unique IDs to status updates sent outside and deduplicate based on IDs.
### Fixes
- Allow IMAP servers not returning UIDNEXT on SELECT and STATUS such as mail.163.com.
- Use the correct securejoin strings used in the UI, remove old TODO ([#5047](https://github.com/deltachat/deltachat-core-rust/pull/5047)).
- Do not emit events about webxdc update events logged into debug log webxdc.
### Tests
- Check that `receive_status_update` has forward compatibility and unique webxdc IDs will be ignored by previous Delta Chat versions.
## [1.131.7] - 2023-11-24
### Fixes
- Revert "fix: check UIDNEXT with a STATUS command before going IDLE". This attempts to fix mail.163.com which has broken STATUS command.
## [1.131.6] - 2023-11-21
### Fixes
@@ -3263,3 +3319,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4
[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5
[1.131.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.5...v1.131.6
[1.131.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.6...v1.131.7
[1.131.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.7...v1.131.8
[1.131.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.8...v1.131.9

View File

@@ -91,6 +91,12 @@ All errors should be handled in one of these ways:
- With `.log_err().ok()`.
- Bubbled up with `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.

482
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.131.6"
version = "1.131.9"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"
@@ -11,6 +11,10 @@ panic = 'abort'
opt-level = 1
[profile.test]
# Make anyhow `backtrace` feature useful.
# With `debug = 0` there are no line numbers in the backtrace
# produced with RUST_BACKTRACE=1.
debug = 1
opt-level = 0
# Always optimize dependencies.
@@ -26,6 +30,9 @@ 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" }
format-flowed = { path = "./format-flowed" }
@@ -44,15 +51,15 @@ chrono = { version = "0.4", default-features=false, features = ["clock", "std"]
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
fd-lock = "3.0.11"
fast-socks5 = "0.9"
fd-lock = "4"
futures = "0.3"
futures-lite = "2.0.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"] }
iroh = { git = "https://github.com/deltachat/iroh", branch = "0.4-update-quic", default-features = false }
iroh = { git = "https://github.com/n0-computer/iroh", branch = "maint-0.4", default-features = false }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
@@ -96,13 +103,14 @@ uuid = { version = "1", features = ["serde", "v4"] }
[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"
log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.8.0"
testdir = "0.9.0"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.131.6"
version = "1.131.9"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -25,7 +25,6 @@ typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
typedef struct _dc_http_response dc_http_response_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -1112,6 +1111,7 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* 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.
@@ -1124,6 +1124,7 @@ uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reactio
/**
* 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.
@@ -5185,72 +5186,6 @@ int dc_provider_get_status (const dc_provider_t* prov
void dc_provider_unref (dc_provider_t* provider);
/**
* Return an HTTP(S) GET response.
* This function can be used to download remote content for HTML emails.
*
* @memberof dc_context_t
* @param context The context object to take proxy settings from.
* @param url HTTP or HTTPS URL.
* @return The response must be released using dc_http_response_unref() after usage.
* NULL is returned on errors.
*/
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
/**
* @class dc_http_response_t
*
* An object containing an HTTP(S) GET response.
* Created by dc_get_http_response().
*/
/**
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
/**
* Returns HTTP response encoding, e.g. "utf-8".
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
*/
char* dc_http_response_get_encoding (const dc_http_response_t* response);
/**
* Returns HTTP response contents.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
*/
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
/**
* Returns HTTP response content size.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
* @return The blob size.
*/
size_t dc_http_response_get_size (const dc_http_response_t* response);
/**
* Free an HTTP response object.
*
* @memberof dc_http_response_t
* @param response HTTP response as returned by dc_get_http_response().
*/
void dc_http_response_unref (const dc_http_response_t* response);
/**
* @class dc_lot_t
*
@@ -5350,6 +5285,7 @@ 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.
*/
@@ -5357,6 +5293,7 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* 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
@@ -5368,6 +5305,7 @@ 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.
@@ -5383,6 +5321,7 @@ char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32
*
* 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.
@@ -6621,7 +6560,7 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the name of the verified contact
#define DC_STR_CONTACT_VERIFIED 35
/// "Cannot verify %1$s."
/// "Cannot establish guaranteed end-to-end encryption with %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact that cannot be verified

View File

@@ -31,7 +31,6 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::preconfigure_keypair;
use deltachat::message::MsgId;
use deltachat::net::read_url_blob;
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;
@@ -4113,10 +4112,17 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
let ffi_contact = &*contact;
let ctx = &*ffi_contact.context;
block_on(ffi_contact.contact.is_verified(ctx))
if block_on(ffi_contact.contact.is_verified(ctx))
.context("is_verified failed")
.log_err(ctx)
.unwrap_or_default() as libc::c_int
.unwrap_or_default()
{
// Return value is essentially a boolean,
// but we return 2 for true for backwards compatibility.
2
} else {
0
}
}
#[no_mangle]
@@ -4590,96 +4596,6 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
// this may change once we start localizing string.
}
// dc_http_response_t
pub type dc_http_response_t = net::HttpResponse;
#[no_mangle]
pub unsafe extern "C" fn dc_get_http_response(
context: *const dc_context_t,
url: *const libc::c_char,
) -> *mut dc_http_response_t {
if context.is_null() || url.is_null() {
eprintln!("ignoring careless call to dc_get_http_response()");
return ptr::null_mut();
}
let context = &*context;
let url = to_string_lossy(url);
if let Ok(response) = block_on(read_url_blob(context, &url))
.context("read_url_blob")
.log_err(context)
{
Box::into_raw(Box::new(response))
} else {
ptr::null_mut()
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_mimetype(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
return ptr::null_mut();
}
let response = &*response;
response.mimetype.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_encoding(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
return ptr::null_mut();
}
let response = &*response;
response.encoding.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_blob(
response: *const dc_http_response_t,
) -> *mut libc::c_char {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_blob()");
return ptr::null_mut();
}
let response = &*response;
let blob_len = response.blob.len();
let ptr = libc::malloc(blob_len);
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
ptr as *mut libc::c_char
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_get_size(
response: *const dc_http_response_t,
) -> libc::size_t {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_get_size()");
return 0;
}
let response = &*response;
response.blob.len()
}
#[no_mangle]
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
if response.is_null() {
eprintln!("ignoring careless call to dc_http_response_unref()");
return;
}
drop(Box::from_raw(response));
}
// -- Accounts
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.131.6"
version = "1.131.9"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -30,7 +30,7 @@ walkdir = "2.3.3"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.20", optional = true, features = ["ws"] }
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]

View File

@@ -1,5 +1,4 @@
use anyhow::Result;
use deltachat::contact::VerifiedStatus;
use deltachat::context::Context;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -57,7 +56,7 @@ impl ContactObject {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None,
};
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
let is_verified = contact.is_verified(context).await?;
let is_profile_verified = contact.is_profile_verified(context).await?;
let verifier_id = contact

View File

@@ -33,10 +33,8 @@ async fn main() -> Result<(), std::io::Error> {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
log::info!("JSON-RPC WebSocket server listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
Ok(())
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.131.6"
version = "1.131.9"
license = "MPL-2.0"
edition = "2021"

View File

@@ -284,13 +284,8 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
let verified_state = contact.is_verified(context).await?;
let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
} else {
""
}
let verified_str = if contact.is_verified(context).await? {
""
} else {
""
};

View File

@@ -429,3 +429,51 @@ def test_aeap_flow_verified(acfactory):
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# Bob verifies Alice.
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code, _svg = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
assert not bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Autocrypt group does not propagate verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
assert bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert carol_contact_alice_snapshot.is_verified

View File

@@ -43,3 +43,15 @@ def test_webxdc(acfactory) -> None:
assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.131.6"
version = "1.131.9"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"

View File

@@ -3,6 +3,13 @@ unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
# Timing attack on RSA.
# Delta Chat does not use RSA for new keys
# and this requires precise measurement of the decryption time by the attacker.
# There is no fix at the time of writing this (2023-11-28).
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
"RUSTSEC-2023-0071",
]
[bans]
@@ -26,7 +33,13 @@ skip = [
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ 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" },
{ name = "quick-error", version = "<2.0" },
@@ -47,12 +60,16 @@ skip = [
{ name = "time", version = "<0.3" },
{ name = "untrusted", version = "0.7.1" },
{ name = "wasi", version = "<0.11" },
{ name = "windows_aarch64_msvc", version = "<0.48" },
{ name = "windows_i686_gnu", version = "<0.48" },
{ name = "windows_i686_msvc", version = "<0.48" },
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
{ name = "windows_aarch64_msvc", version = "<0.52" },
{ name = "windows_i686_gnu", version = "<0.52" },
{ name = "windows_i686_msvc", version = "<0.52" },
{ name = "windows-sys", version = "<0.52" },
{ name = "windows-targets", version = "<0.52" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
{ name = "windows_x86_64_gnu", version = "<0.52" },
{ name = "windows_x86_64_msvc", version = "<0.52" },
]
@@ -84,4 +101,6 @@ license-files = [
github = [
"async-email",
"deltachat",
"djc",
"n0-computer", # iroh
]

486
fuzz/Cargo.lock generated
View File

@@ -47,7 +47,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
"once_cell",
"version_check",
]
@@ -158,10 +158,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833"
dependencies = [
"concurrent-queue",
"event-listener",
"event-listener 2.5.3",
"futures-core",
]
[[package]]
name = "async-channel"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ca33f4bc4ed1babef42cad36cc1f51fa88be00420404e5b1e80ab1b18f7678c"
dependencies = [
"concurrent-queue",
"event-listener 4.0.0",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-compression"
version = "0.3.15"
@@ -181,7 +194,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8"
dependencies = [
"async-channel",
"async-channel 1.8.0",
"base64 0.21.0",
"bytes",
"chrono",
@@ -484,9 +497,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "3.3.4"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -495,9 +508,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "2.3.4"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -568,9 +581,12 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.78"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfb-mode"
@@ -646,9 +662,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
[[package]]
name = "concurrent-queue"
version = "2.0.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400"
dependencies = [
"crossbeam-utils",
]
@@ -906,10 +922,10 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.123.0"
version = "1.131.9"
dependencies = [
"anyhow",
"async-channel",
"async-channel 2.1.1",
"async-imap",
"async-native-tls",
"async-smtp",
@@ -928,6 +944,7 @@ dependencies = [
"futures",
"futures-lite",
"hex",
"hickory-resolver",
"humansize",
"image",
"iroh",
@@ -943,6 +960,7 @@ dependencies = [
"parking_lot",
"percent-encoding",
"pgp",
"pin-project",
"qrcodegen",
"quick-xml",
"rand 0.8.5",
@@ -968,7 +986,6 @@ dependencies = [
"tokio-tar",
"tokio-util",
"toml",
"trust-dns-resolver",
"url",
"uuid 1.2.2",
]
@@ -1434,6 +1451,12 @@ dependencies = [
"syn 1.0.107",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.2.8"
@@ -1482,10 +1505,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
name = "event-listener"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3"
dependencies = [
"event-listener 4.0.0",
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
@@ -1495,11 +1539,12 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fast-socks5"
version = "0.8.1"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2687b5a6108f18ba8621e0e618a3be1dcc2768632dad24b7cea1f87975375a9"
checksum = "d449e348301d5fb9b0e5781510d8235ffe3bbac3286bd305462736a9e7043039"
dependencies = [
"anyhow",
"async-trait",
"log",
"thiserror",
"tokio",
@@ -1525,14 +1570,20 @@ dependencies = [
]
[[package]]
name = "fd-lock"
version = "3.0.13"
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fd-lock"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93f7a0db71c99f68398f80653ed05afb0b00e062e1a20c7ff849c4edfabbbcc"
dependencies = [
"cfg-if",
"rustix 0.38.14",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1593,7 +1644,7 @@ dependencies = [
"futures-sink",
"nanorand",
"pin-project",
"spin 0.9.6",
"spin 0.9.8",
]
[[package]]
@@ -1680,17 +1731,16 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
[[package]]
name = "futures-lite"
version = "1.13.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
checksum = "d3831c2651acb5177cbd83943f3d9c8912c5ad03c76afcc0e9511ba568ec5ebb"
dependencies = [
"fastrand",
"fastrand 2.0.1",
"futures-core",
"futures-io",
"memchr",
"parking",
"pin-project-lite",
"waker-fn",
]
[[package]]
@@ -1758,9 +1808,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.8"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
dependencies = [
"cfg-if",
"js-sys",
@@ -1819,7 +1869,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.2",
"slab",
"tokio",
"tokio-util",
@@ -1835,13 +1885,19 @@ dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
name = "hashlink"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -1862,6 +1918,51 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-proto"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091a6fbccf4860009355e3efc52ff4acf37a63489aad7435372d44ceeb6fbbcf"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.8.5",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b8f021164e6a984c9030023544c57789c51760065cd510572fedcfb04164e8"
dependencies = [
"cfg-if",
"futures-util",
"hickory-proto",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "hkdf"
version = "0.12.3"
@@ -2052,7 +2153,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
]
[[package]]
@@ -2103,9 +2214,8 @@ checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e"
[[package]]
name = "iroh"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4fb9858c8cd3dd924a5da5bc511363845a9bcfdfac066bb2ef8454eb6111546"
version = "0.4.2"
source = "git+https://github.com/n0-computer/iroh?branch=maint-0.4#9881b7886235035a1124e4371f7a4cd59379e51b"
dependencies = [
"abao",
"anyhow",
@@ -2126,8 +2236,9 @@ dependencies = [
"quinn",
"rand 0.7.3",
"rcgen",
"ring",
"ring 0.16.20",
"rustls",
"rustls-webpki",
"serde",
"serde-error",
"ssh-key",
@@ -2140,7 +2251,6 @@ dependencies = [
"tracing-futures",
"tracing-subscriber",
"walkdir",
"webpki",
"x509-parser",
"zeroize",
]
@@ -2219,9 +2329,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.148"
version = "0.2.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
[[package]]
name = "libm"
@@ -2237,9 +2347,9 @@ checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "libsqlite3-sys"
version = "0.26.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"openssl-sys",
@@ -2404,7 +2514,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
]
[[package]]
@@ -2484,15 +2594,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -2638,11 +2739,11 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.55"
version = "0.10.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
checksum = "79a4c6c3a2b158f7f8f2a2fc5a969fa3a068df6fc9dbb4a43845436e3af7c800"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.0",
"cfg-if",
"foreign-types",
"libc",
@@ -2670,18 +2771,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.25.0+1.1.1t"
version = "300.1.6+3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3173cd3626c43e3854b1b727422a276e568d9ec5fe8cec197822cf52cfb743d6"
checksum = "439fac53e092cd7442a3660c85dde4643ab3b5bd39040912388dcdabf6b88085"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.90"
version = "0.9.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
checksum = "3812c071ba60da8b5677cc12bcb1d42989a65553772897a7e0355545a819838f"
dependencies = [
"cc",
"libc",
@@ -2754,9 +2855,9 @@ dependencies = [
[[package]]
name = "parking"
version = "2.0.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
[[package]]
name = "parking_lot"
@@ -3062,11 +3163,12 @@ checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
[[package]]
name = "quic-rpc"
version = "0.5.2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d453504fc3e456160ae3b9ebe4d83c1f6477af167aa9b67e2d7bf11a096f179d"
checksum = "6d60c2fc2390baad4b9d41ae9957ae88c3095496f88e252ef50722df8b5b78d7"
dependencies = [
"bincode",
"educe",
"flume",
"futures",
"pin-project",
@@ -3086,18 +3188,18 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-xml"
version = "0.30.0"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.9.3"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445cbfe2382fa023c4f2f3c7e1c95c03dcc1df2bf23cebcb2b13e1402c4394d1"
checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75"
dependencies = [
"bytes",
"pin-project-lite",
@@ -3108,18 +3210,17 @@ dependencies = [
"thiserror",
"tokio",
"tracing",
"webpki",
]
[[package]]
name = "quinn-proto"
version = "0.9.5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989"
checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a"
dependencies = [
"bytes",
"rand 0.8.5",
"ring",
"ring 0.16.20",
"rustc-hash",
"rustls",
"rustls-native-certs",
@@ -3127,20 +3228,19 @@ dependencies = [
"thiserror",
"tinyvec",
"tracing",
"webpki",
]
[[package]]
name = "quinn-udp"
version = "0.3.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7"
dependencies = [
"bytes",
"libc",
"quinn-proto",
"socket2 0.4.7",
"socket2 0.5.4",
"tracing",
"windows-sys 0.42.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -3217,7 +3317,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
]
[[package]]
@@ -3240,7 +3340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b"
dependencies = [
"pem",
"ring",
"ring 0.16.20",
"time 0.3.20",
"yasna",
]
@@ -3260,7 +3360,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
"redox_syscall",
"thiserror",
]
@@ -3387,11 +3487,25 @@ dependencies = [
"libc",
"once_cell",
"spin 0.5.2",
"untrusted",
"untrusted 0.7.1",
"web-sys",
"winapi",
]
[[package]]
name = "ring"
version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "684d5e6e18f669ccebf64a92236bb7db9a34f07be010e3627368182027180866"
dependencies = [
"cc",
"getrandom 0.2.11",
"libc",
"spin 0.9.8",
"untrusted 0.9.0",
"windows-sys 0.48.0",
]
[[package]]
name = "ripemd"
version = "0.1.3"
@@ -3445,9 +3559,9 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.29.0"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"bitflags 2.4.0",
"fallible-iterator",
@@ -3522,13 +3636,13 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.20.8"
version = "0.21.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9"
dependencies = [
"ring",
"ring 0.17.6",
"rustls-webpki",
"sct",
"webpki",
]
[[package]]
@@ -3552,6 +3666,16 @@ dependencies = [
"base64 0.21.0",
]
[[package]]
name = "rustls-webpki"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.17.6",
"untrusted 0.9.0",
]
[[package]]
name = "rustversion"
version = "1.0.11"
@@ -3617,8 +3741,8 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
dependencies = [
"ring",
"untrusted",
"ring 0.16.20",
"untrusted 0.7.1",
]
[[package]]
@@ -3674,9 +3798,9 @@ dependencies = [
[[package]]
name = "self_cell"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6"
[[package]]
name = "semver"
@@ -3735,9 +3859,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.1"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
dependencies = [
"serde",
]
@@ -3897,9 +4021,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.6"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
@@ -3959,7 +4083,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b"
dependencies = [
"async-channel",
"async-channel 1.8.0",
"cfg-if",
"futures-core",
"pin-project-lite",
@@ -4064,7 +4188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
dependencies = [
"cfg-if",
"fastrand",
"fastrand 1.8.0",
"redox_syscall",
"rustix 0.36.7",
"windows-sys 0.42.0",
@@ -4280,9 +4404,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.2"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
@@ -4292,24 +4416,24 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.1"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.3"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap",
"nom8",
"indexmap 2.1.0",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
@@ -4325,6 +4449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -4390,52 +4515,6 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "trust-dns-proto"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69"
dependencies = [
"async-trait",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"idna",
"ipnet",
"once_cell",
"rand 0.8.5",
"smallvec",
"thiserror",
"tinyvec",
"tokio",
"tracing",
"url",
]
[[package]]
name = "trust-dns-resolver"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f"
dependencies = [
"cfg-if",
"futures-util",
"ipconfig",
"lru-cache",
"once_cell",
"parking_lot",
"rand 0.8.5",
"resolv-conf",
"smallvec",
"thiserror",
"tokio",
"tracing",
"trust-dns-proto",
]
[[package]]
name = "try-lock"
version = "0.2.3"
@@ -4475,7 +4554,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
"regex",
]
@@ -4506,6 +4585,12 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.4.1"
@@ -4523,7 +4608,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
]
[[package]]
@@ -4532,7 +4617,7 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
dependencies = [
"getrandom 0.2.8",
"getrandom 0.2.11",
"serde",
]
@@ -4554,12 +4639,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "waker-fn"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "walkdir"
version = "2.3.3"
@@ -4674,16 +4753,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.22.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "weezl"
version = "0.1.7"
@@ -4774,7 +4843,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
@@ -4792,6 +4870,21 @@ dependencies = [
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
@@ -4804,6 +4897,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.32.0"
@@ -4828,6 +4927,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.32.0"
@@ -4852,6 +4957,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.32.0"
@@ -4876,6 +4987,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.32.0"
@@ -4900,6 +5017,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
@@ -4912,6 +5035,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.32.0"
@@ -4936,6 +5065,21 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.10.1"

View File

@@ -56,5 +56,5 @@
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.131.6"
"version": "1.131.9"
}

View File

@@ -156,6 +156,8 @@ def test_markseen_invalid_message_ids(acfactory):
chat = contact1.create_chat()
chat.send_text("one message")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
# Skip configuration-related warnings, but not errors.
ac1._evtracker.ensure_event_not_queued("DC_EVENT_ERROR")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")

View File

@@ -1 +1 @@
2023-11-21
2023-12-02

View File

@@ -8,11 +8,14 @@ use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::{sleep, Duration};
use uuid::Uuid;
#[cfg(not(target_os = "ios"))]
use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};
use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::stock_str::StockStrings;
@@ -303,6 +306,7 @@ impl Accounts {
const CONFIG_NAME: &str = "accounts.toml";
/// Lockfile name.
#[cfg(not(target_os = "ios"))]
const LOCKFILE_NAME: &str = "accounts.lock";
/// Database file name.
@@ -338,22 +342,16 @@ impl Drop for Config {
}
impl Config {
/// Creates a new Config for `file`, but doesn't open/sync it.
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
let dir = file.parent().context("Cannot get config file directory")?;
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
if !lock {
let cfg = Self {
file,
inner,
lock_task: None,
};
return Ok(cfg);
}
#[cfg(target_os = "ios")]
async fn create_lock_task(_dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
// Do not lock accounts.toml on iOS.
// This results in 0xdead10cc crashes on suspend.
// iOS itself ensures that multiple instances of Delta Chat are not running.
Ok(None)
}
#[cfg(not(target_os = "ios"))]
async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
let lockfile = dir.join(LOCKFILE_NAME);
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
let (locked_tx, locked_rx) = oneshot::channel();
@@ -384,12 +382,32 @@ impl Config {
rx.await?;
Ok(())
});
locked_rx.await?;
Ok(Some(lock_task))
}
/// Creates a new Config for `file`, but doesn't open/sync it.
async fn new_nosync(file: PathBuf, lock: bool) -> Result<Self> {
let dir = file.parent().context("Cannot get config file directory")?;
let inner = InnerConfig {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
};
if !lock {
let cfg = Self {
file,
inner,
lock_task: None,
};
return Ok(cfg);
}
let lock_task = Self::create_lock_task(dir.to_path_buf()).await?;
let cfg = Self {
file,
inner,
lock_task: Some(lock_task),
lock_task,
};
locked_rx.await?;
Ok(cfg)
}

View File

@@ -1,5 +1,6 @@
//! # Chat module.
use std::cmp;
use std::collections::{HashMap, HashSet};
use std::convert::{TryFrom, TryInto};
use std::fmt;
@@ -21,7 +22,7 @@ use crate::constants::{
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, VerifiedStatus};
use crate::contact::{self, Contact, ContactAddress, ContactId, Origin};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc;
use crate::download::DownloadState;
@@ -34,7 +35,7 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::peerstate::Peerstate;
use crate::receive_imf::ReceivedMsg;
use crate::smtp::send_msg_to_smtp;
use crate::sql;
@@ -289,6 +290,7 @@ impl ChatId {
/// Create a group or mailinglist raw database record with the given parameters.
/// The function does not add SELF nor checks if the record already exists.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn create_multiuser_record(
context: &Context,
chattype: Chattype,
@@ -297,9 +299,10 @@ impl ChatId {
create_blocked: Blocked,
create_protected: ProtectionStatus,
param: Option<String>,
timestamp: i64,
) -> Result<Self> {
let grpname = strip_rtlo_characters(grpname);
let smeared_time = create_smeared_timestamp(context);
let timestamp = cmp::min(timestamp, smeared_time(context));
let row_id =
context.sql.insert(
"INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);",
@@ -308,7 +311,7 @@ impl ChatId {
&grpname,
grpid,
create_blocked,
smeared_time,
timestamp,
create_protected,
param.unwrap_or_default(),
),
@@ -318,7 +321,7 @@ impl ChatId {
if create_protected == ProtectionStatus::Protected {
chat_id
.add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time)
.add_protection_msg(context, ProtectionStatus::Protected, None, timestamp)
.await?;
}
@@ -503,7 +506,7 @@ impl ChatId {
let contact_ids = get_chat_contacts(context, self).await?;
for contact_id in contact_ids {
let contact = Contact::get_by_id(context, contact_id).await?;
if contact.is_verified(context).await? != VerifiedStatus::BidirectVerified {
if !contact.is_verified(context).await? {
bail!("{} is not verified.", contact.get_display_name());
}
}
@@ -1202,11 +1205,7 @@ impl ChatId {
let peerstate = Peerstate::from_addr(context, addr).await?;
match peerstate
.filter(|peerstate| {
peerstate
.peek_key(PeerstateVerifiedStatus::Unverified)
.is_some()
})
.filter(|peerstate| peerstate.peek_key(false).is_some())
.map(|peerstate| peerstate.prefer_encrypt)
{
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{addr}\n"),
@@ -3444,9 +3443,7 @@ pub(crate) async fn add_contact_to_chat_ex(
}
} else {
// else continue and send status mail
if chat.is_protected()
&& contact.is_verified(context).await? != VerifiedStatus::BidirectVerified
{
if chat.is_protected() && !contact.is_verified(context).await? {
error!(
context,
"Only bidirectional verified contacts can be added to protected chats."
@@ -3688,7 +3685,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
async fn rename_ex(
context: &Context,
sync: sync::Sync,
mut sync: sync::Sync,
chat_id: ChatId,
new_name: &str,
) -> Result<()> {
@@ -3734,6 +3731,7 @@ async fn rename_ex(
}
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_msgs_changed(chat_id, msg.id);
sync = Nosync;
}
context.emit_event(EventType::ChatModified(chat_id));
success = true;
@@ -4332,7 +4330,7 @@ mod tests {
use crate::contact::{Contact, ContactAddress};
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use crate::test_utils::{sync, TestContext, TestContextManager};
use strum::IntoEnumIterator;
use tokio::fs;
@@ -6886,102 +6884,113 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_alter_chat() -> Result<()> {
let alices = [
TestContext::new_alice().await,
TestContext::new_alice().await,
];
for a in &alices {
async fn test_sync_blocked() -> 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?;
}
let bob = TestContext::new_bob().await;
let ba_chat = bob.create_chat(&alices[0]).await;
let ba_chat = bob.create_chat(alice0).await;
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
let a0b_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
alices[1].recv_msg(&sent_msg).await;
let ab_contact_ids = [
alices[0].add_or_lookup_contact(&bob).await.id,
alices[1].add_or_lookup_contact(&bob).await.id,
];
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
alice1.recv_msg(&sent_msg).await;
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
async fn sync(alices: &[TestContext]) -> Result<()> {
let sync_msg = alices.get(0).unwrap().pop_sent_msg().await;
alices.get(1).unwrap().recv_msg(&sync_msg).await;
Ok(())
}
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
a0b_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(&alices[0]).await?;
sync(&alices).await?;
assert_eq!(alices[1].get_chat(&bob).await.blocked, Blocked::Not);
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
a0b_chat_id.accept(alice0).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
a0b_chat_id.block(alice0).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Yes);
a0b_chat_id.unblock(alice0).await?;
sync(alice0, alice1).await;
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Not);
// Unblocking a 1:1 chat doesn't unblock the contact currently.
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
Contact::unblock(alice0, a0b_contact_id).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(&alices[0], ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(alices[1].add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(&alices[0], ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(!alices[1].add_or_lookup_contact(&bob).await.is_blocked());
assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
Contact::block(alice0, a0b_contact_id).await?;
sync(alice0, alice1).await;
assert!(alice1.add_or_lookup_contact(&bob).await.is_blocked());
Contact::unblock(alice0, a0b_contact_id).await?;
sync(alice0, alice1).await;
assert!(!alice1.add_or_lookup_contact(&bob).await.is_blocked());
// Test accepting and blocking groups. This way we test:
// - Group chats synchronisation.
// - That blocking a group deletes it on other devices.
let fiona = TestContext::new_fiona().await;
let fiona_grp_chat_id = fiona
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&alices[0]])
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[alice0])
.await;
let sent_msg = fiona.send_text(fiona_grp_chat_id, "hi").await;
let a0_grp_chat_id = alices[0].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alices[1].recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
let a0_grp_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat_id = alice1.recv_msg(&sent_msg).await.chat_id;
let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Request);
a0_grp_chat_id.accept(&alices[0]).await?;
sync(&alices).await?;
let a1_grp_chat = Chat::load_from_db(&alices[1], a1_grp_chat_id).await?;
a0_grp_chat_id.accept(alice0).await?;
sync(alice0, alice1).await;
let a1_grp_chat = Chat::load_from_db(alice1, a1_grp_chat_id).await?;
assert_eq!(a1_grp_chat.blocked, Blocked::Not);
a0_grp_chat_id.block(&alices[0]).await?;
sync(&alices).await?;
assert!(Chat::load_from_db(&alices[1], a1_grp_chat_id)
.await
.is_err());
a0_grp_chat_id.block(alice0).await?;
sync(alice0, alice1).await;
assert!(Chat::load_from_db(alice1, a1_grp_chat_id).await.is_err());
assert!(
!alices[1]
!alice1
.sql
.exists("SELECT COUNT(*) FROM chats WHERE id=?", (a1_grp_chat_id,))
.await?
);
// Test syncing of chat visibility on a self-chat. This way we test:
// - Self-chat synchronisation.
// - That sync messages don't unarchive the self-chat.
let a0self_chat_id = alices[0].get_self_chat().await.id;
Ok(())
}
/// Tests syncing of chat visibility on a self-chat. This way we test:
/// - Self-chat synchronisation.
/// - That sync messages don't unarchive the self-chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_visibility() -> 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?;
}
let a0self_chat_id = alice0.get_self_chat().await.id;
assert_eq!(
alices[1].get_self_chat().await.get_visibility(),
alice1.get_self_chat().await.get_visibility(),
ChatVisibility::Normal
);
let mut visibilities =
ChatVisibility::iter().chain(std::iter::once(ChatVisibility::Normal));
visibilities.next();
for v in visibilities {
a0self_chat_id.set_visibility(&alices[0], v).await?;
sync(&alices).await?;
for a in &alices {
a0self_chat_id.set_visibility(alice0, v).await?;
sync(alice0, alice1).await;
for a in [alice0, alice1] {
assert_eq!(a.get_self_chat().await.get_visibility(), v);
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_muted() -> 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?;
}
let bob = TestContext::new_bob().await;
let a0b_chat_id = alice0.create_chat(&bob).await.id;
alice1.create_chat(&bob).await;
assert_eq!(
alices[1].get_chat(&bob).await.mute_duration,
alice1.get_chat(&bob).await.mute_duration,
MuteDuration::NotMuted
);
let mute_durations = [
@@ -6990,8 +6999,8 @@ mod tests {
MuteDuration::NotMuted,
];
for m in mute_durations {
set_muted(&alices[0], a0b_chat_id, m).await?;
sync(&alices).await?;
set_muted(alice0, a0b_chat_id, m).await?;
sync(alice0, alice1).await;
let m = match m {
MuteDuration::Until(time) => MuteDuration::Until(
SystemTime::UNIX_EPOCH
@@ -7001,42 +7010,76 @@ mod tests {
),
_ => m,
};
assert_eq!(alices[1].get_chat(&bob).await.mute_duration, m);
assert_eq!(alice1.get_chat(&bob).await.mute_duration, m);
}
Ok(())
}
let a0_broadcast_id = create_broadcast_list(&alices[0]).await?;
sync(&alices).await?;
let a0_broadcast_chat = Chat::load_from_db(&alices[0], a0_broadcast_id).await?;
set_chat_name(&alices[0], a0_broadcast_id, "Broadcast list 42").await?;
sync(&alices).await?;
let a1_broadcast_id = get_chat_id_by_grpid(&alices[1], &a0_broadcast_chat.grpid)
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_broadcast() -> 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?;
}
let bob = TestContext::new_bob().await;
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
let a0_broadcast_id = create_broadcast_list(alice0).await?;
sync(alice0, alice1).await;
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
let a1_broadcast_chat = Chat::load_from_db(&alices[1], a1_broadcast_id).await?;
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
add_contact_to_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert_eq!(a1_broadcast_chat.get_name(), a0_broadcast_chat.get_name());
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
add_contact_to_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
sync(alice0, alice1).await;
let a1b_contact_id = Contact::lookup_id_by_addr(
alice1,
&bob.get_config(Config::Addr).await?.unwrap(),
Origin::Hidden,
)
.await?
.unwrap();
assert_eq!(
get_chat_contacts(&alices[1], a1_broadcast_id).await?,
vec![ab_contact_ids[1]]
get_chat_contacts(alice1, a1_broadcast_id).await?,
vec![a1b_contact_id]
);
let sent_msg = alices[1].send_text(a1_broadcast_id, "hi").await;
let sent_msg = alice1.send_text(a1_broadcast_id, "hi").await;
let msg = bob.recv_msg(&sent_msg).await;
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
assert_eq!(chat.get_type(), Chattype::Mailinglist);
let msg = alices[0].recv_msg(&sent_msg).await;
let msg = alice0.recv_msg(&sent_msg).await;
assert_eq!(msg.chat_id, a0_broadcast_id);
remove_contact_from_chat(&alices[0], a0_broadcast_id, ab_contact_ids[0]).await?;
sync(&alices).await?;
assert!(get_chat_contacts(&alices[1], a1_broadcast_id)
.await?
.is_empty());
remove_contact_from_chat(alice0, a0_broadcast_id, a0b_contact_id).await?;
sync(alice0, alice1).await;
assert!(get_chat_contacts(alice1, a1_broadcast_id).await?.is_empty());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync_name() -> 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?;
}
let a0_broadcast_id = create_broadcast_list(alice0).await?;
sync(alice0, alice1).await;
let a0_broadcast_chat = Chat::load_from_db(alice0, a0_broadcast_id).await?;
set_chat_name(alice0, a0_broadcast_id, "Broadcast list 42").await?;
sync(alice0, alice1).await;
let a1_broadcast_id = get_chat_id_by_grpid(alice1, &a0_broadcast_chat.grpid)
.await?
.unwrap()
.0;
let a1_broadcast_chat = Chat::load_from_db(alice1, a1_broadcast_id).await?;
assert_eq!(a1_broadcast_chat.get_type(), Chattype::Broadcast);
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
Ok(())
}
}

View File

@@ -5,6 +5,7 @@ use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
use serde::{Deserialize, Serialize};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -13,8 +14,10 @@ use crate::constants::DC_VERSION_STR;
use crate::contact::addr_cmp;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{get_provider_by_id, Provider};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
/// The available configuration keys.
@@ -31,6 +34,8 @@ use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
EnumProperty,
PartialOrd,
Ord,
Serialize,
Deserialize,
)]
#[strum(serialize_all = "snake_case")]
pub enum Config {
@@ -340,6 +345,24 @@ pub enum Config {
VerifiedOneOnOneChats,
}
impl Config {
/// Whether the config option is synced across devices.
///
/// 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
/// 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 {
matches!(
self,
Self::Displayname | Self::MdnsEnabled | Self::ShowEmails
)
}
}
impl Context {
/// Returns true if configuration value is set for the given key.
pub async fn config_exists(&self, key: Config) -> Result<bool> {
@@ -460,6 +483,16 @@ 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
}
pub(crate) async fn set_config_ex(
&self,
sync: sync::Sync,
key: Config,
mut value: Option<&str>,
) -> Result<()> {
let better_value;
match key {
Config::Selfavatar => {
self.sql
@@ -486,10 +519,11 @@ impl Context {
ret?
}
Config::Displayname => {
let value = value.map(improve_single_line_input);
self.sql
.set_raw_config(key.as_ref(), value.as_deref())
.await?;
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
@@ -522,6 +556,23 @@ impl Context {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
}
if sync != Sync {
return Ok(());
}
let Some(val) = value else {
return Ok(());
};
let val = val.to_string();
if self
.add_sync_item(SyncData::Config { key, val })
.await
.log_err(self)
.is_err()
{
return Ok(());
}
self.send_sync_msg().await.log_err(self).ok();
Ok(())
}
@@ -653,7 +704,7 @@ mod tests {
use super::*;
use crate::constants;
use crate::test_utils::TestContext;
use crate::test_utils::{sync, TestContext};
#[test]
fn test_to_string() {
@@ -797,4 +848,65 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> 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?;
}
let mdns_enabled = alice0.get_config_bool(Config::MdnsEnabled).await?;
// Alice1 has a different config value.
alice1
.set_config_bool(Config::MdnsEnabled, !mdns_enabled)
.await?;
// This changes nothing, but still sends a sync message.
alice0
.set_config_bool(Config::MdnsEnabled, mdns_enabled)
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config_bool(Config::MdnsEnabled).await?,
mdns_enabled
);
// 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?);
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert!(!alice1.get_config_bool(Config::MdnsEnabled).await?);
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
alice0
.set_config_bool(Config::ShowEmails, !show_emails)
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config_bool(Config::ShowEmails).await?,
!show_emails
);
// `Config::SyncMsgs` mustn't be synced.
alice0.set_config_bool(Config::SyncMsgs, false).await?;
alice0.set_config_bool(Config::SyncMsgs, true).await?;
alice0.set_config_bool(Config::MdnsEnabled, true).await?;
sync(&alice0, &alice1).await;
assert!(alice1.get_config_bool(Config::MdnsEnabled).await?);
// Usual sync scenario.
alice0
.set_config(Config::Displayname, Some("Alice Sync"))
.await?;
sync(&alice0, &alice1).await;
assert_eq!(
alice1.get_config(Config::Displayname).await?,
Some("Alice Sync".to_string())
);
Ok(())
}
}

View File

@@ -34,6 +34,7 @@ use crate::provider::{Protocol, Socket, UsernamePattern};
use crate::smtp::Smtp;
use crate::socks::Socks5Config;
use crate::stock_str;
use crate::sync::Sync::*;
use crate::tools::{time, EmailAddress};
use crate::{chat, e2ee, provider};
@@ -132,7 +133,9 @@ async fn on_configure_completed(
for def in config_defaults {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
context
.set_config_ex(Nosync, def.key, Some(def.value))
.await?;
} else {
info!(
context,

View File

@@ -30,7 +30,7 @@ use crate::login_param::LoginParam;
use crate::message::MessageState;
use crate::mimeparser::AvatarAction;
use crate::param::{Param, Params};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::peerstate::Peerstate;
use crate::sql::{self, params_iter};
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
@@ -348,24 +348,6 @@ pub(crate) enum Modifier {
Created,
}
/// Verification status of the contact.
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
#[repr(u8)]
pub enum VerifiedStatus {
/// Contact is not verified.
Unverified = 0,
/// SELF has verified the fingerprint of a contact. Currently unused.
Verified = 1,
/// SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
BidirectVerified = 2,
}
impl Default for VerifiedStatus {
fn default() -> Self {
Self::Unverified
}
}
impl Contact {
/// Loads a single contact object from the database.
///
@@ -1055,11 +1037,9 @@ impl Contact {
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(PeerstateVerifiedStatus::Unverified)
.is_some()
}) {
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,
@@ -1074,11 +1054,11 @@ impl Contact {
.fingerprint()
.to_string();
let fingerprint_other_verified = peerstate
.peek_key(PeerstateVerifiedStatus::BidirectVerified)
.peek_key(true)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
let fingerprint_other_unverified = peerstate
.peek_key(PeerstateVerifiedStatus::Unverified)
.peek_key(false)
.map(|k| k.fingerprint().to_string())
.unwrap_or_default();
if loginparam.addr < peerstate.addr {
@@ -1281,20 +1261,20 @@ impl Contact {
/// otherwise use is_chat_protected().
/// Use [Self::get_verifier_id] to display the verifier contact
/// in the info section of the contact profile.
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
pub async fn is_verified(&self, context: &Context) -> Result<bool> {
// We're always sort of secured-verified as we could verify the key on this device any time with the key
// on this device
if self.id == ContactId::SELF {
return Ok(VerifiedStatus::BidirectVerified);
return Ok(true);
}
if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? {
if peerstate.is_using_verified_key() {
return Ok(VerifiedStatus::BidirectVerified);
return Ok(true);
}
}
Ok(VerifiedStatus::Unverified)
Ok(false)
}
/// Returns the `ContactId` that verified the contact.
@@ -1349,7 +1329,7 @@ impl Contact {
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
} else {
// 1:1 chat does not exist.
Ok(self.is_verified(context).await? == VerifiedStatus::BidirectVerified)
Ok(self.is_verified(context).await?)
}
}

View File

@@ -423,6 +423,9 @@ impl Context {
/// Stops the IO scheduler.
pub async fn stop_io(&self) {
self.scheduler.stop(self).await;
if let Err(err) = self.sql.checkpoint(self).await {
error!(self, "Failed to checkpoint the database: {err:#}.");
}
}
/// Restarts the IO scheduler if it was running before

View File

@@ -54,7 +54,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
match context
.write_status_update_inner(
&msg_id,
StatusUpdateItem {
&StatusUpdateItem {
payload: json!({
"event": event,
"time": time,
@@ -62,6 +62,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
info: None,
summary: None,
document: None,
uid: None,
},
)
.await
@@ -70,10 +71,17 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
eprintln!("Can't log event to webxdc status update: {err:#}");
}
Ok(serial) => {
context.emit_event(EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
});
if let Some(serial) = serial {
if !matches!(event, EventType::WebxdcStatusUpdate { .. }) {
context.emit_event(EventType::WebxdcStatusUpdate {
msg_id,
status_update_serial: serial,
});
}
} else {
// This should not happen as the update has no `uid`.
error!(context, "Debug logging update is not created.");
};
}
}
}

View File

@@ -7,7 +7,7 @@ use crate::aheader::{Aheader, EncryptPreference};
use crate::config::Config;
use crate::context::Context;
use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey};
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::peerstate::Peerstate;
use crate::pgp;
#[derive(Debug)]
@@ -94,7 +94,7 @@ impl EncryptHelper {
pub async fn encrypt(
self,
context: &Context,
min_verified: PeerstateVerifiedStatus,
verified: bool,
mail_to_encrypt: lettre_email::PartBuilder,
peerstates: Vec<(Option<Peerstate>, &str)>,
) -> Result<String> {
@@ -107,7 +107,7 @@ impl EncryptHelper {
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
{
let key = peerstate
.take_key(min_verified)
.take_key(verified)
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
keyring.push(key);
verifier_addresses.push(addr);
@@ -118,7 +118,7 @@ impl EncryptHelper {
// Encrypt to secondary verified keys
// if we also encrypt to the introducer ("verifier") of the key.
if min_verified == PeerstateVerifiedStatus::BidirectVerified {
if verified {
for (peerstate, _addr) in peerstates {
if let Some(peerstate) = peerstate {
if let (Some(key), Some(verifier)) = (

View File

@@ -9,6 +9,7 @@ use std::{
collections::{BTreeMap, BTreeSet, HashMap},
iter::Peekable,
mem::take,
time::Duration,
};
use anyhow::{bail, format_err, Context as _, Result};
@@ -16,6 +17,8 @@ use async_channel::Receiver;
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
use futures::{StreamExt, TryStreamExt};
use num_traits::FromPrimitive;
use ratelimit::Ratelimit;
use tokio::sync::RwLock;
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::config::Config;
@@ -38,7 +41,7 @@ use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str;
use crate::tools::create_id;
use crate::tools::{create_id, duration_to_str};
pub(crate) mod capabilities;
mod client;
@@ -91,6 +94,7 @@ pub struct Imap {
login_failed_once: bool,
pub(crate) connectivity: ConnectivityStore,
ratelimit: RwLock<Ratelimit>,
}
#[derive(Debug)]
@@ -252,6 +256,8 @@ impl Imap {
session: None,
login_failed_once: false,
connectivity: Default::default(),
// 1 connection per minute + a burst of 2.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(120, 0), 2.0)),
};
Ok(imap)
@@ -300,10 +306,20 @@ impl Imap {
}
self.connectivity.set_connecting(context).await;
let ratelimit_duration = self.ratelimit.read().await.until_can_send();
if !ratelimit_duration.is_zero() {
warn!(
context,
"IMAP got rate limited, waiting for {} until can connect",
duration_to_str(ratelimit_duration),
);
tokio::time::sleep(ratelimit_duration).await;
}
let oauth2 = self.config.lp.oauth2;
info!(context, "Connecting to IMAP server");
self.ratelimit.write().await.send();
let connection_res: Result<Client> = if self.config.lp.security == Socket::Starttls
|| self.config.lp.security == Socket::Plain
{
@@ -572,9 +588,6 @@ impl Imap {
/// When selecting a folder for the first time, sets the uid_next to the current
/// mailbox.uid_next so that no old emails are fetched.
///
/// Makes sure that UIDNEXT is known for `selected_mailbox`
/// and errors out if UIDNEXT cannot be determined.
///
/// Returns Result<new_emails> (i.e. whether new emails arrived),
/// if in doubt, returns new_emails=true so emails are fetched.
pub(crate) async fn select_with_uidvalidity(
@@ -592,11 +605,18 @@ impl Imap {
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {folder}"))?;
let old_uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get old UID validity for folder {folder}"))?;
let old_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
let new_uid_validity = mailbox
.uid_validity
.with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
uid_next
Some(uid_next)
} else {
warn!(
context,
@@ -621,18 +641,15 @@ impl Imap {
.await
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
status
.uid_next
.with_context(|| format!("STATUS {folder} (UIDNEXT) did not return UIDNEXT"))?
if status.uid_next.is_none() {
// This happens with mail.163.com as of 2023-11-26.
// It does not return UIDNEXT on SELECT and returns invalid
// `* STATUS "INBOX" ()` response on explicit request for UIDNEXT.
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT.");
}
status.uid_next
};
mailbox.uid_next = Some(new_uid_next);
let old_uid_validity = get_uidvalidity(context, folder)
.await
.with_context(|| format!("failed to get old UID validity for folder {folder}"))?;
let old_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
mailbox.uid_next = new_uid_next;
if new_uid_validity == old_uid_validity {
let new_emails = if newly_selected == NewlySelected::No {
@@ -641,7 +658,7 @@ impl Imap {
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
// new messages is only one command, just as a SELECT command)
true
} else {
} else if let Some(new_uid_next) = new_uid_next {
if new_uid_next < old_uid_next {
warn!(
context,
@@ -651,7 +668,11 @@ impl Imap {
context.schedule_resync().await?;
}
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
} else {
// We have no UIDNEXT and if in doubt, return true.
true
};
return Ok(new_emails);
}
@@ -660,6 +681,7 @@ impl Imap {
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = new_uid_next.unwrap_or_default();
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
@@ -876,11 +898,7 @@ impl Imap {
.as_ref()
.with_context(|| format!("Expected {folder:?} to be selected"))?
.uid_next
.with_context(|| {
format!(
"Expected UIDNEXT to be determined for {folder:?} by select_with_uidvalidity"
)
})?;
.unwrap_or_default();
let new_uid_next = max(
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
mailbox_uid_next,

View File

@@ -17,8 +17,8 @@ use crate::net::tls::wrap_tls;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
/// IMAP write and read timeout.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30);
/// IMAP connection, write and read timeout.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug)]
pub(crate) struct Client {

View File

@@ -9,7 +9,7 @@ use super::session::Session;
use super::Imap;
use crate::config::Config;
use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, get_uid_next, FolderMeaning};
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::log::LogExt;
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
@@ -29,29 +29,6 @@ impl Session {
return Ok(self);
}
// Despite checking for unsolicited EXISTS above,
// we may have missed EXISTS if the message was
// received when the folder was not selected.
let status = self
.status(folder, "(UIDNEXT)")
.await
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
if let Some(uid_next) = status.uid_next {
let expected_uid_next = get_uid_next(context, folder)
.await
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
if uid_next > expected_uid_next {
info!(
context,
"Skipping IDLE on {folder:?} because UIDNEXT {uid_next}>{expected_uid_next} indicates there are new messages."
);
return Ok(self);
}
} else {
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
// Go to IDLE anyway if STATUS is broken.
}
if let Ok(()) = idle_interrupt_receiver.try_recv() {
info!(context, "skip idle, got interrupt");
return Ok(self);

View File

@@ -808,6 +808,7 @@ mod tests {
use tokio::task;
use super::*;
use crate::key;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext};
@@ -920,6 +921,37 @@ mod tests {
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_import_second_key() -> Result<()> {
let alice = &TestContext::new_alice().await;
let chat = alice.create_chat(alice).await;
let sent = alice.send_text(chat.id, "Encrypted with old key").await;
let export_dir = tempfile::tempdir().unwrap();
let alice = &TestContext::new().await;
alice.configure_addr("alice@example.org").await;
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
let alice = &TestContext::new_alice().await;
let old_key = key::load_self_secret_key(alice).await?;
imex(alice, ImexMode::ImportSelfKeys, export_dir.path(), None).await?;
let new_key = key::load_self_secret_key(alice).await?;
assert_ne!(new_key, old_key);
assert_eq!(
key::load_self_secret_keyring(alice).await?,
vec![new_key, old_key]
);
let msg = alice.recv_msg(&sent).await;
assert!(msg.get_showpadlock());
assert_eq!(msg.chat_id, alice.get_self_chat().await.id);
assert_eq!(msg.get_text(), "Encrypted with old key");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_backup() -> Result<()> {
for set_verified_oneonone_chats in [true, false] {

View File

@@ -16,6 +16,7 @@ use tokio::runtime::Handle;
use crate::config::Config;
use crate::constants::KeyGenType;
use crate::context::Context;
use crate::log::LogExt;
use crate::pgp::KeyPair;
use crate::tools::{time, EmailAddress};
@@ -125,6 +126,25 @@ pub(crate) async fn load_self_secret_key(context: &Context) -> Result<SignedSecr
}
}
pub(crate) async fn load_self_secret_keyring(context: &Context) -> Result<Vec<SignedSecretKey>> {
let keys = context
.sql
.query_map(
r#"SELECT private_key
FROM keypairs
WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr")
ORDER BY is_default DESC"#,
(),
|row| row.get::<_, Vec<u8>>(0),
|keys| keys.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?
.into_iter()
.filter_map(|bytes| SignedSecretKey::from_slice(&bytes).log_err(context).ok())
.collect();
Ok(keys)
}
impl DcKey for SignedPublicKey {
fn to_asc(&self, header: Option<(&str, &str)>) -> String {
// Not using .to_armored_string() to make clear *why* it is

View File

@@ -22,7 +22,7 @@ use crate::location;
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
use crate::peerstate::Peerstate;
use crate::simplify::escape_message_footer_marks;
use crate::stock_str;
use crate::tools::IsNoneOrEmpty;
@@ -312,7 +312,7 @@ impl<'a> MimeFactory<'a> {
}
}
fn min_verified(&self) -> PeerstateVerifiedStatus {
fn verified(&self) -> bool {
match &self.loaded {
Loaded::Message { chat } => {
if chat.is_protected() {
@@ -321,15 +321,15 @@ impl<'a> MimeFactory<'a> {
// In order to do this, it is necessary that they can be sent
// to a key that is not yet verified.
// This has to work independently of whether the chat is protected right now.
PeerstateVerifiedStatus::Unverified
false
} else {
PeerstateVerifiedStatus::BidirectVerified
true
}
} else {
PeerstateVerifiedStatus::Unverified
false
}
}
Loaded::Mdn { .. } => PeerstateVerifiedStatus::Unverified,
Loaded::Mdn { .. } => false,
}
}
@@ -627,7 +627,7 @@ impl<'a> MimeFactory<'a> {
));
}
let min_verified = self.min_verified();
let verified = self.verified();
let grpimage = self.grpimage();
let force_plaintext = self.should_force_plaintext();
let skip_autocrypt = self.should_skip_autocrypt();
@@ -723,7 +723,7 @@ impl<'a> MimeFactory<'a> {
&& self.should_do_gossip(context).await?
{
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
if let Some(header) = peerstate.render_gossip_header(min_verified) {
if let Some(header) = peerstate.render_gossip_header(verified) {
message = message.header(Header::new("Autocrypt-Gossip".into(), header));
is_gossiped = true;
}
@@ -756,7 +756,7 @@ impl<'a> MimeFactory<'a> {
}
let encrypted = encrypt_helper
.encrypt(context, min_verified, message, peerstates)
.encrypt(context, verified, message, peerstates)
.await?;
outer_message
@@ -924,9 +924,7 @@ impl<'a> MimeFactory<'a> {
let mut meta_part = None;
let send_verified_headers = match chat.typ {
// In single chats, the protection status isn't necessarily the same for both sides,
// so we don't send the Chat-Verified header:
Chattype::Single => false,
Chattype::Single => true,
Chattype::Group => true,
// Mailinglists and broadcast lists can actually never be verified:
Chattype::Mailinglist => false,

View File

@@ -27,7 +27,7 @@ use crate::decrypt::{
use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{load_self_secret_key, DcKey, Fingerprint, SignedPublicKey};
use crate::key::{load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
};
@@ -259,14 +259,12 @@ impl MimeMessage {
}
}
// remove headers that are allowed _only_ in the encrypted part
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
// them in signed-only emails, but has no value currently.
Self::remove_secured_headers(&mut headers);
let from = from.context("No from in message")?;
let private_keyring = vec![load_self_secret_key(context)
.await
.context("Failed to get own key")?];
let private_keyring = load_self_secret_keyring(context).await?;
let mut decryption_info =
prepare_decryption(context, &mail, &from.addr, message_time).await?;
@@ -307,10 +305,11 @@ impl MimeMessage {
content
});
if let (Ok(mail), true) = (mail, encrypted) {
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed:
if !signatures.is_empty() {
// Handle any gossip headers if the mail was encrypted. See section
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
// but only if the mail was correctly signed. Probably it's ok to not require
// encryption here, but let's follow the standard.
let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
gossiped_addr = update_gossip_peerstates(
context,
@@ -320,6 +319,9 @@ impl MimeMessage {
gossip_headers,
)
.await?;
// Remove unsigned subject from messages displayed with padlock.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
}
// let known protected headers from the decrypted
@@ -327,24 +329,20 @@ impl MimeMessage {
// Signature was checked for original From, so we
// do not allow overriding it.
let mut signed_from = None;
// We do not want to allow unencrypted subject in encrypted emails because the
// user might falsely think that the subject is safe.
// See <https://github.com/deltachat/deltachat-core-rust/issues/1790>.
headers.remove("subject");
let mut inner_from = None;
MimeMessage::merge_headers(
context,
&mut headers,
&mut recipients,
&mut signed_from,
&mut inner_from,
&mut list_post,
&mut chat_disposition_notification_to,
&mail.headers,
);
if let Some(signed_from) = signed_from {
if addr_cmp(&signed_from.addr, &from.addr) {
if let (Some(inner_from), true) = (inner_from, !signatures.is_empty()) {
if addr_cmp(&inner_from.addr, &from.addr) {
from_is_signed = true;
} else {
// There is a From: header in the encrypted &
@@ -362,6 +360,8 @@ impl MimeMessage {
}
}
if signatures.is_empty() {
Self::remove_secured_headers(&mut headers);
// If it is not a read receipt, degrade encryption.
if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) {
if message_time > peerstate.last_seen_autocrypt
@@ -1379,6 +1379,11 @@ impl MimeMessage {
.and_then(|msgid| parse_message_id(msgid).ok())
}
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
headers.remove("secure-join-fingerprint");
headers.remove("chat-verified");
}
fn merge_headers(
context: &Context,
headers: &mut HashMap<String, String>,

View File

@@ -26,17 +26,6 @@ pub enum PeerstateKeyType {
PublicKey,
}
/// Verification status of the contact peerstate.
#[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)]
#[repr(u8)]
pub enum PeerstateVerifiedStatus {
/// Peerstate is not verified.
Unverified = 0,
//Verified = 1, // not used
/// Peerstate is verified and we assume that the contact has verified our peerstate.
BidirectVerified = 2,
}
/// Peerstate represents the state of an Autocrypt peer.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Peerstate {
@@ -373,8 +362,8 @@ impl Peerstate {
}
/// Returns the contents of the `Autocrypt-Gossip` header for outgoing messages.
pub fn render_gossip_header(&self, min_verified: PeerstateVerifiedStatus) -> Option<String> {
if let Some(key) = self.peek_key(min_verified) {
pub fn render_gossip_header(&self, verified: bool) -> Option<String> {
if let Some(key) = self.peek_key(verified) {
let header = Aheader::new(
self.addr.clone(),
key.clone(), // TODO: avoid cloning
@@ -397,43 +386,41 @@ impl Peerstate {
/// Converts the peerstate into the contact public key.
///
/// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key.
pub fn take_key(mut self, min_verified: PeerstateVerifiedStatus) -> Option<SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.take(),
PeerstateVerifiedStatus::Unverified => {
self.public_key.take().or_else(|| self.gossip_key.take())
}
pub fn take_key(mut self, verified: bool) -> Option<SignedPublicKey> {
if verified {
self.verified_key.take()
} else {
self.public_key.take().or_else(|| self.gossip_key.take())
}
}
/// Returns a reference to the contact public key.
///
/// `min_verified` determines the minimum required verification status of the key.
/// `verified` determines the required verification status of the key.
/// If verified key is requested, returns the verified key,
/// otherwise returns the Autocrypt key.
///
/// Returned key is suitable for sending in `Autocrypt-Gossip` header.
///
/// Returns `None` if there is no suitable public key.
pub fn peek_key(&self, min_verified: PeerstateVerifiedStatus) -> Option<&SignedPublicKey> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key.as_ref(),
PeerstateVerifiedStatus::Unverified => {
self.public_key.as_ref().or(self.gossip_key.as_ref())
}
pub fn peek_key(&self, verified: bool) -> Option<&SignedPublicKey> {
if verified {
self.verified_key.as_ref()
} else {
self.public_key.as_ref().or(self.gossip_key.as_ref())
}
}
/// Returns a reference to the contact's public key fingerprint.
///
/// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key.
fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> {
match min_verified {
PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(),
PeerstateVerifiedStatus::Unverified => self
.public_key_fingerprint
fn peek_key_fingerprint(&self, verified: bool) -> Option<&Fingerprint> {
if verified {
self.verified_key_fingerprint.as_ref()
} else {
self.public_key_fingerprint
.as_ref()
.or(self.gossip_key_fingerprint.as_ref()),
.or(self.gossip_key_fingerprint.as_ref())
}
}
@@ -443,10 +430,9 @@ impl Peerstate {
/// Note that verified groups always use the verified key no matter if the
/// opportunistic key matches or not.
pub(crate) fn is_using_verified_key(&self) -> bool {
let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified);
let verified = self.peek_key_fingerprint(true);
verified.is_some()
&& verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified)
verified.is_some() && verified == self.peek_key_fingerprint(false)
}
/// Set this peerstate to verified
@@ -719,9 +705,7 @@ pub(crate) async fn maybe_do_aeap_transition(
// addresses with an MUA.
&& mime_parser.has_chat_version()
// Check if the message is signed correctly.
// If it's not signed correctly, the whole autocrypt header will be mostly
// ignored anyway and the message shown as not encrypted, so we don't
// have to handle this case.
// Although checking `from_is_signed` below is sufficient, let's play it safe.
&& !mime_parser.signatures.is_empty()
// Check if the From: address was also in the signed part of the email.
// Without this check, an attacker could replay a message from Alice

View File

@@ -63,6 +63,11 @@ pub struct ReceivedMsg {
/// Whether IMAP messages should be immediately deleted.
pub needs_delete_job: bool,
/// Whether the From address was repeated in the signed part
/// (and we know that the signer intended to send from this address).
#[cfg(test)]
pub(crate) from_is_signed: bool,
}
/// Emulates reception of a message from the network.
@@ -98,6 +103,22 @@ pub async fn receive_imf(
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
}
/// Inserts a tombstone into `msgs` table
/// to prevent downloading the same message in the future.
///
/// Returns tombstone database row ID.
async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId> {
let row_id = context
.sql
.insert(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?;
let msg_id = MsgId::new(u32::try_from(row_id)?);
Ok(msg_id)
}
/// Receive a message and add it to the database.
///
/// Returns an error on database failure or if the message is broken,
@@ -137,14 +158,7 @@ pub(crate) async fn receive_imf_inner(
return Ok(None);
}
let row_id = context
.sql
.execute(
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
(rfc724_mid, DC_CHAT_ID_TRASH),
)
.await?;
let msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
let msg_ids = vec![insert_tombstone(context, rfc724_mid).await?];
return Ok(Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
@@ -152,6 +166,8 @@ pub(crate) async fn receive_imf_inner(
sort_timestamp: 0,
msg_ids,
needs_delete_job: false,
#[cfg(test)]
from_is_signed: false,
}));
}
Ok(mime_parser) => mime_parser,
@@ -229,25 +245,76 @@ pub(crate) async fn receive_imf_inner(
update_verified_keys(context, &mut mime_parser, from_id).await?;
// Add parts
let received_msg = add_parts(
context,
&mut mime_parser,
imf_raw,
incoming,
&to_ids,
rfc724_mid,
sent_timestamp,
rcvd_timestamp,
from_id,
seen || replace_partial_download.is_some(),
is_partial_download,
replace_partial_download,
fetching_existing_messages,
prevent_rename,
)
.await
.context("add_parts error")?;
let received_msg;
if let Some(securejoin_step) = mime_parser.get_header(HeaderDef::SecureJoin) {
info!(context, "Received securejoin step {securejoin_step}.");
let res;
if incoming {
res = handle_securejoin_handshake(context, &mime_parser, from_id)
.await
.context("error in Secure-Join message handling")?;
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate =
Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
let to_id = to_ids.get(0).copied().unwrap_or_default();
// handshake may mark contacts as verified and must be processed before chats are created
res = observe_securejoin_on_other_device(context, &mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
}
match res {
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
let msg_id = insert_tombstone(context, rfc724_mid).await?;
received_msg = Some(ReceivedMsg {
chat_id: DC_CHAT_ID_TRASH,
state: MessageState::InSeen,
sort_timestamp: sent_timestamp,
msg_ids: vec![msg_id],
needs_delete_job: res == securejoin::HandshakeMessage::Done,
#[cfg(test)]
from_is_signed: mime_parser.from_is_signed,
});
}
securejoin::HandshakeMessage::Propagate => {
received_msg = None;
}
}
} else {
received_msg = None;
}
let verified_encryption =
has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?;
let received_msg = if let Some(received_msg) = received_msg {
received_msg
} else {
// Add parts
add_parts(
context,
&mut mime_parser,
imf_raw,
incoming,
&to_ids,
rfc724_mid,
sent_timestamp,
rcvd_timestamp,
from_id,
seen || replace_partial_download.is_some(),
is_partial_download,
replace_partial_download,
fetching_existing_messages,
prevent_rename,
verified_encryption,
)
.await
.context("add_parts error")?
};
if !from_id.is_special() {
contact::update_last_seen(context, from_id, sent_timestamp).await?;
@@ -388,7 +455,10 @@ pub(crate) async fn receive_imf_inner(
///
/// Also returns whether it is blocked or not and its origin.
///
/// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()`
/// * `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change every time the user gets a new email from
/// a mailing list.
///
/// Returns `None` if From field does not contain a valid contact address.
pub async fn from_field_to_contact_id(
@@ -452,6 +522,7 @@ async fn add_parts(
mut replace_msg_id: Option<MsgId>,
fetching_existing_messages: bool,
prevent_rename: bool,
verified_encryption: VerifiedEncryption,
) -> Result<ReceivedMsg> {
let mut chat_id = None;
let mut chat_id_blocked = Blocked::Not;
@@ -510,38 +581,6 @@ async fn add_parts(
if incoming {
to_id = ContactId::SELF;
// Whether the message is a part of securejoin handshake that should be marked as seen
// automatically.
let securejoin_seen;
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
match handle_securejoin_handshake(context, mime_parser, from_id)
.await
.context("error in Secure-Join message handling")?
{
securejoin::HandshakeMessage::Done => {
chat_id = Some(DC_CHAT_ID_TRASH);
needs_delete_job = true;
securejoin_seen = true;
}
securejoin::HandshakeMessage::Ignore => {
chat_id = Some(DC_CHAT_ID_TRASH);
securejoin_seen = true;
}
securejoin::HandshakeMessage::Propagate => {
// process messages as "member added" normally
securejoin_seen = false;
}
}
// Peerstate could be updated by handling the Securejoin handshake.
let contact = Contact::get_by_id(context, from_id).await?;
mime_parser.decryption_info.peerstate =
Peerstate::from_addr(context, contact.get_addr()).await?;
} else {
securejoin_seen = false;
}
let test_normal_chat = if from_id == ContactId::UNDEFINED {
None
} else {
@@ -614,6 +653,8 @@ async fn add_parts(
create_blocked,
from_id,
to_ids,
&verified_encryption,
sent_timestamp,
)
.await?
{
@@ -664,6 +705,7 @@ async fn add_parts(
from_id,
to_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
}
@@ -676,6 +718,7 @@ async fn add_parts(
allow_creation,
mailinglist_header,
mime_parser,
sent_timestamp,
)
.await?
{
@@ -754,15 +797,8 @@ async fn add_parts(
false => None,
};
if let Some(chat) = chat {
let mut new_protection = match has_verified_encryption(
context,
mime_parser,
from_id,
to_ids,
Chattype::Single,
)
.await?
{
debug_assert!(chat.typ == Chattype::Single);
let mut new_protection = match verified_encryption {
VerifiedEncryption::Verified => ProtectionStatus::Protected,
VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected,
};
@@ -795,7 +831,6 @@ async fn add_parts(
|| is_mdn
|| is_reaction
|| is_location_kml
|| securejoin_seen
|| chat_id_blocked == Blocked::Yes
{
MessageState::InSeen
@@ -813,21 +848,7 @@ async fn add_parts(
let self_sent =
from_id == ContactId::SELF && to_ids.len() == 1 && to_ids.contains(&ContactId::SELF);
// handshake may mark contacts as verified and must be processed before chats are created
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
match observe_securejoin_on_other_device(context, mime_parser, to_id)
.await
.context("error in Secure-Join watching")?
{
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
chat_id = Some(DC_CHAT_ID_TRASH);
}
securejoin::HandshakeMessage::Propagate => {
// process messages as "member added" normally
chat_id = None;
}
}
} else if mime_parser.sync_items.is_some() && self_sent {
if mime_parser.sync_items.is_some() && self_sent {
chat_id = Some(DC_CHAT_ID_TRASH);
}
@@ -865,6 +886,8 @@ async fn add_parts(
Blocked::Not,
from_id,
to_ids,
&verified_encryption,
sent_timestamp,
)
.await?
{
@@ -907,6 +930,7 @@ async fn add_parts(
from_id,
to_ids,
is_partial_download.is_some(),
&verified_encryption,
)
.await?;
}
@@ -1071,10 +1095,16 @@ async fn add_parts(
if !chat_id.is_special() && is_partial_download.is_none() {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_protected() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
{
// For outgoing emails in the 1:1 chat we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
if chat.is_protected() && (incoming || chat.typ != Chattype::Single) {
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
@@ -1386,6 +1416,8 @@ RETURNING id
sort_timestamp,
msg_ids: created_db_entries,
needs_delete_job,
#[cfg(test)]
from_is_signed: mime_parser.from_is_signed,
})
}
@@ -1449,7 +1481,7 @@ async fn calc_sort_timestamp(
always_sort_to_bottom: bool,
incoming: bool,
) -> Result<i64> {
let mut sort_timestamp = message_timestamp;
let mut sort_timestamp = min(message_timestamp, smeared_time(context));
let last_msg_time: Option<i64> = if always_sort_to_bottom {
// get newest message for this chat
@@ -1486,7 +1518,7 @@ async fn calc_sort_timestamp(
}
}
Ok(min(sort_timestamp, smeared_time(context)))
Ok(sort_timestamp)
}
async fn lookup_chat_by_reply(
@@ -1584,6 +1616,7 @@ async fn is_probably_private_reply(
/// than two members, a new ad hoc group is created.
///
/// On success the function returns the found/created (chat_id, chat_blocked) tuple.
#[allow(clippy::too_many_arguments)]
async fn create_or_lookup_group(
context: &Context,
mime_parser: &mut MimeMessage,
@@ -1592,6 +1625,8 @@ async fn create_or_lookup_group(
create_blocked: Blocked,
from_id: ContactId,
to_ids: &[ContactId],
verified_encryption: &VerifiedEncryption,
timestamp: i64,
) -> Result<Option<(ChatId, Blocked)>> {
let grpid = if let Some(grpid) = try_getting_grpid(mime_parser) {
grpid
@@ -1604,7 +1639,7 @@ async fn create_or_lookup_group(
member_ids.push(ContactId::SELF);
}
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids)
let res = create_adhoc_group(context, mime_parser, create_blocked, &member_ids, timestamp)
.await
.context("could not create ad hoc group")?
.map(|chat_id| (chat_id, create_blocked));
@@ -1635,9 +1670,7 @@ async fn create_or_lookup_group(
}
let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await?
{
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
@@ -1688,6 +1721,7 @@ async fn create_or_lookup_group(
create_blocked,
create_protected,
None,
timestamp,
)
.await
.with_context(|| format!("Failed to create group '{grpname}' for grpid={grpid}"))?;
@@ -1730,6 +1764,7 @@ async fn create_or_lookup_group(
///
/// Optionally returns better message to replace the original system message.
/// is_partial_download: whether the message is not fully downloaded.
#[allow(clippy::too_many_arguments)]
async fn apply_group_changes(
context: &Context,
mime_parser: &mut MimeMessage,
@@ -1738,6 +1773,7 @@ async fn apply_group_changes(
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
verified_encryption: &VerifiedEncryption,
) -> Result<(Vec<String>, Option<String>)> {
if chat_id.is_special() {
// Do not apply group changes to the trash chat.
@@ -1798,9 +1834,7 @@ async fn apply_group_changes(
};
if mime_parser.get_header(HeaderDef::ChatVerified).is_some() {
if let VerifiedEncryption::NotVerified(err) =
has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await?
{
if let VerifiedEncryption::NotVerified(err) = verified_encryption {
warn!(context, "Verification problem: {err:#}.");
let s = format!("{err}. See 'Info' for more details");
mime_parser.replace_msg_by_error(&s);
@@ -2021,6 +2055,7 @@ async fn create_or_lookup_mailinglist(
allow_creation: bool,
list_id_header: &str,
mime_parser: &MimeMessage,
timestamp: i64,
) -> Result<Option<(ChatId, Blocked)>> {
let listid = mailinglist_header_listid(list_id_header)?;
@@ -2052,6 +2087,7 @@ async fn create_or_lookup_mailinglist(
blocked,
ProtectionStatus::Unprotected,
param,
timestamp,
)
.await
.with_context(|| {
@@ -2236,6 +2272,7 @@ async fn create_adhoc_group(
mime_parser: &MimeMessage,
create_blocked: Blocked,
member_ids: &[ContactId],
timestamp: i64,
) -> Result<Option<ChatId>> {
if mime_parser.is_mailinglist_message() {
info!(
@@ -2280,6 +2317,7 @@ async fn create_adhoc_group(
create_blocked,
ProtectionStatus::Unprotected,
None,
timestamp,
)
.await?;
chat::add_to_chat_contacts_table(context, new_chat_id, member_ids).await?;
@@ -2356,8 +2394,7 @@ async fn update_verified_keys(
/// Checks whether the message is allowed to appear in a protected chat.
///
/// This means that it is encrypted, signed with a verified key,
/// and if it's a group, all the recipients are verified.
/// This means that it is encrypted and signed with a verified key.
///
/// Also propagates gossiped keys to verified if needed.
async fn has_verified_encryption(
@@ -2365,21 +2402,15 @@ async fn has_verified_encryption(
mimeparser: &MimeMessage,
from_id: ContactId,
to_ids: &[ContactId],
chat_type: Chattype,
) -> Result<VerifiedEncryption> {
use VerifiedEncryption::*;
if from_id == ContactId::SELF && chat_type == Chattype::Single {
// For outgoing emails in the 1:1 chat, we have an exception that
// they are allowed to be unencrypted:
// 1. They can't be an attack (they are outgoing, not incoming)
// 2. Probably the unencryptedness is just a temporary state, after all
// the user obviously still uses DC
// -> Showing info messages everytime would be a lot of noise
// 3. The info messages that are shown to the user ("Your chat partner
// likely reinstalled DC" or similar) would be wrong.
return Ok(Verified);
}
// We do not need to check if we are verified with ourself.
let to_ids = to_ids
.iter()
.copied()
.filter(|id| *id != ContactId::SELF)
.collect::<Vec<ContactId>>();
if !mimeparser.was_encrypted() {
return Ok(NotVerified("This message is not encrypted".to_string()));
@@ -2409,17 +2440,6 @@ async fn has_verified_encryption(
}
}
// we do not need to check if we are verified with ourself
let to_ids = to_ids
.iter()
.copied()
.filter(|id| *id != ContactId::SELF)
.collect::<Vec<ContactId>>();
if to_ids.is_empty() {
return Ok(Verified);
}
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
Ok(Verified)
}
@@ -2429,7 +2449,15 @@ async fn mark_recipients_as_verified(
from_id: ContactId,
to_ids: Vec<ContactId>,
mimeparser: &MimeMessage,
) -> Result<(), anyhow::Error> {
) -> Result<()> {
if to_ids.is_empty() {
return Ok(());
}
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(());
}
let rows = context
.sql
.query_map(
@@ -2582,11 +2610,6 @@ pub(crate) async fn get_prefetch_parent_message(
/// Looks up contact IDs from the database given the list of recipients.
///
/// Returns vector of IDs guaranteed to be unique.
///
/// * param `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change every time the user gets a new email from
/// a mailing list.
async fn add_or_lookup_contacts_by_address_list(
context: &Context,
address_list: &[SingleInfo],

View File

@@ -322,7 +322,7 @@ async fn test_no_from() {
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
assert!(chats.get_msg_id(0).is_err());
receive_imf(
let received = receive_imf(
context,
b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\
To: bob@example.com\n\
@@ -335,8 +335,13 @@ async fn test_no_from() {
false,
)
.await
.unwrap()
.unwrap();
// Check that tombstone MsgId is returned.
assert_eq!(received.msg_ids.len(), 1);
assert!(!received.msg_ids[0].is_special());
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
// Check that the message is not shown to the user:
assert!(chats.is_empty());
@@ -3105,7 +3110,8 @@ async fn test_thunderbird_autocrypt() -> Result<()> {
let t = TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
receive_imf(&t, raw, false).await?;
let received_msg = receive_imf(&t, raw, false).await?.unwrap();
assert!(received_msg.from_is_signed);
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
.await?
@@ -3186,7 +3192,8 @@ async fn test_thunderbird_unsigned() -> Result<()> {
// Alice receives an unsigned message from Bob.
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml");
receive_imf(&alice, raw, false).await?;
let received_msg = receive_imf(&alice, raw, false).await?.unwrap();
assert!(!received_msg.from_is_signed);
let msg = alice.get_last_msg().await;
assert!(!msg.get_showpadlock());
@@ -3195,6 +3202,27 @@ async fn test_thunderbird_unsigned() -> Result<()> {
Ok(())
}
/// Bob receives an encrypted unsigned message with only an unencrypted Subject.
///
/// Test that the message is displayed without any errors,
/// but also without a padlock, but with the Subject.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_unsigned_with_unencrypted_subject() -> Result<()> {
let bob = TestContext::new_bob().await;
let raw = include_bytes!(
"../../test-data/message/thunderbird_encrypted_unsigned_with_unencrypted_subject.eml"
);
receive_imf(&bob, raw, false).await?;
let msg = bob.get_last_msg().await;
assert!(!msg.get_showpadlock());
assert!(msg.error().is_none());
assert_eq!(msg.get_subject(), "Hello!");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mua_user_adds_member() -> Result<()> {
let t = TestContext::new_alice().await;

View File

@@ -243,6 +243,7 @@ async fn fingerprint_equals_sender(
/// next with this incoming setup-contact/secure-join handshake message.
///
/// [`receive_imf`]: crate::receive_imf::receive_imf
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum HandshakeMessage {
/// The message has been fully handled and should be removed/delete.
///
@@ -515,12 +516,8 @@ pub(crate) async fn handle_securejoin_handshake(
}
"vg-member-added-received" | "vc-contact-confirm-received" => {
/*==========================================================
==== Alice - the inviter side ====
==== Step 8 in "Out-of-band verified groups" protocol ====
==========================================================*/
Ok(HandshakeMessage::Done) // "Done" deletes the message
// Deprecated steps, delete them immediately.
Ok(HandshakeMessage::Done)
}
_ => {
warn!(context, "invalid step: {}", step);
@@ -541,10 +538,10 @@ pub(crate) async fn handle_securejoin_handshake(
/// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm,
/// we can mark the peer as verified as well.
///
/// - if we see the self-sent-message vg-member-added-received
/// - if we see the self-sent-message vg-request-with-auth/vc-request-with-auth
/// we know that we're an joiner-observer.
/// the joining device has marked the peer as verified on vg-member-added/vc-contact-confirm
/// before sending vg-member-added-received - so, if we observe vg-member-added-received,
/// the joining device has marked the peer as verified
/// before sending vg-request-with-auth/vc-request-with-auth - so, if we observe vg-member-added-received,
/// we can mark the peer as verified as well.
pub(crate) async fn observe_securejoin_on_other_device(
context: &Context,
@@ -563,9 +560,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
"vg-request-with-auth"
| "vc-request-with-auth"
| "vg-member-added"
| "vc-contact-confirm"
| "vg-member-added-received"
| "vc-contact-confirm-received" => {
| "vc-contact-confirm" => {
if !encrypted_and_signed(
context,
mime_message,
@@ -774,7 +769,6 @@ mod tests {
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
use crate::contact::VerifiedStatus;
use crate::peerstate::Peerstate;
use crate::receive_imf::receive_imf;
use crate::stock_str::chat_protection_enabled;
@@ -893,17 +887,11 @@ mod tests {
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
// exactly one one-to-one chat should be visible for both now
// (check this before calling alice.create_chat() explicitly below)
@@ -946,35 +934,18 @@ mod tests {
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
.await
.unwrap();
assert_eq!(
contact_bob.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&bob.ctx).await.unwrap(), false);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
// Step 7: Bob receives vc-contact-confirm
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
// Check Bob got the verified message in his 1:1 chat.
{
let chat = bob.create_chat(&alice).await;
let msg = get_chat_msg(&bob, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg.get_text(), expected_text);
}
// Check Bob sent the final message
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
let chat = bob.create_chat(&alice).await;
let msg = get_chat_msg(&bob, chat.get_id(), 0, 1).await;
assert!(msg.is_info());
let expected_text = chat_protection_enabled(&bob).await;
assert_eq!(msg.get_text(), expected_text);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1063,17 +1034,11 @@ mod tests {
)
.await?;
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1090,25 +1055,12 @@ mod tests {
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
// Step 7: Bob receives vc-contact-confirm, sends vc-contact-confirm-received
// Step 7: Bob receives vc-contact-confirm
bob.recv_msg(&sent).await;
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vc-contact-confirm-received"
);
Ok(())
}
@@ -1231,17 +1183,11 @@ mod tests {
.await?
.expect("Contact not found");
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id).await?;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, false);
// Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added
alice.recv_msg(&sent).await;
assert_eq!(
contact_bob.is_verified(&alice.ctx).await?,
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_bob.is_verified(&alice.ctx).await?, true);
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
@@ -1277,19 +1223,13 @@ mod tests {
.expect("Error looking up contact")
.expect("Contact not found");
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
assert_eq!(
contact_bob.is_verified(&bob.ctx).await?,
VerifiedStatus::Unverified
);
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
// Step 7: Bob receives vg-member-added, sends vg-member-added-received
// Step 7: Bob receives vg-member-added
bob.recv_msg(&sent).await;
{
// Bob has Alice verified, message shows up in the group chat.
assert_eq!(
contact_alice.is_verified(&bob.ctx).await?,
VerifiedStatus::BidirectVerified
);
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, true);
let chat = bob.get_chat(&alice).await;
assert_eq!(
chat.blocked,
@@ -1305,14 +1245,6 @@ mod tests {
}
}
let sent = bob.pop_sent_msg().await;
let msg = alice.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(
msg.get_header(HeaderDef::SecureJoin).unwrap(),
"vg-member-added-received"
);
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
assert!(bob_chat.is_protected());
assert!(bob_chat.typ == Chattype::Group);

View File

@@ -16,7 +16,7 @@ use crate::context::Context;
use crate::events::EventType;
use crate::mimeparser::MimeMessage;
use crate::sync::Sync::*;
use crate::tools::time;
use crate::tools::{create_smeared_timestamp, time};
use crate::{chat, stock_str};
/// Starts the securejoin protocol with the QR `invite`.
@@ -58,7 +58,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Group { .. } => {
// For a secure-join we need to create the group and add the contact. The group will
// only become usable once the protocol is finished.
// TODO: how does this group become usable?
let group_chat_id = state.joining_chat_id(context).await?;
if !is_contact_in_chat(context, group_chat_id, invite.contact_id()).await? {
chat::add_to_chat_contacts_table(context, group_chat_id, &[invite.contact_id()])
@@ -193,6 +192,7 @@ impl BobState {
Blocked::Not,
ProtectionStatus::Unprotected, // protection is added later as needed
None,
create_smeared_timestamp(context),
)
.await?
}

View File

@@ -345,17 +345,6 @@ impl BobState {
.await?;
context.emit_event(EventType::ContactsChanged(None));
self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived)
.await
.map_err(|_| {
warn!(
context,
"Failed to send vc-contact-confirm-received/vg-member-added-received"
);
})
// This is not an error affecting the protocol outcome.
.ok();
self.update_next(&context.sql, SecureJoinStep::Completed)
.await?;
Ok(Some(BobHandshakeStage::Completed))
@@ -401,9 +390,6 @@ async fn send_handshake_message(
msg.param.set(Param::Arg2, invite.authcode());
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
BobHandshakeMsg::ContactConfirmReceived => {
msg.param.set_int(Param::GuaranteeE2ee, 1);
}
};
// Sends our own fingerprint in the Secure-Join-Fingerprint header.
@@ -425,8 +411,6 @@ enum BobHandshakeMsg {
Request,
/// vc-request-with-auth or vg-request-with-auth
RequestWithAuth,
/// vc-contact-confirm-received or vg-member-added-received
ContactConfirmReceived,
}
impl BobHandshakeMsg {
@@ -454,10 +438,6 @@ impl BobHandshakeMsg {
QrInvite::Contact { .. } => "vc-request-with-auth",
QrInvite::Group { .. } => "vg-request-with-auth",
},
Self::ContactConfirmReceived => match invite {
QrInvite::Contact { .. } => "vc-contact-confirm-received",
QrInvite::Group { .. } => "vg-member-added-received",
},
}
}
}

View File

@@ -27,8 +27,8 @@ use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
/// SMTP write and read timeout.
const SMTP_TIMEOUT: Duration = Duration::from_secs(30);
/// SMTP connection, write and read timeout.
const SMTP_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Default)]
pub(crate) struct Smtp {

View File

@@ -131,6 +131,23 @@ impl Sql {
// drop closes the connection
}
/// Flushes the WAL file.
pub(crate) async fn checkpoint(&self, context: &Context) -> Result<()> {
let busy = self
.call_write(move |conn| {
let busy = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", (), |row| {
let busy: bool = row.get(0)?;
Ok(busy)
})?;
Ok(busy)
})
.await?;
if busy {
warn!(context, "Failed to checkpoint WAL");
}
Ok(())
}
/// Imports the database from a separate file with the given passphrase.
pub(crate) async fn import(&self, path: &Path, passphrase: String) -> Result<()> {
let path_str = path

View File

@@ -763,6 +763,28 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
.await?;
}
if dbversion < 105 {
// Create UNIQUE uid column and drop unused update_item_read column.
sql.execute_migration(
r#"CREATE TABLE new_msgs_status_updates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
msg_id INTEGER,
update_item TEXT DEFAULT '',
uid TEXT UNIQUE
);
INSERT OR IGNORE INTO new_msgs_status_updates SELECT
id, msg_id, update_item, NULL
FROM msgs_status_updates;
DROP TABLE msgs_status_updates;
ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
"#,
105,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -92,7 +92,7 @@ pub enum StockMessage {
#[strum(props(fallback = "%1$s verified."))]
ContactVerified = 35,
#[strum(props(fallback = "Cannot verify %1$s"))]
#[strum(props(fallback = "Cannot establish guaranteed end-to-end encryption with %1$s"))]
ContactNotVerified = 36,
#[strum(props(fallback = "Changed setup for %1$s"))]
@@ -832,7 +832,7 @@ pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> St
.replace1(addr)
}
/// Stock string: `Cannot verify %1$s`.
/// Stock string: `Cannot establish guaranteed end-to-end encryption with %1$s`.
pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
let addr = &contact.get_name_n_addr();
translated(context, StockMessage::ContactNotVerified)

View File

@@ -20,7 +20,7 @@ use crate::tools::time;
use crate::{stock_str, token};
/// Whether to send device sync messages. Aimed for usage in the internal API.
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub(crate) enum Sync {
Nosync,
Sync,
@@ -35,6 +35,15 @@ impl From<Sync> for bool {
}
}
impl From<bool> for Sync {
fn from(sync: bool) -> Sync {
match sync {
false => Sync::Nosync,
true => Sync::Sync,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct QrTokenData {
pub(crate) invitenumber: String,
@@ -50,6 +59,10 @@ pub(crate) enum SyncData {
id: chat::SyncId,
action: chat::SyncAction,
},
Config {
key: Config,
val: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
@@ -263,6 +276,10 @@ impl Context {
AddQrToken(token) => self.add_qr_token(token).await,
DeleteQrToken(token) => self.delete_qr_token(token).await,
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
SyncData::Config { key, val } => match key.is_synced() {
true => self.set_config_ex(Sync::Nosync, *key, Some(val)).await,
false => Ok(()),
},
},
SyncDataOrUnknown::Unknown(data) => {
warn!(self, "Ignored unknown sync item: {data}.");

View File

@@ -1067,6 +1067,13 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
peerstate.save_to_db(&this.sql).await.unwrap();
}
/// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on
/// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
let sync_msg = alice0.pop_sent_msg().await;
alice1.recv_msg(&sync_msg).await;
}
/// Pretty-print an event to stdout
///
/// Done during tests this is captured by `cargo test` and associated with the test itself.

View File

@@ -1,12 +1,11 @@
use anyhow::Result;
use pretty_assertions::assert_eq;
use crate::chat::ProtectionStatus;
use crate::chat::{self, Chat, ProtectionStatus};
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::constants::DC_GCL_FOR_FORWARDING;
use crate::contact::VerifiedStatus;
use crate::contact::{Contact, Origin};
use crate::constants::{Chattype, DC_GCL_FOR_FORWARDING};
use crate::contact::{Contact, ContactId, Origin};
use crate::message::{Message, Viewtype};
use crate::mimefactory::MimeFactory;
use crate::mimeparser::SystemMessage;
@@ -70,10 +69,7 @@ async fn check_verified_oneonone_chat(broken_by_classical_email: bool) {
tcm.send_recv(&bob, &alice, "Using DC again").await;
let contact = alice.add_or_lookup_contact(&bob).await;
assert_eq!(
contact.is_verified(&alice.ctx).await.unwrap(),
VerifiedStatus::BidirectVerified
);
assert_eq!(contact.is_verified(&alice.ctx).await.unwrap(), true);
// Bob's chat is marked as verified again
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
@@ -121,10 +117,7 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
// Alice and Fiona should now be verified because of gossip
let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await;
assert_eq!(
alice_fiona_contact.is_verified(&alice).await.unwrap(),
VerifiedStatus::BidirectVerified
);
assert!(alice_fiona_contact.is_verified(&alice).await.unwrap(),);
// Alice should have a hidden protected chat with Fiona
{
@@ -684,7 +677,7 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
// Bob sent a message with a new key, so he most likely doesn't have
// the old key anymore. This means that Alice's device should show
// him as unverified:
VerifiedStatus::Unverified
false
);
let chat = alice.get_chat(&bob_new).await;
assert_eq!(chat.is_protected(), false);
@@ -782,14 +775,49 @@ async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()>
Ok(())
}
/// Tests that on the second device of a protected group creator the first message is
/// `SystemMessage::ChatProtectionEnabled` and the second one is the message populating the group.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_protected_grp_multidev() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let alice1 = &tcm.alice().await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[])
.await;
assert_eq!(
get_chat_msg(alice, group_id, 0, 1).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
let sent = alice.send_text(group_id, "Hey").await;
// This sleep is necessary to reproduce the bug when the original message is sorted over the
// "protection enabled" message so that these messages have different timestamps. The better way
// would be to adjust the system time here if we could mock the system clock for the tests.
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
let msg = alice1.recv_msg(&sent).await;
let group1 = Chat::load_from_db(alice1, msg.chat_id).await?;
assert_eq!(group1.get_type(), Chattype::Group);
assert!(group1.is_protected());
assert_eq!(
chat::get_chat_contacts(alice1, group1.id).await?,
vec![ContactId::SELF]
);
assert_eq!(
get_chat_msg(alice1, group1.id, 0, 2).await.get_info_type(),
SystemMessage::ChatProtectionEnabled
);
assert_eq!(get_chat_msg(alice1, group1.id, 1, 2).await.id, msg.id);
Ok(())
}
// ============== Helper Functions ==============
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
let contact = this.add_or_lookup_contact(other).await;
assert_eq!(
contact.is_verified(this).await.unwrap(),
VerifiedStatus::BidirectVerified
);
assert_eq!(contact.is_verified(this).await.unwrap(), true);
let chat = this.get_chat(other).await;
let (expect_protected, expect_broken) = match protected {

View File

@@ -5,6 +5,7 @@
//! - `id` - status update serial number
//! - `msg_id` - ID of the message in the `msgs` table
//! - `update_item` - JSON representation of the status update
//! - `uid` - "id" field of the update, used for deduplication
//!
//! Status updates are scheduled for sending by adding a record
//! to `smtp_status_updates_table` SQL table.
@@ -14,7 +15,6 @@
//! - `last_serial` - serial number of the last status update to send
//! - `descr` - text to send along with the updates
use std::convert::TryFrom;
use std::path::Path;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
@@ -37,6 +37,7 @@ use crate::mimefactory::wrapped_base64_encode;
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::param::Params;
use crate::tools::create_id;
use crate::tools::strip_rtlo_characters;
use crate::tools::{create_smeared_timestamp, get_abs_path};
@@ -62,11 +63,6 @@ const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png";
/// it is planned to raise that limit as needed in subsequent versions.
const WEBXDC_SENDING_LIMIT: u64 = 655360;
/// Be more tolerant for .xdc sizes on receiving -
/// might be, the senders version uses already a larger limit
/// and not showing the .xdc on some devices would be even worse ux.
const WEBXDC_RECEIVING_LIMIT: u64 = 4194304;
/// Raw information read from manifest.toml
#[derive(Debug, Deserialize, Default)]
#[non_exhaustive]
@@ -178,6 +174,13 @@ pub struct StatusUpdateItem {
/// for a voting app.
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
/// Unique ID for deduplication.
/// This can be used if the message is sent over multiple transports.
///
/// If there is no ID, message is always considered to be unique.
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<String>,
}
/// Update items as passed to the UIs.
@@ -210,14 +213,6 @@ impl Context {
return Ok(false);
}
if file.len() as u64 > WEBXDC_RECEIVING_LIMIT {
info!(
self,
"{} exceeds receiving limit of {} bytes", &filename, WEBXDC_RECEIVING_LIMIT
);
return Ok(false);
}
let archive = match async_zip::read::mem::ZipFileReader::new(file.to_vec()).await {
Ok(archive) => archive,
Err(_) => {
@@ -317,7 +312,14 @@ impl Context {
timestamp: i64,
can_info_msg: bool,
from_id: ContactId,
) -> Result<StatusUpdateSerial> {
) -> Result<Option<StatusUpdateSerial>> {
let Some(status_update_serial) = self
.write_status_update_inner(&instance.id, &status_update_item)
.await?
else {
return Ok(None);
};
if can_info_msg {
if let Some(ref info) = status_update_item.info {
if let Some(info_msg_id) =
@@ -376,10 +378,6 @@ impl Context {
self.emit_msgs_changed(instance.chat_id, instance.id);
}
let status_update_serial = self
.write_status_update_inner(&instance.id, status_update_item)
.await?;
if instance.viewtype == Viewtype::Webxdc {
self.emit_event(EventType::WebxdcStatusUpdate {
msg_id: instance.id,
@@ -387,23 +385,43 @@ impl Context {
});
}
Ok(status_update_serial)
Ok(Some(status_update_serial))
}
/// Inserts a status update item into `msgs_status_updates` table.
///
/// Returns serial ID of the status update if a new item is inserted.
pub(crate) async fn write_status_update_inner(
&self,
instance_id: &MsgId,
status_update_item: StatusUpdateItem,
) -> Result<StatusUpdateSerial> {
let rowid = self
status_update_item: &StatusUpdateItem,
) -> Result<Option<StatusUpdateSerial>> {
let _lock = self.sql.write_lock().await;
let uid = status_update_item.uid.as_deref();
let Some(rowid) = self
.sql
.insert(
"INSERT INTO msgs_status_updates (msg_id, update_item) VALUES(?, ?);",
(instance_id, serde_json::to_string(&status_update_item)?),
.query_row_optional(
"INSERT INTO msgs_status_updates (msg_id, update_item, uid) VALUES(?, ?, ?)
ON CONFLICT (uid) DO NOTHING
RETURNING id",
(
instance_id,
serde_json::to_string(&status_update_item)?,
uid,
),
|row| {
let id: u32 = row.get(0)?;
Ok(id)
},
)
.await?;
let status_update_serial = StatusUpdateSerial(u32::try_from(rowid)?);
Ok(status_update_serial)
.await?
else {
let uid = uid.unwrap_or("-");
info!(self, "Ignoring duplicate status update with uid={uid}");
return Ok(None);
};
let status_update_serial = StatusUpdateSerial(rowid);
Ok(Some(status_update_serial))
}
/// Returns the update_item with `status_update_serial` from the webxdc with message id `msg_id`.
@@ -432,13 +450,8 @@ impl Context {
update_str: &str,
descr: &str,
) -> Result<()> {
let status_update_item: StatusUpdateItem =
if let Ok(item) = serde_json::from_str::<StatusUpdateItem>(update_str) {
item
} else {
bail!("create_status_update_record: no valid update item.");
};
let status_update_item: StatusUpdateItem = serde_json::from_str(update_str)
.with_context(|| format!("Failed to parse webxdc update item from {update_str:?}"))?;
self.send_webxdc_status_update_struct(instance_msg_id, status_update_item, descr)
.await?;
Ok(())
@@ -449,17 +462,27 @@ impl Context {
pub async fn send_webxdc_status_update_struct(
&self,
instance_msg_id: MsgId,
status_update: StatusUpdateItem,
mut status_update: StatusUpdateItem,
descr: &str,
) -> Result<()> {
let mut instance = Message::load_from_db(self, instance_msg_id).await?;
if instance.viewtype != Viewtype::Webxdc {
bail!("send_webxdc_status_update: is no webxdc message");
let mut instance = Message::load_from_db(self, instance_msg_id)
.await
.with_context(|| {
format!("Failed to load message {instance_msg_id} from the database")
})?;
let viewtype = instance.viewtype;
if viewtype != Viewtype::Webxdc {
bail!("send_webxdc_status_update: message {instance_msg_id} is not a webxdc message, but a {viewtype} message.");
}
let chat = Chat::load_from_db(self, instance.chat_id).await?;
if let Some(reason) = chat.why_cant_send(self).await? {
bail!("cannot send to {}: {}", chat.id, reason);
let chat_id = instance.chat_id;
let chat = Chat::load_from_db(self, chat_id)
.await
.with_context(|| format!("Failed to load chat {chat_id} from the database"))?;
if let Some(reason) = chat.why_cant_send(self).await.with_context(|| {
format!("Failed to check if webxdc update can be sent to chat {chat_id}")
})? {
bail!("Cannot send to {chat_id}: {reason}.");
}
let send_now = !matches!(
@@ -467,6 +490,7 @@ impl Context {
MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft
);
status_update.uid = Some(create_id());
let status_update_serial: StatusUpdateSerial = self
.create_status_update_record(
&mut instance,
@@ -475,7 +499,9 @@ impl Context {
send_now,
ContactId::SELF,
)
.await?;
.await
.context("Failed to create status update")?
.context("Duplicate status update UID was generated")?;
if send_now {
self.sql.insert(
@@ -483,7 +509,7 @@ impl Context {
ON CONFLICT(msg_id)
DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr",
(instance.id, status_update_serial, status_update_serial, descr),
).await?;
).await.context("Failed to insert webxdc update into SMTP queue")?;
self.scheduler.interrupt_smtp().await;
}
Ok(())
@@ -655,7 +681,10 @@ impl Context {
let (update_item_str, serial) = row;
let update_item = StatusUpdateItemAndSerial
{
item: serde_json::from_str(&update_item_str)?,
item: StatusUpdateItem {
uid: None, // Erase UIDs, apps, bots and tests don't need to know them.
..serde_json::from_str(&update_item_str)?
},
serial,
max_serial,
};
@@ -863,8 +892,6 @@ mod tests {
async fn test_webxdc_file_limits() -> Result<()> {
assert!(WEBXDC_SENDING_LIMIT >= 32768);
assert!(WEBXDC_SENDING_LIMIT < 16777216);
assert!(WEBXDC_RECEIVING_LIMIT >= WEBXDC_SENDING_LIMIT * 2);
assert!(WEBXDC_RECEIVING_LIMIT < 16777216);
Ok(())
}
@@ -1348,17 +1375,38 @@ mod tests {
info: None,
document: None,
summary: None,
uid: Some("iecie2Ze".to_string()),
},
1640178619,
true,
ContactId::SELF,
)
.await?
.unwrap();
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
// Update with duplicate update ID is received.
// Whatever the payload is, update should be ignored just because ID is duplicate.
let update_id1_duplicate = t
.create_status_update_record(
&mut instance,
StatusUpdateItem {
payload: json!({"nothing": "this should be ignored"}),
info: None,
document: None,
summary: None,
uid: Some("iecie2Ze".to_string()),
},
1640178619,
true,
ContactId::SELF,
)
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":1}]"#
);
assert_eq!(update_id1_duplicate, None);
assert!(t
.send_webxdc_status_update(instance.id, "\n\n\n", "")
@@ -1384,15 +1432,17 @@ mod tests {
info: None,
document: None,
summary: None,
uid: None,
},
1640178619,
true,
ContactId::SELF,
)
.await?;
.await?
.unwrap();
assert_eq!(
t.get_webxdc_status_updates(instance.id, update_id1).await?,
r#"[{"payload":{"foo2":"bar2"},"serial":2,"max_serial":2}]"#
r#"[{"payload":{"foo2":"bar2"},"serial":3,"max_serial":3}]"#
);
t.create_status_update_record(
&mut instance,
@@ -1401,6 +1451,7 @@ mod tests {
info: None,
document: None,
summary: None,
uid: None,
},
1640178619,
true,
@@ -1410,9 +1461,9 @@ mod tests {
assert_eq!(
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":3},
{"payload":{"foo2":"bar2"},"serial":2,"max_serial":3},
{"payload":true,"serial":3,"max_serial":3}]"#
r#"[{"payload":{"foo":"bar"},"serial":1,"max_serial":4},
{"payload":{"foo2":"bar2"},"serial":3,"max_serial":4},
{"payload":true,"serial":4,"max_serial":4}]"#
);
t.send_webxdc_status_update(
@@ -1423,8 +1474,8 @@ mod tests {
.await?;
assert_eq!(
t.get_webxdc_status_updates(instance.id, update_id2).await?,
r#"[{"payload":true,"serial":3,"max_serial":4},
{"payload":1,"serial":4,"max_serial":4}]"#
r#"[{"payload":true,"serial":4,"max_serial":5},
{"payload":1,"serial":5,"max_serial":5}]"#
);
Ok(())
@@ -1468,7 +1519,7 @@ mod tests {
t.receive_status_update(
ContactId::SELF,
instance.id,
r#"{"updates":[{"payload":{"foo":"bar"}}]}"#,
r#"{"updates":[{"payload":{"foo":"bar"}, "someTrash": "definitely TrAsH"}]}"#,
)
.await?;
assert_eq!(
@@ -1654,6 +1705,8 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_webxdc_status_update_object_range() -> Result<()> {
use regex::Regex;
let t = TestContext::new_alice().await;
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
let instance = send_webxdc_instance(&t, chat_id).await?;
@@ -1672,7 +1725,13 @@ mod tests {
)
.await?
.unwrap();
assert_eq!(json, "{\"updates\":[{\"payload\":2},\n{\"payload\":3}]}");
let json = Regex::new(r#""uid":"[^"]*""#)
.unwrap()
.replace_all(&json, "XXX");
assert_eq!(
json,
"{\"updates\":[{\"payload\":2,XXX},\n{\"payload\":3,XXX}]}"
);
assert_eq!(
t.sql

View File

@@ -0,0 +1,91 @@
From - Sun, 19 Nov 2023 01:08:24 GMT
X-Mozilla-Status: 0800
X-Mozilla-Status2: 00000000
Message-ID: <38a2a29b-8261-403b-abb5-56b0a87d2ff4@example.org>
Date: Sat, 18 Nov 2023 22:08:23 -0300
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Content-Language: en-US
To: bob@example.net
From: Alice <alice@example.org>
Autocrypt: addr=alice@example.org; keydata=
xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E/jFiKZWj
1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37mdw+DKs0YvNIlc+A
RjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsrtS2JgJ+tLSYUvNJeMJXm/cDL
XKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJF0De11/7G62dHNHuhmtgRLsTN4Q372Q9
KNdYEFLHaN91jEzyD/+aHNskATxtcGhppI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yf
VAyA69t5fctQRb4+bTwL+sS9KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiL
vYUfdNJstAqvLf04mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3E
q8e6TbOY7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT
AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcFFQgJCgsF
FgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nnYq5pOuHGUndZ7jYK
cOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdtHoulEG4RPGVboDaY9tuMOL3/
GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJ
x15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQl
nfISJ17GBLmH1YxmPPZ3CRHC6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJ
YskyNndtv0iaNRT7YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ
8myMwA+ybfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw
eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVFuPGrSdRU
06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXFpgtZUNV3iGOSOcSE
LldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6FdKYxVd4NBVH9abZ7t8Tm4qC
urZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBgU3q5W+wrtZ56kI9mxJec62KHpyLZ0rTE
xEAeVbChUJOo11vUtJfTrDhI6lhqyr72o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTS
UxOz60xNggEfDVtfgfjBZrBbiHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h
8l019MYmGadpQgbuA4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfu
g2fuRf258Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY
AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXwByRZ5Hri
EOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxCjWrnUDvvJEwP1W3j
UXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ9eLjEM++rbOD4vWbYXRwaDiH
LetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvgPxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYT
XhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+bQw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajj
Wy7b9TuT38t1HArv4m/LyVuBHiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pK
MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa
j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa
/qMLjKwBpKEd/w==
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
attachmentreminder=0; deliveryformat=0
X-Identity-Key: id3
Fcc: imap://alice%40example.org@in.example.org/Sent
Subject: Hello!
Content-Type: multipart/encrypted;
protocol="application/pgp-encrypted";
boundary="------------HVIlnYsnz3YPWPVlor40isrE"
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------HVIlnYsnz3YPWPVlor40isrE
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------HVIlnYsnz3YPWPVlor40isrE
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
wcDMA/K57StIWPW6AQv/cTRuyZRuX+Qkmxcbo34Df32PL2O3LSuwChdd4pd+WRNU5r08E0eLeFGv
k/C/2iFrNhgmvsJEnDtTZVc7+qykMeXwwC1f2qT7OAvTbAF9Me1dBCQy8QAPSzOowNu8434qOPwz
fZkoB4pdkKlAhGMUBdtbWYg3EDdpBUCpAuT8l3qhlfdSTRh+da2UVh9kix3SXSmLarMZGlJikh3T
pRLAlPOtRa7k5R4rc/OCD9HUL9EHGUk4rV86+mr4Wpp1aIhdTNksfrFXvgRFx0dcU33uS74X/zme
c1AOuJK8ed3F2Zc5IYTyE+ps1jBqVH1+CM697d5FnIMx96VyB4txDBBBjyq+oh+SY+xUxvtR/EzN
mR0qfiSCwOKwGdzPWDkBoCePYnykiR4shVdVV3qjYZwBmY5IWNahyBtIB470V4wMiJXdZFMQu5XD
NO4DJ6/x2UCcUiUokAwvKTSHflAHocZJ2ICfzhlJjJSiqEttwyG0xU0ZhTdZIbwzRWzYzDgMakFO
wcBMA+PY3JvEjuMiAQf+MXJB44iQ/Ti9WDH3MOX0N+X0APf5mzeqU9MpSZ+7mAnTRVmz/iMD989J
ngxu1mv3vQBjdNokIAfBYk29qyRBULXaof+6x5VJSWoopV6t1vNd+DB2HgLkbdCJuzikapCE+QAm
gmgoknQap+3l4D1RkMys7w1awsMYK0wR1Iucjb8M2I4f5ObPSMS5211ZBJuYOf1OQt0jX1jCNTOi
Q5tbufJ6EjAvP6XOYTY3in8+p7yocBgQhXaK8NB8jdg+h6IE/NNX1W5v1tMx17WRtIQGwh4cOlB6
Fsu87eMx8x9Ew2YdpN6yvIHddy9M9k3NCROT+5rIq7+1GTy0WoI/KxNwtdLBEwE2h7VFmJ5qEWhi
lmunWBIlA71IdNqpi+9lbg/QPwCgvRow05Gv0FyKbSvDA/fN18+CLuz8RicNFRbiPgwxMLRE3lZB
jj7BEDa53fjnjBJG4lA9mUaB/ALScQwkGXqKPpeDN4Iexy8eBsZoBczJcPna7GNgSYiXxbWo2P8+
T71oSht+igsyi8gYDNwmhcsQxSWtF7f53irOKWgEpgz2hMjWi764GGBzAJ231suyV2eZXVJm2Im7
RUDu3bJNu/Q/CvxQQdcUXHEoHrTzDZ3KQPW0/oYln2WZwkRQcVVGSLVINgVTdTkI8GEuXH/4yjYF
tUq68esMQ2WOWZ7IiTNPU9T2F5kEwR2sf55XgLckj0OLJn5CkSAWBJhHdL/9er06u8ksTs6V9UEo
i+15XUAiNzsuIcydoGF9sSuO9nm62xYO8uY8yQl/z2Q7CXKefkXBSeaNcYyQKrCnLfwnuvR2WFE5
OJ0HM5i/qcWmqWSvSYzdDqUbI95q9Fgn78F/bcJ9ZTdPDkWLi//SUzcWeUWgRC57U4CFTApj74EJ
lT/gVlH/y88RNcvGGAPHHS11adLmln7CbLEF9tmRM3ou78nMm7VhXT8C/QZiTvJgG3yZbeRFlC39
=hdiO
-----END PGP MESSAGE-----
--------------HVIlnYsnz3YPWPVlor40isrE--