mirror of
https://github.com/chatmail/core.git
synced 2026-10-04 04:00:27 +03:00
Compare commits
56
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c41cac76d2 | ||
|
|
3de65e4f0e | ||
|
|
2a5058a308 | ||
|
|
b79125b947 | ||
|
|
ed647aa49c | ||
|
|
8858c01f3f | ||
|
|
54b093d89f | ||
|
|
b6f116d26a | ||
|
|
362cc76c83 | ||
|
|
aa0800b6e0 | ||
|
|
5a5597e990 | ||
|
|
94cc8a3858 | ||
|
|
6b62fed6fa | ||
|
|
b766a9a8fa | ||
|
|
3d3a6352e9 | ||
|
|
0eddb98354 | ||
|
|
72e711e59a | ||
|
|
a144a450e1 | ||
|
|
b87cd8937a | ||
|
|
fae6a63023 | ||
|
|
54e16429b8 | ||
|
|
946120e427 | ||
|
|
e1d58706ce | ||
|
|
7876115d86 | ||
|
|
0a7e697f50 | ||
|
|
06150d6dc2 | ||
|
|
310dda494c | ||
|
|
8f39d7510f | ||
|
|
44ab013351 | ||
|
|
f7c97d8557 | ||
|
|
78caa6f53c | ||
|
|
349c9650e3 | ||
|
|
96e84d85a5 | ||
|
|
57b2c000e6 | ||
|
|
8014c2af2f | ||
|
|
5db55ac4de | ||
|
|
44c2febbe1 | ||
|
|
555b8b8b51 | ||
|
|
dc9bba0697 | ||
|
|
3858c47ceb | ||
|
|
6b1d982cfc | ||
|
|
5a71fe101e | ||
|
|
c41657d7fd | ||
|
|
6c3a866892 | ||
|
|
5007fc49c1 | ||
|
|
b1da53a56b | ||
|
|
d8912a98ac | ||
|
|
373f1840a6 | ||
|
|
81982273e8 | ||
|
|
5be489790f | ||
|
|
81ca5aa1ca | ||
|
|
15488ed210 | ||
|
|
f03088f247 | ||
|
|
d4115e720d | ||
|
|
abb0a119cb | ||
|
|
63364da79c |
+110
@@ -1,5 +1,112 @@
|
||||
# Changelog
|
||||
|
||||
## [2.62.0] - 2026-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Remove unused ImapMessageMoved event.
|
||||
- [**breaking**] get rid of `Qr::FprWithoutAddr` and `Qr::FprMismatch` variants.
|
||||
- removed `DC_QR_FPR_WITHOUT_ADDR` and `DC_QR_FPR_MISMATCH` constants from CFFI
|
||||
- removed FprWithoutAddr and FprMismatch variants from JSON-RPC QrObject returned by `check_qr`
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Don't show "member added" messages in channels ([#8730](https://github.com/chatmail/core/pull/8730)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't notify about a reaction sent by a blocked contact ([#8729](https://github.com/chatmail/core/pull/8729)).
|
||||
- keep a message read status if a failure arrives later.
|
||||
- prefer login errors over connection errors in the log message ([#8727](https://github.com/chatmail/core/pull/8727)).
|
||||
- blocked or email contacts contacts are never recently seen or old ([#8733](https://github.com/chatmail/core/pull/8733)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove code that moves messages between IMAP folders.
|
||||
|
||||
## [2.61.0] - 2026-09-21
|
||||
|
||||
### API-Changes
|
||||
|
||||
- add `init_transports()` for multi-relay onboarding.
|
||||
- add JSON-RPC API `is_sending_finished()`.
|
||||
- [**breaking**] remove verification methods from the FFI and JSON-RPC APIs.
|
||||
- `dc_contact_is_verified()` and `dc_contact_get_verifier_id()` are removed.
|
||||
- the JSON-RPC Contact object loses the `isVerified` and `verifierId` fields. A bot reading `snapshot.is_verified` now gets an `AttributeError` at runtime.
|
||||
- the Python bindings lose `Contact.is_verified()` and `Contact.get_verifier()`.
|
||||
- `DC_STR_CONTACT_VERIFIED` (35) is removed, so UIs should stop registering a translation for it. A stock id core does not know is logged and otherwise ignored, so an un-updated client keeps working.
|
||||
- [**breaking**] remove default value for "addr" config.
|
||||
- [**breaking**] remove `addr` field from Account objects in JSON-RPC APIs.
|
||||
- `list_transports()` should be used instead.
|
||||
- [**breaking**] remove `is_chatmail` and the XCHATMAIL capability.
|
||||
- `is_chatmail` is no longer a known config key.
|
||||
- [**breaking**] remove `Contact.get_name_n_addr()` and related APIs.
|
||||
- `dc_contact_get_name_n_addr()` CFFI is removed
|
||||
- JSON-RPC contact objects don't have nameAndAddr field anymore
|
||||
- [**breaking**] replace `was_seen_recently` by `freshness` in contact object.
|
||||
- use contact's `freshness` instead of `seen_recently`
|
||||
|
||||
### Fixes
|
||||
|
||||
- always emit `AccountsBackgroundFetchDone`.
|
||||
- make `background_fetch` not wait on or trigger SMTP connections.
|
||||
- do not send a sync message when changing `configured_addr`.
|
||||
- emit `SmtpMessageSent` event after deleting the message from SMTP queue.
|
||||
- don't use extra STUN nine server for fallback.
|
||||
- use `max_smtp_rcpt_to` chunking for the actual transport we are sending from.
|
||||
- use correct `From` address when sending MDNs.
|
||||
- use correct address for Bcc-self in unencrypted mails.
|
||||
- don't emit configure progress events during background relay additions.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- better quality of image recoding ([#8682](https://github.com/chatmail/core/pull/8682)).
|
||||
- perform background fetch from all transports.
|
||||
- queue messages for SMTP before encryption.
|
||||
- [**breaking**] stop tracking contact verification.
|
||||
- the statistics JSON sent to the self-reporting-bot on Android changes: Contacts have `encrypted` instead of `verified` and lose `transitive_chain` properties and message stats have `encrypted` instead of `verified` and `unverified_encrypted`, and securejoin invites lose `already_verified`. The collecting bot stores incoming reports verbatim but analysis will have to make sense of older and newer reports.
|
||||
- remove last usage of XDELTAPUSH capability.
|
||||
- base server-side message deletion on `force_encryption`.
|
||||
- do not restart I/O when setting `configured_addr`.
|
||||
- do not use ConfiguredAddr when connecting to SMTP.
|
||||
- mark autorelays for relay operators ([#8701](https://github.com/chatmail/core/pull/8701)).
|
||||
- try fasted relays to attempt first configure on.
|
||||
- do not mark message as failed for which we got a read receipt before.
|
||||
- a single NDN does not mark a group message as failed.
|
||||
|
||||
### Documentation
|
||||
|
||||
- update `sys.msgsize_max_recommended` documentation.
|
||||
- add hint about how to reset an invitation ([#8160](https://github.com/chatmail/core/pull/8160)).
|
||||
- mention `get_app_version()` API in the changelog for 2.59.0.
|
||||
- remove `protect_autocrypt` setting.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cleanup "primary" wording in comments.
|
||||
- update rustls to 0.23.45.
|
||||
- fix nightly "cargo" warnings.
|
||||
- fix some types in `deltachat_rpc_client`.
|
||||
|
||||
### Refactor
|
||||
|
||||
- add `Encryption.is_encrypted()`.
|
||||
- separate QueuedEncryption.
|
||||
- sql: disable double-quoted string literals.
|
||||
- add smtp::queue module.
|
||||
- substitute configure progress macro with simple function call.
|
||||
|
||||
### Tests
|
||||
|
||||
- cleanup `get_smtp_rows_for_msg()`.
|
||||
- cross-core securejoin invites for every chat type.
|
||||
- explicitly empty url in appversion updates ([#8702](https://github.com/chatmail/core/pull/8702)).
|
||||
- do not talk about "inconsistent key state" in `test_securejoin_after_contact_resetup`.
|
||||
- rename `test_aeap_transition_{0,1}` to `test_aeap_transition_{single,group}`.
|
||||
- do not fetch all messages when `direct_imap` is created.
|
||||
- `direct_imap`: always pass `mark_seen=False` to fetch().
|
||||
- add pseudo transport explicitly rather than by setting ConfiguredAddr.
|
||||
|
||||
## [2.60.0] - 2026-09-11
|
||||
|
||||
### API-Changes
|
||||
@@ -106,6 +213,7 @@
|
||||
|
||||
- [**breaking**] Remove deprecated `dc_chat_is_protected()`.
|
||||
- Deprecate `dc_chat_get_info_json()` ([#8580](https://github.com/chatmail/core/pull/8580))
|
||||
- New `get_app_version()` JSON-RPC API to get information about available updates.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
@@ -8793,3 +8901,5 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[2.58.0]: https://github.com/chatmail/core/compare/v2.57.0..v2.58.0
|
||||
[2.59.0]: https://github.com/chatmail/core/compare/v2.58.0..v2.59.0
|
||||
[2.60.0]: https://github.com/chatmail/core/compare/v2.59.0..v2.60.0
|
||||
[2.61.0]: https://github.com/chatmail/core/compare/v2.60.0..v2.61.0
|
||||
[2.62.0]: https://github.com/chatmail/core/compare/v2.61.0..v2.62.0
|
||||
|
||||
Generated
+11
-11
@@ -1327,7 +1327,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -1435,7 +1435,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.5.0",
|
||||
@@ -1456,14 +1456,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"deltachat-jsonrpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1479,7 +1479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1508,7 +1508,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -5282,9 +5282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.37"
|
||||
version = "0.23.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||
checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"brotli-decompressor",
|
||||
@@ -5292,7 +5292,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.13",
|
||||
"rustls-webpki 0.103.15",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -5329,9 +5329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.89"
|
||||
@@ -184,7 +184,6 @@ base64 = "0.22"
|
||||
chrono = { version = "0.4.44", default-features = false }
|
||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
||||
deltachat-jsonrpc-bindings = { path = "deltachat-jsonrpc-bindings", default-features = false }
|
||||
deltachat = { path = ".", default-features = false }
|
||||
futures = "0.3.32"
|
||||
futures-lite = "2.6.1"
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
let mut name = sanitize_name(name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)".
|
||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
||||
if name == addr {
|
||||
name = "".to_string();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
+88
-124
@@ -453,18 +453,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* always auto-downloaded.
|
||||
* 0 = no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `protect_autocrypt` = Enable Header Protection for Autocrypt header.
|
||||
* This is an experimental option not compatible to other MUAs
|
||||
* and older Delta Chat versions.
|
||||
* 1 = enable.
|
||||
* 0 = disable (default).
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `is_chatmail` = (deprecated) 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* This is deprecated, UIs should not behave differently
|
||||
* for chatmail relays and classical email servers.
|
||||
* Most usages in UIs can be replaced by `force_encryption`.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
@@ -697,6 +688,21 @@ char* dc_get_connectivity_html (dc_context_t* context);
|
||||
void dc_configure (dc_context_t* context);
|
||||
|
||||
|
||||
/**
|
||||
* Add fake transport that cannot be used to connect.
|
||||
*
|
||||
* Used for offline tests only.
|
||||
*
|
||||
* To add a transport, use JSON-RPC calls `add_or_update_transport`
|
||||
* and `add_transport_from_qr` instead.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param addr The email address of the new transport.
|
||||
*/
|
||||
void dc_add_pseudo_transport (dc_context_t* context, const char *addr);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the context is already configured.
|
||||
*
|
||||
@@ -1778,8 +1784,6 @@ int dc_is_contact_in_chat (dc_context_t* context, uint32_t ch
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2278,7 +2282,7 @@ void dc_block_contact (dc_context_t* context, uint32_t co
|
||||
/**
|
||||
* Get encryption info for a contact.
|
||||
* Get a multi-line encryption info, containing your fingerprint and the
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -2445,14 +2449,12 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
|
||||
void dc_stop_ongoing_process (dc_context_t* context);
|
||||
|
||||
|
||||
// out-of-band verification
|
||||
// securejoin
|
||||
|
||||
#define DC_QR_ASK_VERIFYCONTACT 200 // id=contact
|
||||
#define DC_QR_ASK_VERIFYGROUP 202 // text1=groupname
|
||||
#define DC_QR_ASK_VERIFYBROADCAST 204 // text1=broadcast name
|
||||
#define DC_QR_FPR_OK 210 // id=contact
|
||||
#define DC_QR_FPR_MISMATCH 220 // id=contact
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_BACKUP_TOO_NEW 255
|
||||
@@ -2480,7 +2482,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* The QR code state is returned in dc_lot_t::state as:
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYCONTACT with dc_lot_t::id=Contact ID:
|
||||
* ask whether to verify the contact;
|
||||
* ask whether to start chatting with the contact;
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
@@ -2489,17 +2491,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, start the protocol with dc_join_securejoin().
|
||||
*
|
||||
* - DC_QR_FPR_OK with dc_lot_t::id=Contact ID:
|
||||
* contact fingerprint verified,
|
||||
* contact fingerprint matches,
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
|
||||
* scanned fingerprint does not match last seen fingerprint.
|
||||
*
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
|
||||
* the scanned QR code contains a fingerprint but no e-mail address;
|
||||
* suggest the user to establish an encrypted connection first.
|
||||
*
|
||||
* - DC_QR_ACCOUNT dc_lot_t::text1=domain:
|
||||
* ask the user if they want to create an account on the given domain,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
@@ -2565,23 +2560,24 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
|
||||
|
||||
|
||||
/**
|
||||
* Get QR code text that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* Get QR code text that will offer a SecureJoin invitation.
|
||||
*
|
||||
* The scanning device will pass the scanned content to dc_check_qr() then;
|
||||
* if dc_check_qr() returns
|
||||
* DC_QR_ASK_VERIFYCONTACT, DC_QR_ASK_VERIFYGROUP or DC_QR_ASK_VERIFYBROADCAST
|
||||
* an out-of-band-verification can be joined using dc_join_securejoin()
|
||||
* the SecureJoin protocol can be started using dc_join_securejoin()
|
||||
*
|
||||
* The returned text will also work as a normal https:-link,
|
||||
* so that the QR code is useful also without Delta Chat being installed
|
||||
* or can be passed to contacts through other channels.
|
||||
*
|
||||
* To reset invitations, pass the link to dc_set_config_from_qr().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param chat_id If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If set to 0, the Setup-Contact protocol is offered in the QR code.
|
||||
* the SecureJoin QR code for the group is returned.
|
||||
* If set to 0, the setup contact QR code is returned.
|
||||
* See https://securejoin.delta.chat/
|
||||
* for details about both protocols.
|
||||
* @return The text that should go to the QR code,
|
||||
@@ -2607,7 +2603,7 @@ char* dc_get_securejoin_qr (dc_context_t* context, uint32_t ch
|
||||
char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* Continue the SecureJoin protocol
|
||||
* started on another device with dc_get_securejoin_qr().
|
||||
* This function is typically called when dc_check_qr() returns
|
||||
* lot.state=DC_QR_ASK_VERIFYCONTACT, lot.state=DC_QR_ASK_VERIFYGROUP or lot.state=DC_QR_ASK_VERIFYBROADCAST
|
||||
@@ -3188,17 +3184,28 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
|
||||
/**
|
||||
* Perform a background fetch for all accounts in parallel with a timeout.
|
||||
* Pauses the scheduler, fetches from all transports at once and then resumes the scheduler.
|
||||
* The fetch for an account ends as soon as one of its transports received messages.
|
||||
*
|
||||
* dc_accounts_background_fetch() was created for the iOS Background fetch.
|
||||
* For an account with IO stopped, the scheduler is paused
|
||||
* and every transport is fetched concurrently on a dedicated connection.
|
||||
* The account is done as soon as one transport received messages, the others stop.
|
||||
* Only one batch of messages is fetched per transport this way,
|
||||
* so a larger backlog is left to the next call or to started IO.
|
||||
*
|
||||
* For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
* and the account is done once every transport is.
|
||||
*
|
||||
* The call never waits for outgoing messages and never triggers sending them itself.
|
||||
* Received messages may still queue replies, securejoin handshakes for example,
|
||||
* which go out only while IO is running.
|
||||
*
|
||||
* The `DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE` event is emitted at the end,
|
||||
* also on timeout, when another background fetch is already running
|
||||
* and when the call is ignored because the timeout is too small,
|
||||
* so it is safe to wait for the event whenever `accounts` is not NULL.
|
||||
* Process all events until you get this one and you can safely return to the background
|
||||
* without forgetting to create notifications caused by timing race conditions.
|
||||
* without forgetting to create a generic notification if no message was fetched.
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
@@ -3573,7 +3580,6 @@ dc_lot_t* dc_chatlist_get_summary2 (dc_context_t* context, uint32_t ch
|
||||
* last-message-state: @ref DC_STATE constant
|
||||
* last-message-date:
|
||||
* avatar-path: path-to-blobfile
|
||||
* is_verified: yes/no
|
||||
* @return a UTF8-encoded JSON string containing all requested info. Must be freed using dc_str_unref(). NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_info_json (dc_context_t* context, size_t chat_id);
|
||||
@@ -4897,6 +4903,38 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
int dc_msg_is_pinned (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* @defgroup DC_FRESHNESS DC_FRESHNESS
|
||||
*
|
||||
* These constants describe the freshness of a contact,
|
||||
* as returned by dc_contact_get_freshness().
|
||||
*
|
||||
* @addtogroup DC_FRESHNESS
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contact shall not be highlighted, e.g. neither shown with a "seen recently" dot
|
||||
* nor with a "not seen for a long time" hint.
|
||||
*/
|
||||
#define DC_FRESHNESS_NORMAL 0
|
||||
|
||||
/**
|
||||
* Contact was seen recently, the UI shall highlight it e.g. with a little green dot on the avatar.
|
||||
*/
|
||||
#define DC_FRESHNESS_RECENTLY_SEEN 1
|
||||
|
||||
/**
|
||||
* Contact was not seen for a long time, the UI shall highlight it e.g. with a string
|
||||
* below the contact name (e.g. "Seen 2 months ago").
|
||||
*/
|
||||
#define DC_FRESHNESS_OLD 2
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_contact_t
|
||||
*
|
||||
@@ -4912,7 +4950,6 @@ uint32_t dc_msg_get_saved_msg_id (const dc_msg_t* msg);
|
||||
* By default, these names are equal,
|
||||
* but functions working with contact names
|
||||
* (e.g. dc_contact_get_name(), dc_contact_get_display_name(),
|
||||
* dc_contact_get_name_n_addr(),
|
||||
* dc_create_contact() or dc_add_address_book())
|
||||
* only affect the given-name.
|
||||
*/
|
||||
@@ -4962,7 +4999,7 @@ char* dc_contact_get_addr (const dc_contact_t* contact);
|
||||
* The function does not return the contact name as received from the network.
|
||||
*
|
||||
* This name is typically used in a form where the user can edit the name of a contact.
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name() or dc_contact_get_name_n_addr().
|
||||
* To get a fine name to display in lists etc., use dc_contact_get_display_name().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5017,23 +5054,6 @@ char* dc_contact_get_display_name (const dc_contact_t* contact);
|
||||
#define dc_contact_get_first_name dc_contact_get_display_name
|
||||
|
||||
|
||||
/**
|
||||
* Get a summary of name and address.
|
||||
*
|
||||
* The returned string is either "Name (email@domain.com)" or just
|
||||
* "email@domain.com" if the name is unset.
|
||||
*
|
||||
* The summary is typically used when asking the user something about the contact.
|
||||
* The attached e-mail address makes the question unique, e.g. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return A summary string, must be released using dc_str_unref().
|
||||
* Never returns NULL.
|
||||
*/
|
||||
char* dc_contact_get_name_n_addr (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Get the contact's profile image.
|
||||
* This is the image set by each remote user on their own
|
||||
@@ -5087,18 +5107,18 @@ int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact was seen recently.
|
||||
* Get the contact's freshness.
|
||||
*
|
||||
* The UI shall hightlight contacts that are recently seen by a little green dot on the avatar
|
||||
* and contacts that were not seen for a long time by a string below the contact name (e.g. "Seen 2 months ago")
|
||||
*
|
||||
* The UI may highlight these contacts,
|
||||
* eg. draw a little green dot on the avatars of the users recently seen.
|
||||
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
|
||||
* To get the time a contact was seen, use dc_contact_get_last_seen().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 1=contact seen recently, 0=contact not seen recently.
|
||||
* @return One of the @ref DC_FRESHNESS constants.
|
||||
*/
|
||||
int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
int dc_contact_get_freshness (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
@@ -5113,19 +5133,6 @@ int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* See dc_contact_get_verifier_id() for a guidance how to display these information.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 0: contact is not verified.
|
||||
* 2: SELF and contact have verified their fingerprints in both directions.
|
||||
*/
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
/**
|
||||
* Returns whether contact is a bot.
|
||||
*
|
||||
@@ -5150,36 +5157,6 @@ int dc_contact_is_bot (dc_contact_t* contact);
|
||||
int dc_contact_is_key_contact (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
*
|
||||
* As verifier may be unknown,
|
||||
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
|
||||
*
|
||||
* UI should display the information in the contact's profile as follows:
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() != 0,
|
||||
* display text "Introduced by ..."
|
||||
* with the name of the contact
|
||||
* formatted by dc_contact_get_name().
|
||||
* Prefix the text by a green checkmark.
|
||||
*
|
||||
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
|
||||
* display "Introduced" prefixed by a green checkmark.
|
||||
*
|
||||
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
|
||||
* display nothing
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
uint32_t dc_contact_get_verifier_id (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_lot_t
|
||||
*
|
||||
@@ -5953,14 +5930,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_DELETED 104
|
||||
|
||||
/**
|
||||
* Emitted when a message was successfully moved on IMAP.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) Info string in English language.
|
||||
*/
|
||||
#define DC_EVENT_IMAP_MESSAGE_MOVED 105
|
||||
|
||||
/**
|
||||
* Emitted before going into IDLE on the Inbox folder.
|
||||
*
|
||||
@@ -6210,7 +6179,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
|
||||
/**
|
||||
* Contact(s) created, renamed, verified, blocked or deleted.
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) contact_id of the changed contact or 0 on batch-changes or deletion.
|
||||
* @param data2 0
|
||||
@@ -6282,8 +6251,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
*
|
||||
* @param data1 (int) The ID of the inviting contact.
|
||||
* @param data2 (int) The progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
* 1000=vg-member-added/vc-contact-confirm received
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
@@ -6374,6 +6342,10 @@ void dc_event_unref(dc_event_t* event);
|
||||
* A call made while another background fetch is running gets the event immediately,
|
||||
* and the running fetch keeps emitting events until its own marker.
|
||||
*
|
||||
* The event carries no data identifying the call it belongs to,
|
||||
* so it marks your own call only if no concurrent background fetch is happening.
|
||||
* Your own call has finished when dc_accounts_background_fetch() returns.
|
||||
*
|
||||
* This event is only emitted by the account manager
|
||||
*/
|
||||
|
||||
@@ -6669,12 +6641,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to build the string returned by dc_get_contact_encrinfo().
|
||||
#define DC_STR_FINGERPRINTS 30
|
||||
|
||||
/// "%1$s verified"
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// - %1$s will be replaced by the name of the verified contact
|
||||
#define DC_STR_CONTACT_VERIFIED 35
|
||||
|
||||
/// "Archived chats"
|
||||
///
|
||||
/// Used as the name for the corresponding chatlist entry.
|
||||
@@ -6853,7 +6819,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Added as an info-message directly after scanning a QR code for joining a group.
|
||||
/// May be followed by the info-messages
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES, #DC_STR_CONTACT_VERIFIED and #DC_STR_MSGADDMEMBER.
|
||||
/// #DC_STR_SECURE_JOIN_REPLIES and #DC_STR_MSGADDMEMBER.
|
||||
///
|
||||
/// `%1$s` and `%2$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_STARTED 117
|
||||
@@ -6862,15 +6828,13 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Info-message on scanning a QR code for joining a group.
|
||||
/// Added after #DC_STR_SECURE_JOIN_STARTED.
|
||||
/// If the handshake allows to skip a step and go for #DC_STR_CONTACT_VERIFIED directly,
|
||||
/// this info-message is skipped.
|
||||
///
|
||||
/// `%1$s` will be replaced by the name of the inviter.
|
||||
#define DC_STR_SECURE_JOIN_REPLIES 118
|
||||
|
||||
/// "Scan to chat with %1$s"
|
||||
///
|
||||
/// Subtitle for verification qrcode svg image generated by the core.
|
||||
/// Subtitle for the invite qrcode svg image generated by the core.
|
||||
///
|
||||
/// `%1$s` will be replaced by name of the inviter.
|
||||
#define DC_STR_SETUP_CONTACT_QR_DESC 119
|
||||
|
||||
+20
-57
@@ -31,6 +31,7 @@ use deltachat::key::preconfigure_keypair;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::qr_code_generator::{create_qr_svg, generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::transport::add_pseudo_transport;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -414,6 +415,21 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
|
||||
spawn_configure(ctx.clone());
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_add_pseudo_transport(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_add_pseudo_transport()");
|
||||
return;
|
||||
}
|
||||
|
||||
let ctx = unsafe { &*context };
|
||||
let addr = to_string_lossy(addr);
|
||||
block_on(add_pseudo_transport(ctx, &addr)).log_err(ctx).ok();
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_is_configured(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -477,7 +493,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ImapConnected(_) => 102,
|
||||
EventType::SmtpMessageSent(_) => 103,
|
||||
EventType::ImapMessageDeleted(_) => 104,
|
||||
EventType::ImapMessageMoved(_) => 105,
|
||||
EventType::ImapInboxIdle => 106,
|
||||
EventType::NewBlobFile(_) => 150,
|
||||
EventType::DeletedBlobFile(_) => 151,
|
||||
@@ -544,7 +559,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
@@ -619,7 +633,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImapConnected(_)
|
||||
| EventType::SmtpMessageSent(_)
|
||||
| EventType::ImapMessageDeleted(_)
|
||||
| EventType::ImapMessageMoved(_)
|
||||
| EventType::ImapInboxIdle
|
||||
| EventType::NewBlobFile(_)
|
||||
| EventType::DeletedBlobFile(_)
|
||||
@@ -714,7 +727,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ImapConnected(msg)
|
||||
| EventType::SmtpMessageSent(msg)
|
||||
| EventType::ImapMessageDeleted(msg)
|
||||
| EventType::ImapMessageMoved(msg)
|
||||
| EventType::NewBlobFile(msg)
|
||||
| EventType::DeletedBlobFile(msg)
|
||||
| EventType::Warning(msg)
|
||||
@@ -2397,7 +2409,7 @@ pub unsafe extern "C" fn dc_get_securejoin_qr_svg(
|
||||
chat_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to generate_verification_qr()");
|
||||
eprintln!("ignoring careless call to dc_get_securejoin_qr_svg()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = unsafe { &*context };
|
||||
@@ -3963,18 +3975,6 @@ pub unsafe extern "C" fn dc_contact_get_display_name(
|
||||
ffi_contact.contact.get_display_name().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_name_n_addr(
|
||||
contact: *mut dc_contact_t,
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_name_n_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.get_name_n_addr().strdup()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_profile_image(
|
||||
contact: *mut dc_contact_t,
|
||||
@@ -4030,13 +4030,13 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
pub unsafe extern "C" fn dc_contact_get_freshness(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
|
||||
eprintln!("ignoring careless call to dc_contact_get_freshness()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
ffi_contact.contact.was_seen_recently() as libc::c_int
|
||||
u32::from(ffi_contact.contact.get_freshness()) as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
@@ -4049,27 +4049,6 @@ pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> li
|
||||
ffi_contact.contact.is_blocked() as libc::c_int
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_is_verified()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
|
||||
if block_on(ffi_contact.contact.is_verified(&ffi_contact.context))
|
||||
.context("is_verified failed")
|
||||
.log_err(&ffi_contact.context)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// Return value is essentially a boolean,
|
||||
// but we return 2 for true for backwards compatibility.
|
||||
2
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
@@ -4088,22 +4067,6 @@ pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -
|
||||
unsafe { (*contact).contact.is_key_contact() as libc::c_int }
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_verifier_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = unsafe { &*contact };
|
||||
let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(&ffi_contact.context))
|
||||
.context("failed to get verifier")
|
||||
.log_err(&ffi_contact.context)
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
verifier_contact_id.to_u32()
|
||||
}
|
||||
// dc_lot_t
|
||||
|
||||
pub type dc_lot_t = lot::Lot;
|
||||
|
||||
@@ -47,8 +47,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
||||
Qr::FprOk { .. } => None,
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::BackupTooNew { .. } => None,
|
||||
@@ -103,8 +101,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
||||
@@ -132,8 +128,6 @@ impl Lot {
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::AskJoinBroadcast { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::BackupTooNew { .. } => Default::default(),
|
||||
@@ -181,12 +175,6 @@ pub enum LotState {
|
||||
/// id=contact
|
||||
QrFprOk = 210,
|
||||
|
||||
/// id=contact
|
||||
QrFprMismatch = 220,
|
||||
|
||||
/// text1=formatted fingerprint
|
||||
QrFprWithoutAddr = 230,
|
||||
|
||||
/// text1=domain
|
||||
QrAccount = 250,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc-bindings"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
description = "Autogenerate DeltaChat JSON-RPC API bindings at build time"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.62.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2024"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -278,10 +278,26 @@ impl CommandApi {
|
||||
|
||||
/// Performs a background fetch for all accounts in parallel with a timeout.
|
||||
///
|
||||
/// For an account with IO stopped, the scheduler is paused
|
||||
/// and every transport is fetched concurrently on a dedicated connection.
|
||||
/// The account is done as soon as one transport received messages, the others stop.
|
||||
/// Only one batch of messages is fetched per transport this way,
|
||||
/// so a larger backlog is left to the next call or to started IO.
|
||||
///
|
||||
/// For an account with IO running, IMAP IDLE is interrupted on every transport
|
||||
/// and the account is done once every transport is.
|
||||
///
|
||||
/// The call never waits for outgoing messages and never triggers sending them itself.
|
||||
/// Received messages may still queue replies, securejoin handshakes for example,
|
||||
/// which go out only while IO is running.
|
||||
/// Use `is_sending_finished()` to tell whether the outgoing queue is empty.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout,
|
||||
/// and immediately if another background fetch is already running.
|
||||
/// Process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// without forgetting to create a generic notification if no message was fetched.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it marks your own call only if no concurrent background fetch is happening.
|
||||
async fn background_fetch(&self, timeout_in_seconds: f64) -> Result<()> {
|
||||
let future = {
|
||||
let lock = self.accounts.read().await;
|
||||
@@ -292,6 +308,11 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops an ongoing `background_fetch()` call, making it return early
|
||||
/// without waiting for the remaining transports or for the timeout.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted as usual.
|
||||
/// Does nothing if no background fetch is running.
|
||||
async fn stop_background_fetch(&self) -> Result<()> {
|
||||
self.accounts.read().await.stop_background_fetch();
|
||||
Ok(())
|
||||
@@ -525,6 +546,18 @@ impl CommandApi {
|
||||
ctx.add_transport_from_qr(&qr).await
|
||||
}
|
||||
|
||||
/// Adds an initial transport on the chatmail relay that answers fastest
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
async fn init_transports(&self, account_id: u32, qr: Option<String>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.init_transports(qr.as_deref()).await
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to remove a transport.
|
||||
@@ -826,6 +859,8 @@ impl CommandApi {
|
||||
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// To reset invitations, pass the link to `set_config_from_qr()`.
|
||||
///
|
||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||
/// If `chat_id` is unset, setup contact QR code is returned.
|
||||
async fn get_chat_securejoin_qr_code(
|
||||
@@ -839,20 +874,19 @@ impl CommandApi {
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
|
||||
/// Get QR code (text and SVG) that will offer a SecureJoin invitation.
|
||||
/// The QR code is compatible to the OPENPGP4FPR format
|
||||
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
///
|
||||
/// The scanning device will pass the scanned content to `checkQr()` then;
|
||||
/// if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
/// an out-of-band-verification can be joined using `secure_join()`
|
||||
/// the securejoin protocol can be started using `secure_join()`
|
||||
///
|
||||
/// @deprecated as of 2026-03; use create_qr_svg(get_chat_securejoin_qr_code()) instead.
|
||||
///
|
||||
/// chat_id: If set to a group-chat-id,
|
||||
/// the Verified-Group-Invite protocol is offered in the QR code;
|
||||
/// works for protected groups as well as for normal groups.
|
||||
/// If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
/// the SecureJoin QR code for the group is returned.
|
||||
/// If not set, the setup contact QR code is returned.
|
||||
/// See https://securejoin.delta.chat/ for details about both protocols.
|
||||
///
|
||||
/// return format: `[code, svg]`
|
||||
@@ -868,7 +902,7 @@ impl CommandApi {
|
||||
Ok((qr, svg))
|
||||
}
|
||||
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
/// Continue the SecureJoin protocol
|
||||
/// started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
/// This function is typically called when `check_qr()` returns
|
||||
/// type=AskVerifyContact or type=AskVerifyGroup.
|
||||
@@ -951,8 +985,6 @@ impl CommandApi {
|
||||
/// If the group is already _promoted_ (any message was sent to the group),
|
||||
/// all group members are informed by a special status message that is sent automatically by this function.
|
||||
///
|
||||
/// If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
///
|
||||
/// Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
async fn add_contact_to_chat(
|
||||
&self,
|
||||
@@ -1774,7 +1806,7 @@ impl CommandApi {
|
||||
|
||||
/// Get encryption info for a contact.
|
||||
/// Get a multi-line encryption info, containing your fingerprint and the
|
||||
/// fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
/// fingerprint of the contact, used e.g. to compare the fingerprints out-of-band.
|
||||
async fn get_contact_encryption_info(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
@@ -13,7 +13,6 @@ pub enum Account {
|
||||
Configured {
|
||||
id: u32,
|
||||
display_name: Option<String>,
|
||||
addr: Option<String>,
|
||||
// size: u32,
|
||||
profile_image: Option<String>,
|
||||
color: String,
|
||||
@@ -29,7 +28,6 @@ impl Account {
|
||||
pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result<Self> {
|
||||
if ctx.is_configured().await? {
|
||||
let display_name = ctx.get_config(Config::Displayname).await?;
|
||||
let addr = ctx.get_config(Config::Addr).await?;
|
||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||
let color = color_int_to_hex_string(
|
||||
Contact::get_by_id(ctx, ContactId::SELF)
|
||||
@@ -41,7 +39,6 @@ impl Account {
|
||||
Ok(Account::Configured {
|
||||
id,
|
||||
display_name,
|
||||
addr,
|
||||
profile_image,
|
||||
color,
|
||||
private_tag,
|
||||
|
||||
@@ -9,6 +9,8 @@ use deltachat::context::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
@@ -69,7 +71,7 @@ pub struct FullChat {
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32,
|
||||
can_send: bool,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
mailing_list_address: Option<String>,
|
||||
}
|
||||
|
||||
@@ -92,16 +94,17 @@ impl FullChat {
|
||||
|
||||
let can_send = chat.can_send(context).await?;
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
let freshness = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.first() {
|
||||
Some(contact) => Contact::get_by_id(context, *contact)
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.context("failed to load contact for get_freshness")?
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
ContactFreshness::Normal
|
||||
};
|
||||
|
||||
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
||||
@@ -126,7 +129,7 @@ impl FullChat {
|
||||
is_muted: chat.is_muted(),
|
||||
ephemeral_timer,
|
||||
can_send,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
mailing_list_address,
|
||||
})
|
||||
}
|
||||
@@ -137,7 +140,7 @@ impl FullChat {
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
/// - self_in_group
|
||||
/// - was_seen_recently
|
||||
/// - freshness
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
|
||||
@@ -11,6 +11,8 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use crate::api::types::contact::ContactFreshness;
|
||||
|
||||
use super::chat::JsonrpcChatType;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
@@ -68,7 +70,7 @@ pub enum ChatListItemFetchResult {
|
||||
is_contact_request: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
last_message_type: Option<MessageViewtype>,
|
||||
last_message_id: Option<u32>,
|
||||
},
|
||||
@@ -127,22 +129,20 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let (dm_chat_contact, freshness) = if chat.get_type() == Chattype::Single {
|
||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||
let contact = chat_contacts.first();
|
||||
let was_seen_recently = match contact {
|
||||
let freshness = match contact {
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
.get_freshness()
|
||||
.into(),
|
||||
None => ContactFreshness::Normal,
|
||||
};
|
||||
(
|
||||
contact.map(|contact_id| contact_id.to_u32()),
|
||||
was_seen_recently,
|
||||
)
|
||||
(contact.map(|contact_id| contact_id.to_u32()), freshness)
|
||||
} else {
|
||||
(None, false)
|
||||
(None, ContactFreshness::Normal)
|
||||
};
|
||||
|
||||
let color = color_int_to_hex_string(chat.get_color(ctx).await?);
|
||||
@@ -170,7 +170,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
freshness,
|
||||
last_message_type: message_type,
|
||||
last_message_id: last_msgid.map(|id| id.to_u32()),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::key::{DcKey, SignedPublicKey};
|
||||
use serde::Serialize;
|
||||
@@ -6,6 +7,27 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum ContactFreshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen,
|
||||
/// Contact was not seen for a long time.
|
||||
Old,
|
||||
}
|
||||
|
||||
impl From<contact::Freshness> for ContactFreshness {
|
||||
fn from(freshness: contact::Freshness) -> Self {
|
||||
match freshness {
|
||||
contact::Freshness::Normal => ContactFreshness::Normal,
|
||||
contact::Freshness::RecentlySeen => ContactFreshness::RecentlySeen,
|
||||
contact::Freshness::Old => ContactFreshness::Old,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
pub struct ContactObject {
|
||||
@@ -17,7 +39,6 @@ pub struct ContactObject {
|
||||
id: u32,
|
||||
name: String,
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// Is the contact a key contact.
|
||||
@@ -31,40 +52,9 @@ pub struct ContactObject {
|
||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
||||
e2ee_avail: bool,
|
||||
|
||||
/// True if the contact
|
||||
/// can be added to protected chats
|
||||
/// because SELF and contact have verified their fingerprints in both directions.
|
||||
///
|
||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
||||
is_verified: bool,
|
||||
|
||||
/// The contact ID that verified a contact.
|
||||
///
|
||||
/// As verifier may be unknown,
|
||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
||||
///
|
||||
/// UI should display the information in the contact's profile as follows:
|
||||
///
|
||||
/// - If `verifierId` != 0,
|
||||
/// display text "Introduced by ..."
|
||||
/// with the name of the contact.
|
||||
/// Prefix the text by a green checkmark.
|
||||
///
|
||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
||||
/// display "Introduced" prefixed by a green checkmark.
|
||||
///
|
||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
||||
/// display nothing
|
||||
///
|
||||
/// This contains the contact ID of the verifier.
|
||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
||||
/// If it is None/Null, we don't have verifier information or
|
||||
/// the contact is not verified.
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
freshness: ContactFreshness,
|
||||
|
||||
/// If the contact is a bot.
|
||||
is_bot: bool,
|
||||
@@ -79,14 +69,6 @@ impl ContactObject {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await?;
|
||||
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.flatten()
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
|
||||
Ok(ContactObject {
|
||||
address: contact.get_addr().to_owned(),
|
||||
color: color_int_to_hex_string(contact.get_color()),
|
||||
@@ -96,14 +78,11 @@ impl ContactObject {
|
||||
id: contact.id.to_u32(),
|
||||
name: contact.get_name().to_owned(),
|
||||
profile_image, //BLOBS
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_key_contact: contact.is_key_contact(),
|
||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
||||
is_verified,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
freshness: contact.get_freshness().into(),
|
||||
is_bot: contact.is_bot(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,9 +44,6 @@ pub enum EventType {
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted { msg: String },
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved { msg: String },
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
@@ -337,8 +334,7 @@ pub enum EventType {
|
||||
contact_id: u32,
|
||||
|
||||
/// Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: u16,
|
||||
},
|
||||
@@ -497,7 +493,6 @@ impl From<CoreEventType> for EventType {
|
||||
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||
|
||||
@@ -7,7 +7,7 @@ use typescript_type_def::TypeDef;
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum QrObject {
|
||||
/// Ask the user whether to verify the contact.
|
||||
/// Ask the user whether to start chatting with the contact.
|
||||
///
|
||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||
AskVerifyContact {
|
||||
@@ -61,23 +61,13 @@ pub enum QrObject {
|
||||
/// Whether the inviter supports the new Securejoin v3 protocol
|
||||
is_v3: bool,
|
||||
},
|
||||
/// Contact fingerprint is verified.
|
||||
/// Contact fingerprint matches.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
/// Contact ID.
|
||||
contact_id: u32,
|
||||
},
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
@@ -296,11 +286,6 @@ impl From<Qr> for QrObject {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprMismatch { contact_id } => {
|
||||
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||
QrObject::FprMismatch { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::Backup2 {
|
||||
ref node_addr,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -259,19 +259,13 @@ 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_str = if contact.is_verified(context).await? {
|
||||
" √"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let line = format!(
|
||||
"{}{} <{}>",
|
||||
"{} <{}>",
|
||||
if !name.is_empty() {
|
||||
name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() { addr } else { "addr unset" }
|
||||
);
|
||||
|
||||
@@ -1119,11 +1113,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
let name = contact.get_display_name();
|
||||
let addr = contact.get_addr();
|
||||
|
||||
let mut res = format!(
|
||||
"Contact info for: {}:\nIcon: {}\n",
|
||||
name_n_addr,
|
||||
"Contact info for: {name} ({addr}):\nIcon: {}\n",
|
||||
match contact.get_profile_image(&context).await? {
|
||||
Some(image) => image.to_str().unwrap().to_string(),
|
||||
None => "NoIcon".to_string(),
|
||||
|
||||
@@ -173,7 +173,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 39] = [
|
||||
const CHAT_COMMANDS: [&str; 38] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"start-realtime",
|
||||
@@ -182,7 +182,6 @@ const CHAT_COMMANDS: [&str; 39] = [
|
||||
"createchat",
|
||||
"creategroup",
|
||||
"createbroadcast",
|
||||
"createprotected",
|
||||
"addmember",
|
||||
"removemember",
|
||||
"groupname",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
|
||||
@@ -139,6 +139,18 @@ class Account:
|
||||
"""Add a new transport using a QR code."""
|
||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
||||
|
||||
@futuremethod
|
||||
def init_transports(self, qr: Optional[str] = None):
|
||||
"""Add an initial transport on the chatmail relay that answers fastest.
|
||||
|
||||
The profile then adds further ones in the background.
|
||||
A ``DCACCOUNT:`` or ``DCLOGIN:`` ``qr`` code adds a single transport
|
||||
while securejoin codes add the inviter's relays to the candidates.
|
||||
|
||||
Does nothing if the profile already has a transport.
|
||||
"""
|
||||
yield self._rpc.init_transports.future(self.id, qr)
|
||||
|
||||
def delete_transport(self, addr: str):
|
||||
"""Delete a transport."""
|
||||
self._rpc.delete_transport(self.id, addr)
|
||||
@@ -272,7 +284,7 @@ class Account:
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
@property
|
||||
def device_contact(self) -> Chat:
|
||||
def device_contact(self) -> Contact:
|
||||
"""Account's device contact."""
|
||||
return Contact(self, SpecialContactId.DEVICE)
|
||||
|
||||
@@ -360,7 +372,7 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
||||
"""Continue the SecureJoin protocol started on another device.
|
||||
|
||||
The function returns immediately and the handshake runs in background, sending
|
||||
and receiving several messages.
|
||||
|
||||
@@ -38,7 +38,6 @@ class EventType(str, Enum):
|
||||
IMAP_CONNECTED = "ImapConnected"
|
||||
SMTP_MESSAGE_SENT = "SmtpMessageSent"
|
||||
IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
|
||||
IMAP_MESSAGE_MOVED = "ImapMessageMoved"
|
||||
IMAP_INBOX_IDLE = "ImapInboxIdle"
|
||||
NEW_BLOB_FILE = "NewBlobFile"
|
||||
DELETED_BLOB_FILE = "DeletedBlobFile"
|
||||
@@ -70,6 +69,7 @@ class EventType(str, Enum):
|
||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||
ACCOUNTS_BACKGROUND_FETCH_DONE = "AccountsBackgroundFetchDone"
|
||||
CHATLIST_CHANGED = "ChatlistChanged"
|
||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
||||
|
||||
@@ -48,6 +48,13 @@ class DeltaChat:
|
||||
"""Stop ongoing background fetch."""
|
||||
self.rpc.stop_background_fetch()
|
||||
|
||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||
"""Wait until the next account manager event and return it."""
|
||||
while True:
|
||||
next_event = AttrDict(self.rpc.wait_for_event(0))
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network conditions might have changed."""
|
||||
self.rpc.maybe_network()
|
||||
|
||||
@@ -121,7 +121,7 @@ class Message:
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
def send_webxdc_realtime_data(self, data):
|
||||
"""Send data to the realtime channel."""
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ class RPCAccountFactory:
|
||||
"""Create a new unconfigured bot."""
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
def get_credentials(self) -> tuple[str, str]:
|
||||
"""Generate new credentials for chatmail account."""
|
||||
domain = os.environ["CHATMAIL_DOMAIN"]
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
|
||||
@@ -171,7 +171,7 @@ class DirectImap:
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
@@ -180,9 +180,6 @@ class DirectImap:
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> list[bytes]:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from deltachat_rpc_client import Account, EventType, const
|
||||
from deltachat_rpc_client import EventType, const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deltachat_rpc_client.pytestplugin import RPCAccountFactory
|
||||
@@ -144,7 +144,7 @@ def test_download_on_demand(acf: RPCAccountFactory, rpcdata) -> None:
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
|
||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> [Account, Account, Account]:
|
||||
def get_multi_account_test_setup(acf: RPCAccountFactory) -> list:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
|
||||
@@ -25,13 +25,12 @@ def test_qr_setup_contact(acf, alice_and_remote_bob, version) -> None:
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
# Test that Bob can also scan a QR code
|
||||
@@ -56,13 +55,12 @@ def test_qr_setup_contact_multitransport(acf, alice_and_remote_bob, version) ->
|
||||
remote_eval(f"bob.secure_join({qr_code!r})")
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
# The old core still marks Alice as verified, so the handshake is unchanged on the wire.
|
||||
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
|
||||
|
||||
|
||||
@@ -137,3 +135,88 @@ def test_keyupdate_against_core_2_48_march_2026(acf, alice_and_remote_bob, repla
|
||||
if replace_relay:
|
||||
remote_eval("bob_contact_alice.create_chat().send_text('hello after replacement')")
|
||||
assert alice.wait_for_incoming_msg().get_snapshot().text == "hello after replacement"
|
||||
|
||||
|
||||
class LocalSide:
|
||||
"""Alice on the core under test."""
|
||||
|
||||
def __init__(self, account, peer_contact):
|
||||
self.account = account
|
||||
self.peer_contact = peer_contact
|
||||
self.chat = None
|
||||
|
||||
def make_qr(self, invite):
|
||||
if invite == "group":
|
||||
self.chat = self.account.create_group("Group")
|
||||
elif invite == "broadcast":
|
||||
self.chat = self.account.create_broadcast("Channel")
|
||||
return self.chat.get_qr_code() if self.chat else self.account.get_qr_code()
|
||||
|
||||
def join(self, qr):
|
||||
self.account.secure_join(qr)
|
||||
|
||||
def wait_inviter(self):
|
||||
self.account.wait_for_securejoin_inviter_success()
|
||||
|
||||
def wait_joiner(self):
|
||||
self.account.wait_for_securejoin_joiner_success()
|
||||
|
||||
def send_text(self, text):
|
||||
chat = self.chat or self.peer_contact.create_chat()
|
||||
chat.send_text(text)
|
||||
|
||||
def next_text(self):
|
||||
return self.account.wait_for_incoming_msg().get_snapshot().text
|
||||
|
||||
|
||||
class RemoteSide:
|
||||
"""Bob on the other core, driven through remote_eval."""
|
||||
|
||||
def __init__(self, remote_eval):
|
||||
self.remote_eval = remote_eval
|
||||
self.chat = None
|
||||
|
||||
def make_qr(self, invite):
|
||||
if invite == "contact":
|
||||
return self.remote_eval("bob.get_qr_code()")
|
||||
create = {"group": "bob.create_group('Group')", "broadcast": "bob.create_broadcast('Channel')"}[invite]
|
||||
self.remote_eval(f"locals().update(chat={create})")
|
||||
self.chat = "chat"
|
||||
return self.remote_eval("chat.get_qr_code()")
|
||||
|
||||
def join(self, qr):
|
||||
self.remote_eval(f"bob.secure_join({qr!r})")
|
||||
|
||||
def wait_inviter(self):
|
||||
self.remote_eval("bob.wait_for_securejoin_inviter_success()")
|
||||
|
||||
def wait_joiner(self):
|
||||
self.remote_eval("bob.wait_for_securejoin_joiner_success()")
|
||||
|
||||
def send_text(self, text):
|
||||
chat = self.chat or "bob_contact_alice.create_chat()"
|
||||
self.remote_eval(f"{chat}.send_text({text!r})")
|
||||
|
||||
def next_text(self):
|
||||
return self.remote_eval("bob.wait_for_incoming_msg().get_snapshot().text")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["2.48.0"])
|
||||
@pytest.mark.parametrize("invite", ["contact", "group", "broadcast"])
|
||||
@pytest.mark.parametrize("remote_invites", [False, True], ids=["local-invites", "remote-invites"])
|
||||
def test_securejoin_invite(alice_and_remote_bob, version, invite, remote_invites):
|
||||
"""Every invite link type works with either core as the inviter."""
|
||||
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
|
||||
local = LocalSide(alice, alice_contact_bob)
|
||||
remote = RemoteSide(remote_eval)
|
||||
inviter, joiner = (remote, local) if remote_invites else (local, remote)
|
||||
|
||||
joiner.join(inviter.make_qr(invite))
|
||||
joiner.wait_joiner()
|
||||
inviter.wait_inviter()
|
||||
# Group and broadcast joins add an info message first.
|
||||
if invite != "contact":
|
||||
joiner.next_text()
|
||||
|
||||
inviter.send_text("hello")
|
||||
assert joiner.next_text() == "hello"
|
||||
|
||||
@@ -24,6 +24,12 @@ def wait_for_imap_message(imap):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def test_init_transports(acf):
|
||||
account = acf.get_unconfigured_account()
|
||||
account.init_transports(acf.get_account_qr())
|
||||
assert len(account.list_transports()) == 1
|
||||
|
||||
|
||||
def test_add_second_address(acf) -> None:
|
||||
account = acf.new_configured_account()
|
||||
assert len(account.list_transports()) == 1
|
||||
@@ -52,10 +58,9 @@ def test_add_second_address(acf) -> None:
|
||||
|
||||
|
||||
def test_change_address(acf) -> None:
|
||||
"""Test Alice configuring a second transport and setting it as a primary one."""
|
||||
"""Test Alice configuring a second transport and removing the first one."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("configured_addr")
|
||||
bob.create_chat(alice)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
@@ -65,22 +70,14 @@ def test_change_address(acf) -> None:
|
||||
sender_addr1 = msg1.sender.get_snapshot().address
|
||||
|
||||
alice.stop_io()
|
||||
old_alice_addr = alice.get_config("configured_addr")
|
||||
old_alice_addr = alice.list_transports()[0]["addr"]
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr in alice_vcard
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
with pytest.raises(JsonRpcError):
|
||||
# Cannot use the address that is not
|
||||
# configured for any transport.
|
||||
alice.set_config("configured_addr", bob_addr)
|
||||
|
||||
# Load old address so it is cached.
|
||||
assert alice.get_config("configured_addr") == old_alice_addr
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
# Make sure that setting `configured_addr` invalidated the cache.
|
||||
assert alice.get_config("configured_addr") == new_alice_addr
|
||||
alice.delete_transport(old_alice_addr)
|
||||
|
||||
alice_vcard = alice.self_contact.make_vcard()
|
||||
assert old_alice_addr not in alice_vcard
|
||||
@@ -98,6 +95,25 @@ def test_change_address(acf) -> None:
|
||||
assert sender_addr2 == new_alice_addr
|
||||
|
||||
|
||||
def test_remove_transport_keep_messages(acf) -> None:
|
||||
"""Test that deleting current sending transport keeps queued messages."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr = acf.get_account_qr()
|
||||
alice.add_transport_from_qr(qr)
|
||||
|
||||
alice.stop_io()
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.delete_transport(alice.list_transports()[0]["addr"])
|
||||
alice.start_io()
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert bob_msg.sender.get_snapshot().address == new_alice_addr
|
||||
|
||||
|
||||
def test_download_on_demand(acf, rpcdata) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.set_config("download_limit", "1")
|
||||
@@ -209,10 +225,6 @@ def test_transport_sync_new_as_primary(acf, log) -> None:
|
||||
|
||||
log.section("ac1 changes the primary transport")
|
||||
ac1.set_config("configured_addr", transport2["addr"])
|
||||
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
|
||||
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
|
||||
assert ac1_clone.get_config("configured_addr") == transport1["addr"]
|
||||
|
||||
log.section("ac1_clone receives a message via the new transport")
|
||||
ac1_chat = ac1.create_chat(bob)
|
||||
@@ -383,3 +395,14 @@ def test_background_fetch_no_duplicates(acf, direct_imap, dc):
|
||||
|
||||
dc.background_fetch(300)
|
||||
assert len(messages_with_text(alice_chat, "hello")) == 1
|
||||
|
||||
|
||||
def test_multitransport_mdn(acf):
|
||||
"""Test sending an MDN right after configuring two transports."""
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice.add_transport_from_qr(acf.get_account_qr())
|
||||
alice.bring_online()
|
||||
alice.create_chat(bob)
|
||||
bob_msg = bob.create_chat(alice).send_text("Hello!")
|
||||
alice.wait_for_incoming_msg().mark_seen()
|
||||
assert bob.wait_for_event(EventType.MSG_READ).msg_id == bob_msg.id
|
||||
|
||||
@@ -4,10 +4,9 @@ import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
from deltachat_rpc_client.const import ChatType
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
def test_qr_setup_contact(acf, tmp_path) -> None:
|
||||
def test_qr_setup_contact(acf) -> None:
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
qr_code = alice.get_qr_code()
|
||||
@@ -15,33 +14,15 @@ def test_qr_setup_contact(acf, tmp_path) -> None:
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Test that if Bob imports a key,
|
||||
# backwards verification is not lost
|
||||
# because default key is not changed.
|
||||
logging.info("Bob 2 is created")
|
||||
bob2 = acf.new_configured_account()
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob tries to import a key")
|
||||
# Importing a second key is not allowed.
|
||||
with pytest.raises(JsonRpcError):
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "1"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acf) -> None:
|
||||
@@ -81,26 +62,24 @@ def test_qr_securejoin(acf):
|
||||
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
assert alice_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
snapshot = bob.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You were added by {}.".format(alice.get_config("addr"))
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.create_contact(alice)
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
assert bob_contact_alice_snapshot.e2ee_avail
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
# Alice observes the securejoin protocol on the second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.create_contact(bob)
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
assert alice2_contact_bob_snapshot.e2ee_avail
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins the group via alice2")
|
||||
@@ -151,9 +130,9 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
assert snapshot2.chat_id == chat.id
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
# Check that the chat partner is verified.
|
||||
# Check that the chat partner's key is known.
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
assert contact_snapshot.is_verified
|
||||
assert contact_snapshot.e2ee_avail
|
||||
|
||||
chat = get_broadcast(ac)
|
||||
chat_msgs = chat.get_messages()
|
||||
@@ -167,17 +146,13 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
if inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
assert member_added_msg.info_contact_id == contact_snapshot.id
|
||||
else:
|
||||
if not inviter_side:
|
||||
if chat_msgs[0].get_snapshot().text == "You joined the channel.":
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
else:
|
||||
member_added_msg = chat_msgs.pop(1).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
assert member_added_msg.is_info
|
||||
|
||||
hello_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert hello_msg.text == "Hello everyone!"
|
||||
@@ -242,7 +217,7 @@ def test_qr_securejoin_broadcast(acf, all_devices_online):
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "You joined the channel."
|
||||
|
||||
get_broadcast(alice2).get_messages()[2].resend()
|
||||
get_broadcast(alice2).get_messages()[1].resend()
|
||||
snapshot = fiona.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello everyone!"
|
||||
|
||||
@@ -350,8 +325,8 @@ def test_setup_contact_resetup(acf) -> None:
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acf) -> None:
|
||||
"""Tests verified group recovery by reverifying then removing and adding a member back."""
|
||||
def test_group_member_added_recovery(acf) -> None:
|
||||
"""Tests group recovery after a member resets its key."""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates a group")
|
||||
@@ -362,11 +337,7 @@ def test_verified_group_member_added_recovery(acf) -> None:
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
logging.info("ac3 joins the group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_joiner_success()
|
||||
ac3.wait_for_incoming_msg_event() # Member added
|
||||
@@ -376,7 +347,7 @@ def test_verified_group_member_added_recovery(acf) -> None:
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acf.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
logging.info("ac2 scans ac3's QR code again")
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
@@ -416,14 +387,6 @@ def test_verified_group_member_added_recovery(acf) -> None:
|
||||
snapshot = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2)
|
||||
ac1_contact_ac3 = ac1.create_contact(ac3)
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id != ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
"""Regression test for
|
||||
@@ -431,13 +394,13 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
"""
|
||||
ac1, ac2, ac3, ac4 = acf.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: verify with ac2")
|
||||
logging.info("ac3: set up contact with ac2")
|
||||
qr_code = ac2.get_qr_code()
|
||||
ac3.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_inviter_success()
|
||||
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# in order for ac2 to have pending bobstate with a group
|
||||
# we first create a fully joined group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
logging.info("ac1: create a group that ac2 fully joins")
|
||||
@@ -446,7 +409,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
# ensure ac1 can write and ac2 receives messages in the chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
snapshot = ac2.wait_for_incoming_msg().get_snapshot()
|
||||
@@ -459,11 +422,11 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.create_contact(ac2).get_snapshot().is_verified
|
||||
assert ac2.create_contact(ac3).get_snapshot().is_verified
|
||||
# we meanwhile expect the ac3/ac2 setup-contact started in the beginning to have completed
|
||||
assert ac3.create_contact(ac2).get_snapshot().e2ee_avail
|
||||
assert ac2.create_contact(ac3).get_snapshot().e2ee_avail
|
||||
|
||||
logging.info("ac3: create a verified group VG with ac2")
|
||||
logging.info("ac3: create a group VG with ac2")
|
||||
vg = ac3.create_group("ac3-created")
|
||||
vg.add_contact(ac3.create_contact(ac2))
|
||||
|
||||
@@ -486,7 +449,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acf):
|
||||
|
||||
def test_qr_new_group_unblocked(acf):
|
||||
"""Regression test for a bug introduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
ac2 scans a group QR code created by ac1.
|
||||
This results in creation of a blocked single chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
@@ -513,13 +476,13 @@ def test_qr_new_group_unblocked(acf):
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||
def test_aeap_flow_verified(acf):
|
||||
def test_aeap_flow(acf):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
|
||||
addr, password = acf.get_credentials()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
logging.info("ac1: create group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello")
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
@@ -555,65 +518,15 @@ def test_aeap_flow_verified(acf):
|
||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||
|
||||
|
||||
def test_gossip_verification(acf) -> None:
|
||||
alice, bob, carol = acf.get_online_accounts(3)
|
||||
|
||||
# Bob verifies Alice.
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Bob verifies Carol.
|
||||
qr_code = carol.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||
|
||||
logging.info("Bob creates an Autocrypt group")
|
||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||
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.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Autocrypt group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Group propagates verification using Autocrypt-Gossip header.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
logging.info("Bob creates a Securejoin group")
|
||||
bob_group_chat = bob.create_group("Securejoin Group")
|
||||
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.wait_for_incoming_msg().get_snapshot()
|
||||
assert snapshot.text == "Hello Securejoin group"
|
||||
assert snapshot.show_padlock
|
||||
|
||||
# Securejoin propagates verification.
|
||||
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert not carol_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
"""
|
||||
Regression test for a bug that prevented joining verified group with a QR code
|
||||
if the group is already created and contains
|
||||
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
|
||||
Regression test for a bug that prevented joining a group with a QR code
|
||||
if the group already contains a contact with the same address as the inviter,
|
||||
but different key fingerprint while a securejoin with that contact is still pending.
|
||||
"""
|
||||
ac1, ac2, ac3 = acf.get_online_accounts(3)
|
||||
|
||||
# ac3 creates protected group with ac1.
|
||||
# ac3 creates a group with ac1.
|
||||
ac3_chat = ac3.create_group("Group")
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
@@ -626,28 +539,24 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
assert snapshot.text == "You were added by {}.".format(ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
# ac2 sets up contact with ac1
|
||||
qr_code = ac1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 is verified for ac2.
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||
assert ac2_contact_ac1.get_snapshot().e2ee_avail
|
||||
|
||||
# ac1 resetups the account.
|
||||
ac1 = acf.resetup_account(ac1)
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 goes offline.
|
||||
ac1.remove()
|
||||
|
||||
# Scanning a QR code results in creating an unprotected group with an inviter.
|
||||
# In this case inviter is ac1 which has an inconsistent key state.
|
||||
# Normally inviter becomes verified as a result of Securejoin protocol
|
||||
# and then the group chat becomes verified when "Member added" is received,
|
||||
# but in this case ac1 is offline and this Securejoin process will never finish.
|
||||
# Scanning a QR code creates a group with the inviter, here ac1.
|
||||
# Normally the securejoin protocol
|
||||
# would complete and "Member added" would arrive,
|
||||
# but ac1 is offline so it never finishes.
|
||||
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
|
||||
ac2.secure_join(ac1_qr_code)
|
||||
|
||||
@@ -664,16 +573,13 @@ def test_securejoin_after_contact_resetup(acf) -> None:
|
||||
ac2_chat = snapshot.chat
|
||||
assert len(ac2_chat.get_contacts()) == 3
|
||||
|
||||
# ac1 is still "not verified" for ac2 due to inconsistent state.
|
||||
assert not ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
|
||||
def test_withdraw_securejoin_qr(acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a group")
|
||||
alice_chat = alice.create_group("Group")
|
||||
logging.info("Bob joins verified group")
|
||||
logging.info("Bob joins the group")
|
||||
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
|
||||
@@ -421,7 +421,6 @@ def test_dont_move_sync_msgs(acf, direct_imap):
|
||||
addr, password = acf.get_credentials()
|
||||
ac1 = acf.get_unconfigured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1.set_config("fix_is_chatmail", "1")
|
||||
ac1.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac1.start_io()
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
@@ -1188,7 +1187,6 @@ def test_leave_broadcast(acf, all_devices_online):
|
||||
|
||||
def check_account(ac, contact, inviter_side, please_wait_info_msg=False):
|
||||
chat = get_broadcast(ac)
|
||||
contact_snapshot = contact.get_snapshot()
|
||||
chat_msgs = chat.get_messages()
|
||||
|
||||
encrypted_msg = chat_msgs.pop(0).get_snapshot()
|
||||
@@ -1200,14 +1198,11 @@ def test_leave_broadcast(acf, all_devices_online):
|
||||
assert "invited you to join this channel" in first_msg.text
|
||||
assert first_msg.is_info
|
||||
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
if inviter_side:
|
||||
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
|
||||
else:
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
if not inviter_side:
|
||||
member_added_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert member_added_msg.text == "You joined the channel."
|
||||
assert member_added_msg.is_info
|
||||
|
||||
leave_msg = chat_msgs.pop(0).get_snapshot()
|
||||
assert leave_msg.text == "You left the channel."
|
||||
|
||||
@@ -1354,6 +1349,22 @@ def test_background_fetch(acf, dc):
|
||||
break
|
||||
|
||||
|
||||
def test_background_fetch_does_not_wait_for_sending(dc, acf):
|
||||
alice, bob = acf.get_online_accounts(2)
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
|
||||
alice.stop_io()
|
||||
text = "x" * 200_000
|
||||
for _ in range(50):
|
||||
alice_chat_bob.send_text(text)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
alice.start_io()
|
||||
dc.background_fetch(50)
|
||||
dc.wait_for_event(EventType.ACCOUNTS_BACKGROUND_FETCH_DONE)
|
||||
assert not dc.is_sending_finished()
|
||||
|
||||
|
||||
def test_message_exists(acf):
|
||||
ac1, ac2 = acf.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "2.61.0-dev"
|
||||
"version": "2.62.0"
|
||||
}
|
||||
|
||||
+69
-18
@@ -72,8 +72,7 @@ CREATE TABLE contacts (
|
||||
-- empty string for "address-contacts".
|
||||
fingerprint TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- ID of the contact that has "introduced" us to this contact
|
||||
-- by sharing the key with a verified attribute or in a "verified" chat.
|
||||
-- Unused. Was the ID of the contact that introduced this contact's key.
|
||||
verifier INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX contacts_index1 ON contacts (name COLLATE NOCASE);
|
||||
@@ -131,9 +130,7 @@ CREATE TABLE chats (
|
||||
-- 0 means the timer is disabled.
|
||||
ephemeral_timer INTEGER,
|
||||
|
||||
-- Deprecated, but still used to send Chat-Verified headers
|
||||
-- for existing protected chats.
|
||||
-- All new chats are created as "not protected".
|
||||
-- Unused. Was 1 for protected chats.
|
||||
protected INTEGER DEFAULT 0,
|
||||
|
||||
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossip_timestamp table
|
||||
@@ -400,14 +397,51 @@ CREATE TABLE bobstate (
|
||||
chat_id INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
CREATE TABLE smtp2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
display_name TEXT NOT NULL, -- Display name to put into the From field.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
|
||||
-- Unencrypted payload with some headers.
|
||||
mime BLOB NOT NULL,
|
||||
|
||||
-- True if Autocrypt header should be added before sending.
|
||||
should_attach_pubkey INTEGER NOT NULL,
|
||||
|
||||
-- True if OpenPGP-encrypted message may use compression.
|
||||
should_compress INTEGER NOT NULL,
|
||||
|
||||
-- True if encrypted message should be signed.
|
||||
should_sign INTEGER NOT NULL,
|
||||
|
||||
-- ID of the message in `msgs` table
|
||||
msg_id INTEGER NOT NULL,
|
||||
|
||||
-- Space-separated recipient addresses.
|
||||
recipients TEXT NOT NULL,
|
||||
|
||||
-- Space-separated addresses the message was sent to.
|
||||
sent_to TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- If true, copy should be sent to self in addition to the recipient list.
|
||||
--
|
||||
-- For encrypted messages copy is sent to all addresses.
|
||||
-- For unencrypted messages, copy is sent to the From address only.
|
||||
bcc_self INTEGER NOT NULL,
|
||||
|
||||
-- True if the message is encrypted.
|
||||
-- If true, at most one of the shared_secret or encryption_fingerprints should be non-empty.
|
||||
-- If false, both must be empty.
|
||||
is_encrypted INTEGER NOT NULL,
|
||||
|
||||
-- Shared secret if the message is to be encrypted symmetrically.
|
||||
shared_secret TEXT NOT NULL DEFAULT '',
|
||||
|
||||
-- Space-separated fingerprints of the keys the message should be encrypted to.
|
||||
encryption_fingerprints TEXT NOT NULL DEFAULT '',
|
||||
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE smtp_mdns (
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
|
||||
@@ -596,9 +630,12 @@ CREATE TABLE broadcast_secrets(
|
||||
|
||||
|
||||
-- Candidate chatmail relays for automatic relay management.
|
||||
-- Holds the hosts a QR code contributed and the default relays already tried;
|
||||
-- the default list itself lives in `autorelay.rs` and is not stored here.
|
||||
CREATE TABLE relay_candidates(
|
||||
host TEXT PRIMARY KEY NOT NULL,
|
||||
last_tried INTEGER NOT NULL DEFAULT 0 -- Timestamp of the last connection attempt.
|
||||
-- Timestamp of the last connection attempt, 0 if the host was never tried.
|
||||
last_tried INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE transports (
|
||||
@@ -646,7 +683,11 @@ CREATE TABLE imap (
|
||||
transport_id INTEGER NOT NULL, -- ID of the transport in the `transports` table.
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
folder TEXT NOT NULL, -- IMAP folder
|
||||
target TEXT NOT NULL, -- Destination folder. Empty string means that the message shall be deleted.
|
||||
|
||||
-- Destination folder. Empty string means that the message shall be deleted.
|
||||
-- Since we don't move messages between IMAP folders anymore,
|
||||
-- this is always either empty or equal to `folder`.
|
||||
target TEXT NOT NULL,
|
||||
uid INTEGER NOT NULL, -- UID
|
||||
uidvalidity INTEGER NOT NULL,
|
||||
UNIQUE (transport_id, folder, uid, uidvalidity)
|
||||
@@ -693,13 +734,13 @@ CREATE TABLE stats_securejoin_uipaths(
|
||||
) STRICT;
|
||||
CREATE TABLE stats_securejoin_invites(
|
||||
already_existed INTEGER NOT NULL,
|
||||
already_verified INTEGER NOT NULL,
|
||||
already_verified INTEGER NOT NULL, -- unused, always 0
|
||||
type TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE TABLE stats_msgs(
|
||||
chattype INTEGER PRIMARY KEY,
|
||||
verified INTEGER NOT NULL DEFAULT 0,
|
||||
unverified_encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
verified INTEGER NOT NULL DEFAULT 0, -- unused, always 0
|
||||
unverified_encrypted INTEGER NOT NULL DEFAULT 0, -- counts all encrypted messages
|
||||
unencrypted INTEGER NOT NULL DEFAULT 0,
|
||||
only_to_self INTEGER NOT NULL DEFAULT 0,
|
||||
last_counted_msg_id INTEGER NOT NULL DEFAULT 0
|
||||
@@ -781,3 +822,13 @@ CREATE TABLE sending_domains(
|
||||
domain TEXT PRIMARY KEY,
|
||||
dkim_works INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- Replaced with smtp2.
|
||||
CREATE TABLE smtp (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "2.61.0-dev"
|
||||
version = "2.62.0"
|
||||
license = "MPL-2.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -71,17 +71,6 @@ class Contact:
|
||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self) -> bool:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
|
||||
@@ -104,11 +104,11 @@ class DirectImap:
|
||||
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
assert not self._idling
|
||||
return list(self.conn.fetch())
|
||||
return list(self.conn.fetch(mark_seen=False))
|
||||
|
||||
def get_unread_messages(self) -> List[str]:
|
||||
assert not self._idling
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False), mark_seen=False)]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
@@ -183,7 +183,7 @@ class DirectImap:
|
||||
self.conn.append(bytes(msg, encoding="ascii"), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)), mark_seen=False)]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
@@ -193,9 +193,6 @@ class IdleManager:
|
||||
def __init__(self, direct_imap) -> None:
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> List[bytes]:
|
||||
|
||||
@@ -11,6 +11,9 @@ import random
|
||||
from queue import Queue
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
from .capi import lib
|
||||
from .cutil import as_dc_charpointer
|
||||
|
||||
import pytest
|
||||
from _pytest._code import Source
|
||||
|
||||
@@ -366,6 +369,7 @@ class ACFactory:
|
||||
ac.open(passphrase)
|
||||
acname = ac._logid
|
||||
addr = f"{acname}@offline.org"
|
||||
lib.dc_add_pseudo_transport(ac._dc_context, as_dc_charpointer(addr))
|
||||
ac.update_config(
|
||||
{
|
||||
"configured_addr": addr,
|
||||
@@ -374,6 +378,7 @@ class ACFactory:
|
||||
)
|
||||
self._preconfigure_key(ac)
|
||||
self._acsetup.init_logging(ac)
|
||||
assert ac.is_configured(), "Pseudo configured account should look like if it is configured"
|
||||
return ac
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs) -> Account:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import time
|
||||
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
@@ -63,9 +61,9 @@ class TestGroupStressTests:
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
def test_qr_group_join_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello")
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
@@ -86,15 +84,11 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
|
||||
lp.sec("ac2: read message and check that it's a verified chat")
|
||||
lp.sec("ac2: read message and check that it is encrypted")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
@@ -109,23 +103,13 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac1: add ac3 to verified group")
|
||||
lp.sec("ac1: add ac3 to the group")
|
||||
chat1.add_contact(ac3)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert not msg.error
|
||||
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
for ac2_contact in chat2.get_contacts():
|
||||
if ac2_contact == ac2_ac1_contact or ac2_contact.id == dc.const.DC_CONTACT_ID_SELF:
|
||||
continue
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# verification is not gossiped here:
|
||||
assert ac2.get_self_contact().get_verifier(ac2_contact) is None
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# System message about the added member.
|
||||
@@ -195,10 +179,10 @@ def test_ephemeral_timer(acfactory, lp):
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
def test_see_new_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Alice creates a group and sends a QR invitation to Bob.
|
||||
- Bob joins the group and sends a message there. Alice sees it.
|
||||
- Alice's second devices goes online, but doesn't see Bob in the group.
|
||||
"""
|
||||
@@ -215,7 +199,7 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
ac1_offl.import_self_keys(str(dir))
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
@@ -242,12 +226,12 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
def test_use_new_group_after_going_online(acfactory, data, tmp_path, lp):
|
||||
"""Another test for the bug #3836:
|
||||
- Bob has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
- Alice creates a group and sends a QR invitation to Bob.
|
||||
- Bob joins the group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the verified group.
|
||||
- Bob's second devices goes online, but sees a contact request instead of the group.
|
||||
- The "member added" message is not a system message but a plain text message.
|
||||
- Bob's second device doesn't display the Alice's avatar (bug #5354).
|
||||
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
|
||||
@@ -269,7 +253,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
|
||||
avatar_path = data.get_path("d.png")
|
||||
ac1.set_avatar(avatar_path)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
lp.sec("ac1: create group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello")
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
|
||||
@@ -796,9 +796,8 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""Tests joining a group via QR code
|
||||
when the database already contains a contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
@@ -822,13 +821,7 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
# ac1 should see both ac3 and ac2 as verified.
|
||||
assert len(ac1_chat.get_contacts()) == 3
|
||||
# Until we reset verifications and then send the _verified header,
|
||||
# the verification of ac2 is not gossiped here:
|
||||
for contact in ac1_chat.get_contacts():
|
||||
is_ac2 = contact.addr == ac2.get_config("addr")
|
||||
assert contact.is_verified() != is_ac2
|
||||
|
||||
|
||||
def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
|
||||
@@ -115,7 +115,6 @@ class TestOfflineContact:
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_get_blocked(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
@@ -133,7 +132,7 @@ class TestOfflineContact:
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
contact1 = ac1.create_contact(ac1.get_config("configured_addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
@@ -224,7 +223,7 @@ class TestOfflineChat:
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.addr == ac2.get_config("configured_addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
@@ -457,7 +456,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -553,7 +552,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -605,7 +604,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
@@ -622,7 +621,7 @@ class TestOfflineChat:
|
||||
contacts = ac2.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == ac_contact.get_config("addr")
|
||||
assert contact2.addr == ac_contact.get_config("configured_addr")
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2 + E2EE_INFO_MSGS
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2026-09-11
|
||||
2026-09-22
|
||||
@@ -596,24 +596,6 @@ and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Verifications
|
||||
|
||||
Keys obtained using [SecureJoin](https://securejoin.readthedocs.io) protocol
|
||||
and corresponding contacts
|
||||
are considered "verified".
|
||||
|
||||
As an extension to `Autocrypt-Gossip` header,
|
||||
chatmail clients can add `_verified=1` attribute
|
||||
(underscore marks the attribute as non-critical)
|
||||
to indicate that they have the gossiped key
|
||||
and the corresponding contact marked as verified.
|
||||
|
||||
When receiving such `Autocrypt-Gossip` header
|
||||
in a message signed by a verified key,
|
||||
chatmail clients mark the gossiped key
|
||||
as indirectly verified.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
+5
-1
@@ -482,11 +482,15 @@ impl Accounts {
|
||||
/// return immediately even before the timeout expiration
|
||||
/// or finishing fetching.
|
||||
///
|
||||
/// Pending outgoing messages are not waited for and not triggered.
|
||||
///
|
||||
/// The `AccountsBackgroundFetchDone` event is emitted at the end,
|
||||
/// process all events until you get this one and you can safely return to the background
|
||||
/// without forgetting to create notifications caused by timing race conditions.
|
||||
/// If another background fetch is already running,
|
||||
/// nothing is fetched and the event is emitted immediately.
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it only safely refers to your call if no concurrent background fetch is happening.
|
||||
///
|
||||
/// Returns a future that resolves when background fetch is done,
|
||||
/// but does not capture `&self`.
|
||||
@@ -524,7 +528,7 @@ impl Accounts {
|
||||
pub async fn is_sending_finished(&self) -> Result<bool> {
|
||||
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
|
||||
for account in accounts {
|
||||
if !smtp::is_queue_empty(&account).await? {
|
||||
if !smtp::queue::is_empty(&account).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,13 +43,6 @@ pub struct Aheader {
|
||||
pub addr: String,
|
||||
pub public_key: SignedPublicKey,
|
||||
pub prefer_encrypt: EncryptPreference,
|
||||
|
||||
/// Whether `_verified` attribute is present.
|
||||
///
|
||||
/// `_verified` attribute is an extension to `Autocrypt-Gossip`
|
||||
/// header that is used to tell that the sender
|
||||
/// marked this key as verified.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for Aheader {
|
||||
@@ -58,12 +51,6 @@ impl fmt::Display for Aheader {
|
||||
if self.prefer_encrypt == EncryptPreference::Mutual {
|
||||
write!(fmt, " prefer-encrypt=mutual;")?;
|
||||
}
|
||||
// TODO After we reset all existing verifications,
|
||||
// we want to start sending the _verified attribute
|
||||
// if self.verified {
|
||||
// write!(fmt, " _verified=1;")?;
|
||||
// }
|
||||
|
||||
// adds a whitespace every 78 characters, this allows
|
||||
// email crate to wrap the lines according to RFC 5322
|
||||
// (which may insert a linebreak before every whitespace)
|
||||
@@ -114,8 +101,6 @@ impl Aheader {
|
||||
.and_then(|raw| EncryptPreference::new(&raw).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let verified = attributes.remove("_verified").is_some();
|
||||
|
||||
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
|
||||
// Autocrypt-Level0: unknown attribute, treat the header as invalid
|
||||
if attributes.keys().any(|k| !k.starts_with('_')) {
|
||||
@@ -126,7 +111,6 @@ impl Aheader {
|
||||
addr,
|
||||
public_key,
|
||||
prefer_encrypt,
|
||||
verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -145,7 +129,6 @@ mod tests {
|
||||
|
||||
assert_eq!(h.addr, "me@mail.com");
|
||||
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
|
||||
assert_eq!(h.verified, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -243,7 +226,6 @@ mod tests {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt=mutual;")
|
||||
@@ -259,7 +241,6 @@ mod tests {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("prefer-encrypt")
|
||||
@@ -273,24 +254,9 @@ mod tests {
|
||||
addr: "TeSt@eXaMpLe.cOm".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false
|
||||
}
|
||||
)
|
||||
.contains("test@example.com")
|
||||
);
|
||||
|
||||
// We don't send the _verified header yet:
|
||||
assert!(
|
||||
!format!(
|
||||
"{}",
|
||||
Aheader {
|
||||
addr: "test@example.com".to_string(),
|
||||
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: true
|
||||
}
|
||||
)
|
||||
.contains("_verified")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -388,4 +388,40 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// some systems may not get the update URL from the relays, but use a specific one.
|
||||
// this is esp. useful when the app binary is not signed.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_app_versions_no_url() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
let writable = true;
|
||||
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||
|
||||
let account_id1 = accounts.add_account().await?;
|
||||
let json = r##"{
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "deltawin",
|
||||
"sources": [
|
||||
{
|
||||
"sourceId": "windows-3.11",
|
||||
"versionInteger": 300,
|
||||
"versionString": "3.00.000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##;
|
||||
mockup_app_versions(&accounts, account_id1, 1, json).await;
|
||||
|
||||
let version = get_app_version(&accounts, "deltawin", "windows-3.11")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(version.version_integer, 300);
|
||||
assert_eq!(version.version_string, "3.00.000");
|
||||
assert_eq!(version.download_url, ""); // no url given results in default empty string
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+144
-45
@@ -1,29 +1,24 @@
|
||||
//! # Automatic relay handling (experimental, still in development)
|
||||
//! # Automatic multi-relay onboarding
|
||||
//!
|
||||
//! Chatmail relays create an account on first login,
|
||||
//! so a profile can add further transports on its own without user interaction.
|
||||
//! Candidate hosts come from the `relay_candidates` table,
|
||||
//! which migrations seed with a list of known chatmail relays.
|
||||
//!
|
||||
//! Status of implementation:
|
||||
//! Additions are attempted right before going into IMAP IDLE,
|
||||
//! i.e. only while connected and with nothing more important to do,
|
||||
//! and only if a UI opted in via [`Config::Autorelay`].
|
||||
//! Once a profile has reached `NUM_TRANSPORTS_TARGET` transports,
|
||||
//! [`Config::AutorelayFinished`] is set and nothing is ever added again,
|
||||
//! so deleting a transport later does not pull in a replacement.
|
||||
//! Support for automatically onboarding a profile on transport
|
||||
//! candidates without the user choosing a relay.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, format_err};
|
||||
use deltachat_contact_tools::addr_normalize;
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use rand::seq::IndexedRandom;
|
||||
use rand::seq::{IndexedRandom, SliceRandom};
|
||||
use rusqlite::Transaction;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::configure::{EnteredLoginParam, SILENT_PROGRESS, configure};
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::login_param::{EnteredCertificateChecks, EnteredImapLoginParam};
|
||||
use crate::{configure::EnteredLoginParam, context::Context, tools::time};
|
||||
use crate::net::{connect_tcp, proxy::ProxyConfig};
|
||||
use crate::{context::Context, tools::time};
|
||||
|
||||
/// The target number of transports.
|
||||
const NUM_TRANSPORTS_TARGET: usize = 3;
|
||||
@@ -32,6 +27,87 @@ const AUTOMATIC_ADDITION_DEBOUNCE_SECONDS: i64 = 60 * 60; // one hour
|
||||
/// How long we ignore a relay candidate after failing to connect to it:
|
||||
const BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY: i64 = 60 * 60 * 24 * 7; // one week
|
||||
|
||||
/// Sorted relay list a profile can attempt to onboard on without the user choosing one.
|
||||
const DEFAULT_RELAY_CANDIDATES: &[&str] = &[
|
||||
"chat.adminforge.de", // iroh relay 404s
|
||||
"chat.me.ke",
|
||||
"chat.nuvon.app",
|
||||
"chat.tinydispatch.org",
|
||||
"chat.vim.wtf",
|
||||
"chatmail.uk",
|
||||
"chtml.ca",
|
||||
"deltachat.me",
|
||||
"e2e.sus.fr",
|
||||
"e2ee.wang",
|
||||
"mailchat.pl",
|
||||
"nchrcht.la10cy.net",
|
||||
"nine.testrun.org",
|
||||
"sweetfern.net",
|
||||
"tarpit.fun",
|
||||
];
|
||||
|
||||
/// Records the hosts of `addrs` as relay candidates.
|
||||
pub(crate) async fn add_relay_candidates(context: &Context, addrs: &[String]) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.transaction(|tx| hosts_of(addrs).try_for_each(|host| save_relay_candidate(tx, host, 0)))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Adds a first transport on the relay candidate that answers fastest.
|
||||
///
|
||||
/// All candidates are probed at once with a TCP connection to their HTTPS port
|
||||
/// and configured in the order in which the connections complete,
|
||||
/// stopping at the first success. Candidates that fail the probe are skipped.
|
||||
/// Answering TCP fastest is used as a network proximity measure,
|
||||
/// which keeps latency low for initial onboarding,
|
||||
/// and it avoids relays that are down or black-holing traffic.
|
||||
pub(crate) async fn add_transport_from_candidates(
|
||||
context: &Context,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
let mut candidates = triable_relay_candidates(context, time()).await?;
|
||||
candidates.shuffle(&mut rand::rng());
|
||||
let mut probes = JoinSet::new();
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let load_cache = false;
|
||||
for host in candidates {
|
||||
let ctx = context.clone();
|
||||
let proxy_config = proxy_config.clone();
|
||||
probes.spawn(async move {
|
||||
let res = match proxy_config {
|
||||
_ if skip_network => Ok(()),
|
||||
Some(proxy) => proxy.connect(&ctx, &host, 443, load_cache).await.map(drop),
|
||||
None => connect_tcp(&ctx, &host, 443, load_cache).await.map(drop),
|
||||
};
|
||||
(host, res)
|
||||
});
|
||||
}
|
||||
|
||||
let mut last_err = format_err!("No relay candidates");
|
||||
let mark_as_autorelay = true;
|
||||
while let Some(res) = probes.join_next().await {
|
||||
let (host, res) = res?;
|
||||
if let Err(err) = res {
|
||||
warn!(context, "Failed to connect to relay {host}: {err:#}.");
|
||||
last_err = err;
|
||||
continue;
|
||||
}
|
||||
let param = login_param_from_host(&host, mark_as_autorelay);
|
||||
match configure(context, ¶m, skip_network).await {
|
||||
Ok(()) => {
|
||||
info!(context, "Added a transport on relay {host}.");
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to add relay {host}: {err:#}.");
|
||||
last_err = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_add_additional_relays(
|
||||
context: Context,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
@@ -93,10 +169,7 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
return Ok(relay_added);
|
||||
}
|
||||
|
||||
// First, query all candidates that were not tried since `BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY` seconds.
|
||||
// Hosts that are already used are excluded.
|
||||
let candidates = load_relay_candidates(context, now).await?;
|
||||
|
||||
let candidates = triable_relay_candidates(context, now).await?;
|
||||
let Some(host) = candidates.choose(&mut rand::rng()) else {
|
||||
info!(
|
||||
context,
|
||||
@@ -113,13 +186,13 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE relay_candidates SET last_tried=? WHERE host=?",
|
||||
(now, host),
|
||||
)
|
||||
.transaction(|tx| save_relay_candidate(tx, host, now))
|
||||
.await?;
|
||||
let param = login_param_from_host(host);
|
||||
let res = crate::configure::configure(context, ¶m, skip_network).await;
|
||||
let mark_as_autorelay = true;
|
||||
let param = login_param_from_host(host, mark_as_autorelay);
|
||||
let res = SILENT_PROGRESS
|
||||
.scope((), configure(context, ¶m, skip_network))
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
warn!(
|
||||
context,
|
||||
@@ -134,37 +207,63 @@ async fn maybe_add_additional_relays_inner(context: &Context, skip_network: bool
|
||||
Ok(relay_added)
|
||||
}
|
||||
|
||||
async fn load_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>, anyhow::Error> {
|
||||
async fn triable_relay_candidates(context: &Context, now: i64) -> Result<Vec<String>> {
|
||||
let cutoff_timestamp = now.saturating_sub(BACKOFF_PERIOD_FOR_NOT_WORKING_RELAY);
|
||||
let candidates: Vec<String> = context
|
||||
let mut last_tried: BTreeMap<String, i64> = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
// This also selects candidates which have last_tried in the future,
|
||||
// essentially treating them as never tried,
|
||||
// so if some timestamp far in the future is accidentally stored,
|
||||
// we are not stuck never trying the candidate.
|
||||
// After trying the candidate, last_tried will be corrected to the current time.
|
||||
"SELECT host FROM relay_candidates WHERE (last_tried<? OR last_tried>?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM transports
|
||||
WHERE substr(addr, instr(addr, '@') + 1) = host
|
||||
)",
|
||||
(cutoff_timestamp, now),
|
||||
|row| Ok(row.get::<_, String>(0)?),
|
||||
)
|
||||
.query_map_collect("SELECT host, last_tried FROM relay_candidates", (), |row| {
|
||||
Ok((row.get(0)?, row.get(1)?))
|
||||
})
|
||||
.await?;
|
||||
for host in DEFAULT_RELAY_CANDIDATES {
|
||||
last_tried.entry(host.to_string()).or_insert(0);
|
||||
}
|
||||
let self_addrs = context.get_self_addrs().await?;
|
||||
let used_hosts: Vec<&str> = hosts_of(&self_addrs).collect();
|
||||
|
||||
// We also try candidates which have `last_tried` in the future,
|
||||
// which on next failure get `last_tried` reset to the current time.
|
||||
let candidates = last_tried
|
||||
.into_iter()
|
||||
.filter(|(host, last_tried)| {
|
||||
(*last_tried < cutoff_timestamp || *last_tried > now)
|
||||
&& !used_hosts.contains(&host.as_str())
|
||||
})
|
||||
.map(|(host, _)| host)
|
||||
.collect();
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str) -> EnteredLoginParam {
|
||||
/// Returns the host of each address in `addrs`.
|
||||
fn hosts_of(addrs: &[String]) -> impl Iterator<Item = &str> {
|
||||
addrs.iter().filter_map(|a| Some(a.rsplit_once('@')?.1))
|
||||
}
|
||||
|
||||
/// Records `host` as a relay candidate, overwriting a stored `last_tried`.
|
||||
fn save_relay_candidate(tx: &Transaction, host: &str, last_tried: i64) -> Result<()> {
|
||||
tx.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)
|
||||
ON CONFLICT(host) DO UPDATE SET last_tried=excluded.last_tried",
|
||||
(host, last_tried),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn login_param_from_host(host: &str, mark_as_autorelay: bool) -> EnteredLoginParam {
|
||||
let rng = &mut rand::rng();
|
||||
let username = Alphanumeric.sample_string(rng, 9);
|
||||
let addr = username + "@" + host;
|
||||
let addr = addr_normalize(&addr);
|
||||
|
||||
// `mark_as_autorelay` is a temporary precaution hack
|
||||
// while introducing onboarding on multiple community relays from a list:
|
||||
// though relay operators were asked to get on that list, unexpected things can happen,
|
||||
// and they want to return to allow only manual onboarding.
|
||||
// this is possible by failing on `password_len == 23`.
|
||||
|
||||
// 22 * log2(26 * 2 + 10) = 130 bits of entropy
|
||||
let password = Alphanumeric.sample_string(rng, 22);
|
||||
let password = Alphanumeric.sample_string(rng, if mark_as_autorelay { 23 } else { 22 });
|
||||
|
||||
EnteredLoginParam {
|
||||
addr,
|
||||
|
||||
@@ -1,42 +1,150 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::EventType;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
/// Tests that the default relays are candidates without a row in the table.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_defaults() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
assert!(DEFAULT_RELAY_CANDIDATES.is_sorted());
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
assert_eq!(candidates, DEFAULT_RELAY_CANDIDATES);
|
||||
|
||||
let tried = DEFAULT_RELAY_CANDIDATES[0];
|
||||
save_relay_candidates(t, &[tried], now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
assert_eq!(candidates.len(), DEFAULT_RELAY_CANDIDATES.len() - 1);
|
||||
assert!(!candidates.contains(&tried.to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a transport is added on a candidate from the given addresses.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
let addrs_from_qr = [
|
||||
"alice@example.org".to_string(),
|
||||
"bob@example.org".to_string(),
|
||||
];
|
||||
let skip_network = true;
|
||||
add_relay_candidates(t, &addrs_from_qr).await?;
|
||||
add_transport_from_candidates(t, skip_network).await?;
|
||||
|
||||
let transports = t.list_transports().await?;
|
||||
assert_eq!(transports.len(), 1);
|
||||
assert!(transports[0].addr.ends_with("@example.org"));
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["example.org"]);
|
||||
assert!(configure_progress_emitted(t).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests correct add_transport_from_candidates error handling.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_transport_from_candidates_failure() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
mark_defaults_tried(t, time()).await?;
|
||||
save_relay_candidates(t, &["bad host", "worse host"], 0).await?;
|
||||
|
||||
let skip_network = true;
|
||||
let err = add_transport_from_candidates(t, skip_network)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(format!("{err:#}").contains("Bad email-address"));
|
||||
assert!(!t.is_configured().await?);
|
||||
let untried = untried_relay_candidates(t).await?;
|
||||
assert_eq!(untried, ["bad host", "worse host"]);
|
||||
t.assert_warns_or_errors(&[
|
||||
"Failed to add relay bad host",
|
||||
"Failed to add relay worse host",
|
||||
])
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn untried_relay_candidates(t: &TestContext) -> Result<Vec<String>> {
|
||||
t.sql
|
||||
.query_map_vec(
|
||||
"SELECT host FROM relay_candidates WHERE last_tried=0 ORDER BY host",
|
||||
(),
|
||||
|row| Ok(row.get(0)?),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn save_relay_candidates(t: &TestContext, hosts: &[&str], last_tried: i64) -> Result<()> {
|
||||
t.sql
|
||||
.transaction(|tx| {
|
||||
for host in hosts {
|
||||
save_relay_candidate(tx, host, last_tried)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Keeps the default relays out of `triable_relay_candidates()`.
|
||||
async fn mark_defaults_tried(t: &TestContext, now: i64) -> Result<()> {
|
||||
save_relay_candidates(t, DEFAULT_RELAY_CANDIDATES, now).await
|
||||
}
|
||||
|
||||
/// Consumes emitted events, telling whether a configure progress is among them.
|
||||
async fn configure_progress_emitted(t: &TestContext) -> bool {
|
||||
t.evtracker
|
||||
.get_matching_opt(t, |evt| matches!(evt, EventType::ConfigureProgress { .. }))
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
/// Tests that saving a candidate overwrites its stored timestamp.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_relay_candidate() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
let now = time();
|
||||
|
||||
for last_tried in [0, now, 0] {
|
||||
t.sql
|
||||
.transaction(|tx| save_relay_candidate(tx, "relay.example", last_tried))
|
||||
.await?;
|
||||
let stored: Option<i64> = t
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT last_tried FROM relay_candidates WHERE host=?",
|
||||
("relay.example",),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(stored, Some(last_tried));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_triable_relay_candidates_single() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
|
||||
// This host should be returned by load_relay_candidates():
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("never_tried.example", 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &["never_tried.example", "example.org"], 0).await?;
|
||||
save_relay_candidates(t, &["recent.example"], now).await?;
|
||||
|
||||
// This host was recently tried and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("recent.example", now),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// This host is already in use (alice@example.org) and should not be returned:
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("example.org", 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let candidates = load_relay_candidates(t, now).await?;
|
||||
let candidates = triable_relay_candidates(t, now).await?;
|
||||
|
||||
assert_eq!(candidates, vec!["never_tried.example".to_string()]);
|
||||
|
||||
@@ -44,22 +152,15 @@ async fn test_load_relay_candidates_single() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_relay_candidates_multiple() -> Result<()> {
|
||||
async fn test_triable_relay_candidates_multiple() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example"], 0).await?;
|
||||
|
||||
let mut candidates = load_relay_candidates(t, now).await?;
|
||||
let mut candidates = triable_relay_candidates(t, now).await?;
|
||||
candidates.sort();
|
||||
|
||||
assert_eq!(
|
||||
@@ -160,13 +261,8 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
("relay.example", 0),
|
||||
)
|
||||
.await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["relay.example"], 0).await?;
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
|
||||
@@ -179,6 +275,7 @@ async fn test_maybe_add_additional_relays_add_one() -> Result<()> {
|
||||
|
||||
let transports_after = t.count_transports().await?;
|
||||
assert_eq!(transports_after, transports_before + 1);
|
||||
assert!(!configure_progress_emitted(t).await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -189,15 +286,8 @@ async fn test_maybe_add_additional_relays_add_multiple() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
for host in ["a.example", "b.example", "c.example", "d.example"] {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(host, 0),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
mark_defaults_tried(t, now).await?;
|
||||
save_relay_candidates(t, &["a.example", "b.example", "c.example", "d.example"], 0).await?;
|
||||
|
||||
let skip_network = true;
|
||||
let relay_added = maybe_add_additional_relays_inner(t, skip_network).await?;
|
||||
@@ -218,14 +308,9 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
enable_config(t).await;
|
||||
let now = time();
|
||||
|
||||
t.sql.execute("DELETE FROM relay_candidates", ()).await?;
|
||||
mark_defaults_tried(t, now).await?;
|
||||
for i in 1..10 {
|
||||
t.sql
|
||||
.execute(
|
||||
"INSERT INTO relay_candidates (host, last_tried) VALUES (?, ?)",
|
||||
(format!("{i}.invalid.example"), 0),
|
||||
)
|
||||
.await?;
|
||||
save_relay_candidates(t, &[format!("{i}.invalid.example").as_str()], 0).await?;
|
||||
}
|
||||
|
||||
let transports_before = t.count_transports().await?;
|
||||
@@ -254,7 +339,7 @@ async fn test_maybe_add_additional_relays_failure() -> Result<()> {
|
||||
|
||||
// ...but not all, because there might be many relay candidates
|
||||
// and we don't want to try all of them in a single call:
|
||||
assert_eq!(load_relay_candidates(t, now).await?.is_empty(), false);
|
||||
assert_eq!(triable_relay_candidates(t, now).await?.is_empty(), false);
|
||||
|
||||
t.assert_warns_or_errors(&[
|
||||
"DNS lookup with memory cache failure",
|
||||
|
||||
+13
-46
@@ -42,7 +42,7 @@ const RINGING_SECONDS: i64 = 120;
|
||||
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
|
||||
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
|
||||
|
||||
const STUN_PORT: u16 = 3478;
|
||||
const TURN_PORT: u16 = 3478;
|
||||
|
||||
/// Set if incoming call was ended explicitly
|
||||
/// by the other side before we accepted it.
|
||||
@@ -658,12 +658,9 @@ pub(crate) async fn create_ice_servers_from_metadata(
|
||||
Ok((expiration_timestamp, ice_servers))
|
||||
}
|
||||
|
||||
/// STUN or TURN server with unresolved DNS name.
|
||||
/// TURN server with unresolved DNS name.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub(crate) enum UnresolvedIceServer {
|
||||
/// STUN server.
|
||||
Stun { hostname: String, port: u16 },
|
||||
|
||||
/// TURN server with the username and password.
|
||||
Turn {
|
||||
hostname: String,
|
||||
@@ -688,28 +685,6 @@ pub(crate) async fn resolve_ice_servers(
|
||||
|
||||
for unresolved_ice_server in unresolved_ice_servers {
|
||||
match unresolved_ice_server {
|
||||
UnresolvedIceServer::Stun { hostname, port } => {
|
||||
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
|
||||
Ok(addrs) => {
|
||||
let urls: Vec<String> = addrs
|
||||
.into_iter()
|
||||
.map(|addr| format!("stun:{addr}"))
|
||||
.collect();
|
||||
let stun_server = IceServer {
|
||||
urls,
|
||||
username: None,
|
||||
credential: None,
|
||||
};
|
||||
result.push(stun_server);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to resolve STUN {hostname}:{port}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname,
|
||||
port,
|
||||
@@ -742,25 +717,17 @@ pub(crate) async fn resolve_ice_servers(
|
||||
|
||||
/// Creates JSON with ICE servers when no TURN servers are known.
|
||||
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
|
||||
// Do not use public STUN server from https://stunprotocol.org/.
|
||||
// It changes the hostname every year
|
||||
// (e.g. stunserver2025.stunprotocol.org
|
||||
// which was previously stunserver2024.stunprotocol.org)
|
||||
// because of bandwidth costs:
|
||||
// <https://github.com/jselbie/stunserver/issues/50>
|
||||
|
||||
vec![
|
||||
UnresolvedIceServer::Stun {
|
||||
hostname: "nine.testrun.org".to_string(),
|
||||
port: STUN_PORT,
|
||||
},
|
||||
UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: STUN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
},
|
||||
]
|
||||
// As long as we can't rely on most chat profiles
|
||||
// having a multi-relay setup and offering TURN servers,
|
||||
// fall back to a service run by Delta Chat developers
|
||||
// which ensures that no IP addresses or other metadata is
|
||||
// persistently logged.
|
||||
vec![UnresolvedIceServer::Turn {
|
||||
hostname: "turn.delta.chat".to_string(),
|
||||
port: TURN_PORT,
|
||||
username: "public".to_string(),
|
||||
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
|
||||
}]
|
||||
}
|
||||
|
||||
/// Returns JSON with ICE servers.
|
||||
|
||||
@@ -779,3 +779,13 @@ async fn test_end_text_call() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that fallback ice servers just carry the Turn one
|
||||
#[test]
|
||||
fn test_fallback_ice_servers() {
|
||||
let hostnames: Vec<String> = create_fallback_ice_servers()
|
||||
.into_iter()
|
||||
.map(|UnresolvedIceServer::Turn { hostname, .. }| hostname)
|
||||
.collect();
|
||||
assert_eq!(hostnames, ["turn.delta.chat"]);
|
||||
}
|
||||
|
||||
+96
-194
@@ -11,7 +11,6 @@ use std::time::Duration;
|
||||
use anyhow::{Context as _, Result, anyhow, bail, ensure};
|
||||
use chrono::TimeZone;
|
||||
use deltachat_contact_tools::{ContactAddress, sanitize_bidi_characters, sanitize_single_line};
|
||||
use humansize::{BINARY, format_size};
|
||||
use mail_builder::mime::MimePart;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -27,26 +26,22 @@ use crate::constants::{
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc;
|
||||
use crate::download::{
|
||||
DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD, PRE_MSG_SIZE_WARNING_THRESHOLD,
|
||||
};
|
||||
use crate::download::{DownloadState, PRE_MSG_ATTACHMENT_SIZE_THRESHOLD};
|
||||
use crate::ensure_and_debug_assert_eq;
|
||||
use crate::ephemeral::{Timer as EphemeralTimer, start_chat_ephemeral_timers};
|
||||
use crate::events::EventType;
|
||||
use crate::key;
|
||||
use crate::key::{Fingerprint, self_fingerprint};
|
||||
use crate::location;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::logged_debug_assert;
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::reaction::broadcast_reactions;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::smtp::queue::{ToBeQueuedMail, enqueue_mail};
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
@@ -336,16 +331,18 @@ impl ChatId {
|
||||
Ok(chat_id)
|
||||
}
|
||||
|
||||
async fn set_selfavatar_timestamp(self, context: &Context, timestamp: i64) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
pub(crate) fn set_selfavatar_timestamp(
|
||||
self,
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts
|
||||
SET selfavatar_sent=?
|
||||
WHERE id IN(SELECT contact_id FROM chats_contacts WHERE chat_id=? AND add_timestamp >= remove_timestamp)",
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
) ?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2686,7 +2683,7 @@ async fn prepare_send_msg(
|
||||
|
||||
let skip_fn = |reason: &CantSendReason| match reason {
|
||||
CantSendReason::ContactRequest => {
|
||||
// Allow securejoin messages, they are supposed to repair the verification.
|
||||
// Allow securejoin messages.
|
||||
// If the chat is a contact request, let the user accept it later.
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
}
|
||||
@@ -2776,11 +2773,8 @@ async fn render_mime_message_and_pre_message(
|
||||
context: &Context,
|
||||
msg: &mut Message,
|
||||
mimefactory: MimeFactory,
|
||||
) -> Result<(Option<RenderedEmail>, RenderedEmail)> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
|
||||
bcc_self: bool,
|
||||
) -> Result<(Option<ToBeQueuedMail>, ToBeQueuedMail)> {
|
||||
let needs_pre_message = msg.viewtype.has_file()
|
||||
&& mimefactory.will_be_encrypted() // unencrypted is likely email, we don't want to spam by sending multiple messages
|
||||
&& msg
|
||||
@@ -2797,53 +2791,27 @@ async fn render_mime_message_and_pre_message(
|
||||
|
||||
let mut mimefactory_post_msg = mimefactory.clone();
|
||||
mimefactory_post_msg.set_as_post_message();
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory_post_msg.into_queued_mail(context))
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr.clone(),
|
||||
side_effects,
|
||||
)?;
|
||||
let (queued_msg, side_effects) =
|
||||
Box::pin(mimefactory_post_msg.into_queued_mail(context, bcc_self))
|
||||
.await
|
||||
.context("Failed to render post-message")?;
|
||||
|
||||
let mut mimefactory_pre_msg = mimefactory;
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&rendered_msg);
|
||||
mimefactory_pre_msg.set_as_pre_message_for(&queued_msg.rfc724_mid);
|
||||
let (queued_pre_msg, pre_side_effects) =
|
||||
Box::pin(mimefactory_pre_msg.into_queued_mail(context))
|
||||
Box::pin(mimefactory_pre_msg.into_queued_mail(context, bcc_self))
|
||||
.await
|
||||
.context("pre-message failed to render")?;
|
||||
let rendered_pre_msg = mimefactory::render_queued_mail(
|
||||
queued_pre_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
pre_side_effects,
|
||||
)?;
|
||||
|
||||
if rendered_pre_msg.message.len() > PRE_MSG_SIZE_WARNING_THRESHOLD {
|
||||
warn!(
|
||||
context,
|
||||
"Pre-message for message {} is larger than expected: {}.",
|
||||
msg.id,
|
||||
rendered_pre_msg.message.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok((Some(rendered_pre_msg), rendered_msg))
|
||||
Ok((
|
||||
Some((queued_pre_msg, pre_side_effects)),
|
||||
(queued_msg, side_effects),
|
||||
))
|
||||
} else {
|
||||
let (queued_msg, side_effects) = Box::pin(mimefactory.into_queued_mail(context)).await?;
|
||||
let rendered_msg = mimefactory::render_queued_mail(
|
||||
queued_msg,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
side_effects,
|
||||
)?;
|
||||
let (queued_msg, side_effects) =
|
||||
Box::pin(mimefactory.into_queued_mail(context, bcc_self)).await?;
|
||||
|
||||
Ok((None, rendered_msg))
|
||||
Ok((None, (queued_msg, side_effects)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2857,6 +2825,8 @@ async fn render_mime_message_and_pre_message(
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let now = time();
|
||||
|
||||
let cmd = msg.param.get_cmd();
|
||||
if cmd == SystemMessage::GroupNameChanged || cmd == SystemMessage::GroupDescriptionChanged {
|
||||
msg.chat_id
|
||||
@@ -2888,12 +2858,14 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
let mut recipients = mimefactory.recipients();
|
||||
let recipients = mimefactory.recipients();
|
||||
debug_assert!(!recipients.iter().any(|s| s.is_empty()));
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
|
||||
// Default Webxdc integrations are hidden messages and must not be sent out:
|
||||
if (msg.param.get_int(Param::WebxdcIntegration).is_some() && msg.hidden)
|
||||
// This may happen eg. for groups with only SELF and bcc_self disabled:
|
||||
|| (!context.get_config_bool(Config::BccSelf).await? && recipients.is_empty())
|
||||
|| (!bcc_self && recipients.is_empty())
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
@@ -2906,8 +2878,9 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let (rendered_pre_msg, rendered_msg) =
|
||||
match render_mime_message_and_pre_message(context, msg, mimefactory).await {
|
||||
let is_encrypted = mimefactory.will_be_encrypted();
|
||||
let (queued_pre_msg_pair, queued_msg_pair) =
|
||||
match render_mime_message_and_pre_message(context, msg, mimefactory, bcc_self).await {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
message::set_msg_failed(context, msg, &err.to_string()).await?;
|
||||
@@ -2915,29 +2888,13 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
}
|
||||
}?;
|
||||
|
||||
if let (post_msg, Some(pre_msg)) = (&rendered_msg, &rendered_pre_msg) {
|
||||
info!(
|
||||
context,
|
||||
"Message {} sizes: pre-message: {}; post-message: {}.",
|
||||
msg.id,
|
||||
format_size(pre_msg.message.len(), BINARY),
|
||||
format_size(post_msg.message.len(), BINARY),
|
||||
);
|
||||
if let Some((pre_msg, _)) = &queued_pre_msg_pair {
|
||||
msg.pre_rfc724_mid = pre_msg.rfc724_mid.clone();
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Message {} will be sent in one shot (no pre- and post-message). Size: {}.",
|
||||
msg.id,
|
||||
format_size(rendered_msg.message.len(), BINARY),
|
||||
);
|
||||
}
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
smtp::add_self_recipients(context, &mut recipients, rendered_msg.is_encrypted).await?;
|
||||
}
|
||||
let (queued_msg, side_effects) = queued_msg_pair;
|
||||
|
||||
if needs_encryption && !rendered_msg.is_encrypted {
|
||||
if needs_encryption && !is_encrypted {
|
||||
let addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
let text = stock_str::unencrypted_email(
|
||||
context,
|
||||
@@ -2967,95 +2924,62 @@ async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Ve
|
||||
);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
|
||||
if let Some(last_added_location_timestamp) =
|
||||
rendered_msg.side_effects.last_added_location_timestamp
|
||||
{
|
||||
location::set_kml_sent_timestamp(context, msg.chat_id, last_added_location_timestamp)
|
||||
.await?;
|
||||
if let Some(ref side_effects) = side_effects {
|
||||
msg.subject.clone_from(&side_effects.subject);
|
||||
}
|
||||
|
||||
if rendered_msg.side_effects.avatar_is_attached
|
||||
|| rendered_pre_msg
|
||||
.as_ref()
|
||||
.is_some_and(|msg| msg.side_effects.avatar_is_attached)
|
||||
{
|
||||
msg.chat_id
|
||||
.set_selfavatar_timestamp(context, now)
|
||||
.await
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
}
|
||||
|
||||
if rendered_msg.is_encrypted {
|
||||
if is_encrypted {
|
||||
msg.param.set_int(Param::GuaranteeE2ee, 1);
|
||||
} else {
|
||||
msg.param.remove(Param::GuaranteeE2ee);
|
||||
}
|
||||
msg.subject.clone_from(&rendered_msg.side_effects.subject);
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
|
||||
if let Some(sync_ids) = rendered_msg.side_effects.sync_ids_to_delete {
|
||||
t.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
if !recipients.is_empty() {
|
||||
let mut stmt = t.prepare(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
)?;
|
||||
let all_recipients = recipients.join(" ");
|
||||
if let Some(pre_msg) = &rendered_pre_msg {
|
||||
let row_id = stmt.insert((
|
||||
&pre_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&pre_msg.message,
|
||||
.transaction(|transaction| {
|
||||
// Sort the message to the bottom. Employ `msgs_index7` to compute `timestamp`.
|
||||
transaction.execute(
|
||||
"
|
||||
UPDATE msgs SET
|
||||
timestamp=(
|
||||
SELECT MAX(timestamp) FROM msgs INDEXED BY msgs_index7 WHERE
|
||||
-- From `InFresh` to `OutDelivered` inclusive, except `OutDraft`.
|
||||
state IN(10,13,16,18,20,24,26) AND
|
||||
hidden IN(0,1) AND
|
||||
chat_id=? AND
|
||||
id<=?
|
||||
),
|
||||
pre_rfc724_mid=?, subject=?, param=?
|
||||
WHERE id=?
|
||||
",
|
||||
(
|
||||
msg.chat_id,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id);
|
||||
&msg.pre_rfc724_mid,
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.id,
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut row_ids = Vec::new();
|
||||
if let Some((queued_pre_msg, pre_side_effects)) = queued_pre_msg_pair {
|
||||
let row_id = enqueue_mail(
|
||||
transaction,
|
||||
now,
|
||||
msg.id,
|
||||
&queued_pre_msg,
|
||||
pre_side_effects.as_ref(),
|
||||
)
|
||||
.context("Failed to enqueue pre-message")?;
|
||||
row_ids.push(row_id)
|
||||
}
|
||||
let row_id = stmt.insert((
|
||||
&rendered_msg.rfc724_mid,
|
||||
&all_recipients,
|
||||
&rendered_msg.message,
|
||||
msg.id,
|
||||
))?;
|
||||
row_ids.push(row_id);
|
||||
}
|
||||
Ok(row_ids)
|
||||
};
|
||||
context.sql.transaction(trans_fn).await
|
||||
row_ids.push(
|
||||
enqueue_mail(transaction, now, msg.id, &queued_msg, side_effects.as_ref())
|
||||
.context("Failed to enqueue message")?,
|
||||
);
|
||||
Ok(row_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Sends a text message to the given chat.
|
||||
@@ -3994,15 +3918,7 @@ pub(crate) async fn add_contact_to_chat_ext(
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
let contact_addr = contact.get_addr().to_lowercase();
|
||||
let added_by = if from_handshake && chat.typ == Chattype::OutBroadcast {
|
||||
// The contact was added via a QR code rather than explicit user action,
|
||||
// so it could be confusing to say 'You added member Alice'.
|
||||
// And in a broadcast, SELF is the only one who can add members,
|
||||
// so, no information is lost by just writing 'Member Alice added' instead.
|
||||
ContactId::UNDEFINED
|
||||
} else {
|
||||
ContactId::SELF
|
||||
};
|
||||
let added_by = ContactId::SELF;
|
||||
msg.text = stock_str::msg_add_member_local(context, contact.id, added_by).await;
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
@@ -4016,6 +3932,11 @@ pub(crate) async fn add_contact_to_chat_ext(
|
||||
.await?
|
||||
.context("Failed to find broadcast shared secret")?;
|
||||
msg.param.set(PARAM_BROADCAST_SECRET, secret);
|
||||
|
||||
// We don't show "member added" info-messages in channels,
|
||||
// because there can be a lot members added,
|
||||
// and these messages would clutter the timeline.
|
||||
msg.hidden = true;
|
||||
}
|
||||
send_msg(context, chat_id, &mut msg).await?;
|
||||
|
||||
@@ -5004,7 +4925,7 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"INSERT INTO devmsglabels (label) VALUES ("core-welcome-image"), ("core-welcome")"#,
|
||||
"INSERT INTO devmsglabels (label) VALUES ('core-welcome-image'), ('core-welcome')",
|
||||
(),
|
||||
)
|
||||
.await?;
|
||||
@@ -5183,7 +5104,7 @@ async fn set_contacts_by_fingerprints(
|
||||
if contacts == contacts_old {
|
||||
return Ok(());
|
||||
}
|
||||
let broadcast_contacts_added = context
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
// For broadcast channels, we only add members,
|
||||
@@ -5200,31 +5121,12 @@ async fn set_contacts_by_fingerprints(
|
||||
let mut statement = transaction.prepare(
|
||||
"INSERT OR IGNORE INTO chats_contacts (chat_id, contact_id) VALUES (?, ?)",
|
||||
)?;
|
||||
let mut broadcast_contacts_added = Vec::new();
|
||||
for contact_id in &contacts {
|
||||
if statement.execute((id, contact_id))? > 0 && chat.typ == Chattype::OutBroadcast {
|
||||
broadcast_contacts_added.push(*contact_id);
|
||||
}
|
||||
statement.execute((id, contact_id))?;
|
||||
}
|
||||
Ok(broadcast_contacts_added)
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
let timestamp = time();
|
||||
for added_id in broadcast_contacts_added {
|
||||
let msg = stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED).await;
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
id,
|
||||
&msg,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
Some(timestamp),
|
||||
timestamp,
|
||||
None,
|
||||
Some(ContactId::SELF),
|
||||
Some(added_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+23
-58
@@ -1702,7 +1702,10 @@ async fn test_shall_attach_selfavatar() -> Result<()> {
|
||||
add_contact_to_chat(alice, chat_id, contact_id).await?;
|
||||
assert!(shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
chat_id.set_selfavatar_timestamp(alice, time()).await?;
|
||||
alice
|
||||
.sql
|
||||
.transaction(|transaction| chat_id.set_selfavatar_timestamp(transaction, time()))
|
||||
.await?;
|
||||
assert!(!shall_attach_selfavatar(alice, chat_id).await?);
|
||||
|
||||
alice.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending
|
||||
@@ -3000,13 +3003,13 @@ async fn test_broadcast_change_name() -> Result<()> {
|
||||
|
||||
tcm.section("Bob receives the name-change system message");
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||
assert_eq!(msg.subject, "My great broadcast");
|
||||
let bob_chat = Chat::load_from_db(bob, msg.chat_id).await?;
|
||||
assert_eq!(bob_chat.name, "My great broadcast");
|
||||
|
||||
tcm.section("Fiona receives the name-change system message");
|
||||
let msg = fiona.recv_msg(&sent).await;
|
||||
assert_eq!(msg.subject, "Re: My great broadcast");
|
||||
assert_eq!(msg.subject, "My great broadcast");
|
||||
let fiona_chat = Chat::load_from_db(fiona, msg.chat_id).await?;
|
||||
assert_eq!(fiona_chat.name, "My great broadcast");
|
||||
}
|
||||
@@ -3340,41 +3343,27 @@ async fn test_broadcast_recipients_sync1() -> Result<()> {
|
||||
alice2.assert_warn("unknown grpid").await;
|
||||
|
||||
let member_added = alice1.pop_sent_msg().await;
|
||||
let a2_charlie_added = alice2.recv_msg(&member_added).await;
|
||||
alice2.recv_msg_trash(&member_added).await;
|
||||
let _c_member_added = charlie.recv_msg(&member_added).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
assert_eq!(a2_chatlist.get_msg_id(0)?.unwrap(), a2_charlie_added.id);
|
||||
|
||||
// Alice1 will now sync the full member list to Alice2:
|
||||
sync(alice1, alice2).await;
|
||||
let a2_bob_contact = alice2.add_or_lookup_contact_id(bob).await;
|
||||
let a2_charlie_contact = alice2.add_or_lookup_contact_id(charlie).await;
|
||||
let a2_chatlist = Chatlist::try_load(alice2, 0, Some("Channel"), None).await?;
|
||||
let msg_id = a2_chatlist.get_msg_id(0)?.unwrap();
|
||||
let a2_bob_added = Message::load_from_db(alice2, msg_id).await?;
|
||||
assert_ne!(a2_bob_added.id, a2_charlie_added.id);
|
||||
assert_eq!(
|
||||
a2_bob_added.text,
|
||||
stock_str::msg_add_member_local(alice2, a2_bob_contact, ContactId::UNDEFINED).await
|
||||
);
|
||||
assert_eq!(a2_bob_added.from_id, ContactId::SELF);
|
||||
assert_eq!(
|
||||
a2_bob_added.param.get_cmd(),
|
||||
SystemMessage::MemberAddedToGroup
|
||||
);
|
||||
assert_eq!(
|
||||
ContactId::new(
|
||||
a2_bob_added
|
||||
.param
|
||||
.get_int(Param::ContactAddedRemoved)
|
||||
.unwrap()
|
||||
.try_into()
|
||||
.unwrap()
|
||||
),
|
||||
a2_bob_contact
|
||||
);
|
||||
let a2_chat_id = a2_chatlist.get_chat_id(0).unwrap();
|
||||
|
||||
let a2_chat_members = get_chat_contacts(alice2, a2_charlie_added.chat_id).await?;
|
||||
// Also for Alice2, no info message should be shown;
|
||||
// she should see only the "Messages are end-to-end encrypted" message.
|
||||
let a2_chat_msgs = get_chat_msgs(alice2, a2_chat_id).await?;
|
||||
assert_eq!(a2_chat_msgs.len(), 1);
|
||||
let ChatItem::Message { msg_id } = a2_chat_msgs[0] else {
|
||||
unreachable!()
|
||||
};
|
||||
let a2_msg = Message::load_from_db(alice2, msg_id).await?;
|
||||
assert_eq!(a2_msg.get_info_type(), SystemMessage::ChatE2ee);
|
||||
|
||||
let a2_chat_members = get_chat_contacts(alice2, a2_chat_id).await?;
|
||||
assert!(a2_chat_members.contains(&a2_bob_contact));
|
||||
assert!(a2_chat_members.contains(&a2_charlie_contact));
|
||||
assert_eq!(a2_chat_members.len(), 2);
|
||||
@@ -3756,25 +3745,6 @@ async fn test_broadcast_joining_golden() -> Result<()> {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
alice_bob_contact
|
||||
.get_verifier_id(alice)
|
||||
.await?
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
ContactId::SELF
|
||||
);
|
||||
|
||||
let bob_alice_contact = bob.add_or_lookup_contact_no_key(alice).await;
|
||||
assert_eq!(
|
||||
bob_alice_contact
|
||||
.get_verifier_id(bob)
|
||||
.await?
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
ContactId::SELF
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4057,10 +4027,9 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
bob1.recv_msg(&member_added).await;
|
||||
|
||||
// The single chat should not be visible to the user on any of the devices.
|
||||
// The contact should be marked as verified.
|
||||
check_single_chat_is_hidden_and_contact_is_verified(alice, bob0).await;
|
||||
check_single_chat_is_hidden_and_contact_is_verified(bob0, alice).await;
|
||||
check_single_chat_is_hidden_and_contact_is_verified(bob1, alice).await;
|
||||
check_single_chat_is_hidden(alice, bob0).await;
|
||||
check_single_chat_is_hidden(bob0, alice).await;
|
||||
check_single_chat_is_hidden(bob1, alice).await;
|
||||
|
||||
tcm.section("Alice sends first message to broadcast.");
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
|
||||
@@ -4087,10 +4056,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_single_chat_is_hidden_and_contact_is_verified(
|
||||
t: &TestContext,
|
||||
contact: &TestContext,
|
||||
) {
|
||||
async fn check_single_chat_is_hidden(t: &TestContext, contact: &TestContext) {
|
||||
let contact = t.add_or_lookup_contact_no_key(contact).await;
|
||||
if let Some(single_chat) = ChatIdBlocked::lookup_by_contact(t, contact.id)
|
||||
.await
|
||||
@@ -4098,7 +4064,6 @@ async fn check_single_chat_is_hidden_and_contact_is_verified(
|
||||
{
|
||||
assert_eq!(single_chat.blocked, Blocked::Yes);
|
||||
}
|
||||
assert!(contact.is_verified(t).await.unwrap());
|
||||
}
|
||||
|
||||
/// Test that only the owner of the broadcast channel
|
||||
|
||||
+32
-88
@@ -18,8 +18,8 @@ use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{get_abs_path, time};
|
||||
use crate::transport::{add_pseudo_transport, send_sync_transports, transport_addrs};
|
||||
use crate::tools::get_abs_path;
|
||||
use crate::transport::transport_addrs;
|
||||
use crate::{constants, stats};
|
||||
|
||||
/// The available configuration keys.
|
||||
@@ -42,10 +42,12 @@ use crate::{constants, stats};
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum Config {
|
||||
/// Deprecated(2026-04).
|
||||
/// Use ConfiguredAddr, [`crate::login_param::EnteredLoginParam`],
|
||||
/// or add_transport{from_qr}()/list_transports() instead.
|
||||
///
|
||||
/// Email address, used in the `From:` field.
|
||||
/// Email address used by the deprecated configure() procedure.
|
||||
///
|
||||
/// Use add_transport{from_qr}() to configure new transports,
|
||||
/// Use list_transports() to learn about configured transports,
|
||||
/// including their addresses.
|
||||
Addr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
@@ -195,9 +197,9 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DeleteDeviceAfter,
|
||||
|
||||
/// The primary email address, used for sending and background fetch.
|
||||
/// Deprecated(2026-09).
|
||||
///
|
||||
/// Device-local, other devices keep their own primary transport.
|
||||
/// Use ConfiguredLoginParam and list_transports() instead.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// Deprecated(2026-04).
|
||||
@@ -305,18 +307,6 @@ pub enum Config {
|
||||
/// True if account is configured.
|
||||
Configured,
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// Most usages in UIs can be replaced by `force_encryption`.
|
||||
///
|
||||
/// True if account is a chatmail account.
|
||||
IsChatmail,
|
||||
|
||||
/// True if `IsChatmail` mustn't be autoconfigured. For tests.
|
||||
FixIsChatmail,
|
||||
|
||||
/// True if account is muted.
|
||||
IsMuted,
|
||||
|
||||
@@ -499,11 +489,6 @@ impl Config {
|
||||
| Self::ForceEncryption,
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(self, Config::ConfiguredAddr)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -549,7 +534,6 @@ impl Context {
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
};
|
||||
Ok(val)
|
||||
@@ -683,10 +667,6 @@ impl Context {
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
|
||||
let _pause = match key.needs_io_restart() {
|
||||
true => self.scheduler.pause(self).await?,
|
||||
_ => Default::default(),
|
||||
};
|
||||
if key == Config::StatsSending {
|
||||
let old_value = self.get_config(key).await?;
|
||||
let old_value = bool_from_config(old_value.as_deref());
|
||||
@@ -766,63 +746,28 @@ impl Context {
|
||||
bail!("Cannot unset configured_addr");
|
||||
};
|
||||
|
||||
if !self.is_configured().await? {
|
||||
info!(
|
||||
self,
|
||||
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
|
||||
);
|
||||
add_pseudo_transport(self, addr).await?;
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
|
||||
.await?;
|
||||
} else {
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE config SET value=? WHERE keyname='configured_addr'",
|
||||
(addr,),
|
||||
)?;
|
||||
self.sql
|
||||
.transaction(|transaction| {
|
||||
if transaction.query_row(
|
||||
"SELECT COUNT(*) FROM transports WHERE addr=?",
|
||||
(addr,),
|
||||
|row| {
|
||||
let res: i64 = row.get(0)?;
|
||||
Ok(res)
|
||||
},
|
||||
)? == 0
|
||||
{
|
||||
bail!("Address does not belong to any transport.");
|
||||
}
|
||||
transaction.execute(
|
||||
"INSERT OR REPLACE INTO config (keyname, value) VALUES ('configured_addr', ?)",
|
||||
(addr,),
|
||||
)?;
|
||||
|
||||
// The timestamp must strictly increase because
|
||||
// other devices ignore the row update otherwise,
|
||||
// and contacts only adopt the re-signed key
|
||||
// if its signature timestamp increases.
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE transports
|
||||
SET add_timestamp=MAX(?, add_timestamp+1)
|
||||
WHERE addr=?",
|
||||
(time(), addr),
|
||||
)
|
||||
.context(
|
||||
"Failed to update add_timestamp for the new primary transport",
|
||||
)?;
|
||||
|
||||
// Clean up SMTP queue.
|
||||
//
|
||||
// The messages in the queue have a different
|
||||
// From address so we cannot send them over
|
||||
// the new SMTP transport.
|
||||
transaction.execute("DELETE FROM smtp", ())?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
// Invalidate the cache so the sync message cannot read a stale primary address.
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
send_sync_transports(self).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
self.sql.uncache_raw_config("configured_addr").await;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
@@ -917,8 +862,7 @@ impl Context {
|
||||
.any(|a| addr_cmp(addr, a)))
|
||||
}
|
||||
|
||||
/// Sets `primary_new` as the new primary self address and saves the old
|
||||
/// primary address (if exists) as a secondary address.
|
||||
/// Sets `primary_new` as the address used for sending.
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
@@ -940,7 +884,7 @@ impl Context {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the primary self address.
|
||||
/// Returns the address of the transport used for sending.
|
||||
/// Returns an error if no self addr is configured.
|
||||
pub async fn get_primary_self_addr(&self) -> Result<String> {
|
||||
self.get_config(Config::ConfiguredAddr)
|
||||
|
||||
+119
-45
@@ -31,7 +31,7 @@ use crate::login_param::EnteredCertificateChecks;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{self, Protocol, Socket};
|
||||
use crate::qr::{login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::qr::{Qr, check_qr, login_param_from_account_qr, login_param_from_login_qr};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::Nosync;
|
||||
use crate::tools::time;
|
||||
@@ -40,27 +40,30 @@ use crate::transport::{
|
||||
ConnectionCandidate, delete_transport_row, maybe_update_sending_transport,
|
||||
purge_transport_caches, send_sync_transports, transport_addrs,
|
||||
};
|
||||
use crate::{EventType, stock_str};
|
||||
use crate::{EventType, autorelay, stock_str};
|
||||
|
||||
/// Maximum number of relays.
|
||||
///
|
||||
/// See <https://github.com/chatmail/core/issues/7608>.
|
||||
pub(crate) const MAX_RELAYS: usize = 5;
|
||||
|
||||
macro_rules! progress {
|
||||
($context:tt, $progress:expr, $comment:expr) => {
|
||||
assert!(
|
||||
$progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::ConfigureProgress {
|
||||
progress: $progress,
|
||||
comment: $comment,
|
||||
});
|
||||
};
|
||||
($context:tt, $progress:expr) => {
|
||||
progress!($context, $progress, None);
|
||||
};
|
||||
tokio::task_local! {
|
||||
pub(crate) static SILENT_PROGRESS: ();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn emit_progress(ctx: &Context, progress: u16) {
|
||||
assert!(
|
||||
progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
if SILENT_PROGRESS.try_with(|_| ()).is_ok() {
|
||||
return;
|
||||
}
|
||||
ctx.emit_event(EventType::ConfigureProgress {
|
||||
progress,
|
||||
comment: None,
|
||||
});
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -124,14 +127,17 @@ impl Context {
|
||||
pub(crate) async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
match self.add_transport_unreported(param).await {
|
||||
Ok(()) => {
|
||||
progress!(self, 1000);
|
||||
emit_progress(self, 1000);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
self.emit_event(EventType::ConfigureProgress {
|
||||
progress: 0,
|
||||
comment: Some(error_msg.clone()),
|
||||
});
|
||||
bail!(error_msg);
|
||||
}
|
||||
}
|
||||
@@ -190,6 +196,62 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an initial transport on the chatmail relay that answers fastest
|
||||
/// and lets the profile add further ones in the background.
|
||||
///
|
||||
/// A `DCACCOUNT:` or `DCLOGIN:` `qr` code adds a single transport
|
||||
/// while securejoin codes add the inviter's relays to the candidates.
|
||||
///
|
||||
/// Does nothing if the profile already has a transport.
|
||||
pub async fn init_transports(&self, qr: Option<&str>) -> Result<()> {
|
||||
if self.is_configured().await? {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(qr) = qr {
|
||||
match check_qr(self, qr).await? {
|
||||
Qr::Account { .. } | Qr::Login { .. } => {
|
||||
return self.add_transport_from_qr(qr).await;
|
||||
}
|
||||
Qr::AskVerifyContact { addrs, .. }
|
||||
| Qr::AskVerifyGroup { addrs, .. }
|
||||
| Qr::AskJoinBroadcast { addrs, .. } => {
|
||||
autorelay::add_relay_candidates(self, &addrs).await?
|
||||
}
|
||||
_ => bail!("QR code does not contain a relay"),
|
||||
}
|
||||
}
|
||||
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
let skip_network = false;
|
||||
let res = autorelay::add_transport_from_candidates(self, skip_network)
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
|
||||
.await;
|
||||
self.free_ongoing().await;
|
||||
|
||||
let configured = self.is_configured().await?;
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) if configured => {
|
||||
warn!(
|
||||
self,
|
||||
"Onboarding interrupted after adding a transport: {err:#}."
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}"));
|
||||
self.emit_event(EventType::ConfigureProgress {
|
||||
progress: 0,
|
||||
comment: Some(error_msg.clone()),
|
||||
});
|
||||
bail!(error_msg);
|
||||
}
|
||||
}
|
||||
self.set_config_bool(Config::Autorelay, true).await?;
|
||||
emit_progress(self, 1000);
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
@@ -321,7 +383,7 @@ async fn get_configured_param(
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
|
||||
progress!(ctx, 200);
|
||||
emit_progress(ctx, 200);
|
||||
|
||||
let param_autoconfig = if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
@@ -343,7 +405,7 @@ async fn get_configured_param(
|
||||
None
|
||||
};
|
||||
|
||||
progress!(ctx, 500);
|
||||
emit_progress(ctx, 500);
|
||||
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
if !servers
|
||||
@@ -437,13 +499,13 @@ pub(crate) async fn configure(
|
||||
param: &EnteredLoginParam,
|
||||
skip_network: bool,
|
||||
) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
emit_progress(ctx, 1);
|
||||
|
||||
let configured_param = get_configured_param(ctx, param, skip_network).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some())?;
|
||||
|
||||
progress!(ctx, 550);
|
||||
emit_progress(ctx, 550);
|
||||
|
||||
if !skip_network {
|
||||
// Spawn SMTP configuration task
|
||||
@@ -469,7 +531,7 @@ pub(crate) async fn configure(
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
emit_progress(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
@@ -483,23 +545,12 @@ pub(crate) async fn configure(
|
||||
}
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
emit_progress(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
smtp_config_task.await??;
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
let is_configured = ctx.is_configured().await?;
|
||||
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("1")).await?;
|
||||
} else if !is_configured {
|
||||
// Reset the setting that may have been set
|
||||
// during failed configuration.
|
||||
ctx.sql.set_raw_config("is_chatmail", Some("0")).await?;
|
||||
}
|
||||
}
|
||||
emit_progress(ctx, 900);
|
||||
|
||||
// Drop the imap connection explicitly
|
||||
// to make sure that it's not forgotten in a future refactoring
|
||||
@@ -507,7 +558,7 @@ pub(crate) async fn configure(
|
||||
drop(imap);
|
||||
}
|
||||
|
||||
progress!(ctx, 910);
|
||||
emit_progress(ctx, 910);
|
||||
|
||||
configured_param
|
||||
.clone()
|
||||
@@ -518,11 +569,11 @@ pub(crate) async fn configure(
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
progress!(ctx, 920);
|
||||
emit_progress(ctx, 920);
|
||||
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
emit_progress(ctx, 940);
|
||||
ctx.update_device_chats()
|
||||
.await
|
||||
.context("Failed to update device chats")?;
|
||||
@@ -566,7 +617,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 300);
|
||||
emit_progress(ctx, 300);
|
||||
|
||||
// `?emailaddress=` query string is excluded on purpose.
|
||||
// It is not part of the URL according to <https://datatracker.ietf.org/doc/draft-ietf-mailmaint-autoconfig/06/>.
|
||||
@@ -581,7 +632,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 310);
|
||||
emit_progress(ctx, 310);
|
||||
|
||||
// Outlook uses always SSL but different domains (this comment describes the next two steps)
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
@@ -593,7 +644,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 320);
|
||||
emit_progress(ctx, 320);
|
||||
|
||||
if let Ok(res) = outlk_autodiscover(
|
||||
ctx,
|
||||
@@ -604,7 +655,7 @@ async fn get_autoconfig(
|
||||
{
|
||||
return Some(res);
|
||||
}
|
||||
progress!(ctx, 330);
|
||||
emit_progress(ctx, 330);
|
||||
|
||||
// always SSL for Thunderbird's database
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
@@ -681,7 +732,8 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_early_configure_failure_is_reported() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let mut param = login_param_from_host("example.org");
|
||||
let mark_as_autorelay = false;
|
||||
let mut param = login_param_from_host("example.org", mark_as_autorelay);
|
||||
|
||||
// An ongoing process, e.g. a backup import,
|
||||
// makes configuration fail without ever contacting a relay.
|
||||
@@ -700,6 +752,28 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that init_transports() fails on a bad code
|
||||
/// and does nothing on a profile that already has a transport.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_init_transports() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
assert!(t.init_transports(Some("not a qr code")).await.is_err());
|
||||
assert!(!t.is_configured().await?);
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let invite = "openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40invite.example&i=TbnwJ6lSvD5&s=0ejvbdFSQxB";
|
||||
alice.init_transports(Some(invite)).await?;
|
||||
let candidates = alice
|
||||
.sql
|
||||
.count("SELECT COUNT(*) FROM relay_candidates", ())
|
||||
.await?;
|
||||
assert_eq!(candidates, 0);
|
||||
assert_eq!(alice.count_transports().await?, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_configured_param() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
@@ -726,7 +800,7 @@ mod tests {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
|
||||
// Setting ConfiguredAddr on an unconfigured account creates a pseudo transport
|
||||
add_pseudo_transport(t, "primary@example.org").await?;
|
||||
t.set_config(Config::ConfiguredAddr, Some("primary@example.org"))
|
||||
.await?;
|
||||
assert_eq!(t.count_transports().await?, 1);
|
||||
|
||||
+50
-141
@@ -38,13 +38,35 @@ use crate::param::{Param, Params};
|
||||
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
|
||||
use crate::{
|
||||
chat, chatlist_events, ensure_and_debug_assert, ensure_and_debug_assert_ne, stock_str,
|
||||
};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
/// If the contact's "last seen" is newer, the contact freshness is set to "recently seen".
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// If the contact's "last seen" is older, the contact freshness is set to "old".
|
||||
const CONTACT_OLD_SECONDS: i64 = 60 * 24 * 60 * 60;
|
||||
|
||||
/// Freshness of a contact, based on when it was last seen.
|
||||
///
|
||||
/// Used by the UI to highlight contacts:
|
||||
/// recently seen contacts get a little green dot on the avatar,
|
||||
/// contacts not seen for a long time get a string below the name (e.g. "Seen 2 months ago").
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Freshness {
|
||||
/// Contact shall not be highlighted.
|
||||
Normal = 0,
|
||||
/// Contact was seen recently.
|
||||
RecentlySeen = 1,
|
||||
/// Contact was not seen for a long time.
|
||||
Old = 2,
|
||||
}
|
||||
|
||||
impl From<Freshness> for u32 {
|
||||
fn from(freshness: Freshness) -> Self {
|
||||
freshness as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
@@ -488,8 +510,8 @@ pub struct Contact {
|
||||
/// The contact ID.
|
||||
pub id: ContactId,
|
||||
|
||||
/// Contact name. It is recommended to use `Contact::get_name`,
|
||||
/// `Contact::get_display_name` or `Contact::get_name_n_addr` to access this field.
|
||||
/// Contact name. It is recommended to use `Contact::get_name`
|
||||
/// or `Contact::get_display_name` to access this field.
|
||||
/// May be empty, initially set to `authname`.
|
||||
name: String,
|
||||
|
||||
@@ -562,10 +584,10 @@ pub enum Origin {
|
||||
/// To: of incoming messages of unknown sender
|
||||
IncomingUnknownTo = 0x40,
|
||||
|
||||
/// Address scanned but not verified.
|
||||
/// Address scanned from a QR code.
|
||||
UnhandledQrScan = 0x80,
|
||||
|
||||
/// Address scanned from a SecureJoin QR code, but not verified yet.
|
||||
/// Address scanned from a SecureJoin QR code.
|
||||
UnhandledSecurejoinQrScan = 0x81,
|
||||
|
||||
/// Reply-To: of incoming message of known sender
|
||||
@@ -596,14 +618,14 @@ pub enum Origin {
|
||||
/// address is in our address book
|
||||
AddressBook = 0x80000,
|
||||
|
||||
/// set on Alice's side for contacts like Bob that have scanned the QR code offered by her. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
|
||||
/// Set on Alice's side for contacts like Bob that have scanned the QR code offered by her.
|
||||
/// Only means the contact has once been established using the "securejoin" procedure.
|
||||
SecurejoinInvited = 0x0100_0000,
|
||||
|
||||
/// Set on Bob's side for contacts scanned from a QR code.
|
||||
/// Only means the contact has been scanned from the QR code,
|
||||
/// but does not mean that securejoin succeeded
|
||||
/// or the key has not changed since the last scan.
|
||||
/// Getting the current key verification status requires calling contact_is_verified() !
|
||||
SecurejoinJoined = 0x0200_0000,
|
||||
|
||||
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
|
||||
@@ -727,10 +749,23 @@ impl Contact {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact was seen recently.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub fn was_seen_recently(&self) -> bool {
|
||||
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
|
||||
/// Returns freshness of the contact.
|
||||
pub fn get_freshness(&self) -> Freshness {
|
||||
if self.id.is_special() || !self.is_key_contact() || self.is_blocked() {
|
||||
return Freshness::Normal;
|
||||
}
|
||||
|
||||
let is_old = time().saturating_sub(self.last_seen) > CONTACT_OLD_SECONDS;
|
||||
if is_old || self.last_seen <= 0 {
|
||||
return Freshness::Old;
|
||||
}
|
||||
|
||||
let seen_recently = time().saturating_sub(self.last_seen) <= SEEN_RECENTLY_SECONDS;
|
||||
if seen_recently {
|
||||
return Freshness::RecentlySeen;
|
||||
}
|
||||
|
||||
Freshness::Normal
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
@@ -1578,7 +1613,7 @@ WHERE addr=?
|
||||
/// May be an empty string.
|
||||
///
|
||||
/// This name is typically used in a form where the user can edit the name of a contact.
|
||||
/// To get a fine name to display in lists etc., use `Contact::get_display_name` or `Contact::get_name_n_addr`.
|
||||
/// To get a fine name to display in lists etc., use `Contact::get_display_name`.
|
||||
pub fn get_name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
@@ -1598,26 +1633,6 @@ WHERE addr=?
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Get a summary of name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
/// "email@domain.com" if the name is unset.
|
||||
///
|
||||
/// The result should only be used locally and never sent over the network
|
||||
/// as it leaks the local contact name.
|
||||
///
|
||||
/// The summary is typically used when asking the user something about the contact.
|
||||
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
|
||||
pub fn get_name_n_addr(&self) -> String {
|
||||
if !self.name.is_empty() {
|
||||
format!("{} ({})", self.name, self.addr)
|
||||
} else if !self.authname.is_empty() {
|
||||
format!("{} ({})", self.authname, self.addr)
|
||||
} else {
|
||||
(&self.addr).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the contact's profile image.
|
||||
/// This is the image set by each remote user on their own
|
||||
/// using set_config(context, "selfavatar", image).
|
||||
@@ -1690,50 +1705,6 @@ WHERE addr=?
|
||||
Ok(self.public_key(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats.
|
||||
///
|
||||
/// If contact is verified
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items and
|
||||
/// in chat member list items.
|
||||
///
|
||||
/// 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<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(true);
|
||||
}
|
||||
|
||||
Ok(self.get_verifier_id(context).await?.is_some())
|
||||
}
|
||||
|
||||
/// Returns the `ContactId` that verified the contact.
|
||||
///
|
||||
/// If this returns Some(_),
|
||||
/// display green checkmark in the profile and "Introduced by ..." line
|
||||
/// with the name of the contact.
|
||||
///
|
||||
/// If this returns `Some(None)`, then the contact is verified,
|
||||
/// but it's unclear by whom.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<Option<ContactId>>> {
|
||||
let verifier_id: u32 = context
|
||||
.sql
|
||||
.query_get_value("SELECT verifier FROM contacts WHERE id=?", (self.id,))
|
||||
.await?
|
||||
.with_context(|| format!("Contact {} does not exist", self.id))?;
|
||||
|
||||
if verifier_id == 0 {
|
||||
Ok(None)
|
||||
} else if verifier_id == self.id.to_u32() {
|
||||
Ok(Some(None))
|
||||
} else {
|
||||
Ok(Some(Some(ContactId::new(verifier_id))))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of real (i.e. non-special) contacts in the database.
|
||||
pub async fn get_real_cnt(context: &Context) -> Result<usize> {
|
||||
if !context.sql.is_open().await {
|
||||
@@ -1997,68 +1968,6 @@ pub(crate) async fn update_last_seen(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Marks contact `contact_id` as verified by `verifier_id`.
|
||||
///
|
||||
/// `verifier_id == None` means that the verifier is unknown.
|
||||
pub(crate) async fn mark_contact_id_as_verified(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
verifier_id: Option<ContactId>,
|
||||
) -> Result<()> {
|
||||
ensure_and_debug_assert_ne!(contact_id, ContactId::SELF,);
|
||||
ensure_and_debug_assert_ne!(
|
||||
Some(contact_id),
|
||||
verifier_id,
|
||||
"Contact cannot be verified by self",
|
||||
);
|
||||
let by_self = verifier_id == Some(ContactId::SELF);
|
||||
let mut verifier_id = verifier_id.unwrap_or(contact_id);
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let contact_fingerprint: String = transaction.query_row(
|
||||
"SELECT fingerprint FROM contacts WHERE id=?",
|
||||
(contact_id,),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if contact_fingerprint.is_empty() {
|
||||
bail!("Non-key-contact {contact_id} cannot be verified");
|
||||
}
|
||||
if verifier_id != ContactId::SELF {
|
||||
let (verifier_fingerprint, verifier_verifier_id): (String, ContactId) = transaction
|
||||
.query_row(
|
||||
"SELECT fingerprint, verifier FROM contacts WHERE id=?",
|
||||
(verifier_id,),
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
if verifier_fingerprint.is_empty() {
|
||||
bail!(
|
||||
"Contact {contact_id} cannot be verified by non-key-contact {verifier_id}"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
verifier_id == contact_id || verifier_verifier_id != ContactId::UNDEFINED,
|
||||
"Contact {contact_id} cannot be verified by unverified contact {verifier_id}",
|
||||
);
|
||||
if verifier_verifier_id == verifier_id {
|
||||
// Avoid introducing incorrect reverse chains: if the verifier itself has an
|
||||
// unknown verifier, it may be `contact_id` actually (directly or indirectly) on
|
||||
// the other device (which is needed for getting "verified by unknown contact"
|
||||
// in the first place).
|
||||
verifier_id = contact_id;
|
||||
}
|
||||
}
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET verifier=?1
|
||||
WHERE id=?2 AND (verifier=0 OR verifier=id OR ?3)",
|
||||
(verifier_id, contact_id, by_self),
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
|
||||
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
|
||||
}
|
||||
|
||||
@@ -253,7 +253,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_authname(), "bla foo");
|
||||
assert_eq!(contact.get_display_name(), "Name one");
|
||||
assert_eq!(contact.get_addr(), "one@eins.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)");
|
||||
|
||||
// modify first added contact
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
@@ -286,7 +285,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "three@drei.sam");
|
||||
assert_eq!(contact.get_addr(), "three@drei.sam");
|
||||
assert_eq!(contact.get_name_n_addr(), "three@drei.sam");
|
||||
|
||||
// add name to third contact from incoming message (this becomes authorized name)
|
||||
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
|
||||
@@ -300,7 +298,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact_id, contact_id_test);
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// manually edit name of third contact (does not changed authorized name)
|
||||
@@ -316,7 +313,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(sth_modified, Modifier::Modified);
|
||||
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
|
||||
assert_eq!(contact.get_authname(), "m. serious");
|
||||
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
|
||||
assert!(!contact.is_blocked());
|
||||
|
||||
// Fourth contact:
|
||||
@@ -334,7 +330,6 @@ async fn test_add_or_lookup() {
|
||||
assert_eq!(contact.get_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
|
||||
assert_eq!(contact.get_addr(), "alice@w.de");
|
||||
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
|
||||
|
||||
// check SELF
|
||||
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
|
||||
@@ -373,7 +368,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "f@example.org");
|
||||
assert_eq!(contact.get_name_n_addr(), "f@example.org");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
|
||||
@@ -399,7 +393,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Flobbyfoo");
|
||||
assert_eq!(contact.get_name_n_addr(), "Flobbyfoo (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
@@ -429,7 +422,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "");
|
||||
assert_eq!(contact.get_display_name(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name_n_addr(), "Foo Flobby (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("flobbyfoo")).await?;
|
||||
@@ -447,7 +439,6 @@ async fn test_contact_name_changes() -> Result<()> {
|
||||
assert_eq!(contact.get_authname(), "Foo Flobby");
|
||||
assert_eq!(contact.get_name(), "Falk");
|
||||
assert_eq!(contact.get_display_name(), "Falk");
|
||||
assert_eq!(contact.get_name_n_addr(), "Falk (f@example.org)");
|
||||
let contacts = Contact::get_all(&t, 0, Some("f@example.org")).await?;
|
||||
assert_eq!(contacts.len(), 0);
|
||||
let contacts = Contact::get_all(&t, 0, Some("falk")).await?;
|
||||
@@ -1057,7 +1048,7 @@ async fn test_last_seen() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
async fn test_contact_freshness() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -1070,15 +1061,57 @@ async fn test_was_seen_recently() -> Result<()> {
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Old);
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
assert_eq!(self_contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_freshness_blocked() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Alice sends message to Bob. Bob receives message, contact's freshness is "recently seen"
|
||||
let msg = tcm.send_recv(&alice, &bob, "moin").await;
|
||||
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
|
||||
// Bob blocks Alice, contact's freshness is "normal" now
|
||||
Contact::block(&bob, msg.from_id).await?;
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert!(contact.is_blocked());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_contact_freshness_address_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
let bob = tcm.bob().await;
|
||||
bob.allow_unencrypted().await?;
|
||||
|
||||
// Alice sends a message to Bob
|
||||
let alice_chat = alice.create_email_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(alice_chat.id, "moin").await;
|
||||
|
||||
// Bob receives message, contact's freshness is "normal", even though the messages was just received
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, msg.from_id).await?;
|
||||
assert!(!contact.is_key_contact());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1096,11 +1129,11 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_ne!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker.clear_events();
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::RecentlySeen);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
@@ -1108,12 +1141,12 @@ async fn test_was_seen_recently_event() -> Result<()> {
|
||||
.interrupt(contact.id, contact.last_seen)
|
||||
.await;
|
||||
|
||||
// Wait for `was_seen_recently()` to turn off.
|
||||
// Wait for "seen recently" to turn off.
|
||||
bob.evtracker.clear_events();
|
||||
SystemTime::shift(Duration::from_secs(SEEN_RECENTLY_SECONDS as u64 * 2));
|
||||
recently_seen_loop.interrupt(ContactId::UNDEFINED, 0).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
assert_eq!(contact.get_freshness(), Freshness::Normal);
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
@@ -1171,27 +1204,6 @@ async fn test_lookup_id_by_addr_recent_accepted() -> Result<()> {
|
||||
test_lookup_id_by_addr_recent_ext(accept_unencrypted_chat).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verified_by_none() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
// Receive a message from Bob to save the public key.
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_create() -> Result<()> {
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
@@ -1240,7 +1252,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_biography = bob.get_config(Config::Selfstatus).await?.unwrap();
|
||||
let chat = bob.create_chat(alice).await;
|
||||
let sent_msg = bob.send_text(chat.id, "moin").await;
|
||||
@@ -1336,7 +1348,7 @@ async fn test_make_n_import_vcard() -> Result<()> {
|
||||
async fn test_import_vcard_key_change() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_addr = &bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
alice.evtracker.clear_events();
|
||||
@@ -1391,13 +1403,11 @@ async fn test_import_vcard_key_change() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_self_is_verified() -> Result<()> {
|
||||
async fn test_self_is_key_contact() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, ContactId::SELF).await?;
|
||||
assert_eq!(contact.is_verified(&alice).await?, true);
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
Ok(())
|
||||
|
||||
+14
-29
@@ -568,20 +568,9 @@ impl Context {
|
||||
self.scheduler.maybe_network().await;
|
||||
}
|
||||
|
||||
/// Deprecated, we are trying to get rid of this global setting.
|
||||
/// It is possible to configure a profile with both chatmail relays
|
||||
/// and classical email servers.
|
||||
///
|
||||
/// Returns true if an account is on a chatmail server.
|
||||
pub async fn is_chatmail(&self) -> Result<bool> {
|
||||
self.get_config_bool(Config::IsChatmail).await
|
||||
}
|
||||
|
||||
/// Returns maximum number of recipients a single email can be sent to.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<u32> {
|
||||
let Some((transport_id, param)) = ConfiguredLoginParam::load(self).await? else {
|
||||
bail!("Not configured");
|
||||
};
|
||||
/// Returns maximum number of recipients a single email can be sent to
|
||||
/// over the transport `transport_id`, which sends from `addr`.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self, transport_id: u32, addr: &str) -> Result<u32> {
|
||||
let metadata_limit = self
|
||||
.metadata
|
||||
.read()
|
||||
@@ -591,9 +580,7 @@ impl Context {
|
||||
if let Some(limit) = metadata_limit {
|
||||
return Ok(limit);
|
||||
}
|
||||
if let Some(limit) =
|
||||
crate::provider::legacy_settings_for_addr(¶m.addr)?.max_smtp_rcpt_to
|
||||
{
|
||||
if let Some(limit) = crate::provider::legacy_settings_for_addr(addr)?.max_smtp_rcpt_to {
|
||||
return Ok(limit);
|
||||
}
|
||||
Ok(constants::DEFAULT_MAX_SMTP_RCPT_TO)
|
||||
@@ -601,9 +588,13 @@ impl Context {
|
||||
|
||||
/// Does a single round of fetching messages from all transports and returns.
|
||||
///
|
||||
/// Can be used even if I/O is currently stopped.
|
||||
/// If I/O is stopped, fetches over a dedicated connection per transport
|
||||
/// and returns as soon as one of them fetched messages.
|
||||
/// If IO is stopped, pauses the scheduler and fetches over a dedicated connection
|
||||
/// per transport, returning as soon as one of them fetched messages.
|
||||
/// If IO is running, interrupts IMAP IDLE on all transports
|
||||
/// and waits until they are done fetching.
|
||||
///
|
||||
/// Does not wait for outgoing messages to be sent out,
|
||||
/// use [`crate::accounts::Accounts::is_sending_finished`] for that.
|
||||
pub async fn background_fetch(&self) -> Result<()> {
|
||||
if !(self.is_configured().await?) {
|
||||
return Ok(());
|
||||
@@ -613,8 +604,9 @@ impl Context {
|
||||
info!(self, "background_fetch started.");
|
||||
|
||||
if self.scheduler.is_running().await {
|
||||
self.scheduler.maybe_network().await;
|
||||
self.wait_for_all_work_done().await;
|
||||
self.scheduler.interrupt_inbox_idle().await;
|
||||
let include_smtp = false;
|
||||
self.wait_for_work_done(include_smtp).await;
|
||||
} else {
|
||||
self.scheduler.background_fetch_any(self).await?;
|
||||
}
|
||||
@@ -880,13 +872,6 @@ impl Context {
|
||||
res.insert("imap_server_id", format!("{server_id:?}"));
|
||||
}
|
||||
|
||||
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
|
||||
res.insert(
|
||||
"fix_is_chatmail",
|
||||
self.get_config_bool(Config::FixIsChatmail)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"is_muted",
|
||||
self.get_config_bool(Config::IsMuted).await?.to_string(),
|
||||
|
||||
@@ -5,12 +5,35 @@ use tempfile::tempdir;
|
||||
use super::*;
|
||||
use crate::chat::{Chat, MuteDuration, get_chat_contacts, get_chat_msgs, send_msg, set_muted};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Chattype, DEFAULT_MAX_SMTP_RCPT_TO};
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
|
||||
use crate::tools::{SystemTime, create_outgoing_rfc724_mid};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_max_smtp_rcpt_to() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(
|
||||
t.get_max_smtp_rcpt_to(2, "alice@example.org").await?,
|
||||
DEFAULT_MAX_SMTP_RCPT_TO
|
||||
);
|
||||
|
||||
for (transport_id, limit) in [(1, 3), (2, 7)] {
|
||||
t.metadata.write().await.insert(
|
||||
transport_id,
|
||||
ServerMetadata {
|
||||
max_smtp_rcpt_to: Some(limit),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(t.get_max_smtp_rcpt_to(2, "alice@example.org").await?, 7);
|
||||
assert_eq!(t.get_max_smtp_rcpt_to(1, "alice@example.org").await?, 3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_db() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
|
||||
+1
-6
@@ -6,7 +6,6 @@ use anyhow::{Result, anyhow, bail, ensure};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::session::Session;
|
||||
use crate::log::warn;
|
||||
@@ -23,9 +22,6 @@ pub(crate) use post_msg_metadata::PostMsgMetadata;
|
||||
/// KiB).
|
||||
pub(crate) const PRE_MSG_ATTACHMENT_SIZE_THRESHOLD: u64 = 140_000;
|
||||
|
||||
/// Max size for pre messages. A warning is emitted when this is exceeded.
|
||||
pub(crate) const PRE_MSG_SIZE_WARNING_THRESHOLD: usize = 150_000;
|
||||
|
||||
/// Download state of the message.
|
||||
#[derive(
|
||||
Debug,
|
||||
@@ -167,8 +163,7 @@ pub(crate) async fn download_msg(
|
||||
.fetch_single_msg(context, &server_folder, server_uid, rfc724_mid)
|
||||
.await?;
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if ephemeral::should_delete_all_downloaded_messages(bcc_self, session.is_chatmail()) {
|
||||
if ephemeral::should_delete_all_downloaded_messages(context).await? {
|
||||
// Now that the message was downloaded, it likely needs to be deleted;
|
||||
// trigger a re-check by interrupting the inbox folder.
|
||||
// This is mainly needed to make the tests pass;
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tests {
|
||||
|
||||
use crate::chat;
|
||||
use crate::chat::send_text_msg;
|
||||
use crate::config::Config;
|
||||
use crate::message::Message;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -61,7 +60,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
async fn test_chatmail_can_send_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config_bool(Config::IsChatmail, true).await?;
|
||||
bob.allow_unencrypted().await?;
|
||||
let bob_chat_id = receive_imf(
|
||||
bob,
|
||||
|
||||
+5
-5
@@ -654,12 +654,11 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
|
||||
pub(crate) async fn delete_expired_imap_messages(
|
||||
context: &Context,
|
||||
transport_id: u32,
|
||||
is_chatmail: bool,
|
||||
) -> Result<()> {
|
||||
let now = time();
|
||||
|
||||
let bcc_self = context.get_config_bool(Config::BccSelf).await?;
|
||||
if should_delete_all_downloaded_messages(bcc_self, is_chatmail) {
|
||||
if should_delete_all_downloaded_messages(context).await? {
|
||||
// This is the only device using this relay.
|
||||
// Mark all downloaded messages for deletion, because they are not needed anymore.
|
||||
//
|
||||
@@ -708,7 +707,7 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
// Single device.
|
||||
// Single device, but unencrypted messages are allowed.
|
||||
// Delete all expired and encrypted messages.
|
||||
context
|
||||
.sql
|
||||
@@ -736,8 +735,9 @@ pub(crate) async fn delete_expired_imap_messages(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn should_delete_all_downloaded_messages(bcc_self: bool, is_chatmail: bool) -> bool {
|
||||
!bcc_self && is_chatmail
|
||||
pub(crate) async fn should_delete_all_downloaded_messages(context: &Context) -> Result<bool> {
|
||||
Ok(!context.get_config_bool(Config::BccSelf).await?
|
||||
&& context.get_config_bool(Config::ForceEncryption).await?)
|
||||
}
|
||||
|
||||
/// Start ephemeral timers for seen messages if they are not started
|
||||
|
||||
@@ -509,7 +509,7 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
for (is_chatmail, other_transport, bcc_self) in [
|
||||
for (force_encryption, other_transport, bcc_self) in [
|
||||
(false, false, false),
|
||||
(false, false, true),
|
||||
(false, true, false),
|
||||
@@ -520,10 +520,12 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
(true, true, true),
|
||||
] {
|
||||
println!(
|
||||
"Testing combination is_chatmail={is_chatmail}, other_transport={other_transport}, bcc_self={bcc_self}"
|
||||
"Testing combination force_encryption={force_encryption}, other_transport={other_transport}, bcc_self={bcc_self}"
|
||||
);
|
||||
|
||||
t.set_config_bool(Config::BccSelf, bcc_self).await?;
|
||||
t.set_config_bool(Config::ForceEncryption, force_encryption)
|
||||
.await?;
|
||||
|
||||
delete_expired_imap_messages(
|
||||
&t,
|
||||
@@ -532,7 +534,6 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
} else {
|
||||
transport_id
|
||||
},
|
||||
is_chatmail,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -548,7 +549,7 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, !bcc_self);
|
||||
assert_eq!(
|
||||
is_deleted(&t, "no_expire_unencrypted@localhost").await?,
|
||||
is_chatmail && !bcc_self
|
||||
force_encryption && !bcc_self
|
||||
);
|
||||
assert_eq!(is_deleted(&t, "future@localhost").await?, !bcc_self);
|
||||
assert_eq!(is_deleted(&t, "expired_post@localhost").await?, true);
|
||||
@@ -563,9 +564,10 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
|
||||
reset_targets(&t).await;
|
||||
}
|
||||
|
||||
// With BccSelf=true, non-expired messages are kept even if `is_chatmail` is true
|
||||
// With BccSelf=true, non-expired messages are kept even if `force_encryption` is true
|
||||
t.set_config_bool(Config::BccSelf, true).await?;
|
||||
delete_expired_imap_messages(&t, transport_id, true).await?;
|
||||
t.set_config_bool(Config::ForceEncryption, true).await?;
|
||||
delete_expired_imap_messages(&t, transport_id).await?;
|
||||
assert_eq!(is_deleted(&t, "expired@localhost").await?, true);
|
||||
assert_eq!(is_deleted(&t, "no_expire@localhost").await?, false);
|
||||
assert_eq!(is_deleted(&t, "done_pre@localhost").await?, false);
|
||||
@@ -676,12 +678,16 @@ async fn test_ephemeral_msg_offline() -> Result<()> {
|
||||
.await?;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(alice, chat.id, &mut msg).await.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
let stmt = "SELECT COUNT(*) FROM smtp2 WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
let now = time();
|
||||
check_msg_will_be_deleted(alice, msg.id, &chat, now, now + i64::from(duration) + 1).await?;
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
alice
|
||||
.assert_warn("No SMTP connection candidates provided")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ pub enum EventType {
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted(String),
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved(String),
|
||||
|
||||
/// Emitted before going into IDLE on the Inbox folder.
|
||||
ImapInboxIdle,
|
||||
|
||||
@@ -214,7 +211,6 @@ pub enum EventType {
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
|
||||
/// and dc_remove_contact_from_chat().
|
||||
///
|
||||
@@ -302,8 +298,7 @@ pub enum EventType {
|
||||
contact_id: ContactId,
|
||||
|
||||
/// Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "introducing myself."
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: u16,
|
||||
},
|
||||
@@ -365,6 +360,9 @@ pub enum EventType {
|
||||
/// A call made while another background fetch is running gets the event immediately,
|
||||
/// and the running fetch keeps emitting events until its own marker.
|
||||
///
|
||||
/// The event carries no data identifying the call it belongs to,
|
||||
/// so it is unambiguous only if there are no concurrent background fetch calls.
|
||||
///
|
||||
/// This event is only emitted by the account manager.
|
||||
AccountsBackgroundFetchDone,
|
||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
||||
@@ -433,7 +431,7 @@ pub enum EventType {
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// One or more transports has changed or another transport is primary now.
|
||||
/// One or more transports has changed or another transport is used for sending now.
|
||||
///
|
||||
/// UI should update the list.
|
||||
///
|
||||
|
||||
@@ -63,7 +63,6 @@ pub enum HeaderDef {
|
||||
ChatGroupDescription,
|
||||
ChatGroupDescriptionChanged,
|
||||
ChatGroupDescriptionTimestamp,
|
||||
ChatVerified,
|
||||
ChatGroupAvatar,
|
||||
ChatUserAvatar,
|
||||
ChatVoiceMessage,
|
||||
|
||||
+28
-104
@@ -129,6 +129,8 @@ pub(crate) struct ServerMetadata {
|
||||
/// True if we think the relay supports push notifications.
|
||||
/// This gates wether we attempt to write an encrypted device token
|
||||
/// to per-transport IMAP metadata key `/private/devicetoken`.
|
||||
///
|
||||
/// Any `/shared/vendor/deltachat/` entry identifies a chatmail relay.
|
||||
pub supports_push: bool,
|
||||
|
||||
/// ICE servers for WebRTC calls.
|
||||
@@ -299,7 +301,8 @@ impl Imap {
|
||||
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
|
||||
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
let mut first_error = None;
|
||||
let mut first_connection_error = None;
|
||||
let mut first_login_error = None;
|
||||
'candidate: for lp in login_params {
|
||||
info!(context, "IMAP trying to connect to {}.", lp.connection);
|
||||
let connection_candidate = lp.connection.clone();
|
||||
@@ -315,7 +318,7 @@ impl Imap {
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
first_connection_error.get_or_insert(err);
|
||||
continue 'candidate;
|
||||
}
|
||||
};
|
||||
@@ -394,12 +397,14 @@ impl Imap {
|
||||
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
first_login_error.get_or_insert(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
Err(first_login_error
|
||||
.or(first_connection_error)
|
||||
.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
}
|
||||
|
||||
/// Prepare a new IMAP session.
|
||||
@@ -418,13 +423,13 @@ impl Imap {
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// FETCH-MOVE-DELETE iteration.
|
||||
/// FETCH-and-DELETE iteration.
|
||||
///
|
||||
/// Prefetches headers and downloads new message from the folder, moves messages away from the
|
||||
/// folder and deletes messages in the folder.
|
||||
/// Prefetches headers and downloads new message from the folder
|
||||
/// and deletes messages in the folder.
|
||||
///
|
||||
/// Returns true if at least one message was fetched.
|
||||
pub async fn fetch_move_delete(
|
||||
pub async fn fetch_delete(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
@@ -450,14 +455,14 @@ impl Imap {
|
||||
|
||||
// Mark expired messages for deletion. Note that `delete_expired_imap_messages` is
|
||||
// not well optimized and should not be called before fetching.
|
||||
delete_expired_imap_messages(context, session.transport_id(), session.is_chatmail())
|
||||
delete_expired_imap_messages(context, session.transport_id())
|
||||
.await
|
||||
.context("delete_expired_imap_messages")?;
|
||||
|
||||
session
|
||||
.move_delete_messages(context, watch_folder)
|
||||
.delete_messages(context, watch_folder)
|
||||
.await
|
||||
.context("move_delete_messages")?;
|
||||
.context("delete_messages")?;
|
||||
|
||||
Ok(msgs_fetched)
|
||||
}
|
||||
@@ -568,16 +573,6 @@ impl Imap {
|
||||
.size
|
||||
.context("imap fetch response does not contain size")?;
|
||||
|
||||
// Determine the target folder where the message should be moved to.
|
||||
//
|
||||
// We only move the messages from the INBOX and Spam folders.
|
||||
// This is required to avoid infinite MOVE loop on IMAP servers
|
||||
// that alias `DeltaChat` folder to other names.
|
||||
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
|
||||
// In this case moving from `INBOX.DeltaChat` to `DeltaChat`
|
||||
// results in the messages getting a new UID,
|
||||
// so the messages will be detected as new
|
||||
// in the `INBOX.DeltaChat` folder again.
|
||||
let delete = if let Some(message_id) = &message_id {
|
||||
message::rfc724_mid_exists_ext(context, message_id, "deleted=1")
|
||||
.await?
|
||||
@@ -615,13 +610,7 @@ impl Imap {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Download only the messages which have reached their target folder if there are
|
||||
// multiple devices. This prevents race conditions in multidevice case, where one
|
||||
// device tries to download the message while another device moves the message at the
|
||||
// same time. Even in single device case it is possible to fail downloading the first
|
||||
// message, move it to the movebox and then download the second message before
|
||||
// downloading the first one, if downloading from inbox before moving is allowed.
|
||||
if folder == target
|
||||
if !delete
|
||||
&& prefetch_should_download(context, &headers, &message_id, fetch_response.flags())
|
||||
.await
|
||||
.context("prefetch_should_download")?
|
||||
@@ -858,75 +847,10 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves batch of messages identified by their UID from the currently
|
||||
/// selected folder to the target folder.
|
||||
async fn move_message_batch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
set: &str,
|
||||
row_ids: Vec<i64>,
|
||||
target: &str,
|
||||
) -> Result<()> {
|
||||
if self.can_move() {
|
||||
match self.uid_mv(set, &target).await {
|
||||
Ok(()) => {
|
||||
// Messages are moved or don't exist, IMAP returns OK response in both cases.
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("DELETE FROM imap WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Cannot delete moved messages from imap table")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} moved to {target}"
|
||||
)));
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Cannot move messages, fallback to COPY/DELETE {} to {}: {}",
|
||||
set,
|
||||
target,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Server does not support MOVE or MOVE failed.
|
||||
// Copy messages to the destination folder if needed and mark records for deletion.
|
||||
info!(
|
||||
context,
|
||||
"Server does not support MOVE, fallback to COPY/DELETE {} to {}", set, target
|
||||
);
|
||||
self.uid_copy(&set, &target).await?;
|
||||
context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let mut stmt = transaction.prepare("UPDATE imap SET target='' WHERE id = ?")?;
|
||||
for row_id in row_ids {
|
||||
stmt.execute((row_id,))?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Cannot plan deletion of messages")?;
|
||||
context.emit_event(EventType::ImapMessageMoved(format!(
|
||||
"IMAP messages {set} copied to {target}"
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Moves and deletes messages as planned in the `imap` table.
|
||||
/// Deletes messages as planned in the `imap` table.
|
||||
///
|
||||
/// This is the only place where messages are moved or deleted on the IMAP server.
|
||||
async fn move_delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
/// This is the only place where messages are deleted on the IMAP server.
|
||||
async fn delete_messages(&mut self, context: &Context, folder: &str) -> Result<()> {
|
||||
let transport_id = self.transport_id();
|
||||
let rows = context
|
||||
.sql
|
||||
@@ -948,23 +872,21 @@ impl Session {
|
||||
|
||||
for (target, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
// Select folder inside the loop to avoid selecting it if there are no pending
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
let folder_exists = self.select_with_uidvalidity(context, folder).await?;
|
||||
ensure!(folder_exists, "No folder {folder}");
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
// Since we don't move messages between IMAP folders anymore,
|
||||
// `target` is always either empty or equal to `folder`.
|
||||
debug_assert!(target.is_empty() || folder == target);
|
||||
|
||||
if target.is_empty() {
|
||||
self.delete_message_batch(context, &uid_set, rowid_set)
|
||||
.await
|
||||
.with_context(|| format!("cannot delete batch of messages {uid_set:?}"))?;
|
||||
} else {
|
||||
self.move_message_batch(context, &uid_set, rowid_set, &target)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("cannot move batch of messages {uid_set:?} to folder {target:?}",)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1384,6 +1306,8 @@ impl Session {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let supports_push =
|
||||
max_smtp_rcpt_to.is_some() || iroh_relay.is_some() || ice_servers.is_some();
|
||||
let ice_servers = if let Some(ice_servers) = ice_servers {
|
||||
ice_servers
|
||||
} else {
|
||||
@@ -1399,7 +1323,7 @@ impl Session {
|
||||
admin,
|
||||
iroh_relay,
|
||||
max_smtp_rcpt_to,
|
||||
supports_push: max_smtp_rcpt_to.is_some() || self.capabilities.has_xdeltapush,
|
||||
supports_push,
|
||||
ice_servers,
|
||||
ice_servers_expiration_timestamp,
|
||||
app_versions,
|
||||
|
||||
@@ -9,10 +9,6 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2177>
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc6851>
|
||||
pub can_move: bool,
|
||||
|
||||
/// True if the server has QUOTA capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc2087>
|
||||
pub can_check_quota: bool,
|
||||
@@ -25,17 +21,6 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc4978>
|
||||
pub can_compress: bool,
|
||||
|
||||
/// True if the server advertises the legacy `XDELTAPUSH` capability.
|
||||
pub has_xdeltapush: bool,
|
||||
|
||||
/// True if the server has an XCHATMAIL capability
|
||||
/// indicating that it is a <https://github.com/deltachat/chatmail> server.
|
||||
///
|
||||
/// This can be used to hide some advanced settings in the UI
|
||||
/// that are only interesting for normal email accounts,
|
||||
/// e.g. the ability to move messages to Delta Chat folder.
|
||||
pub is_chatmail: bool,
|
||||
|
||||
/// Server ID if the server supports ID capability.
|
||||
pub server_id: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
@@ -78,12 +78,9 @@ pub(crate) async fn identify_server(
|
||||
};
|
||||
let capabilities = Capabilities {
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_metadata: caps.has_str("METADATA"),
|
||||
can_compress: caps.has_str("COMPRESS=DEFLATE"),
|
||||
has_xdeltapush: caps.has_str("XDELTAPUSH"),
|
||||
is_chatmail: caps.has_str("XCHATMAIL"),
|
||||
server_id,
|
||||
};
|
||||
Ok(capabilities)
|
||||
|
||||
@@ -99,10 +99,6 @@ impl Session {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
self.capabilities.can_move
|
||||
}
|
||||
|
||||
pub fn can_check_quota(&self) -> bool {
|
||||
self.capabilities.can_check_quota
|
||||
}
|
||||
@@ -111,11 +107,6 @@ impl Session {
|
||||
self.capabilities.can_metadata
|
||||
}
|
||||
|
||||
// Returns true if IMAP server has `XCHATMAIL` capability.
|
||||
pub(crate) fn is_chatmail(&self) -> bool {
|
||||
self.capabilities.is_chatmail
|
||||
}
|
||||
|
||||
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
|
||||
/// order of ascending UIDs.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
|
||||
+1
-3
@@ -961,7 +961,7 @@ mod tests {
|
||||
|
||||
assert!(context2.is_configured().await?);
|
||||
assert_eq!(
|
||||
context2.get_config(Config::Addr).await?,
|
||||
context2.get_config(Config::ConfiguredAddr).await?,
|
||||
Some("alice@example.org".to_string())
|
||||
);
|
||||
Ok(())
|
||||
@@ -981,7 +981,6 @@ mod tests {
|
||||
context1.get_config(Config::BccSelf).await?,
|
||||
Some("0".to_string())
|
||||
);
|
||||
context1.set_config_bool(Config::IsChatmail, true).await?;
|
||||
|
||||
assert_eq!(context1.get_config_bool(Config::IsMuted).await?, false);
|
||||
context1.set_config_bool(Config::IsMuted, true).await?;
|
||||
@@ -1001,7 +1000,6 @@ mod tests {
|
||||
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
|
||||
.await;
|
||||
assert!(context2.is_configured().await?);
|
||||
assert!(context2.is_chatmail().await?);
|
||||
for ctx in [context1, context2] {
|
||||
// BccSelf should be enabled automatically when exporting a backup
|
||||
assert_eq!(ctx.get_config_bool(Config::BccSelf).await?, true);
|
||||
|
||||
@@ -662,6 +662,7 @@ mod tests {
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{TestContext, TestContextManager, alice_keypair};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
|
||||
static KEYPAIR: LazyLock<SignedSecretKey> = LazyLock::new(alice_keypair);
|
||||
|
||||
@@ -811,6 +812,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -821,6 +823,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_generate_secret() {
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -833,6 +836,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
use std::thread;
|
||||
|
||||
let t = TestContext::new().await;
|
||||
add_pseudo_transport(&t, "alice@example.org").await.unwrap();
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
+5
-5
@@ -40,7 +40,7 @@ use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::log::warn;
|
||||
use crate::mimefactory::render_keyupdate_message;
|
||||
use crate::mimefactory::keyupdate_message;
|
||||
use crate::pgp::{pubkey_can_encrypt, relay_addrs};
|
||||
use crate::smtp::insert_into_smtp;
|
||||
use crate::tools::{create_outgoing_rfc724_mid, time};
|
||||
@@ -145,14 +145,14 @@ async fn keyupdate_recipients(
|
||||
|
||||
/// Returns the deduplicated relay addresses to put into the SMTP envelope
|
||||
/// for the keyupdate encrypted to a `chunk` of recipients.
|
||||
fn envelope_recipients(chunk: &[KeyupdateRecipient]) -> String {
|
||||
fn envelope_recipients(chunk: &[KeyupdateRecipient]) -> Vec<String> {
|
||||
let mut addrs = BTreeSet::new();
|
||||
for recipient in chunk {
|
||||
for relay in &recipient.relays {
|
||||
addrs.insert(addr_normalize(relay));
|
||||
}
|
||||
}
|
||||
Vec::from_iter(addrs).join(" ")
|
||||
Vec::from_iter(addrs)
|
||||
}
|
||||
|
||||
/// Returns the relay list in the format stored in [`Config::KeyupdateBaseline`].
|
||||
@@ -192,8 +192,8 @@ pub(crate) async fn maybe_send_keyupdate_message(context: &Context) -> Result<()
|
||||
let envelope = envelope_recipients(chunk);
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let keys = chunk.iter().map(|r| r.public_key.clone()).collect();
|
||||
let rendered_message = render_keyupdate_message(context, &rfc724_mid, keys).await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &envelope, rendered_message).await?;
|
||||
let queued_msg = keyupdate_message(context, &rfc724_mid, keys, envelope).await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_msg).await?;
|
||||
}
|
||||
|
||||
// Record only after queueing, so failed queueing is retried by a later check.
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ pub mod stock_str;
|
||||
pub mod storage_usage;
|
||||
mod sync;
|
||||
mod token;
|
||||
mod transport;
|
||||
pub mod transport;
|
||||
mod update_helper;
|
||||
pub mod webxdc;
|
||||
#[macro_use]
|
||||
|
||||
@@ -601,22 +601,6 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the timestamp of the last time location was sent in the chat.
|
||||
pub async fn set_kml_sent_timestamp(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
(timestamp, chat_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the location of the message.
|
||||
pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> {
|
||||
context
|
||||
|
||||
+10
-13
@@ -1445,13 +1445,10 @@ impl std::fmt::Display for MessageState {
|
||||
}
|
||||
|
||||
impl MessageState {
|
||||
/// Returns true if the message can transition to `OutFailed` state from the current state.
|
||||
/// Returns true if the message can be marked as failed in the current state.
|
||||
pub fn can_fail(self) -> bool {
|
||||
use MessageState::*;
|
||||
matches!(
|
||||
self,
|
||||
OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed.
|
||||
)
|
||||
matches!(self, OutPending | OutDelivered | OutFailed)
|
||||
}
|
||||
|
||||
/// Returns true for any outgoing message states.
|
||||
@@ -1697,7 +1694,7 @@ pub async fn delete_msgs_ext(
|
||||
if !msg.pre_rfc724_mid.is_empty() {
|
||||
stmt.execute((&msg.pre_rfc724_mid,))?;
|
||||
}
|
||||
trans.execute("DELETE FROM smtp WHERE msg_id=?", (msg_id,))?;
|
||||
trans.execute("DELETE FROM smtp2 WHERE msg_id=?", (msg_id,))?;
|
||||
trans.execute(
|
||||
"DELETE FROM download WHERE rfc724_mid=?",
|
||||
(&msg.rfc724_mid,),
|
||||
@@ -1975,15 +1972,15 @@ pub(crate) async fn set_msg_failed(
|
||||
msg: &mut Message,
|
||||
error: &str,
|
||||
) -> Result<()> {
|
||||
if msg.state.can_fail() {
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
} else {
|
||||
warn!(
|
||||
if !msg.state.can_fail() {
|
||||
info!(
|
||||
context,
|
||||
"{} seems to have failed ({}), but state is {}", msg.id, error, msg.state
|
||||
)
|
||||
"Ignoring failed {} in state {}: {}", msg.id, msg.state, error
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
msg.state = MessageState::OutFailed;
|
||||
warn!(context, "{} failed: {}", msg.id, error);
|
||||
msg.error = Some(error.to_string());
|
||||
|
||||
let exists = context
|
||||
|
||||
@@ -454,6 +454,28 @@ async fn test_get_state() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a failure reported after a read receipt leaves the message untouched.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_msg_failed_after_mdn() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let sent = alice.send_text(alice_chat.id, "hi").await;
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
alice.recv_mdn(bob, &bob_msg).await?;
|
||||
let mut msg = sent.load_from_db().await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
|
||||
set_msg_failed(alice, &mut msg, "relay bounced").await?;
|
||||
let msg = sent.load_from_db().await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
assert_eq!(msg.error(), None);
|
||||
let chats = Chatlist::try_load(alice, 0, None, None).await?;
|
||||
assert_eq!(chats.get_msg_id(0)?, Some(msg.id));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_bot() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -626,11 +648,15 @@ async fn test_delete_msgs_offline() -> Result<()> {
|
||||
let chat_id = alice.create_chat_id(bob).await;
|
||||
let mut msg = Message::new_text("hi".to_string());
|
||||
assert!(chat::send_msg_sync(alice, chat_id, &mut msg).await.is_err());
|
||||
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
|
||||
let stmt = "SELECT COUNT(*) FROM smtp2 WHERE msg_id=?";
|
||||
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
delete_msgs(alice, &[msg.id]).await?;
|
||||
assert!(!alice.sql.exists(stmt, (msg.id,)).await?);
|
||||
|
||||
alice
|
||||
.assert_warn("No SMTP connection candidates provided")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -756,3 +782,23 @@ async fn test_get_existing_msg_ids() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_fail() -> Result<()> {
|
||||
use MessageState::*;
|
||||
|
||||
// states that are not allowed to transition to OutFailed
|
||||
assert!(!Undefined.can_fail());
|
||||
assert!(!InFresh.can_fail());
|
||||
assert!(!InNoticed.can_fail());
|
||||
assert!(!InSeen.can_fail());
|
||||
assert!(!OutDraft.can_fail());
|
||||
assert!(!OutMdnRcvd.can_fail());
|
||||
|
||||
// states that are allowed to transition to OutFailed
|
||||
assert!(OutPending.can_fail());
|
||||
assert!(OutDelivered.can_fail());
|
||||
assert!(OutFailed.can_fail());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+110
-193
@@ -36,6 +36,9 @@ use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2, relay_addrs};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::smtp::queue::{
|
||||
Encryption as QueuedEncryption, QueuedMail, SideEffects as QueueSideEffects, ToBeQueuedMail,
|
||||
};
|
||||
use crate::stock_str;
|
||||
use crate::tools::{IsNoneOrEmpty, create_outgoing_rfc724_mid, remove_subject_prefix, time};
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
@@ -96,6 +99,32 @@ enum Encryption {
|
||||
Symmetric { shared_secret: String },
|
||||
}
|
||||
|
||||
impl Encryption {
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self {
|
||||
Self::No => false,
|
||||
Self::Asymmetric { .. } => true,
|
||||
Self::Symmetric { .. } => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts into [`QueuedEncryption`] by dropping email addresses corresponding to the keys.
|
||||
fn into_queued_encryption(self) -> QueuedEncryption {
|
||||
match self {
|
||||
Encryption::No => QueuedEncryption::No,
|
||||
Encryption::Asymmetric { encryption_pubkeys } => QueuedEncryption::Asymmetric {
|
||||
encryption_pubkeys: encryption_pubkeys
|
||||
.into_iter()
|
||||
.map(|(_addr, key)| key)
|
||||
.collect(),
|
||||
},
|
||||
Encryption::Symmetric { shared_secret } => {
|
||||
QueuedEncryption::Symmetric { shared_secret }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to construct mime messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MimeFactory {
|
||||
@@ -195,64 +224,6 @@ pub struct RenderedMessage {
|
||||
sync_ids_to_delete: Option<String>,
|
||||
}
|
||||
|
||||
/// Email message queued, but not sent yet.
|
||||
///
|
||||
/// It is stored unencrypted to
|
||||
/// make it possible to change protected headers
|
||||
/// like the From address and Autocrypt header later.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct QueuedMail {
|
||||
/// Unencrypted queued message.
|
||||
///
|
||||
/// This message has both the headers and the body,
|
||||
/// but without the From, Autocrypt and Message-ID headers.
|
||||
///
|
||||
/// For encrypted messages this is the OpenPGP payload.
|
||||
raw_message: Vec<u8>,
|
||||
|
||||
/// Display name to put in the `From:` field.
|
||||
///
|
||||
/// Email address is not determined yet here.
|
||||
display_name: String,
|
||||
|
||||
/// Message-ID.
|
||||
rfc724_mid: String,
|
||||
|
||||
/// Whether the message is encrypted and encryption keys.
|
||||
encryption: Encryption,
|
||||
|
||||
/// If true, Autocrypt header should be added before sending.
|
||||
should_attach_pubkey: bool,
|
||||
|
||||
/// If true, OpenPGP compression may be used.
|
||||
should_compress: bool,
|
||||
|
||||
/// If true, encrypted message should be signed as well.
|
||||
should_sign: bool,
|
||||
}
|
||||
|
||||
/// Side effects that should be applied at the same time
|
||||
/// as the message is persisted in the queue.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RenderSideEffects {
|
||||
/// Largest timestamp of the location sent in `location.kml` in this message.
|
||||
pub last_added_location_timestamp: Option<i64>,
|
||||
|
||||
/// True if the message has the avatar attached.
|
||||
///
|
||||
/// Timestamp of the last time avatar was gossiped should be updated.
|
||||
pub avatar_is_attached: bool,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
|
||||
/// from `multi_device_sync` once the message is actually queued for sending.
|
||||
pub sync_ids_to_delete: Option<String>,
|
||||
|
||||
/// Subject that was rendered into the message.
|
||||
///
|
||||
/// Used to update the subject on the sent message object.
|
||||
pub subject: String,
|
||||
}
|
||||
|
||||
/// Renders [`QueuedMail`].
|
||||
///
|
||||
/// Adds headers:
|
||||
@@ -266,7 +237,6 @@ pub(crate) fn render_queued_mail(
|
||||
public_key: &SignedPublicKey,
|
||||
secret_key: &SignedSecretKey,
|
||||
from_addr: String,
|
||||
side_effects: RenderSideEffects,
|
||||
) -> Result<RenderedEmail> {
|
||||
let QueuedMail {
|
||||
rfc724_mid,
|
||||
@@ -276,12 +246,15 @@ pub(crate) fn render_queued_mail(
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
recipients: _,
|
||||
sent_to: _,
|
||||
bcc_self: _,
|
||||
} = queued_mail;
|
||||
|
||||
let mut inner_headers: Vec<u8> = Vec::new();
|
||||
let mut outer_headers: Vec<u8> = Vec::new();
|
||||
|
||||
let is_encrypted = !matches!(encryption, Encryption::No);
|
||||
let is_encrypted = encryption.is_encrypted();
|
||||
|
||||
fn add_header(
|
||||
name: &[u8],
|
||||
@@ -362,7 +335,6 @@ pub(crate) fn render_queued_mail(
|
||||
addr: from_addr,
|
||||
public_key: public_key.clone(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false,
|
||||
};
|
||||
let autocrypt_header = mail_builder::headers::raw::Raw::new(aheader.to_string());
|
||||
add_header(
|
||||
@@ -419,18 +391,15 @@ pub(crate) fn render_queued_mail(
|
||||
let sign_key = if should_sign { Some(secret_key) } else { None };
|
||||
|
||||
let message = match encryption {
|
||||
Encryption::No => raw_message,
|
||||
Encryption::Asymmetric { encryption_pubkeys } => {
|
||||
QueuedEncryption::No => raw_message,
|
||||
QueuedEncryption::Asymmetric { encryption_pubkeys } => {
|
||||
let mut full_raw_message = inner_headers.clone();
|
||||
full_raw_message.extend(raw_message);
|
||||
|
||||
// Asymmetric encryption
|
||||
|
||||
// Use SEIPDv2 if all recipients support it.
|
||||
let seipd_version = if encryption_pubkeys
|
||||
.iter()
|
||||
.all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey))
|
||||
{
|
||||
let seipd_version = if encryption_pubkeys.iter().all(pubkey_supports_seipdv2) {
|
||||
SeipdVersion::V2
|
||||
} else {
|
||||
SeipdVersion::V1
|
||||
@@ -440,7 +409,7 @@ pub(crate) fn render_queued_mail(
|
||||
// even for a single-device setup,
|
||||
// to not reveal if we have a multi-device setup to contacts.
|
||||
let mut encryption_keyring = vec![public_key.clone()];
|
||||
encryption_keyring.extend(encryption_pubkeys.iter().map(|(_addr, key)| (*key).clone()));
|
||||
encryption_keyring.extend(encryption_pubkeys);
|
||||
|
||||
let encrypted = crate::pgp::pk_encrypt(
|
||||
full_raw_message,
|
||||
@@ -453,7 +422,7 @@ pub(crate) fn render_queued_mail(
|
||||
let message = wrap_encrypted_part(encrypted);
|
||||
part_to_bytes(message)
|
||||
}
|
||||
Encryption::Symmetric { shared_secret } => {
|
||||
QueuedEncryption::Symmetric { shared_secret } => {
|
||||
let mut full_raw_message = inner_headers.clone();
|
||||
full_raw_message.extend(raw_message);
|
||||
|
||||
@@ -473,23 +442,31 @@ pub(crate) fn render_queued_mail(
|
||||
full_message.extend(message);
|
||||
Ok(RenderedEmail {
|
||||
message: String::from_utf8_lossy(&full_message).to_string(),
|
||||
is_encrypted,
|
||||
rfc724_mid,
|
||||
side_effects,
|
||||
})
|
||||
}
|
||||
|
||||
/// Renders queued mail with the current sending address.
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn render_queued_mail_with_context(
|
||||
queued_mail: QueuedMail,
|
||||
context: &Context,
|
||||
) -> Result<RenderedEmail> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
|
||||
let rendered_mail = render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?;
|
||||
Ok(rendered_mail)
|
||||
}
|
||||
|
||||
/// Result of rendering a message, ready to be submitted to a send job.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedEmail {
|
||||
pub message: String,
|
||||
|
||||
pub is_encrypted: bool,
|
||||
|
||||
/// Message ID (Message in the sense of Email)
|
||||
pub rfc724_mid: String,
|
||||
|
||||
pub side_effects: RenderSideEffects,
|
||||
}
|
||||
|
||||
fn new_address_with_name(name: &str, address: String) -> Address<'static> {
|
||||
@@ -848,8 +825,8 @@ impl MimeFactory {
|
||||
// We don't display avatars for address-contacts, so sending avatars w/o encryption is not
|
||||
// useful and causes e.g. Outlook to reject a message with a big header, see
|
||||
// https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
|
||||
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await
|
||||
&& !matches!(encryption, Encryption::No);
|
||||
let attach_selfavatar =
|
||||
Self::should_attach_selfavatar(context, &msg).await && encryption.is_encrypted();
|
||||
|
||||
ensure_and_debug_assert!(
|
||||
member_timestamps.is_empty()
|
||||
@@ -1101,7 +1078,10 @@ impl MimeFactory {
|
||||
};
|
||||
let self_name = &match self_name {
|
||||
Some(name) => name,
|
||||
None => context.get_config(Config::Addr).await?.unwrap_or_default(),
|
||||
None => context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
stock_str::subject_for_new_contact(context, self_name)
|
||||
}
|
||||
@@ -1340,18 +1320,16 @@ impl MimeFactory {
|
||||
///
|
||||
/// Used for MDNs because they are fully rendered and sent in one go,
|
||||
/// rather than first creating a [`QueuedMail`] and sending it later.
|
||||
pub async fn render(self, context: &Context) -> Result<RenderedEmail> {
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
pub async fn render(self, context: &Context, from_addr: &str) -> Result<RenderedEmail> {
|
||||
// Does not matter, we are not going to return the QueuedMail.
|
||||
let bcc_self = false;
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let (queued_mail, side_effects) = Box::pin(self.into_queued_mail(context)).await?;
|
||||
let rendered_mail = render_queued_mail(
|
||||
queued_mail,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
side_effects,
|
||||
)?;
|
||||
|
||||
let (queued_mail, _side_effects) =
|
||||
Box::pin(self.into_queued_mail(context, bcc_self)).await?;
|
||||
let rendered_mail =
|
||||
render_queued_mail(queued_mail, &public_key, &secret_key, from_addr.to_string())?;
|
||||
Ok(rendered_mail)
|
||||
}
|
||||
|
||||
@@ -1361,7 +1339,8 @@ impl MimeFactory {
|
||||
pub(crate) async fn into_queued_mail(
|
||||
mut self,
|
||||
context: &Context,
|
||||
) -> Result<(QueuedMail, RenderSideEffects)> {
|
||||
bcc_self: bool,
|
||||
) -> Result<ToBeQueuedMail> {
|
||||
let rfc724_mid = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => match &self.pre_message_mode {
|
||||
PreMessageMode::Pre { .. } => {
|
||||
@@ -1383,9 +1362,7 @@ impl MimeFactory {
|
||||
|
||||
let is_encrypted = self.will_be_encrypted();
|
||||
|
||||
let last_added_location_timestamp;
|
||||
let avatar_is_attached;
|
||||
let sync_ids_to_delete;
|
||||
let side_effects: Option<QueueSideEffects>;
|
||||
|
||||
let message: MimePart<'static> = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => {
|
||||
@@ -1393,15 +1370,21 @@ impl MimeFactory {
|
||||
let RenderedMessage {
|
||||
main_part,
|
||||
mut parts,
|
||||
last_added_location_timestamp: tmp_last_added_location_timestamp,
|
||||
avatar_is_attached: tmp_avatar_is_attached,
|
||||
sync_ids_to_delete: tmp_sync_ids_to_delete,
|
||||
last_added_location_timestamp,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
} = self
|
||||
.render_message(context, &mut headers, &grpimage, is_encrypted)
|
||||
.await?;
|
||||
last_added_location_timestamp = tmp_last_added_location_timestamp;
|
||||
avatar_is_attached = tmp_avatar_is_attached;
|
||||
sync_ids_to_delete = tmp_sync_ids_to_delete;
|
||||
|
||||
side_effects = Some(QueueSideEffects {
|
||||
chat_id: msg.chat_id,
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
last_added_location_timestamp,
|
||||
subject: subject_str,
|
||||
});
|
||||
|
||||
if parts.is_empty() {
|
||||
// Single part, render as regular message.
|
||||
main_part
|
||||
@@ -1419,9 +1402,7 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => {
|
||||
last_added_location_timestamp = None;
|
||||
avatar_is_attached = false;
|
||||
sync_ids_to_delete = None;
|
||||
side_effects = None;
|
||||
self.render_mdn()?
|
||||
}
|
||||
};
|
||||
@@ -1431,12 +1412,6 @@ impl MimeFactory {
|
||||
Loaded::Mdn { .. } => self.update_mdn_pubkey_attachment(context).await?,
|
||||
};
|
||||
let is_post_message = self.pre_message_mode == PreMessageMode::Post;
|
||||
let side_effects = RenderSideEffects {
|
||||
avatar_is_attached,
|
||||
sync_ids_to_delete,
|
||||
last_added_location_timestamp,
|
||||
subject: subject_str,
|
||||
};
|
||||
|
||||
let is_securejoin_message = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg.param.get_cmd() == SystemMessage::SecurejoinMessage,
|
||||
@@ -1493,17 +1468,6 @@ impl MimeFactory {
|
||||
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
|
||||
};
|
||||
|
||||
let verifier_id: Option<u32> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT verifier FROM contacts WHERE fingerprint=?",
|
||||
(&fingerprint,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_verified =
|
||||
verifier_id.is_some_and(|verifier_id| verifier_id != 0);
|
||||
|
||||
if !should_do_gossip {
|
||||
continue;
|
||||
}
|
||||
@@ -1514,7 +1478,6 @@ impl MimeFactory {
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
prefer_encrypt: EncryptPreference::NoPreference,
|
||||
verified: is_verified,
|
||||
}
|
||||
.to_string();
|
||||
|
||||
@@ -1554,7 +1517,7 @@ impl MimeFactory {
|
||||
let is_mdn = matches!(self.loaded, Loaded::Mdn { .. });
|
||||
let should_sign = true;
|
||||
|
||||
let message = if self.will_be_encrypted() {
|
||||
let message = if is_encrypted {
|
||||
add_headers_to_encrypted_part(message, headers)
|
||||
} else if is_mdn {
|
||||
// Never add outer multipart/mixed wrapper to MDN
|
||||
@@ -1590,15 +1553,19 @@ impl MimeFactory {
|
||||
})
|
||||
};
|
||||
let raw_message = part_to_bytes(message);
|
||||
let recipients = self.recipients();
|
||||
|
||||
let queued_email = QueuedMail {
|
||||
raw_message,
|
||||
rfc724_mid,
|
||||
display_name,
|
||||
encryption: self.encryption,
|
||||
encryption: self.encryption.into_queued_encryption(),
|
||||
should_attach_pubkey,
|
||||
should_sign,
|
||||
should_compress,
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
bcc_self,
|
||||
};
|
||||
Ok((queued_email, side_effects))
|
||||
}
|
||||
@@ -1654,43 +1621,6 @@ impl MimeFactory {
|
||||
let command = msg.param.get_cmd();
|
||||
let mut placeholdertext = None;
|
||||
|
||||
let send_verified_headers = match chat.typ {
|
||||
Chattype::Single => true,
|
||||
Chattype::Group => true,
|
||||
// Mailinglists and broadcast channels can actually never be verified:
|
||||
Chattype::Mailinglist => false,
|
||||
Chattype::OutBroadcast | Chattype::InBroadcast => false,
|
||||
};
|
||||
|
||||
if send_verified_headers {
|
||||
let was_protected: bool = context
|
||||
.sql
|
||||
.query_get_value("SELECT protected FROM chats WHERE id=?", (chat.id,))
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if was_protected {
|
||||
let unverified_member_exists = context
|
||||
.sql
|
||||
.exists(
|
||||
"SELECT COUNT(*)
|
||||
FROM contacts, chats_contacts
|
||||
WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=?
|
||||
AND contacts.id>9
|
||||
AND contacts.verifier=0",
|
||||
(chat.id,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !unverified_member_exists {
|
||||
headers.push((
|
||||
"Chat-Verified",
|
||||
mail_builder::headers::raw::Raw::new("1").into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group {
|
||||
// Send group ID unless it is an ad hoc group that has no ID.
|
||||
if !chat.grpid.is_empty() {
|
||||
@@ -2277,19 +2207,16 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
pub fn will_be_encrypted(&self) -> bool {
|
||||
match self.encryption {
|
||||
Encryption::No => false,
|
||||
Encryption::Asymmetric { .. } | Encryption::Symmetric { .. } => true,
|
||||
}
|
||||
self.encryption.is_encrypted()
|
||||
}
|
||||
|
||||
pub fn set_as_post_message(&mut self) {
|
||||
self.pre_message_mode = PreMessageMode::Post;
|
||||
}
|
||||
|
||||
pub fn set_as_pre_message_for(&mut self, post_message: &RenderedEmail) {
|
||||
pub fn set_as_pre_message_for(&mut self, rfc724_mid: &str) {
|
||||
self.pre_message_mode = PreMessageMode::Pre {
|
||||
post_msg_rfc724_mid: post_message.rfc724_mid.clone(),
|
||||
post_msg_rfc724_mid: rfc724_mid.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2467,29 +2394,15 @@ async fn non_chat_headers(
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Renders `queued_mail` for SMTP with the own key pair and primary address.
|
||||
async fn render_with_self_key(context: &Context, queued_mail: QueuedMail) -> Result<String> {
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from_addr = context.get_primary_self_addr().await?;
|
||||
let rendered_mail = render_queued_mail(
|
||||
queued_mail,
|
||||
&public_key,
|
||||
&secret_key,
|
||||
from_addr,
|
||||
RenderSideEffects::default(),
|
||||
)?;
|
||||
Ok(rendered_mail.message)
|
||||
}
|
||||
|
||||
pub(crate) async fn render_symm_encrypted_securejoin_message(
|
||||
pub(crate) async fn symm_encrypted_securejoin_message(
|
||||
context: &Context,
|
||||
step: &str,
|
||||
rfc724_mid: &str,
|
||||
should_attach_pubkey: bool,
|
||||
auth: &str,
|
||||
shared_secret: &str,
|
||||
) -> Result<String> {
|
||||
recipients: Vec<String>,
|
||||
) -> Result<QueuedMail> {
|
||||
info!(context, "Sending secure-join message {step:?}.");
|
||||
|
||||
let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join");
|
||||
@@ -2504,7 +2417,7 @@ pub(crate) async fn render_symm_encrypted_securejoin_message(
|
||||
raw_message: part_to_bytes(message),
|
||||
display_name: String::new(),
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
encryption: Encryption::Symmetric {
|
||||
encryption: QueuedEncryption::Symmetric {
|
||||
shared_secret: shared_secret.to_string(),
|
||||
},
|
||||
should_attach_pubkey,
|
||||
@@ -2514,9 +2427,13 @@ pub(crate) async fn render_symm_encrypted_securejoin_message(
|
||||
// there are no compression side channels
|
||||
// leaking information about the tokens.
|
||||
should_compress: false,
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
// Never send a copy of SecureJoin message to self.
|
||||
bcc_self: false,
|
||||
};
|
||||
|
||||
render_with_self_key(context, queued_mail).await
|
||||
Ok(queued_mail)
|
||||
}
|
||||
|
||||
/// Returns the body of a keyupdate message, shaped like a receipt notification.
|
||||
@@ -2545,13 +2462,14 @@ fn keyupdate_body() -> MimePart<'static> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Renders a keyupdate message informing the owners of `recipient_keys`
|
||||
/// Returns a keyupdate message informing the owners of `recipient_keys`
|
||||
/// about the current key and relay list, see [`crate::keyupdate`].
|
||||
pub(crate) async fn render_keyupdate_message(
|
||||
pub(crate) async fn keyupdate_message(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
recipient_keys: Vec<SignedPublicKey>,
|
||||
) -> Result<String> {
|
||||
recipients: Vec<String>,
|
||||
) -> Result<QueuedMail> {
|
||||
info!(
|
||||
context,
|
||||
"Sending keyupdate message to {} recipients.",
|
||||
@@ -2566,11 +2484,8 @@ pub(crate) async fn render_keyupdate_message(
|
||||
raw_message: part_to_bytes(message),
|
||||
display_name: String::new(),
|
||||
rfc724_mid: rfc724_mid.to_string(),
|
||||
encryption: Encryption::Asymmetric {
|
||||
encryption_pubkeys: recipient_keys
|
||||
.into_iter()
|
||||
.map(|key| (String::new(), key))
|
||||
.collect(),
|
||||
encryption: QueuedEncryption::Asymmetric {
|
||||
encryption_pubkeys: recipient_keys,
|
||||
},
|
||||
|
||||
// Attached key with its relay list notation is the actual payload.
|
||||
@@ -2582,9 +2497,11 @@ pub(crate) async fn render_keyupdate_message(
|
||||
|
||||
// Disable compression to avoid side channels, message body is small anyway.
|
||||
should_compress: false,
|
||||
recipients,
|
||||
sent_to: Vec::new(),
|
||||
bcc_self: false,
|
||||
};
|
||||
|
||||
render_with_self_key(context, queued_mail).await
|
||||
Ok(queued_mail)
|
||||
}
|
||||
|
||||
/// Renders MIME part into a vector of bytes.
|
||||
|
||||
@@ -306,9 +306,10 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
let mimefactory =
|
||||
MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
let rendered_msg = mimefactory.render(&bob).await?;
|
||||
assert!(!mimefactory.will_be_encrypted());
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
|
||||
|
||||
assert!(!rendered_msg.is_encrypted);
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
let bob_alice_contact = bob.add_or_lookup_contact(&alice).await;
|
||||
@@ -319,9 +320,10 @@ async fn test_mdn_create_encrypted() -> Result<()> {
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
let rendered_msg = mimefactory.render(&bob).await?;
|
||||
assert!(mimefactory.will_be_encrypted());
|
||||
let bob_addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mimefactory.render(&bob, &bob_addr).await?;
|
||||
|
||||
assert!(rendered_msg.is_encrypted);
|
||||
assert!(!rendered_msg.message.contains("Bob Examplenet"));
|
||||
assert!(!rendered_msg.message.contains("Alice Exampleorg"));
|
||||
|
||||
@@ -363,7 +365,8 @@ async fn test_mdn_autocrypt_throttle() -> Result<()> {
|
||||
rcvd: &Message,
|
||||
) -> Result<bool> {
|
||||
let mf = MimeFactory::from_mdn(bob, rcvd.from_id, rcvd.rfc724_mid.clone(), vec![]).await?;
|
||||
let rendered_msg = mf.render(bob).await?;
|
||||
let addr = bob.get_primary_self_addr().await?;
|
||||
let rendered_msg = mf.render(bob, &addr).await?;
|
||||
let mime = MimeMessage::from_bytes(alice, rendered_msg.message.as_bytes()).await?;
|
||||
Ok(mime.autocrypt_fingerprint.is_some())
|
||||
}
|
||||
@@ -644,7 +647,8 @@ async fn test_render_reply() {
|
||||
let recipients = mimefactory.recipients();
|
||||
assert_eq!(recipients, vec!["charlie@example.net"]);
|
||||
|
||||
let rendered_msg = mimefactory.render(t).await.unwrap();
|
||||
let addr = t.get_primary_self_addr().await.unwrap();
|
||||
let rendered_msg = mimefactory.render(t, &addr).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(rendered_msg.message.as_bytes()).unwrap();
|
||||
assert_eq!(
|
||||
@@ -709,9 +713,9 @@ async fn test_remove_member_bcc() -> Result<()> {
|
||||
let charlie = &tcm.charlie().await;
|
||||
alice.allow_unencrypted().await?;
|
||||
|
||||
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
|
||||
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
|
||||
@@ -874,7 +878,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&charlie.get_config(Config::Addr).await?.unwrap())
|
||||
.starts_with(&charlie.get_config(Config::ConfiguredAddr).await?.unwrap())
|
||||
);
|
||||
|
||||
remove_contact_from_chat(alice, group, bob_id).await?;
|
||||
@@ -885,7 +889,7 @@ async fn test_new_member_is_first_recipient() -> Result<()> {
|
||||
assert!(
|
||||
sent_msg
|
||||
.recipients
|
||||
.starts_with(&bob.get_config(Config::Addr).await?.unwrap())
|
||||
.starts_with(&bob.get_config(Config::ConfiguredAddr).await?.unwrap())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+16
-33
@@ -34,17 +34,6 @@ use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, time, truncate_msg_text, validate_id};
|
||||
use crate::{chatlist_events, location, tools};
|
||||
|
||||
/// Public key extracted from `Autocrypt-Gossip`
|
||||
/// header with associated information.
|
||||
#[derive(Debug)]
|
||||
pub struct GossipedKey {
|
||||
/// Public key extracted from `keydata` attribute.
|
||||
pub public_key: SignedPublicKey,
|
||||
|
||||
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
/// This represents the relevant information of a parsed MIME message
|
||||
@@ -99,7 +88,7 @@ pub(crate) struct MimeMessage {
|
||||
|
||||
/// The addresses for which there was a gossip header
|
||||
/// and their respective gossiped keys.
|
||||
pub gossiped_keys: BTreeMap<String, GossipedKey>,
|
||||
pub gossiped_keys: BTreeMap<String, SignedPublicKey>,
|
||||
|
||||
/// Fingerprint of the key in the Autocrypt header.
|
||||
///
|
||||
@@ -1048,7 +1037,7 @@ impl MimeMessage {
|
||||
};
|
||||
self.gossiped_keys
|
||||
.values()
|
||||
.map(|gossiped_key| gossiped_key.public_key.dc_fingerprint().hex())
|
||||
.map(|gossiped_key| gossiped_key.dc_fingerprint().hex())
|
||||
.chain(sender_fingerprint)
|
||||
.collect()
|
||||
}
|
||||
@@ -2118,9 +2107,9 @@ fn remove_header(
|
||||
async fn parse_gossip_headers(
|
||||
context: &Context,
|
||||
gossip_headers: Vec<String>,
|
||||
) -> Result<BTreeMap<String, GossipedKey>> {
|
||||
) -> Result<BTreeMap<String, SignedPublicKey>> {
|
||||
// XXX split the parsing from the modification part
|
||||
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
|
||||
let mut gossiped_keys: BTreeMap<String, SignedPublicKey> = Default::default();
|
||||
|
||||
for value in &gossip_headers {
|
||||
let header = match Aheader::from_str(value) {
|
||||
@@ -2135,12 +2124,7 @@ async fn parse_gossip_headers(
|
||||
.await
|
||||
.context("Failed to import Autocrypt-Gossip key")?;
|
||||
|
||||
let gossiped_key = GossipedKey {
|
||||
public_key: header.public_key,
|
||||
|
||||
verified: header.verified,
|
||||
};
|
||||
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
|
||||
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
|
||||
}
|
||||
|
||||
Ok(gossiped_keys)
|
||||
@@ -2556,19 +2540,18 @@ async fn handle_ndn(
|
||||
for msg_id in msg_ids {
|
||||
let mut message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.chat_id).await?;
|
||||
if chat.typ == constants::Chattype::OutBroadcast {
|
||||
continue;
|
||||
if chat.typ == constants::Chattype::Single {
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| format!("{err}\n\n{err_msg}"));
|
||||
set_msg_failed(
|
||||
context,
|
||||
&mut message,
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
let aggregated_error = message
|
||||
.error
|
||||
.as_ref()
|
||||
.map(|err| format!("{err}\n\n{err_msg}"));
|
||||
set_msg_failed(
|
||||
context,
|
||||
&mut message,
|
||||
aggregated_error.as_ref().unwrap_or(err_msg),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -131,7 +131,7 @@ async fn test_broadcast_security_attacker_signature() -> Result<()> {
|
||||
|
||||
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
|
||||
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
|
||||
test_shared_secret_decryption_ext(
|
||||
bob,
|
||||
@@ -183,10 +183,7 @@ async fn test_broadcast_security_happy_path() -> Result<()> {
|
||||
|
||||
let secret = load_broadcast_secret(alice, alice_chat_id).await?.unwrap();
|
||||
|
||||
let alice_addr = alice
|
||||
.get_config(crate::config::Config::Addr)
|
||||
.await?
|
||||
.unwrap();
|
||||
let alice_addr = alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
|
||||
test_shared_secret_decryption_ext(bob, &alice_addr, &secret, Some(alice), None).await
|
||||
}
|
||||
@@ -205,7 +202,7 @@ async fn test_qr_code_security() -> Result<()> {
|
||||
// Start a securejoin process, but don't finish it:
|
||||
join_securejoin(bob, &qr).await?;
|
||||
|
||||
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
|
||||
let charlie_addr = charlie.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
|
||||
let alice_fp = self_fingerprint(alice).await?;
|
||||
let secret_for_encryption = format!("securejoin/{alice_fp}/{authcode}");
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::sync::LazyLock;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
use crate::{
|
||||
config::Config,
|
||||
decrypt,
|
||||
@@ -19,6 +20,9 @@ async fn decrypt_bytes(
|
||||
auth_tokens_for_decryption: &[String],
|
||||
) -> Result<pgp::composed::Message<'static>> {
|
||||
let t = &TestContext::new().await;
|
||||
add_pseudo_transport(t, "alice@example.org")
|
||||
.await
|
||||
.expect("Failed to add pseudo transport");
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@example.org"))
|
||||
.await
|
||||
.expect("Failed to configure address");
|
||||
|
||||
@@ -13,6 +13,7 @@ use serde::Deserialize;
|
||||
|
||||
use crate::autorelay::login_param_from_host;
|
||||
use crate::config::Config;
|
||||
use crate::configure::MAX_RELAYS;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::key::Fingerprint;
|
||||
@@ -45,7 +46,7 @@ pub(crate) const DCBACKUP_VERSION: i32 = 5;
|
||||
/// Scanned QR code.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Qr {
|
||||
/// Ask the user whether to verify the contact.
|
||||
/// Ask the user whether to start chatting with the contact.
|
||||
///
|
||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
||||
AskVerifyContact {
|
||||
@@ -125,7 +126,7 @@ pub enum Qr {
|
||||
is_v3: bool,
|
||||
},
|
||||
|
||||
/// Contact fingerprint is verified.
|
||||
/// Contact fingerprint matches.
|
||||
///
|
||||
/// Ask the user if they want to start chatting.
|
||||
FprOk {
|
||||
@@ -133,18 +134,6 @@ pub enum Qr {
|
||||
contact_id: ContactId,
|
||||
},
|
||||
|
||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
||||
FprMismatch {
|
||||
/// Contact ID.
|
||||
contact_id: Option<ContactId>,
|
||||
},
|
||||
|
||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
||||
FprWithoutAddr {
|
||||
/// Key fingerprint.
|
||||
fingerprint: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to create an account on the given domain.
|
||||
Account {
|
||||
/// Server domain name.
|
||||
@@ -500,7 +489,8 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
addrs.push(normalize_address(primary_addr)?);
|
||||
};
|
||||
if let Some(secondary_addrs_raw) = param.get("r") {
|
||||
for secondary_address in secondary_addrs_raw.split(',') {
|
||||
let max_secondary = MAX_RELAYS.saturating_sub(addrs.len());
|
||||
for secondary_address in secondary_addrs_raw.split(',').take(max_secondary) {
|
||||
addrs.push(normalize_address(secondary_address)?)
|
||||
}
|
||||
}
|
||||
@@ -651,24 +641,21 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
|
||||
is_v3,
|
||||
})
|
||||
}
|
||||
} else if let Some(addr) = addrs.first() {
|
||||
let fingerprint = fingerprint.hex();
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup_ext(context, "", addr, &fingerprint, Origin::UnhandledQrScan)
|
||||
.await?;
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.public_key(context).await?.is_some() {
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
} else {
|
||||
Ok(Qr::FprMismatch {
|
||||
contact_id: Some(contact_id),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Ok(Qr::FprWithoutAddr {
|
||||
fingerprint: fingerprint.human_readable(),
|
||||
})
|
||||
let fingerprint = fingerprint.hex();
|
||||
let contact_id: Option<ContactId> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts WHERE fingerprint=?",
|
||||
(fingerprint,),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let Some(contact_id) = contact_id else {
|
||||
bail!("Contact matching the fingerprint is not found");
|
||||
};
|
||||
|
||||
Ok(Qr::FprOk { contact_id })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,7 +825,8 @@ pub(crate) async fn login_param_from_account_qr(
|
||||
.context("Invalid DCACCOUNT scheme")?;
|
||||
|
||||
if !payload.starts_with(HTTPS_SCHEME) {
|
||||
let param = login_param_from_host(payload);
|
||||
let mark_as_autorelay = false;
|
||||
let param = login_param_from_host(payload, mark_as_autorelay);
|
||||
return Ok(param);
|
||||
}
|
||||
|
||||
|
||||
+52
-38
@@ -305,12 +305,10 @@ async fn test_decode_openpgp_invalid_token() -> Result<()> {
|
||||
let ctx = TestContext::new_alice().await;
|
||||
|
||||
// Token cannot contain "/"
|
||||
let qr = check_qr(
|
||||
assert!(check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&g=test%20%3F+test%20%21&x=h-0oKQf2CDK&i=9JEXlxAqGM0&s=0V7LzL/cxRL"
|
||||
).await?;
|
||||
|
||||
assert!(matches!(qr, Qr::FprMismatch { .. }));
|
||||
).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -361,6 +359,23 @@ async fn test_decode_openpgp_secure_join() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
// A bad invite code must not be able to steer us onto arbitrarily many relays.
|
||||
let relays: Vec<String> = (0..20).map(|i| format!("cli%40r{i}.example.org")).collect();
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
&format!(
|
||||
"openpgp4fpr:79252762C34C5096AF57958F4FC3D21A81B0F0A7#a=cli%40deltachat.de&r={}&i=TbnwJ6lSvD5&s=0ejvbdFSQxB",
|
||||
relays.join(",")
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Qr::AskVerifyContact { addrs, .. } = qr {
|
||||
assert_eq!(addrs.len(), MAX_RELAYS);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -373,16 +388,17 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
let alice_contact = bob.add_or_lookup_contact(alice).await;
|
||||
let alice_contact_id = alice_contact.id;
|
||||
|
||||
let qr = check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await?;
|
||||
if let Qr::FprMismatch { contact_id, .. } = qr {
|
||||
assert_ne!(contact_id.unwrap(), alice_contact_id);
|
||||
} else {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
// OPENPGP4FPR may have an address,
|
||||
// but it is not used anymore since key contacts are introduced.
|
||||
// We lookup the contact only by fingerprint and ignore the address.
|
||||
assert!(
|
||||
check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=alice@example.org",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
bob,
|
||||
@@ -398,46 +414,44 @@ async fn test_decode_openpgp_fingerprint() -> Result<()> {
|
||||
bail!("Wrong QR code type");
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
assert!(
|
||||
check_qr(
|
||||
bob,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890#a=bob@example.org",
|
||||
)
|
||||
.await?,
|
||||
Qr::FprMismatch { .. }
|
||||
));
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests OPENPGP4FPR QR codes without an email address.
|
||||
///
|
||||
/// Email address in OPENPGP4FPR QR codes was an extension
|
||||
/// used before switch to identifying contacts by fingerprint.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_openpgp_without_addr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
assert!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"OPENPGP4FPR:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
// Test it again with lowercased "openpgp4fpr:" uri scheme
|
||||
|
||||
let qr = check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::FprWithoutAddr {
|
||||
fingerprint: "1234 5678 9012 3456 7890\n1234 5678 9012 3456 7890".to_string()
|
||||
}
|
||||
assert!(
|
||||
check_qr(
|
||||
&ctx.ctx,
|
||||
"openpgp4fpr:1234567890123456789012345678901234567890",
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let res = check_qr(&ctx.ctx, "OPENPGP4FPR:12345678901234567890").await;
|
||||
|
||||
@@ -85,15 +85,15 @@ pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
/// Returns SVG of the QR code to join the group or verify contact.
|
||||
/// Returns SVG of the QR code to join the group or set up a contact.
|
||||
///
|
||||
/// If `chat_id` is `None`, returns verification QR code.
|
||||
/// If `chat_id` is `None`, returns setup contact QR code.
|
||||
/// Otherwise, returns secure join QR code.
|
||||
pub async fn get_securejoin_qr_svg(context: &Context, chat_id: Option<ChatId>) -> Result<String> {
|
||||
if let Some(chat_id) = chat_id {
|
||||
generate_join_group_qr_code(context, chat_id).await
|
||||
} else {
|
||||
generate_verification_qr(context).await
|
||||
generate_setup_contact_qr(context).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
|
||||
)
|
||||
}
|
||||
|
||||
async fn generate_verification_qr(context: &Context) -> Result<String> {
|
||||
async fn generate_setup_contact_qr(context: &Context) -> Result<String> {
|
||||
let (avatar, displayname, addr, color) = self_info(context).await?;
|
||||
|
||||
inner_generate_secure_join_qr_code(
|
||||
|
||||
+16
-115
@@ -19,7 +19,7 @@ use crate::chat::{
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, EDITED_PREFIX};
|
||||
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
|
||||
use crate::contact::{self, Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::maybe_set_logging_xdc_inner;
|
||||
use crate::download::{DownloadState, msg_is_downloaded_for};
|
||||
@@ -27,7 +27,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::key::{
|
||||
load_self_public_key, load_self_public_key_opt, self_fingerprint, self_fingerprint_opt,
|
||||
};
|
||||
@@ -36,7 +36,7 @@ use crate::message::{
|
||||
self, Message, MessageState, MsgId, Viewtype, insert_tombstone, rfc724_mid_exists,
|
||||
};
|
||||
use crate::mimeparser::{
|
||||
AvatarAction, GossipedKey, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
|
||||
AvatarAction, MimeMessage, PreMessageMode, SystemMessage, parse_message_ids,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub, iroh_topic_from_str};
|
||||
@@ -568,16 +568,12 @@ pub(crate) async fn receive_imf_inner(
|
||||
//
|
||||
// Note that messages with long recipient lists are sent out in chunks,
|
||||
// removing already sent recipients from the job after each chunk.
|
||||
// Self recipients are added at the end so removing the job
|
||||
// removes the last chunk which apparently went out fine.
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
// Self recipients are sent in the end,
|
||||
// so if we received a copy, the message has been sent out
|
||||
// to all recipients.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM smtp \
|
||||
WHERE rfc724_mid=?1 AND (recipients LIKE ?2 OR recipients LIKE ('% ' || ?2))",
|
||||
(rfc724_mid_orig, &self_addr),
|
||||
)
|
||||
.execute("DELETE FROM smtp2 WHERE rfc724_mid=?", (rfc724_mid_orig,))
|
||||
.await?;
|
||||
if !msg_has_pending_smtp_job(context, msg_id).await? {
|
||||
msg_id.set_delivered(context).await?;
|
||||
@@ -658,7 +654,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
)
|
||||
})?
|
||||
} else if let Some(to_id) = to_ids.first().copied().flatten() {
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
observe_securejoin_on_other_device(context, &mime_parser, to_id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
@@ -691,12 +686,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
received_msg = None;
|
||||
}
|
||||
|
||||
let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
|
||||
|
||||
if verified_encryption == VerifiedEncryption::Verified {
|
||||
mark_recipients_as_verified(context, from_id, &mime_parser).await?;
|
||||
}
|
||||
|
||||
let is_old_contact_request;
|
||||
let received_msg = if let Some(received_msg) = received_msg {
|
||||
is_old_contact_request = false;
|
||||
@@ -2118,7 +2107,8 @@ async fn add_parts(
|
||||
let hidden = part.is_reaction;
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen;
|
||||
let is_incoming_fresh =
|
||||
mime_parser.incoming && !seen && chat_id_blocked == Blocked::Not;
|
||||
set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
@@ -2996,7 +2986,7 @@ struct GroupChangesInfo {
|
||||
extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
|
||||
}
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
/// Apply group member list, name and avatar changes from the MIME message.
|
||||
///
|
||||
/// Returns [GroupChangesInfo].
|
||||
///
|
||||
@@ -3064,7 +3054,7 @@ async fn apply_group_changes(
|
||||
// just like we look at ChatGroupMemberRemovedFpr.
|
||||
// The result of the error is that info message
|
||||
// may contain display name of the wrong contact.
|
||||
let fingerprint = key.public_key.dc_fingerprint().hex();
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
if let Some(contact_id) =
|
||||
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
|
||||
{
|
||||
@@ -3743,10 +3733,9 @@ async fn apply_out_broadcast_changes(
|
||||
if from_id == ContactId::SELF {
|
||||
let added_id = lookup_key_contact_by_fingerprint(context, added_fpr).await?;
|
||||
if let Some(added_id) = added_id {
|
||||
if chat::is_contact_in_chat(context, chat.id, added_id).await? {
|
||||
info!(context, "No-op broadcast addition (TRASH)");
|
||||
better_msg.get_or_insert("".to_string());
|
||||
} else {
|
||||
info!(context, "Broadcast addition (TRASH)");
|
||||
better_msg.get_or_insert("".to_string());
|
||||
if !chat::is_contact_in_chat(context, chat.id, added_id).await? {
|
||||
chat::add_to_chat_contacts_table(
|
||||
context,
|
||||
mime_parser.timestamp_sent,
|
||||
@@ -3754,11 +3743,6 @@ async fn apply_out_broadcast_changes(
|
||||
&[added_id],
|
||||
)
|
||||
.await?;
|
||||
let msg =
|
||||
stock_str::msg_add_member_local(context, added_id, ContactId::UNDEFINED)
|
||||
.await;
|
||||
better_msg.get_or_insert(msg);
|
||||
added_removed_id = Some(added_id);
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
} else {
|
||||
@@ -3971,89 +3955,6 @@ async fn create_adhoc_group(
|
||||
Ok(Some((new_chat_id, create_blocked)))
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum VerifiedEncryption {
|
||||
Verified,
|
||||
NotVerified(String), // The string contains the reason why it's not verified
|
||||
}
|
||||
|
||||
/// Checks whether the message is allowed to appear in a protected chat.
|
||||
///
|
||||
/// This means that it is encrypted and signed with a verified key.
|
||||
async fn has_verified_encryption(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> Result<VerifiedEncryption> {
|
||||
use VerifiedEncryption::*;
|
||||
|
||||
if !mimeparser.was_encrypted() {
|
||||
return Ok(NotVerified("This message is not encrypted".to_string()));
|
||||
};
|
||||
|
||||
if from_id == ContactId::SELF {
|
||||
return Ok(Verified);
|
||||
}
|
||||
|
||||
let from_contact = Contact::get_by_id(context, from_id).await?;
|
||||
|
||||
let Some(fingerprint) = from_contact.fingerprint() else {
|
||||
return Ok(NotVerified(
|
||||
"The message was sent without encryption".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
if from_contact.get_verifier_id(context).await?.is_none() {
|
||||
return Ok(NotVerified(
|
||||
"The message was sent by non-verified contact".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let signed_with_verified_key = mimeparser
|
||||
.signature
|
||||
.as_ref()
|
||||
.is_some_and(|(signature, _)| *signature == fingerprint);
|
||||
if signed_with_verified_key {
|
||||
Ok(Verified)
|
||||
} else {
|
||||
Ok(NotVerified(
|
||||
"The message was sent with non-verified encryption".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_recipients_as_verified(
|
||||
context: &Context,
|
||||
from_id: ContactId,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
|
||||
|
||||
// We don't yet send the _verified property in autocrypt headers.
|
||||
// Until we do, we instead accept the Chat-Verified header as indication all contacts are verified.
|
||||
// TODO: Ignore ChatVerified header once we reset existing verifications.
|
||||
let chat_verified = mimeparser.get_header(HeaderDef::ChatVerified).is_some();
|
||||
|
||||
for gossiped_key in mimeparser
|
||||
.gossiped_keys
|
||||
.values()
|
||||
.filter(|gossiped_key| gossiped_key.verified || chat_verified)
|
||||
{
|
||||
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
|
||||
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if to_id == ContactId::SELF || to_id == from_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
///
|
||||
/// For Delta Chat messages it is the last message in the chat of the sender.
|
||||
@@ -4121,7 +4022,7 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
async fn add_or_lookup_key_contacts(
|
||||
context: &Context,
|
||||
address_list: &[SingleInfo],
|
||||
gossiped_keys: &BTreeMap<String, GossipedKey>,
|
||||
gossiped_keys: &BTreeMap<String, SignedPublicKey>,
|
||||
fingerprints: &[Fingerprint],
|
||||
origin: Origin,
|
||||
) -> Result<Vec<Option<ContactId>>> {
|
||||
@@ -4138,7 +4039,7 @@ async fn add_or_lookup_key_contacts(
|
||||
// Iterator has not ran out of fingerprints yet.
|
||||
fp.hex()
|
||||
} else if let Some(key) = gossiped_keys.get(addr) {
|
||||
key.public_key.dc_fingerprint().hex()
|
||||
key.dc_fingerprint().hex()
|
||||
} else if context.is_self_addr(addr).await? {
|
||||
contact_ids.push(Some(ContactId::SELF));
|
||||
continue;
|
||||
|
||||
@@ -15,11 +15,11 @@ use crate::headerdef::HeaderDefMap as _;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{ImexMode, imex};
|
||||
use crate::key;
|
||||
use crate::mimefactory;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::smtp;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::{
|
||||
TestContext, TestContextManager, alice_keypair, get_chat_msg, mark_as_verified,
|
||||
};
|
||||
use crate::test_utils::{TestContext, TestContextManager, alice_keypair, get_chat_msg};
|
||||
use crate::tools::{SystemTime, time};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -726,6 +726,7 @@ async fn test_resend_after_ndn() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// an NDN in a group does not make the whole message as failed
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -758,7 +759,7 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
|
||||
let msg = Message::load_from_db(&t, msg_id).await?;
|
||||
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
assert_eq!(msg.state, MessageState::OutDelivered);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id).await?;
|
||||
assert!(matches!(
|
||||
@@ -766,9 +767,6 @@ async fn test_parse_ndn_group_msg() -> Result<()> {
|
||||
ChatItem::Message { msg_id } if msg_id == msg.id
|
||||
));
|
||||
|
||||
t.assert_warn("Delivery Status Notification (Failure)")
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2725,19 +2723,7 @@ async fn test_read_receipts_dont_create_chats() -> Result<()> {
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
&bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(&bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
// Alice receives the read receipt.
|
||||
receive_imf(&alice, mdn_body.as_bytes(), false).await?;
|
||||
alice.recv_mdn(&bob, &received_msg).await?;
|
||||
|
||||
// Chat should not pop up in the chatlist.
|
||||
let chats = Chatlist::try_load(&alice, 0, None, None).await?;
|
||||
@@ -2760,19 +2746,7 @@ async fn test_read_receipts_dont_unmark_bots() -> Result<()> {
|
||||
.await;
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
// Alice receives the read receipt.
|
||||
receive_imf(alice, mdn_body.as_bytes(), false).await?;
|
||||
alice.recv_mdn(bob, &received_msg).await?;
|
||||
let msg = alice.get_last_msg_in(alice_chat.id).await;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
let ab_contact = alice.add_or_lookup_contact(bob).await;
|
||||
@@ -3164,13 +3138,11 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
|
||||
async fn test_auto_accept_encrypted_group_for_bots() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
let group_id = alice.create_group_with_members("Group", &[bob]).await;
|
||||
let sent = alice.send_text(group_id, "Hello!").await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
@@ -3180,21 +3152,13 @@ async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
|
||||
}
|
||||
|
||||
/// Regression test for a bug where receive_imf() failed
|
||||
/// if the sender of a verification-gossiping message
|
||||
/// if the sender of a gossiping message
|
||||
/// also put itself into the To header.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verification_gossip() -> Result<()> {
|
||||
async fn test_gossip_sender_in_to_header() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
mark_as_verified(alice, bob).await;
|
||||
mark_as_verified(bob, alice).await;
|
||||
|
||||
// This is message sent by Alice with verified encryption
|
||||
// that gossips Fiona's verification,
|
||||
// and for some reason, Alice also put herself into the To: header.
|
||||
let imf_raw =
|
||||
include_bytes!("../../test-data/message/verification-gossip-also-sent-to-from.eml");
|
||||
|
||||
@@ -3202,12 +3166,6 @@ async fn test_verification_gossip() -> Result<()> {
|
||||
let msg = receive_imf(bob, imf_raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.text, "Hello!");
|
||||
assert!(
|
||||
bob.add_or_lookup_contact(fiona)
|
||||
.await
|
||||
.is_verified(bob)
|
||||
.await?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3380,6 +3338,35 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blocked_contact_sends_reaction() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let bob_msg_id = tcm.send_recv_accept(alice, bob, "Hi!").await.id;
|
||||
|
||||
let chat = alice.get_chat(bob).await;
|
||||
chat.id.block(alice).await?;
|
||||
|
||||
crate::reaction::send_reaction(bob, bob_msg_id, "👍").await?;
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_hidden(&sent).await;
|
||||
alice.emit_event(EventType::Test);
|
||||
|
||||
while let Some(ev) = alice.evtracker.recv().await {
|
||||
match ev.typ {
|
||||
EventType::IncomingReaction { .. } => {
|
||||
panic!("Alice is not supposed to receive a notification, since she blocked Bob")
|
||||
}
|
||||
EventType::Test => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_undecryptable() -> Result<()> {
|
||||
let alice = &TestContext::new().await;
|
||||
@@ -3863,7 +3850,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_addr = bob.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_chat_id = create_group(alice, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
@@ -4551,7 +4538,7 @@ async fn test_outgoing_msg_forgery() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let export_dir = tempfile::tempdir().unwrap();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_addr = &alice.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
imex(alice, ImexMode::ExportSelfKeys, export_dir.path(), None).await?;
|
||||
// We need Bob only to encrypt the forged message to Alice's key, actually Bob doesn't
|
||||
// participate in the scenario.
|
||||
@@ -4626,11 +4613,10 @@ async fn test_pre_msg_group_consistency() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
async fn test_group_add_remove_member_missing_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
mark_as_verified(alice, bob).await;
|
||||
let group_id = create_group(alice, "Group").await?;
|
||||
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
|
||||
add_contact_to_chat(alice, group_id, alice_bob_id).await?;
|
||||
@@ -4638,7 +4624,6 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
alice.sql.execute("DELETE FROM public_keys", ()).await?;
|
||||
|
||||
let fiona = &tcm.fiona().await;
|
||||
mark_as_verified(alice, fiona).await;
|
||||
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
|
||||
add_contact_to_chat(alice, group_id, alice_fiona_id).await?;
|
||||
|
||||
@@ -5131,7 +5116,7 @@ async fn test_rename_chat_after_creating_invite() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unverified_member_msg() -> Result<()> {
|
||||
async fn test_member_msg_downloaded() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
@@ -5146,97 +5131,12 @@ async fn test_unverified_member_msg() -> Result<()> {
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
let fiona_sent_msg = fiona.send_text(fiona_chat_id, "Hi").await;
|
||||
|
||||
// The message is by non-verified member,
|
||||
// but the checks have been removed
|
||||
// and the message should be downloaded as usual.
|
||||
let bob_msg = bob.recv_msg(&fiona_sent_msg).await;
|
||||
assert_eq!(bob_msg.download_state, DownloadState::Done);
|
||||
assert_eq!(bob_msg.text, "Hi");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a0 = &tcm.alice().await;
|
||||
let a1 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_chat_id = chat::create_group(bob, "Group").await?;
|
||||
bob.set_chat_protected(bob_chat_id).await;
|
||||
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
|
||||
tcm.exec_securejoin_qr(a0, bob, &qr).await;
|
||||
tcm.exec_securejoin_qr(a1, bob, &qr).await;
|
||||
|
||||
// Shift time by one week to trigger gossip.
|
||||
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
||||
|
||||
let a0_chat_id = a0.get_last_msg().await.chat_id;
|
||||
let a0_sent_msg = a0.send_text(a0_chat_id, "Hi").await;
|
||||
a1.recv_msg(&a0_sent_msg).await;
|
||||
let a1_bob_id = a1.add_or_lookup_contact_id(bob).await;
|
||||
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
|
||||
assert_eq!(
|
||||
a1_fiona.get_verifier_id(a1).await?.unwrap().unwrap(),
|
||||
a1_bob_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a0 = &tcm.alice().await;
|
||||
let a1 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let bob_chat_id = chat::create_group(bob, "Group").await?;
|
||||
bob.set_chat_protected(bob_chat_id).await;
|
||||
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
|
||||
tcm.exec_securejoin_qr(a0, bob, &qr).await;
|
||||
|
||||
let qr = get_securejoin_qr(bob, None).await?;
|
||||
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
|
||||
|
||||
// Bob verifies Fiona for Alice#0.
|
||||
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_fiona_id).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
a0.recv_msg(&sent_msg).await;
|
||||
fiona.recv_msg(&sent_msg).await;
|
||||
let a0_bob = a0.add_or_lookup_contact(bob).await;
|
||||
let a0_fiona = a0.add_or_lookup_contact(fiona).await;
|
||||
assert_eq!(a0_fiona.get_verifier_id(a0).await?, Some(Some(a0_bob.id)));
|
||||
|
||||
let chat_id = a0.create_group_with_members("group", &[fiona]).await;
|
||||
a0.set_chat_protected(chat_id).await;
|
||||
a1.recv_msg(&a0.send_text(chat_id, "Hi").await).await;
|
||||
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
|
||||
assert_eq!(a1_fiona.get_verifier_id(a1).await?, Some(None));
|
||||
|
||||
let some_time_to_regossip = Duration::from_secs(20 * 24 * 3600);
|
||||
SystemTime::shift(some_time_to_regossip);
|
||||
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
|
||||
fiona.set_chat_protected(fiona_chat_id).await;
|
||||
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
|
||||
.await;
|
||||
let a1_bob = a1.add_or_lookup_contact(bob).await;
|
||||
// There was a bug that Bob is verified by Fiona on Alice's other device.
|
||||
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(None));
|
||||
|
||||
SystemTime::shift(some_time_to_regossip);
|
||||
tcm.execute_securejoin(a1, fiona).await;
|
||||
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
|
||||
.await;
|
||||
// But now Bob's verifier id must be updated because Fiona is verified by a known verifier
|
||||
// (moreover, directly), so Alice has reverse verification chains on her devices.
|
||||
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(Some(a1_fiona.id)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that second device assigns outgoing encrypted messages
|
||||
/// to a single chat with key-contact even if the key of the contact is unknown.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -5735,7 +5635,7 @@ async fn test_bcc_not_a_group() -> Result<()> {
|
||||
async fn test_lookup_key_contact_by_address_self() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let addr = &t.get_config(Config::Addr).await?.unwrap();
|
||||
let addr = &t.get_config(Config::ConfiguredAddr).await?.unwrap();
|
||||
assert_eq!(
|
||||
lookup_key_contact_by_address(t, addr, None).await?,
|
||||
Some(ContactId::SELF)
|
||||
@@ -5788,7 +5688,7 @@ async fn test_outgoing_determined_by_signature() -> Result<()> {
|
||||
alice_dev2.configure_addr(different_from).await;
|
||||
key::store_self_keypair(alice_dev2, &alice_keypair()).await?;
|
||||
assert_ne!(
|
||||
alice.get_config(Config::Addr).await?.unwrap(),
|
||||
alice.get_config(Config::ConfiguredAddr).await?.unwrap(),
|
||||
different_from
|
||||
);
|
||||
|
||||
@@ -5847,15 +5747,32 @@ async fn test_mark_message_as_delivered_only_after_sent_out_fully() -> Result<()
|
||||
/// This simulates the case that a message is successfully sent out,
|
||||
/// but the 'OK' answer from the server doesn't arrive,
|
||||
/// so that the SMTP row stays in the database.
|
||||
pub(crate) async fn first_row_in_smtp_queue(alice: &TestContext) -> (MsgId, String) {
|
||||
alice
|
||||
pub(crate) async fn first_row_in_smtp_queue(context: &TestContext) -> (MsgId, String) {
|
||||
let (rowid, msg_id) = context
|
||||
.sql
|
||||
.query_row_optional("SELECT msg_id, mime FROM smtp ORDER BY id", (), |row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let mime: String = row.get(1)?;
|
||||
Ok((msg_id, mime))
|
||||
})
|
||||
.query_row_optional(
|
||||
"SELECT id, msg_id FROM smtp2 ORDER BY id LIMIT 1",
|
||||
(),
|
||||
|row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
let msg_id: MsgId = row.get(1)?;
|
||||
Ok((rowid, msg_id))
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("query_row_optional failed")
|
||||
.expect("No SMTP row found")
|
||||
.expect("No SMTP row found");
|
||||
let query_only = true;
|
||||
let queued_mail = context
|
||||
.sql
|
||||
.transaction_ext(query_only, |transaction| {
|
||||
smtp::queue::load_queued_mail(transaction, rowid)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let rendered_mail = mimefactory::render_queued_mail_with_context(queued_mail, context)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(msg_id, rendered_mail.message)
|
||||
}
|
||||
|
||||
+13
-12
@@ -215,10 +215,17 @@ impl SchedulerState {
|
||||
|
||||
/// Indicate that the network likely has come back.
|
||||
pub(crate) async fn maybe_network(&self) {
|
||||
self.interrupt_inbox_idle().await;
|
||||
self.interrupt_smtp().await;
|
||||
}
|
||||
|
||||
/// Interrupts IDLE on all transports so that they fetch,
|
||||
/// and marks them as having work to do.
|
||||
pub(crate) async fn interrupt_inbox_idle(&self) {
|
||||
let inner = self.inner.read().await;
|
||||
let inboxes = match *inner {
|
||||
InnerSchedulerState::Started(ref scheduler) => {
|
||||
scheduler.maybe_network();
|
||||
scheduler.interrupt_inbox();
|
||||
scheduler
|
||||
.inboxes
|
||||
.iter()
|
||||
@@ -334,7 +341,7 @@ async fn background_fetch_from_transport(
|
||||
|
||||
let folder = connection.folder.clone();
|
||||
connection
|
||||
.fetch_move_delete(context, &mut session, &folder)
|
||||
.fetch_delete(context, &mut session, &folder)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -514,7 +521,6 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
};
|
||||
|
||||
maybe_broadcast_reactions(ctx).await.log_err(ctx).ok();
|
||||
maybe_send_stats(ctx).await.log_err(ctx).ok();
|
||||
|
||||
session
|
||||
.update_metadata(ctx)
|
||||
@@ -527,6 +533,8 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
);
|
||||
}
|
||||
|
||||
maybe_send_stats(ctx).await.log_err(ctx).ok();
|
||||
|
||||
let session = fetch_idle(ctx, imap, session).await?;
|
||||
Ok(session)
|
||||
}
|
||||
@@ -548,9 +556,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, mut session: Session)
|
||||
|
||||
// Fetch the watched folder.
|
||||
connection
|
||||
.fetch_move_delete(ctx, &mut session, &watch_folder)
|
||||
.fetch_delete(ctx, &mut session, &watch_folder)
|
||||
.await
|
||||
.context("fetch_move_delete")?;
|
||||
.context("fetch_delete")?;
|
||||
|
||||
download_known_post_messages_without_pre_message(ctx, &mut session).await?;
|
||||
download_msgs(ctx, &mut session)
|
||||
@@ -799,13 +807,6 @@ impl Scheduler {
|
||||
self.inboxes.iter()
|
||||
}
|
||||
|
||||
fn maybe_network(&self) {
|
||||
for b in self.boxes() {
|
||||
b.conn_state.interrupt();
|
||||
}
|
||||
self.interrupt_smtp();
|
||||
}
|
||||
|
||||
fn maybe_network_lost(&self) {
|
||||
for b in self.boxes() {
|
||||
b.conn_state.interrupt();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use core::fmt;
|
||||
use std::cmp::min;
|
||||
use std::{iter::once, ops::Deref, sync::Arc};
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use humansize::{BINARY, format_size};
|
||||
@@ -531,14 +531,15 @@ impl Context {
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Returns true if all background work is done.
|
||||
async fn all_work_done(&self) -> bool {
|
||||
/// Returns true if all background work is done,
|
||||
/// checking the outgoing message queue only if `include_smtp` is set.
|
||||
async fn work_done(&self, include_smtp: bool) -> bool {
|
||||
let lock = self.scheduler.inner.read().await;
|
||||
let stores: Vec<_> = match *lock {
|
||||
InnerSchedulerState::Started(ref sched) => sched
|
||||
.boxes()
|
||||
.map(|b| &b.conn_state.state)
|
||||
.chain(once(&sched.smtp.state))
|
||||
.chain(include_smtp.then_some(&sched.smtp.state))
|
||||
.map(|state| state.connectivity.clone())
|
||||
.collect(),
|
||||
_ => return false,
|
||||
@@ -555,19 +556,26 @@ impl Context {
|
||||
|
||||
/// Waits until background work is finished.
|
||||
pub async fn wait_for_all_work_done(&self) {
|
||||
let include_smtp = true;
|
||||
self.wait_for_work_done(include_smtp).await
|
||||
}
|
||||
|
||||
/// Waits until background work is finished,
|
||||
/// checking the outgoing message queue only if `include_smtp` is set.
|
||||
pub(crate) async fn wait_for_work_done(&self, include_smtp: bool) {
|
||||
// Ideally we could wait for connectivity change events,
|
||||
// but sleep loop is good enough.
|
||||
|
||||
// First 100 ms sleep in chunks of 10 ms.
|
||||
for _ in 0..10 {
|
||||
if self.all_work_done().await {
|
||||
if self.work_done(include_smtp).await {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
|
||||
// If we are not finished in 100 ms, keep waking up every 100 ms.
|
||||
while !self.all_work_done().await {
|
||||
while !self.work_done(include_smtp).await {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
@@ -576,6 +584,34 @@ impl Context {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_background_fetch_leaves_smtp_alone() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice.start_io().await;
|
||||
alice.wait_for_all_work_done().await;
|
||||
|
||||
let smtp = match *alice.scheduler.inner.read().await {
|
||||
InnerSchedulerState::Started(ref scheduler) => {
|
||||
scheduler.smtp.state.connectivity.clone()
|
||||
}
|
||||
_ => panic!("scheduler is not running"),
|
||||
};
|
||||
smtp.set_working(&alice);
|
||||
|
||||
alice.background_fetch().await?;
|
||||
assert!(!smtp.get_all_work_done());
|
||||
assert!(alice.scheduler.is_running().await);
|
||||
|
||||
alice
|
||||
.assert_warns_or_errors(&[
|
||||
"No IMAP connection candidates provided",
|
||||
"IMAP got rate limited",
|
||||
])
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_combine_connectivities() {
|
||||
|
||||
+25
-53
@@ -7,7 +7,6 @@ use percent_encoding::{AsciiSet, utf8_percent_encode};
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, get_chat_id_by_grpid, load_broadcast_secret};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, NON_ALPHANUMERIC_WITHOUT_DOT};
|
||||
use crate::contact::mark_contact_id_as_verified;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
@@ -34,15 +33,6 @@ pub(crate) use qrinvite::QrInvite;
|
||||
|
||||
use crate::token::Namespace;
|
||||
|
||||
/// Only new QR codes cause a verification on Alice's side.
|
||||
/// When a QR code is too old, it is assumed that there was no direct QR scan,
|
||||
/// and that the QR code was potentially published on a website,
|
||||
/// so, Alice doesn't mark Bob as verified.
|
||||
// TODO For backwards compatibility reasons, this is still using a rather large value.
|
||||
// Set this to a lower value (e.g. 10 minutes)
|
||||
// when Delta Chat v2.22.0 is sufficiently rolled out
|
||||
const VERIFICATION_TIMEOUT_SECONDS: i64 = 7 * 24 * 3600;
|
||||
|
||||
const DISALLOWED_CHARACTERS: &AsciiSet = &NON_ALPHANUMERIC_WITHOUT_DOT.remove(b'_');
|
||||
|
||||
fn inviter_progress(
|
||||
@@ -86,7 +76,7 @@ fn shorten_name(name: &str, length: usize) -> String {
|
||||
pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Result<String> {
|
||||
/*=======================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 1 in "Setup verified contact" protocol ====
|
||||
==== Step 1 in the Setup-Contact protocol ====
|
||||
=======================================================*/
|
||||
|
||||
key::ensure_secret_key_exists(context).await.ok();
|
||||
@@ -123,15 +113,9 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
|
||||
// Invite number is used to request the inviter key.
|
||||
let invitenumber = token::lookup_or_new(context, Namespace::InviteNumber, grpid).await?;
|
||||
|
||||
// Auth token is used to verify the key-contact
|
||||
// if the token is not old
|
||||
// and add the contact to the group
|
||||
// Auth token authenticates the joiner
|
||||
// and adds the contact to the group
|
||||
// if there is an associated group ID.
|
||||
//
|
||||
// We always generate a new auth token
|
||||
// because auth tokens "expire"
|
||||
// and can only be used to join groups
|
||||
// without verification afterwards.
|
||||
let auth = create_id();
|
||||
token::save(context, Namespace::Auth, grpid, &auth, time()).await?;
|
||||
|
||||
@@ -255,7 +239,7 @@ pub async fn join_securejoin_with_ux_info(
|
||||
async fn securejoin(context: &Context, qr: &str) -> Result<ChatId> {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 2 in "Setup verified contact" protocol =====
|
||||
==== Step 2 in the Setup-Contact protocol =====
|
||||
========================================================*/
|
||||
|
||||
info!(context, "Requesting secure-join ...",);
|
||||
@@ -303,8 +287,7 @@ async fn info_chat_id(context: &Context, contact_id: ContactId) -> Result<ChatId
|
||||
Ok(chat_id_blocked.id)
|
||||
}
|
||||
|
||||
/// Checks fingerprint and marks the contact as verified
|
||||
/// if fingerprint matches.
|
||||
/// Checks whether the contact's fingerprint matches.
|
||||
async fn verify_sender_by_fingerprint(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
@@ -313,11 +296,7 @@ async fn verify_sender_by_fingerprint(
|
||||
let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
let is_verified = contact.fingerprint().is_some_and(|fp| &fp == fingerprint);
|
||||
if is_verified {
|
||||
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
|
||||
}
|
||||
Ok(is_verified)
|
||||
Ok(contact.fingerprint().is_some_and(|fp| &fp == fingerprint))
|
||||
}
|
||||
|
||||
/// What to do with a Secure-Join handshake message after it was handled.
|
||||
@@ -421,7 +400,6 @@ pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJ
|
||||
///
|
||||
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
|
||||
/// database; this is done by `receive_imf()` later on as needed.
|
||||
#[expect(clippy::arithmetic_side_effects)]
|
||||
pub(crate) async fn handle_securejoin_handshake(
|
||||
context: &Context,
|
||||
mime_message: &mut MimeMessage,
|
||||
@@ -457,7 +435,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
let mut self_found = false;
|
||||
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
|
||||
for key in mime_message.gossiped_keys.values() {
|
||||
if key.public_key.dc_fingerprint() == self_fingerprint {
|
||||
if key.dc_fingerprint() == self_fingerprint {
|
||||
self_found = true;
|
||||
break;
|
||||
}
|
||||
@@ -474,7 +452,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
SecureJoinStep::Request { ref invitenumber } => {
|
||||
/*=======================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 3 in "Setup verified contact" protocol ====
|
||||
==== Step 3 in the Setup-Contact protocol ====
|
||||
=======================================================*/
|
||||
|
||||
// this message may be unencrypted (Bob, the joiner and the sender, might not have Alice's key yet)
|
||||
@@ -515,7 +493,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
SecureJoinStep::AuthRequired => {
|
||||
/*========================================================
|
||||
==== Bob - the joiner's side =====
|
||||
==== Step 4 in "Setup verified contact" protocol =====
|
||||
==== Step 4 in the Setup-Contact protocol =====
|
||||
========================================================*/
|
||||
bob::handle_auth_required_or_pubkey(context, mime_message).await
|
||||
}
|
||||
@@ -554,21 +532,23 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let addr = ContactAddress::new(&mime_message.from.addr)?;
|
||||
let addr = mime_message.from.addr.clone();
|
||||
let attach_self_pubkey = true;
|
||||
let self_fp = self_fingerprint(context).await?;
|
||||
let shared_secret = format!("securejoin/{self_fp}/{auth}");
|
||||
let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
|
||||
let recipients = vec![addr];
|
||||
let queued_message = mimefactory::symm_encrypted_securejoin_message(
|
||||
context,
|
||||
"vc-pubkey",
|
||||
&rfc724_mid,
|
||||
attach_self_pubkey,
|
||||
auth,
|
||||
&shared_secret,
|
||||
recipients,
|
||||
)
|
||||
.await?;
|
||||
|
||||
insert_into_smtp(context, &rfc724_mid, &addr, rendered_message).await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_message).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
|
||||
Ok(HandshakeMessage::Done)
|
||||
@@ -583,8 +563,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
SecureJoinStep::RequestWithAuth => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Steps 5+6 in "Setup verified contact" protocol ====
|
||||
==== Step 6 in "Out-of-band verified groups" protocol ====
|
||||
==== Steps 5+6 in the Setup-Contact protocol ====
|
||||
==== Step 6 in the Join-Group protocol ====
|
||||
==========================================================*/
|
||||
|
||||
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
|
||||
@@ -611,15 +591,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
let Some((grpid, timestamp)) = context
|
||||
let Some(grpid) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT foreign_key, timestamp FROM tokens WHERE namespc=? AND token=?",
|
||||
"SELECT foreign_key FROM tokens WHERE namespc=? AND token=?",
|
||||
(Namespace::Auth, auth),
|
||||
|row| {
|
||||
let foreign_key: String = row.get(0)?;
|
||||
let timestamp: i64 = row.get(1)?;
|
||||
Ok((foreign_key, timestamp))
|
||||
Ok(foreign_key)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
@@ -652,12 +631,8 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified via Auth code.",);
|
||||
info!(context, "Auth code and fingerprint match.",);
|
||||
|
||||
// Mark the contact as verified if auth code is less than VERIFICATION_TIMEOUT_SECONDS seconds old.
|
||||
if time() < timestamp + VERIFICATION_TIMEOUT_SECONDS {
|
||||
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
|
||||
}
|
||||
if sender_contact.blocked {
|
||||
warn!(context, "Ignoring {step} message: {contact_id} is blocked.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
@@ -690,7 +665,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
Ok(HandshakeMessage::Done)
|
||||
} else {
|
||||
let chat_id = info_chat_id(context, contact_id).await?;
|
||||
// Setup verified contact.
|
||||
// Setup contact.
|
||||
send_alice_handshake_msg(context, contact_id, "vc-contact-confirm")
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
@@ -701,7 +676,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
==== Step 7 in the Setup-Contact protocol ====
|
||||
=======================================================*/
|
||||
SecureJoinStep::ContactConfirm => {
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
@@ -751,16 +726,14 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
/// we can make some conclusions of it.
|
||||
///
|
||||
/// If we see self-sent {vc,vg}-request-with-auth,
|
||||
/// we know that we are Bob (joiner-observer)
|
||||
/// that just marked peer (Alice) as verified
|
||||
/// we know that we are Bob (joiner-observer),
|
||||
/// either after receiving {vc,vg}-auth-required
|
||||
/// or immediately after scanning the QR-code
|
||||
/// if the key was already known.
|
||||
///
|
||||
/// If we see self-sent vc-contact-confirm or vg-member-added message,
|
||||
/// we know that we are Alice (inviter-observer)
|
||||
/// that just marked peer (Bob) as verified
|
||||
/// in response to correct vc-request-with-auth message.
|
||||
/// responding to a correct vc-request-with-auth message.
|
||||
pub(crate) async fn observe_securejoin_on_other_device(
|
||||
context: &Context,
|
||||
mime_message: &MimeMessage,
|
||||
@@ -808,13 +781,12 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
if key.public_key.dc_fingerprint() != contact_fingerprint {
|
||||
if key.dc_fingerprint() != contact_fingerprint {
|
||||
// Fingerprint does not match, ignore.
|
||||
warn!(context, "Fingerprint does not match.");
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
|
||||
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
|
||||
if contact.blocked && step != SecureJoinStep::MemberAdded {
|
||||
// Contact might be blocked after another device had issued the message. Still, to avoid
|
||||
// membership inconsistency on devices, don't ignore "vg-member-added".
|
||||
|
||||
@@ -106,7 +106,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
// If QR code is a group invite
|
||||
// and we are already in the chat,
|
||||
// nothing needs to be done.
|
||||
// Even if Alice is not verified, we don't send anything.
|
||||
context.emit_event(EventType::SecurejoinJoinerProgress {
|
||||
contact_id: invite.contact_id(),
|
||||
progress: JoinerProgress::Succeeded.into_u16(),
|
||||
@@ -270,7 +269,7 @@ pub(super) async fn handle_auth_required_or_pubkey(
|
||||
continue;
|
||||
}
|
||||
|
||||
info!(context, "Fingerprint verified.",);
|
||||
info!(context, "Fingerprint matches.",);
|
||||
let chat_id = private_chat_id(context, &invite).await?;
|
||||
delete_securejoin_wait_msg(context, chat_id)
|
||||
.await
|
||||
@@ -325,22 +324,23 @@ pub(crate) async fn send_handshake_message(
|
||||
if invite.is_v3() && matches!(step, BobHandshakeMsg::Request) {
|
||||
// Send a minimal symmetrically-encrypted vc-request-pubkey message
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let recipients = invite.addrs().join(" ");
|
||||
let recipients = invite.addrs();
|
||||
let alice_fp = invite.fingerprint().hex();
|
||||
let auth = invite.authcode();
|
||||
let shared_secret = format!("securejoin/{alice_fp}/{auth}");
|
||||
let attach_self_pubkey = false;
|
||||
let rendered_message = mimefactory::render_symm_encrypted_securejoin_message(
|
||||
let queued_msg = mimefactory::symm_encrypted_securejoin_message(
|
||||
context,
|
||||
"vc-request-pubkey",
|
||||
&rfc724_mid,
|
||||
attach_self_pubkey,
|
||||
auth,
|
||||
&shared_secret,
|
||||
recipients.to_vec(),
|
||||
)
|
||||
.await?;
|
||||
insert_into_smtp(context, &rfc724_mid, &queued_msg).await?;
|
||||
|
||||
insert_into_smtp(context, &rfc724_mid, &recipients, rendered_message).await?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
} else {
|
||||
let mut msg = Message {
|
||||
@@ -448,8 +448,8 @@ async fn private_chat_id(context: &Context, invite: &QrInvite) -> Result<ChatId>
|
||||
///
|
||||
/// This is the chat in which you want to notify the user as well.
|
||||
///
|
||||
/// When joining a group this is the [`ChatId`] of the group chat, when verifying a
|
||||
/// contact this is the [`ChatId`] of the single chat.
|
||||
/// When joining a group this is the [`ChatId`] of the group chat,
|
||||
/// when setting up a contact this is the [`ChatId`] of the single chat.
|
||||
/// The group chat will be created if it does not yet exist.
|
||||
async fn joining_chat_id(
|
||||
context: &Context,
|
||||
@@ -495,7 +495,7 @@ async fn joining_chat_id(
|
||||
pub(crate) enum JoinerProgress {
|
||||
/// vg-vc-request-with-auth sent.
|
||||
///
|
||||
/// Typically shows as "alice@addr verified, introducing myself."
|
||||
/// Typically shows as "introducing myself."
|
||||
RequestWithAuthSent,
|
||||
/// Completed securejoin.
|
||||
Succeeded,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use regex::Regex;
|
||||
|
||||
@@ -8,7 +6,6 @@ use crate::chat::{CantSendReason, ChatId, add_contact_to_chat, remove_contact_fr
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::key::self_fingerprint;
|
||||
use crate::mimeparser::{GossipedKey, SystemMessage};
|
||||
use crate::qr::Qr;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::{self, messages_e2ee_info_msg};
|
||||
@@ -16,7 +13,6 @@ use crate::test_utils::{
|
||||
AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext, TestContextManager,
|
||||
TimeShiftFalsePositiveNote, get_chat_msg, sync,
|
||||
};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SetupContactCase {
|
||||
@@ -52,7 +48,11 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
let alice_addr = &alice
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
if case == SetupContactCase::AliceHasName {
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice"))
|
||||
@@ -193,10 +193,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
);
|
||||
|
||||
if case == SetupContactCase::WrongAliceGossip {
|
||||
let wrong_pubkey = GossipedKey {
|
||||
public_key: load_self_public_key(&bob).await.unwrap(),
|
||||
verified: false,
|
||||
};
|
||||
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
|
||||
let alice_pubkey = msg
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
@@ -206,7 +203,6 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
||||
|
||||
msg.gossiped_keys
|
||||
.insert(alice_addr.to_string(), alice_pubkey)
|
||||
@@ -215,20 +211,17 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(handshake_msg, HandshakeMessage::Ignore);
|
||||
assert!(contact_bob.is_verified(&alice).await.unwrap());
|
||||
return (alice, bob);
|
||||
}
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
let contact_bob_id = contact_bob.id;
|
||||
assert_eq!(contact_bob.is_key_contact(), true);
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.get_authname(), "");
|
||||
|
||||
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
||||
assert_eq!(contact_bob.e2ee_avail(&alice).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice, contact_bob_id).await.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert!(contact_bob.get_name().is_empty());
|
||||
@@ -248,7 +241,7 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
1
|
||||
);
|
||||
|
||||
// Check Alice got the verified message in her single chat.
|
||||
// Check Alice got the message in her single chat.
|
||||
{
|
||||
let chat = alice.get_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
@@ -280,14 +273,14 @@ async fn test_setup_contact_ext(case: SetupContactCase) -> (TestContext, TestCon
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob has verified Alice already.
|
||||
// Bob has Alice's key already.
|
||||
//
|
||||
// Alice may not have verified Bob yet.
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
// Alice may not have Bob's key yet.
|
||||
assert_eq!(contact_alice.e2ee_avail(&bob.ctx).await.unwrap(), true);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
assert_eq!(contact_alice.e2ee_avail(&bob.ctx).await.unwrap(), true);
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -359,13 +352,11 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
bob_fp.hex()
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await?, false);
|
||||
|
||||
tcm.section("Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm");
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await?, true);
|
||||
assert_eq!(contact_bob.e2ee_avail(alice).await?, true);
|
||||
|
||||
// Check Alice signalled success via the SecurejoinInviterProgress event.
|
||||
let event = alice
|
||||
@@ -399,16 +390,14 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
"vc-contact-confirm"
|
||||
);
|
||||
|
||||
// Bob has verified Alice already.
|
||||
// Bob has Alice's key already.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(alice).await;
|
||||
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
||||
assert_eq!(contact_alice.e2ee_avail(bob).await?, true);
|
||||
|
||||
// Alice confirms that Bob is now verified.
|
||||
//
|
||||
// This does not change anything for Bob.
|
||||
tcm.section("Step 7: Bob receives vc-contact-confirm");
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(bob).await?, true);
|
||||
assert_eq!(contact_alice.e2ee_avail(bob).await?, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -567,13 +556,11 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
|
||||
bob_fp
|
||||
);
|
||||
|
||||
// Alice should not yet have Bob verified
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await?, false);
|
||||
|
||||
tcm.section("Step 5+6: Alice receives vg-request-with-auth, sends vg-member-added");
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await?, true);
|
||||
assert_eq!(contact_bob.e2ee_avail(&alice).await?, true);
|
||||
|
||||
// Check Alice signalled success via the SecurejoinInviterProgress event.
|
||||
let event = alice
|
||||
@@ -615,7 +602,7 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
// Now Alice's chat with Bob should still be hidden, the member-added message should
|
||||
// appear in the group chat.
|
||||
if v3 {
|
||||
assert!(
|
||||
@@ -641,17 +628,17 @@ async fn test_secure_join_group_ext(v3: bool, remove_invite: bool) -> Result<()>
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Bob has verified Alice already.
|
||||
// Bob has Alice's key already.
|
||||
//
|
||||
// Alice may not have verified Bob yet.
|
||||
// Alice may not have Bob's key yet.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob).await?, true);
|
||||
assert_eq!(contact_alice.e2ee_avail(&bob).await?, true);
|
||||
|
||||
tcm.section("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).await?, true);
|
||||
// The message shows up in the group chat.
|
||||
assert_eq!(contact_alice.e2ee_avail(&bob).await?, true);
|
||||
let chat = bob.get_chat(&alice).await;
|
||||
assert_eq!(
|
||||
chat.blocked,
|
||||
@@ -832,11 +819,7 @@ async fn test_unknown_sender() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that Bob gets Alice as verified
|
||||
/// if `vc-contact-confirm` is lost.
|
||||
/// Previously `vc-contact-confirm` was used
|
||||
/// to confirm backward verification,
|
||||
/// but backward verification is not tracked anymore.
|
||||
/// Tests that Bob gets Alice's key if `vc-contact-confirm` is lost.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lost_contact_confirm() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -858,16 +841,16 @@ async fn test_lost_contact_confirm() {
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
// Alice has Bob verified now.
|
||||
// Alice has Bob's key now.
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(&bob).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice).await.unwrap(), true);
|
||||
assert_eq!(contact_bob.e2ee_avail(&alice).await.unwrap(), true);
|
||||
|
||||
// Alice sends vc-contact-confirm, but it gets lost.
|
||||
let _sent_vc_contact_confirm = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob has alice as verified too, even though vc-contact-confirm is lost.
|
||||
// Bob has Alice's key too, even though vc-contact-confirm is lost.
|
||||
let contact_alice = bob.add_or_lookup_contact_no_key(&alice).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob).await.unwrap(), true);
|
||||
assert_eq!(contact_alice.e2ee_avail(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
/// Tests Bob joining two groups by scanning two QR codes
|
||||
@@ -974,7 +957,7 @@ async fn test_parallel_setup_contact(bob_deletes_fiona_contact: bool) -> Result<
|
||||
|
||||
bob.recv_msg_trash(&sent_fiona_vc_contact_confirm).await;
|
||||
let bob_fiona_contact = Contact::get_by_id(bob, bob_fiona_contact_id).await.unwrap();
|
||||
assert_eq!(bob_fiona_contact.is_verified(bob).await.unwrap(), true);
|
||||
assert_eq!(bob_fiona_contact.e2ee_avail(bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Alice gets online and previously started SecureJoin process finishes.
|
||||
@@ -990,7 +973,7 @@ async fn test_parallel_setup_contact(bob_deletes_fiona_contact: bool) -> Result<
|
||||
bob.recv_msg_trash(&sent_alice_vc_contact_confirm).await;
|
||||
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
|
||||
let bob_alice_contact = Contact::get_by_id(bob, bob_alice_contact_id).await.unwrap();
|
||||
assert_eq!(bob_alice_contact.is_verified(bob).await.unwrap(), true);
|
||||
assert_eq!(bob_alice_contact.e2ee_avail(bob).await.unwrap(), true);
|
||||
|
||||
bob.assert_warn("Message does not match expected fingerprint")
|
||||
.await;
|
||||
@@ -1027,8 +1010,6 @@ async fn test_wrong_auth_token() -> Result<()> {
|
||||
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let alice_bob_contact = alice.add_or_lookup_contact(bob).await;
|
||||
assert!(!alice_bob_contact.is_verified(alice).await?);
|
||||
alice.assert_warn("invalid auth code").await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1100,123 +1081,6 @@ async fn test_send_avatar_in_securejoin() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that scanning a QR code week later
|
||||
/// allows Bob to establish a contact with Alice,
|
||||
/// but does not mark Bob as verified for Alice.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_expired_contact_auth_token() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
// Alice creates a QR code.
|
||||
let qr = get_securejoin_qr(alice, None).await?;
|
||||
|
||||
// One week passes, QR code expires.
|
||||
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
||||
|
||||
// Bob scans the QR code.
|
||||
join_securejoin(bob, &qr).await?;
|
||||
|
||||
// vc-request
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// vc-auth-requried
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// vc-request-with-auth
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bob should not be verified for Alice.
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_expired_group_auth_token() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_chat_id = chat::create_group(alice, "Group").await?;
|
||||
|
||||
// Alice creates a group QR code.
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await.unwrap();
|
||||
|
||||
// One week passes, QR code expires.
|
||||
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
||||
|
||||
// Bob scans the QR code.
|
||||
join_securejoin(bob, &qr).await?;
|
||||
|
||||
// vg-request
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// vg-auth-requried
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// vg-request-with-auth
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// vg-member-added
|
||||
let bob_member_added_msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert!(bob_member_added_msg.is_info());
|
||||
assert_eq!(
|
||||
bob_member_added_msg.get_info_type(),
|
||||
SystemMessage::MemberAddedToGroup
|
||||
);
|
||||
|
||||
// Bob should not be verified for Alice.
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that old token is considered expired
|
||||
/// even if sync message just arrived.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_expired_synced_auth_token() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
alice.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
alice2.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
|
||||
// Alice creates a QR code on the second device.
|
||||
let qr = get_securejoin_qr(alice2, None).await?;
|
||||
|
||||
alice2.send_sync_msg().await.unwrap();
|
||||
let sync_msg = alice2.pop_sent_msg().await;
|
||||
|
||||
// One week passes, QR code expires.
|
||||
SystemTime::shift(Duration::from_secs(7 * 24 * 3600));
|
||||
|
||||
alice.recv_msg_trash(&sync_msg).await;
|
||||
|
||||
// Bob scans the QR code.
|
||||
join_securejoin(bob, &qr).await?;
|
||||
|
||||
// vc-request
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// vc-auth-requried
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// vc-request-with-auth
|
||||
alice.recv_msg_trash(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bob should not be verified for Alice.
|
||||
let contact_bob = alice.add_or_lookup_contact_no_key(bob).await;
|
||||
assert_eq!(contact_bob.is_verified(alice).await.unwrap(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that attempting to join already joined group does nothing.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_rejoin_group() -> Result<()> {
|
||||
|
||||
+159
-68
@@ -1,11 +1,15 @@
|
||||
//! # SMTP transport module.
|
||||
|
||||
mod connect;
|
||||
pub(crate) mod queue;
|
||||
pub mod send;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, bail, format_err};
|
||||
use async_smtp::response::{Category, Code, Detail};
|
||||
use async_smtp::{EmailAddress, SmtpTransport};
|
||||
use rusqlite::OptionalExtension as _;
|
||||
use tokio::task;
|
||||
|
||||
use crate::chat::{ChatId, add_info_msg_with_cmd};
|
||||
@@ -13,13 +17,16 @@ use crate::config::Config;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key;
|
||||
use crate::log::{LogExt, warn};
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory;
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::net::session::SessionBufStream;
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::smtp::queue::QueuedMail;
|
||||
use crate::stock_str::unencrypted_email;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::transport::{
|
||||
@@ -34,6 +41,9 @@ pub(crate) struct Smtp {
|
||||
/// Email address we are sending from.
|
||||
from: Option<EmailAddress>,
|
||||
|
||||
/// Transport we are connected to.
|
||||
transport_id: Option<u32>,
|
||||
|
||||
/// Timestamp of last successful send/receive network interaction
|
||||
/// (eg connect or send succeeded). On initialization and disconnect
|
||||
/// it is set to None.
|
||||
@@ -89,19 +99,39 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.connectivity.set_connecting(context);
|
||||
let (_transport_id, lp) = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
self.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.smtp_password,
|
||||
&proxy_config,
|
||||
&lp.addr,
|
||||
lp.strict_tls(proxy_config.is_some())?,
|
||||
)
|
||||
.await
|
||||
let transports = ConfiguredLoginParam::load_all(context).await?;
|
||||
|
||||
// Try to connect to the newest transport first. If sending is unreliable,
|
||||
// user can configure a new transport and it will be the one used.
|
||||
// Conversely, if user just added a new transport and sending got less reliable,
|
||||
// user can restore old state by removing the just added transport.
|
||||
for (transport_id, lp) in transports.into_iter().rev() {
|
||||
info!(context, "Trying to connect to transport {transport_id}.");
|
||||
match self
|
||||
.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.smtp_password,
|
||||
&proxy_config,
|
||||
&lp.addr,
|
||||
lp.strict_tls(proxy_config.is_some())?,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
self.transport_id = Some(transport_id);
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to connect to SMTP transport {transport_id}: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("Failed to connect to any SMTP server");
|
||||
}
|
||||
|
||||
/// Connect using the provided login params.
|
||||
@@ -188,15 +218,6 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
smtp.connectivity.set_working(context);
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("Failed to open SMTP connection")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{err:#}"));
|
||||
return SendResult::Retry;
|
||||
}
|
||||
|
||||
let send_result = smtp.send(context, recipients, message.as_bytes()).await;
|
||||
smtp.last_send_error = send_result.as_ref().err().map(|e| e.to_string());
|
||||
|
||||
@@ -328,21 +349,17 @@ pub(crate) async fn smtp_send(
|
||||
}
|
||||
|
||||
/// Inserts a tombstone for `rfc724_mid`
|
||||
/// and queues the rendered message for SMTP sending.
|
||||
/// and queues the message for SMTP sending.
|
||||
pub(crate) async fn insert_into_smtp(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
recipients: &str,
|
||||
rendered_message: String,
|
||||
queued_msg: &QueuedMail,
|
||||
) -> Result<()> {
|
||||
let now = tools::time();
|
||||
let msg_id = message::insert_tombstone(context, rfc724_mid).await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp (rfc724_mid, recipients, mime, msg_id)
|
||||
VALUES (?1, ?2, ?3, ?4)",
|
||||
(&rfc724_mid, &recipients, &rendered_message, msg_id),
|
||||
)
|
||||
.transaction(|transaction| queue::enqueue_mail(transaction, now, msg_id, queued_msg, None))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -370,23 +387,31 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
// database.
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE smtp SET retries=retries+1 WHERE id=?", (rowid,))
|
||||
.execute("UPDATE smtp2 SET retries=retries+1 WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("failed to update retries count")?;
|
||||
|
||||
let Some((body, recipients, msg_id, retries)) = context
|
||||
let Some((queued_mail, msg_id, retries)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT mime, recipients, msg_id, retries FROM smtp WHERE id=?",
|
||||
(rowid,),
|
||||
|row| {
|
||||
let mime: String = row.get(0)?;
|
||||
let recipients: String = row.get(1)?;
|
||||
let msg_id: MsgId = row.get(2)?;
|
||||
let retries: i64 = row.get(3)?;
|
||||
Ok((mime, recipients, msg_id, retries))
|
||||
},
|
||||
)
|
||||
.transaction_ext(true, |transaction| {
|
||||
let Some((msg_id, retries)) = transaction
|
||||
.query_row(
|
||||
"SELECT msg_id, retries FROM smtp2 WHERE id=?",
|
||||
(rowid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let retries: i64 = row.get(1)?;
|
||||
Ok((msg_id, retries))
|
||||
},
|
||||
)
|
||||
.optional()?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let queued_mail = queue::load_queued_mail(transaction, rowid)
|
||||
.with_context(|| format!("Failed to load queued mail for {rowid}"))?;
|
||||
Ok(Some((queued_mail, msg_id, retries)))
|
||||
})
|
||||
.await?
|
||||
else {
|
||||
return Ok(());
|
||||
@@ -394,7 +419,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
if retries > 6 {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.execute("DELETE FROM smtp2 WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("Failed to remove message with exceeded retry limit from smtp table")?;
|
||||
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
@@ -403,13 +428,28 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Try number {retries} to send message {msg_id} (entry {rowid}) over SMTP."
|
||||
);
|
||||
|
||||
let mut recipients = queued_mail.recipients.clone();
|
||||
if queued_mail.bcc_self {
|
||||
let from_addr = smtp
|
||||
.from
|
||||
.as_ref()
|
||||
.context("No From address available, likely not connected")?
|
||||
.to_string();
|
||||
add_self_recipients(
|
||||
context,
|
||||
&mut recipients,
|
||||
queued_mail.encryption.is_encrypted(),
|
||||
from_addr,
|
||||
)
|
||||
.await
|
||||
.context("Failed to add self recipients")?;
|
||||
}
|
||||
let mut sent_to_set: BTreeSet<String> =
|
||||
BTreeSet::from_iter(queued_mail.sent_to.iter().cloned());
|
||||
let recipients_list = recipients
|
||||
.split(' ')
|
||||
.into_iter()
|
||||
.filter(|addr| !sent_to_set.contains(AsRef::<str>::as_ref(addr)))
|
||||
.filter_map(
|
||||
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
|
||||
Ok(addr) => Some(addr),
|
||||
@@ -421,25 +461,55 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?.max(1);
|
||||
let public_key = key::load_self_public_key(context).await?;
|
||||
let secret_key = key::load_self_secret_key(context).await?;
|
||||
let from_addr = smtp
|
||||
.from
|
||||
.as_ref()
|
||||
.context("No From address available, likely not connected")?
|
||||
.to_string();
|
||||
|
||||
let transport_id = smtp.transport_id.context("Not connected")?;
|
||||
let chunk_size = context
|
||||
.get_max_smtp_rcpt_to(transport_id, &from_addr)
|
||||
.await?
|
||||
.max(1);
|
||||
|
||||
let rendered_mail =
|
||||
mimefactory::render_queued_mail(queued_mail, &public_key, &secret_key, from_addr)?;
|
||||
let body = rendered_mail.message;
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Try number {retries} to send message {msg_id} (entry {rowid}) over SMTP."
|
||||
);
|
||||
let mut unsent = recipients_list.as_slice();
|
||||
let status = loop {
|
||||
let unsent_len = u32::try_from(unsent.len()).context("Too many SMTP recipients")?;
|
||||
let split_index = usize::try_from(chunk_size.min(unsent_len))
|
||||
.context("Failed to convert SMTP chunk size")?;
|
||||
let (chunk, rest) = unsent.split_at(split_index);
|
||||
for sent_to_addr in chunk {
|
||||
let sent_to_addr_string = sent_to_addr.to_string();
|
||||
if !sent_to_set.insert(sent_to_addr_string) {
|
||||
error!(context, "Attempted to send to {sent_to_addr} twice.");
|
||||
}
|
||||
}
|
||||
let status = smtp_send(context, chunk, body.as_str(), smtp, Some(msg_id)).await;
|
||||
if !matches!(status, SendResult::Success) || rest.is_empty() {
|
||||
break status;
|
||||
}
|
||||
let rest_str = rest
|
||||
let sent_to_str = sent_to_set
|
||||
.iter()
|
||||
.map(|a| a.as_ref())
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<&str>>()
|
||||
.join(" ");
|
||||
context
|
||||
.sql
|
||||
.execute("UPDATE smtp SET recipients=? WHERE id=?", (rest_str, rowid))
|
||||
.execute(
|
||||
"UPDATE smtp2 SET sent_to=? WHERE id=?",
|
||||
(sent_to_str, rowid),
|
||||
)
|
||||
.await?;
|
||||
unsent = rest;
|
||||
};
|
||||
@@ -449,8 +519,17 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
SendResult::Success => {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.execute("DELETE FROM smtp2 WHERE id=?", (rowid,))
|
||||
.await?;
|
||||
|
||||
let sent_to_len = sent_to_set.len();
|
||||
debug_assert!(sent_to_len > 0);
|
||||
let info_msg = format!(
|
||||
"Message len={} was SMTP-sent to {sent_to_len} recipients.",
|
||||
body.len()
|
||||
);
|
||||
info!(context, "{info_msg}.");
|
||||
context.emit_event(EventType::SmtpMessageSent(info_msg));
|
||||
}
|
||||
SendResult::Failure(ref err) => {
|
||||
if err
|
||||
@@ -493,7 +572,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.execute("DELETE FROM smtp2 WHERE id=?", (rowid,))
|
||||
.await?;
|
||||
}
|
||||
};
|
||||
@@ -516,7 +595,7 @@ pub(crate) async fn msg_has_pending_smtp_job(
|
||||
) -> Result<bool, Error> {
|
||||
context
|
||||
.sql
|
||||
.exists("SELECT COUNT(*) FROM smtp WHERE msg_id=?", (msg_id,))
|
||||
.exists("SELECT COUNT(*) FROM smtp2 WHERE msg_id=?", (msg_id,))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -549,7 +628,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
|
||||
let rowids = context
|
||||
.sql
|
||||
.query_map_vec("SELECT id FROM smtp ORDER BY id ASC", (), |row| {
|
||||
.query_map_vec("SELECT id FROM smtp2 ORDER BY id ASC", (), |row| {
|
||||
let rowid: i64 = row.get(0)?;
|
||||
Ok(rowid)
|
||||
})
|
||||
@@ -621,11 +700,16 @@ async fn send_mdn_rfc724_mid(
|
||||
} else {
|
||||
mimefactory.recipients()
|
||||
};
|
||||
let rendered_msg = Box::pin(mimefactory.render(context)).await?;
|
||||
let from = smtp
|
||||
.from
|
||||
.as_ref()
|
||||
.context("No From address, not connected")?
|
||||
.to_string();
|
||||
let rendered_msg = Box::pin(mimefactory.render(context, &from)).await?;
|
||||
let body = rendered_msg.message;
|
||||
|
||||
if context.get_config_bool(Config::BccSelf).await? {
|
||||
add_self_recipients(context, &mut recipients, encrypted).await?;
|
||||
add_self_recipients(context, &mut recipients, encrypted, from).await?;
|
||||
}
|
||||
let recipients: Vec<_> = recipients
|
||||
.into_iter()
|
||||
@@ -690,6 +774,19 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
};
|
||||
let (rfc724_mid, contact_id) = msg_row;
|
||||
|
||||
// Connect SMTP after checking that we have an MDN to send,
|
||||
// but before increasing MDN retry counter.
|
||||
// If we don't have an MDN, no need to connect.
|
||||
// If we are offline and cannot connect, it is not a failure of an MDN.
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("SMTP connection failure while preparing to send MDNs")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{err:#}"));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -729,11 +826,11 @@ pub(crate) async fn add_self_recipients(
|
||||
context: &Context,
|
||||
recipients: &mut Vec<String>,
|
||||
encrypted: bool,
|
||||
from: String,
|
||||
) -> Result<()> {
|
||||
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
|
||||
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
|
||||
// messages.
|
||||
let from = context.get_primary_self_addr().await?;
|
||||
// Avoid sending unencrypted messages to all transports,
|
||||
// chatmail relays won't accept them. Normally the user should have
|
||||
// a non-chatmail sending transport to send unencrypted messages.
|
||||
if encrypted {
|
||||
for addr in context.get_self_addrs().await? {
|
||||
if addr != from {
|
||||
@@ -751,9 +848,3 @@ pub(crate) async fn add_self_recipients(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if SMTP queue is empty.
|
||||
pub(crate) async fn is_queue_empty(context: &Context) -> Result<bool> {
|
||||
let sending_finished = !context.sql.exists("SELECT COUNT(*) FROM smtp", ()).await?;
|
||||
Ok(sending_finished)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//! # SMTP queue module.
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, SignedPublicKey};
|
||||
use crate::message::MsgId;
|
||||
use rusqlite::OptionalExtension as _;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Encryption {
|
||||
/// Unencrypted message.
|
||||
No,
|
||||
|
||||
/// The message is encrypted asymmetrically to public keys.
|
||||
Asymmetric {
|
||||
/// OpenPGP keys to use for encryption.
|
||||
///
|
||||
/// The message is always encrypted to self,
|
||||
/// no need to include own key here.
|
||||
encryption_pubkeys: Vec<SignedPublicKey>,
|
||||
},
|
||||
|
||||
/// Symmetrically encrypted message with a shared secret.
|
||||
Symmetric { shared_secret: String },
|
||||
}
|
||||
|
||||
impl Encryption {
|
||||
pub(crate) fn is_encrypted(&self) -> bool {
|
||||
match self {
|
||||
Self::No => false,
|
||||
Self::Asymmetric { .. } => true,
|
||||
Self::Symmetric { .. } => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message queued, but not sent yet.
|
||||
///
|
||||
/// It is stored unencrypted to
|
||||
/// make it possible to change protected headers
|
||||
/// like the From address and Autocrypt header later.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct QueuedMail {
|
||||
/// Unencrypted queued message.
|
||||
///
|
||||
/// This message has both the headers and the body,
|
||||
/// but without the From, Autocrypt and Message-ID headers.
|
||||
///
|
||||
/// For encrypted messages this is the OpenPGP payload.
|
||||
pub(crate) raw_message: Vec<u8>,
|
||||
|
||||
/// Display name to put in the `From:` field.
|
||||
///
|
||||
/// Email address is not determined yet here.
|
||||
pub(crate) display_name: String,
|
||||
|
||||
/// Message-ID.
|
||||
pub(crate) rfc724_mid: String,
|
||||
|
||||
/// Whether the message is encrypted and encryption keys.
|
||||
pub(crate) encryption: Encryption,
|
||||
|
||||
/// If true, Autocrypt header should be added before sending.
|
||||
pub(crate) should_attach_pubkey: bool,
|
||||
|
||||
/// If true, OpenPGP compression may be used.
|
||||
pub(crate) should_compress: bool,
|
||||
|
||||
/// If true, encrypted message should be signed.
|
||||
pub(crate) should_sign: bool,
|
||||
|
||||
/// Recipient addresses.
|
||||
pub(crate) recipients: Vec<String>,
|
||||
|
||||
/// Addresses the messages was already sent to.
|
||||
pub(crate) sent_to: Vec<String>,
|
||||
|
||||
/// If true, own addresses should be added to the list of recipients.
|
||||
///
|
||||
/// For unencrypted messages, only the sending addresses should be added.
|
||||
/// For encrypted messages, all published addresses should be added.
|
||||
pub(crate) bcc_self: bool,
|
||||
}
|
||||
|
||||
/// Side effects that should be applied at the same time
|
||||
/// as the message is persisted in the queue.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SideEffects {
|
||||
/// ID of the chat side effects should be applied to.
|
||||
pub chat_id: ChatId,
|
||||
|
||||
/// Largest timestamp of the location sent in `location.kml` in this message.
|
||||
pub last_added_location_timestamp: Option<i64>,
|
||||
|
||||
/// True if the message has the avatar attached.
|
||||
///
|
||||
/// Timestamp of the last time avatar was gossiped should be updated.
|
||||
pub avatar_is_attached: bool,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
|
||||
/// from `multi_device_sync` once the message is actually queued for sending.
|
||||
pub sync_ids_to_delete: Option<String>,
|
||||
|
||||
/// Subject that was rendered into the message.
|
||||
///
|
||||
/// Used to update the subject on the sent message object.
|
||||
pub subject: String,
|
||||
}
|
||||
|
||||
/// Email message ready to be queued with the side effects that should be applied at the same time.
|
||||
pub(crate) type ToBeQueuedMail = (QueuedMail, Option<SideEffects>);
|
||||
|
||||
/// Process side effects and store queued mail.
|
||||
pub(crate) fn enqueue_mail(
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
now: i64,
|
||||
msg_id: MsgId,
|
||||
queued_mail: &QueuedMail,
|
||||
side_effects: Option<&SideEffects>,
|
||||
) -> Result<i64> {
|
||||
if let Some(side_effects) = side_effects {
|
||||
if let Some(last_added_location_timestamp) = side_effects.last_added_location_timestamp {
|
||||
transaction.execute(
|
||||
"UPDATE chats SET locations_last_sent=? WHERE id=?;",
|
||||
(last_added_location_timestamp, side_effects.chat_id),
|
||||
)?;
|
||||
}
|
||||
|
||||
if side_effects.avatar_is_attached {
|
||||
side_effects
|
||||
.chat_id
|
||||
.set_selfavatar_timestamp(transaction, now)
|
||||
.context("Failed to set selfavatar timestamp")?;
|
||||
}
|
||||
|
||||
if let Some(ref sync_ids) = side_effects.sync_ids_to_delete {
|
||||
transaction.execute(
|
||||
&format!("DELETE FROM multi_device_sync WHERE id IN ({sync_ids})"),
|
||||
(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Store mail into queue.
|
||||
let all_recipients = queued_mail.recipients.join(" ");
|
||||
let is_encrypted = queued_mail.encryption.is_encrypted();
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
"
|
||||
INSERT INTO smtp2 (
|
||||
display_name,
|
||||
rfc724_mid,
|
||||
mime,
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
msg_id,
|
||||
recipients,
|
||||
bcc_self,
|
||||
is_encrypted,
|
||||
shared_secret,
|
||||
encryption_fingerprints
|
||||
)
|
||||
VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
",
|
||||
(
|
||||
&queued_mail.display_name,
|
||||
&queued_mail.rfc724_mid,
|
||||
&queued_mail.raw_message,
|
||||
queued_mail.should_attach_pubkey,
|
||||
queued_mail.should_compress,
|
||||
queued_mail.should_sign,
|
||||
msg_id,
|
||||
&all_recipients,
|
||||
queued_mail.bcc_self,
|
||||
is_encrypted,
|
||||
if let Encryption::Symmetric { ref shared_secret } = queued_mail.encryption {
|
||||
shared_secret
|
||||
} else {
|
||||
""
|
||||
},
|
||||
if let Encryption::Asymmetric {
|
||||
ref encryption_pubkeys,
|
||||
} = queued_mail.encryption
|
||||
{
|
||||
let res: Vec<String> = encryption_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.dc_fingerprint().hex())
|
||||
.collect();
|
||||
res.join(" ")
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
),
|
||||
)
|
||||
.context("Failed to insert a row into smtp2 table")?;
|
||||
let row_id = transaction.last_insert_rowid();
|
||||
Ok(row_id)
|
||||
}
|
||||
|
||||
/// Loads the queued mail from `smtp2` table and the list of recipients.
|
||||
pub(crate) fn load_queued_mail(
|
||||
transaction: &mut rusqlite::Transaction<'_>,
|
||||
row_id: i64,
|
||||
) -> Result<QueuedMail> {
|
||||
let (mut queued_mail, encryption_fingerprints) = transaction
|
||||
.query_row_and_then(
|
||||
"
|
||||
SELECT display_name,
|
||||
rfc724_mid,
|
||||
mime,
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
is_encrypted,
|
||||
shared_secret,
|
||||
encryption_fingerprints,
|
||||
recipients,
|
||||
sent_to,
|
||||
bcc_self
|
||||
FROM smtp2 WHERE id = ?
|
||||
",
|
||||
(row_id,),
|
||||
|row| {
|
||||
let display_name: String = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
let raw_message: Vec<u8> = row.get(2)?;
|
||||
let should_attach_pubkey: bool = row.get(3)?;
|
||||
let should_compress: bool = row.get(4)?;
|
||||
let should_sign: bool = row.get(5)?;
|
||||
let is_encrypted: bool = row.get(6)?;
|
||||
let shared_secret: String = row.get(7)?;
|
||||
let encryption_fingerprints: String = row.get(8)?;
|
||||
let encryption_fingerprints: Vec<String> = if encryption_fingerprints.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
encryption_fingerprints
|
||||
.split(' ')
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
};
|
||||
let recipients: String = row.get(9)?;
|
||||
let recipients: Vec<String> = if recipients.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
recipients.split(' ').map(|s| s.to_string()).collect()
|
||||
};
|
||||
debug_assert!(!recipients.iter().any(|s| s.is_empty()));
|
||||
let sent_to: String = row.get(10)?;
|
||||
let sent_to: Vec<String> = if sent_to.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
sent_to.split(' ').map(|s| s.to_string()).collect()
|
||||
};
|
||||
let bcc_self: bool = row.get(11)?;
|
||||
|
||||
let encryption = match (
|
||||
is_encrypted,
|
||||
shared_secret.is_empty(),
|
||||
encryption_fingerprints.is_empty(),
|
||||
) {
|
||||
(false, true, true) => Encryption::No,
|
||||
(true, false, true) => Encryption::Symmetric { shared_secret },
|
||||
(true, true, _) => Encryption::Asymmetric {
|
||||
// Public keys are loaded below based on the encryption fingerprints.
|
||||
encryption_pubkeys: Vec::new(),
|
||||
},
|
||||
_ => bail!("Invalid encryption in smtp2 row"),
|
||||
};
|
||||
Ok::<_, anyhow::Error>((
|
||||
QueuedMail {
|
||||
raw_message,
|
||||
display_name,
|
||||
rfc724_mid,
|
||||
encryption,
|
||||
should_attach_pubkey,
|
||||
should_compress,
|
||||
should_sign,
|
||||
recipients,
|
||||
sent_to,
|
||||
bcc_self,
|
||||
},
|
||||
encryption_fingerprints,
|
||||
))
|
||||
},
|
||||
)
|
||||
.with_context(|| format!("Failed to select row {row_id} from smtp2 table"))?;
|
||||
|
||||
if let Encryption::Asymmetric {
|
||||
ref mut encryption_pubkeys,
|
||||
} = queued_mail.encryption
|
||||
{
|
||||
for fingerprint in encryption_fingerprints {
|
||||
let public_key_bytes: Option<Vec<u8>> = transaction
|
||||
.query_row(
|
||||
"SELECT public_key FROM public_keys WHERE fingerprint=?",
|
||||
(fingerprint,),
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.context("Failed to select public key by fingerprint")?;
|
||||
if let Some(public_key_bytes) = public_key_bytes {
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
encryption_pubkeys.push(public_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(queued_mail)
|
||||
}
|
||||
|
||||
/// Returns true if SMTP queue is empty.
|
||||
pub(crate) async fn is_empty(context: &Context) -> Result<bool> {
|
||||
let sending_finished = !context.sql.exists("SELECT COUNT(*) FROM smtp2", ()).await?;
|
||||
Ok(sending_finished)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use async_smtp::{EmailAddress, Envelope, SendableEmail};
|
||||
use super::Smtp;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::warn;
|
||||
use crate::tools;
|
||||
|
||||
@@ -39,8 +38,6 @@ impl Smtp {
|
||||
context.ratelimit.write().await.send();
|
||||
}
|
||||
|
||||
let message_len_bytes = message.len();
|
||||
|
||||
let envelope =
|
||||
Envelope::new(self.from.clone(), recipients.to_vec()).map_err(Error::Envelope)?;
|
||||
let mail = SendableEmail::new(envelope, message);
|
||||
@@ -55,12 +52,6 @@ impl Smtp {
|
||||
|
||||
transport.send(mail).await.map_err(Error::SmtpSend)?;
|
||||
|
||||
let info_msg = format!(
|
||||
"Message len={message_len_bytes} was SMTP-sent to {} recipients.",
|
||||
recipients.len()
|
||||
);
|
||||
info!(context, "{info_msg}.");
|
||||
context.emit_event(EventType::SmtpMessageSent(info_msg));
|
||||
self.last_success = Some(tools::Time::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -741,6 +741,11 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
|
||||
// Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode.
|
||||
conn.pragma_update(None, "synchronous", "NORMAL".to_string())?;
|
||||
|
||||
// Disable double-quoted string literals misfeature.
|
||||
// <https://sqlite.org/quirks.html#double_quoted_string_literals_are_accepted>
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DML, false)?;
|
||||
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DDL, false)?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
|
||||
@@ -2642,6 +2642,44 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 166)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"
|
||||
UPDATE msgs
|
||||
SET state=24, -- OutFailed
|
||||
error='Message sending canceled by upgrade'
|
||||
WHERE state=20 AND id IN (SELECT msg_id FROM smtp); -- OutPending
|
||||
DELETE FROM smtp;
|
||||
CREATE TABLE smtp2 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
display_name TEXT NOT NULL,
|
||||
rfc724_mid TEXT NOT NULL,
|
||||
mime BLOB NOT NULL,
|
||||
should_attach_pubkey INTEGER NOT NULL,
|
||||
should_compress INTEGER NOT NULL,
|
||||
should_sign INTEGER NOT NULL,
|
||||
msg_id INTEGER NOT NULL,
|
||||
recipients TEXT NOT NULL,
|
||||
sent_to TEXT NOT NULL DEFAULT '',
|
||||
bcc_self INTEGER NOT NULL,
|
||||
is_encrypted INTEGER NOT NULL,
|
||||
shared_secret TEXT NOT NULL DEFAULT '',
|
||||
encryption_fingerprints TEXT NOT NULL DEFAULT '',
|
||||
retries INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 167)?;
|
||||
if dbversion < migration_version {
|
||||
// The default relay candidates are no longer seeded into the table.
|
||||
sql.execute_migration("DELETE FROM relay_candidates", migration_version)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -16,15 +16,15 @@ async fn test_clear_config_cache() -> anyhow::Result<()> {
|
||||
// This test checks that the config cache is invalidated in `execute_migration()`.
|
||||
|
||||
let t = TestContext::new().await;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
|
||||
assert_eq!(t.sql.get_raw_config_bool("cached_key").await?, false);
|
||||
|
||||
t.sql
|
||||
.execute_migration(
|
||||
"INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
|
||||
"INSERT INTO config (keyname, value) VALUES ('cached_key', '1')",
|
||||
1000,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
|
||||
assert_eq!(t.sql.get_raw_config_bool("cached_key").await?, true);
|
||||
assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
|
||||
|
||||
Ok(())
|
||||
@@ -110,7 +110,6 @@ async fn test_key_contacts_migration_autocrypt() -> Result<()> {
|
||||
pgp_bob.fingerprint().unwrap(),
|
||||
pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint()
|
||||
);
|
||||
assert_eq!(pgp_bob.get_verifier_id(&t).await?, None);
|
||||
|
||||
// Hidden address-contact can't be looked up by name.
|
||||
assert!(
|
||||
@@ -149,7 +148,6 @@ async fn test_key_contacts_migration_email1() -> Result<()> {
|
||||
assert_eq!(email_bob.origin, Origin::OutgoingTo);
|
||||
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
|
||||
assert_eq!(email_bob.fingerprint(), None);
|
||||
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -183,7 +181,6 @@ async fn test_key_contacts_migration_email2() -> Result<()> {
|
||||
assert_eq!(email_bob.origin, Origin::OutgoingTo);
|
||||
assert_eq!(email_bob.e2ee_avail(&t).await?, false);
|
||||
assert_eq!(email_bob.fingerprint(), None);
|
||||
assert_eq!(email_bob.get_verifier_id(&t).await?, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -227,7 +224,6 @@ async fn test_key_contacts_migration_verified() -> Result<()> {
|
||||
pgp_bob.fingerprint().unwrap(),
|
||||
pgp_bob.public_key(&t).await?.unwrap().dc_fingerprint()
|
||||
);
|
||||
assert_eq!(pgp_bob.get_verifier_id(&t).await?, Some(None));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+34
-149
@@ -3,7 +3,7 @@
|
||||
//! If this is enabled, a JSON file with some anonymous statistics
|
||||
//! will be sent to a bot once a week.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_derive::FromSql;
|
||||
@@ -16,7 +16,7 @@ use serde::Serialize;
|
||||
use crate::chat::{self, ChatId, MuteDuration};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_VERSION_STR};
|
||||
use crate::contact::{Contact, ContactId, Origin, import_vcard, mark_contact_id_as_verified};
|
||||
use crate::contact::{Contact, ContactId, Origin, import_vcard};
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, load_self_public_key};
|
||||
use crate::log::LogExt;
|
||||
@@ -42,7 +42,8 @@ struct Statistics {
|
||||
/// Size of the public key in bytes (encoded in binary, not base64).
|
||||
pubkey_size: usize,
|
||||
stats_id: String,
|
||||
is_chatmail: bool,
|
||||
/// Whether all transports are chatmail relays, `None` if not known for all of them.
|
||||
is_chatmail: Option<bool>,
|
||||
contact_stats: Vec<ContactStat>,
|
||||
message_stats: BTreeMap<Chattype, MessageStats>,
|
||||
securejoin_sources: SecurejoinSources,
|
||||
@@ -52,21 +53,12 @@ struct Statistics {
|
||||
sending_disabled_timestamps: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, PartialEq)]
|
||||
enum VerifiedStatus {
|
||||
Direct,
|
||||
Transitive,
|
||||
TransitiveViaBot,
|
||||
Opportunistic,
|
||||
Unencrypted,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ContactStat {
|
||||
#[serde(skip_serializing)]
|
||||
id: ContactId,
|
||||
|
||||
verified: VerifiedStatus,
|
||||
encrypted: bool,
|
||||
|
||||
// If one of the boolean properties is false,
|
||||
// we leave them away.
|
||||
@@ -79,9 +71,6 @@ struct ContactStat {
|
||||
|
||||
last_seen: u64,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
transitive_chain: Option<u32>,
|
||||
|
||||
/// Whether the contact was established after stats-sending was enabled
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
new: bool,
|
||||
@@ -93,8 +82,7 @@ fn is_false(b: &bool) -> bool {
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
struct MessageStats {
|
||||
verified: u32,
|
||||
unverified_encrypted: u32,
|
||||
encrypted: u32,
|
||||
unencrypted: u32,
|
||||
only_to_self: u32,
|
||||
}
|
||||
@@ -159,14 +147,10 @@ struct JoinedInvite {
|
||||
/// Whether the contact already existed before.
|
||||
/// If this is false, then a contact was newly created.
|
||||
already_existed: bool,
|
||||
/// If a contact already existed,
|
||||
/// this tells us whether the contact was verified already.
|
||||
already_verified: bool,
|
||||
/// The type of the invite:
|
||||
/// "contact" for single chat invites that setup a verified contact,
|
||||
/// "contact" for single chat invites,
|
||||
/// "group" for invites that invite to a group,
|
||||
/// "broadcast" for invites that invite to a broadcast channel.
|
||||
/// The invite also performs the contact verification 'along the way'.
|
||||
typ: String,
|
||||
}
|
||||
|
||||
@@ -367,16 +351,22 @@ async fn get_stats(context: &Context) -> Result<String> {
|
||||
let sending_disabled_timestamps =
|
||||
get_timestamps(context, "stats_sending_disabled_events").await?;
|
||||
|
||||
let number_of_transports = context.count_transports().await?;
|
||||
let metadata = context.metadata.read().await;
|
||||
let is_chatmail = (!metadata.is_empty() && metadata.len() == number_of_transports)
|
||||
.then(|| metadata.values().all(|m| m.supports_push));
|
||||
drop(metadata);
|
||||
|
||||
let stats = Statistics {
|
||||
core_version: DC_VERSION_STR.to_string(),
|
||||
number_of_transports: context.count_transports().await?,
|
||||
number_of_transports,
|
||||
key_create_timestamps,
|
||||
number_of_keys,
|
||||
key_version: self_public_key.primary_key.version().into(),
|
||||
key_algorithm: format!("{:?}", self_public_key.algorithm()),
|
||||
pubkey_size: DcKey::to_bytes(&self_public_key).len(),
|
||||
stats_id: stats_id(context).await?,
|
||||
is_chatmail: context.is_chatmail().await?,
|
||||
is_chatmail,
|
||||
contact_stats: get_contact_stats(context, last_old_contact).await?,
|
||||
message_stats: get_message_stats(context).await?,
|
||||
securejoin_sources: get_securejoin_source_stats(context).await?,
|
||||
@@ -423,7 +413,6 @@ async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
|
||||
.await?
|
||||
.first()
|
||||
.context("Statistics bot vCard does not contain a contact")?;
|
||||
mark_contact_id_as_verified(context, contact_id, Some(ContactId::SELF)).await?;
|
||||
|
||||
let chat_id = if let Some(res) = ChatId::lookup_by_contact(context, contact_id).await? {
|
||||
// Already exists, no need to create.
|
||||
@@ -438,84 +427,30 @@ async fn get_stats_chat_id(context: &Context) -> Result<ChatId, anyhow::Error> {
|
||||
}
|
||||
|
||||
async fn get_contact_stats(context: &Context, last_old_contact: u32) -> Result<Vec<ContactStat>> {
|
||||
let mut verified_by_map: BTreeMap<ContactId, ContactId> = BTreeMap::new();
|
||||
let mut bot_ids: BTreeSet<ContactId> = BTreeSet::new();
|
||||
|
||||
let mut contacts = context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT id, fingerprint<>'', verifier, last_seen, is_bot FROM contacts c
|
||||
"SELECT id, fingerprint<>'', last_seen, is_bot FROM contacts c
|
||||
WHERE id>9 AND origin>? AND addr<>?",
|
||||
(Origin::Hidden, STATISTICS_BOT_EMAIL),
|
||||
|row| {
|
||||
let id = row.get(0)?;
|
||||
let is_encrypted: bool = row.get(1)?;
|
||||
let verifier: ContactId = row.get(2)?;
|
||||
let last_seen: u64 = row.get(3)?;
|
||||
let bot: bool = row.get(4)?;
|
||||
|
||||
let verified = match (is_encrypted, verifier) {
|
||||
(true, ContactId::SELF) => VerifiedStatus::Direct,
|
||||
(true, ContactId::UNDEFINED) => VerifiedStatus::Opportunistic,
|
||||
(true, _) => VerifiedStatus::Transitive, // TransitiveViaBot will be filled later
|
||||
(false, _) => VerifiedStatus::Unencrypted,
|
||||
};
|
||||
|
||||
if verifier != ContactId::UNDEFINED {
|
||||
verified_by_map.insert(id, verifier);
|
||||
}
|
||||
|
||||
if bot {
|
||||
bot_ids.insert(id);
|
||||
}
|
||||
let encrypted: bool = row.get(1)?;
|
||||
let last_seen: u64 = row.get(2)?;
|
||||
let bot: bool = row.get(3)?;
|
||||
|
||||
Ok(ContactStat {
|
||||
id,
|
||||
verified,
|
||||
encrypted,
|
||||
bot,
|
||||
single_chat: false, // will be filled later
|
||||
last_seen,
|
||||
transitive_chain: None, // will be filled later
|
||||
new: id.to_u32() > last_old_contact,
|
||||
})
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Fill TransitiveViaBot and transitive_chain
|
||||
for contact in &mut contacts {
|
||||
if contact.verified == VerifiedStatus::Transitive {
|
||||
let mut transitive_chain: u32 = 0;
|
||||
let mut has_bot = false;
|
||||
let mut current_verifier_id = contact.id;
|
||||
|
||||
while current_verifier_id != ContactId::SELF && transitive_chain < 100 {
|
||||
current_verifier_id = match verified_by_map.get(¤t_verifier_id) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
// The chain ends here, probably because some verification was done
|
||||
// before we started recording verifiers.
|
||||
// It's unclear how long the chain really is.
|
||||
transitive_chain = 0;
|
||||
break;
|
||||
}
|
||||
};
|
||||
if bot_ids.contains(¤t_verifier_id) {
|
||||
has_bot = true;
|
||||
}
|
||||
transitive_chain = transitive_chain.saturating_add(1);
|
||||
}
|
||||
|
||||
if transitive_chain > 0 {
|
||||
contact.transitive_chain = Some(transitive_chain);
|
||||
}
|
||||
|
||||
if has_bot {
|
||||
contact.verified = VerifiedStatus::TransitiveViaBot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill single_chat
|
||||
for contact in &mut contacts {
|
||||
let single_chat = context
|
||||
@@ -537,18 +472,16 @@ async fn get_message_stats(context: &Context) -> Result<BTreeMap<Chattype, Messa
|
||||
let mut map: BTreeMap<Chattype, MessageStats> = context
|
||||
.sql
|
||||
.query_map_collect(
|
||||
"SELECT chattype, verified, unverified_encrypted, unencrypted, only_to_self
|
||||
"SELECT chattype, unverified_encrypted, unencrypted, only_to_self
|
||||
FROM stats_msgs",
|
||||
(),
|
||||
|row| {
|
||||
let chattype: Chattype = row.get(0)?;
|
||||
let verified: u32 = row.get(1)?;
|
||||
let unverified_encrypted: u32 = row.get(2)?;
|
||||
let unencrypted: u32 = row.get(3)?;
|
||||
let only_to_self: u32 = row.get(4)?;
|
||||
let encrypted: u32 = row.get(1)?;
|
||||
let unencrypted: u32 = row.get(2)?;
|
||||
let only_to_self: u32 = row.get(3)?;
|
||||
let message_stats = MessageStats {
|
||||
verified,
|
||||
unverified_encrypted,
|
||||
encrypted,
|
||||
unencrypted,
|
||||
only_to_self,
|
||||
};
|
||||
@@ -620,32 +553,6 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|
||||
(),
|
||||
)?;
|
||||
|
||||
// This table will hold all verified chats,
|
||||
// i.e. all chats that only contain verified contacts.
|
||||
t.execute(
|
||||
"CREATE TEMP TABLE temp.verified_chats (
|
||||
id INTEGER PRIMARY KEY
|
||||
) STRICT",
|
||||
(),
|
||||
)?;
|
||||
|
||||
// Verified chats are chats that are not empty,
|
||||
// and do not contain any unverified contacts
|
||||
t.execute(
|
||||
"INSERT INTO temp.verified_chats
|
||||
SELECT id FROM chats
|
||||
WHERE id>9
|
||||
AND id NOT IN (SELECT id FROM temp.empty_chats)
|
||||
AND NOT EXISTS(
|
||||
SELECT *
|
||||
FROM contacts, chats_contacts
|
||||
WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=chats.id
|
||||
AND contacts.id>9
|
||||
AND contacts.verifier=0
|
||||
)",
|
||||
(),
|
||||
)?;
|
||||
|
||||
// This table will hold all single chats.
|
||||
t.execute(
|
||||
"CREATE TEMP TABLE temp.chat_with_correct_type (
|
||||
@@ -672,21 +579,11 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|
||||
.to_string();
|
||||
let params = (last_counted_msg_id, ContactId::SELF, stats_bot_chat_id);
|
||||
|
||||
let verified: u32 = t.query_row(
|
||||
&format!(
|
||||
"SELECT COUNT(*) FROM msgs
|
||||
WHERE chat_id IN temp.verified_chats
|
||||
AND {general_requirements}"
|
||||
),
|
||||
params,
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
let unverified_encrypted: u32 = t.query_row(
|
||||
let encrypted: u32 = t.query_row(
|
||||
&format!(
|
||||
// (param GLOB '*\nc=1*' OR param GLOB 'c=1*') matches all messages that are end-to-end encrypted
|
||||
"SELECT COUNT(*) FROM msgs
|
||||
WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
|
||||
WHERE chat_id NOT IN temp.empty_chats
|
||||
AND (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
|
||||
AND {general_requirements}"
|
||||
),
|
||||
@@ -697,7 +594,7 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|
||||
let unencrypted: u32 = t.query_row(
|
||||
&format!(
|
||||
"SELECT COUNT(*) FROM msgs
|
||||
WHERE chat_id NOT IN temp.verified_chats AND chat_id NOT IN temp.empty_chats
|
||||
WHERE chat_id NOT IN temp.empty_chats
|
||||
AND NOT (param GLOB '*\nc=1*' OR param GLOB 'c=1*')
|
||||
AND {general_requirements}"
|
||||
),
|
||||
@@ -715,7 +612,6 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
|
||||
t.execute("DROP TABLE temp.verified_chats", ())?;
|
||||
t.execute("DROP TABLE temp.empty_chats", ())?;
|
||||
t.execute("DROP TABLE temp.chat_with_correct_type", ())?;
|
||||
|
||||
@@ -726,18 +622,11 @@ async fn update_message_stats_inner(context: &Context, chattype: Chattype) -> Re
|
||||
)?;
|
||||
t.execute(
|
||||
"UPDATE stats_msgs SET
|
||||
verified=verified+?,
|
||||
unverified_encrypted=unverified_encrypted+?,
|
||||
unencrypted=unencrypted+?,
|
||||
only_to_self=only_to_self+?
|
||||
WHERE chattype=?",
|
||||
(
|
||||
verified,
|
||||
unverified_encrypted,
|
||||
unencrypted,
|
||||
only_to_self,
|
||||
chattype,
|
||||
),
|
||||
(encrypted, unencrypted, only_to_self, chattype),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
@@ -851,9 +740,6 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
|
||||
// then its origin is UnhandledSecurejoinQrScan.
|
||||
let already_existed = contact.origin > Origin::UnhandledSecurejoinQrScan;
|
||||
|
||||
// Check whether the contact was verified already before the QR scan.
|
||||
let already_verified = contact.is_verified(context).await?;
|
||||
|
||||
let typ = match invite {
|
||||
QrInvite::Contact { .. } => "contact",
|
||||
QrInvite::Group { .. } => "group",
|
||||
@@ -863,9 +749,10 @@ pub(crate) async fn count_securejoin_invite(context: &Context, invite: &QrInvite
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
// already_verified is unused but NOT NULL without a default.
|
||||
"INSERT INTO stats_securejoin_invites (already_existed, already_verified, type)
|
||||
VALUES (?, ?, ?)",
|
||||
(already_existed, already_verified, typ),
|
||||
VALUES (?, 0, ?)",
|
||||
(already_existed, typ),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -876,16 +763,14 @@ async fn get_securejoin_invite_stats(context: &Context) -> Result<Vec<JoinedInvi
|
||||
context
|
||||
.sql
|
||||
.query_map_vec(
|
||||
"SELECT already_existed, already_verified, type FROM stats_securejoin_invites",
|
||||
"SELECT already_existed, type FROM stats_securejoin_invites",
|
||||
(),
|
||||
|row| {
|
||||
let already_existed: bool = row.get(0)?;
|
||||
let already_verified: bool = row.get(1)?;
|
||||
let typ: String = row.get(2)?;
|
||||
let typ: String = row.get(1)?;
|
||||
|
||||
Ok(JoinedInvite {
|
||||
already_existed,
|
||||
already_verified,
|
||||
typ,
|
||||
})
|
||||
},
|
||||
|
||||
+33
-33
@@ -4,11 +4,13 @@ use super::*;
|
||||
use crate::chat::{
|
||||
Chat, create_broadcast, create_group, create_group_unencrypted, get_chat_contacts,
|
||||
};
|
||||
use crate::imap::ServerMetadata;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::qr::check_qr;
|
||||
use crate::securejoin::{get_securejoin_qr, join_securejoin, join_securejoin_with_ux_info};
|
||||
use crate::test_utils::{TestContext, TestContextManager, get_chat_msg};
|
||||
use crate::tools::SystemTime;
|
||||
use crate::transport::add_pseudo_transport;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::{Number, Value};
|
||||
|
||||
@@ -30,9 +32,6 @@ async fn test_maybe_send_stats() -> Result<()> {
|
||||
assert!(chat.is_encrypted(alice).await?);
|
||||
let contacts = get_chat_contacts(alice, chat_id).await?;
|
||||
assert_eq!(contacts.len(), 1);
|
||||
let contact = Contact::get_by_id(alice, contacts[0]).await?;
|
||||
assert!(contact.is_verified(alice).await?);
|
||||
|
||||
let msg = get_chat_msg(alice, chat_id, 1, 2).await;
|
||||
assert_eq!(msg.get_filename().unwrap(), "statistics.txt");
|
||||
|
||||
@@ -113,10 +112,9 @@ async fn test_stats_one_contact() -> Result<()> {
|
||||
contact_info.get("direct_chat").unwrap(),
|
||||
&serde_json::Value::Bool(true)
|
||||
);
|
||||
assert!(contact_info.get("transitive_chain").is_none(),);
|
||||
assert_eq!(
|
||||
contact_info.get("verified").unwrap(),
|
||||
&serde_json::Value::String("Opportunistic".to_string())
|
||||
contact_info.get("encrypted").unwrap(),
|
||||
&serde_json::Value::Bool(true)
|
||||
);
|
||||
assert_eq!(
|
||||
contact_info.get("new").unwrap(),
|
||||
@@ -168,25 +166,16 @@ async fn test_message_stats() -> Result<()> {
|
||||
check_stats(&update_get_stats(alice).await, &expected);
|
||||
|
||||
alice.send_text(encrypted_chat.id, "foo").await;
|
||||
expected
|
||||
.get_mut(&Chattype::Single)
|
||||
.unwrap()
|
||||
.unverified_encrypted += 1;
|
||||
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
|
||||
check_stats(&update_get_stats(alice).await, &expected);
|
||||
|
||||
alice.send_text(encrypted_chat.id, "foo").await;
|
||||
expected
|
||||
.get_mut(&Chattype::Single)
|
||||
.unwrap()
|
||||
.unverified_encrypted += 1;
|
||||
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
|
||||
check_stats(&update_get_stats(alice).await, &expected);
|
||||
|
||||
let group = alice.create_group_with_members("Pizza", &[bob]).await;
|
||||
alice.send_text(group, "foo").await;
|
||||
expected
|
||||
.get_mut(&Chattype::Group)
|
||||
.unwrap()
|
||||
.unverified_encrypted += 1;
|
||||
expected.get_mut(&Chattype::Group).unwrap().encrypted += 1;
|
||||
check_stats(&update_get_stats(alice).await, &expected);
|
||||
|
||||
tcm.execute_securejoin(alice, bob).await;
|
||||
@@ -208,7 +197,7 @@ async fn test_message_stats() -> Result<()> {
|
||||
|
||||
let group = alice.create_group_with_members("Pizza 2", &[bob]).await;
|
||||
alice.send_text(group, "foo").await;
|
||||
expected.get_mut(&Chattype::Group).unwrap().verified += 1;
|
||||
expected.get_mut(&Chattype::Group).unwrap().encrypted += 1;
|
||||
check_stats(&update_get_stats(alice).await, &expected);
|
||||
|
||||
let empty_broadcast = create_broadcast(alice, "Channel".to_string()).await?;
|
||||
@@ -245,13 +234,13 @@ async fn test_message_stats() -> Result<()> {
|
||||
|
||||
SystemTime::shift(Duration::from_secs(8 * 24 * 3600));
|
||||
tcm.send_recv(alice, bob, "Hi").await;
|
||||
expected.get_mut(&Chattype::Single).unwrap().verified += 1;
|
||||
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
|
||||
update_message_stats(alice).await?;
|
||||
update_message_stats(alice).await?;
|
||||
tcm.send_recv(alice, bob, "Hi").await;
|
||||
expected.get_mut(&Chattype::Single).unwrap().verified += 1;
|
||||
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
|
||||
tcm.send_recv(alice, bob, "Hi").await;
|
||||
expected.get_mut(&Chattype::Single).unwrap().verified += 1;
|
||||
expected.get_mut(&Chattype::Single).unwrap().encrypted += 1;
|
||||
|
||||
check_stats(&send_and_read_stats(alice).await, &expected);
|
||||
alice.assert_warn("Missing securejoin source").await;
|
||||
@@ -428,7 +417,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
|
||||
tcm.exec_securejoin_qr(alice, bob, &qr).await;
|
||||
expected.push(JoinedInvite {
|
||||
already_existed: false,
|
||||
already_verified: false,
|
||||
typ: "contact".to_string(),
|
||||
});
|
||||
check_stats(alice, &expected).await;
|
||||
@@ -437,7 +425,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
|
||||
tcm.exec_securejoin_qr(alice, bob, &qr).await;
|
||||
expected.push(JoinedInvite {
|
||||
already_existed: true,
|
||||
already_verified: true,
|
||||
typ: "contact".to_string(),
|
||||
});
|
||||
check_stats(alice, &expected).await;
|
||||
@@ -449,7 +436,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
|
||||
tcm.exec_securejoin_qr(alice, bob, &qr).await;
|
||||
expected.push(JoinedInvite {
|
||||
already_existed: true,
|
||||
already_verified: true,
|
||||
typ: "group".to_string(),
|
||||
});
|
||||
check_stats(alice, &expected).await;
|
||||
@@ -463,7 +449,6 @@ async fn test_stats_securejoin_invites() -> Result<()> {
|
||||
tcm.exec_securejoin_qr(alice, bob, &qr).await;
|
||||
expected.push(JoinedInvite {
|
||||
already_existed: true,
|
||||
already_verified: false,
|
||||
typ: "group".to_string(),
|
||||
});
|
||||
check_stats(alice, &expected).await;
|
||||
@@ -480,18 +465,33 @@ async fn test_stats_securejoin_invites() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stats_is_chatmail() -> Result<()> {
|
||||
async fn is_chatmail(context: &TestContext) -> Result<Value> {
|
||||
let stats: Value = serde_json::from_str(&get_stats(context).await?)?;
|
||||
Ok(stats.get("is_chatmail").unwrap().clone())
|
||||
}
|
||||
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.set_config_bool(Config::StatsSending, true).await?;
|
||||
assert!(is_chatmail(alice).await?.is_null());
|
||||
|
||||
let r = get_stats(alice).await?;
|
||||
let r: serde_json::Value = serde_json::from_str(&r)?;
|
||||
assert_eq!(r.get("is_chatmail").unwrap().as_bool().unwrap(), false);
|
||||
alice.metadata.write().await.insert(
|
||||
0,
|
||||
ServerMetadata {
|
||||
supports_push: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(is_chatmail(alice).await?, Value::Bool(true));
|
||||
|
||||
alice.set_config_bool(Config::IsChatmail, true).await?;
|
||||
add_pseudo_transport(alice, "alice@example.net").await?;
|
||||
assert!(is_chatmail(alice).await?.is_null());
|
||||
|
||||
let r = get_stats(alice).await?;
|
||||
let r: serde_json::Value = serde_json::from_str(&r)?;
|
||||
assert_eq!(r.get("is_chatmail").unwrap().as_bool().unwrap(), true);
|
||||
alice
|
||||
.metadata
|
||||
.write()
|
||||
.await
|
||||
.insert(1, ServerMetadata::default());
|
||||
assert_eq!(is_chatmail(alice).await?, Value::Bool(false));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user