mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
Compare commits
2 Commits
link2xt/im
...
iequidoo/4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a697d4316 | ||
|
|
dfdedf073d |
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "16"
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
|
||||
4
.github/workflows/jsonrpc.yml
vendored
4
.github/workflows/jsonrpc.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 16.x
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: npm install
|
||||
|
||||
4
.github/workflows/node-docs.yml
vendored
4
.github/workflows/node-docs.yml
vendored
@@ -16,10 +16,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 18.x
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
node-version: 16.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
working-directory: node
|
||||
|
||||
12
.github/workflows/node-package.yml
vendored
12
.github/workflows/node-package.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "16"
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
@@ -66,8 +66,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
|
||||
container: debian:10
|
||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||
container: ubuntu:18.04
|
||||
steps:
|
||||
# Working directory is owned by 1001:1001 by default.
|
||||
# Change it to our user.
|
||||
@@ -78,7 +80,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "16"
|
||||
- run: apt-get update
|
||||
|
||||
# Python is needed for node-gyp
|
||||
@@ -141,7 +143,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "16"
|
||||
- name: Get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
|
||||
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
node-version: "16"
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
|
||||
161
CHANGELOG.md
161
CHANGELOG.md
@@ -1,156 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.131.3] - 2023-11-15
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update async-imap to 0.9.4 which does not ignore EOF on FETCH.
|
||||
- Reset gossiped timestamp on securejoin.
|
||||
- sync: Ignore unknown sync items to provide forward compatibility and avoid creating empty message bubbles.
|
||||
- sync: Skip sync when chat name is set to the current one.
|
||||
- Return connectivity HTML with an error when IO is stopped.
|
||||
|
||||
## [1.131.2] - 2023-11-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: add `Account.get_chat_by_contact()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Do not post "... verified" messages on QR scan success.
|
||||
- Never drop better message from `apply_group_changes()`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Assign MDNs to the trash chat early to prevent received MDNs from creating or unblocking 1:1 chats.
|
||||
- Allow to securejoin groups when 1:1 chat with the inviter is a contact request.
|
||||
- Add "setup changed" message for verified key before the message.
|
||||
- Ignore special chats when calculating similar chats.
|
||||
|
||||
## [1.131.1] - 2023-11-13
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not skip actual message parts when group change messages are inserted.
|
||||
|
||||
## [1.131.0] - 2023-11-13
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync chat contacts across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Sync creating broadcast lists across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Sync Chat::name across devices ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
- Multi-device broadcast lists ([#4953](https://github.com/deltachat/deltachat-core-rust/pull/4953)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Encode chat name in the `List-ID` header to avoid SMTPUTF8 errors.
|
||||
- Ignore errors from generating sync messages.
|
||||
- `Context::execute_sync_items`: Ignore all errors ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
|
||||
- Allow to send unverified securejoin messages to protected chats ([#4982](https://github.com/deltachat/deltachat-core-rust/pull/4982)).
|
||||
|
||||
## [1.130.0] - 2023-11-10
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Emit JoinerProgress(1000) event when Bob verifies Alice.
|
||||
- JSON-RPC: add `ContactObject.is_profile_verified` property.
|
||||
- Hide `ChatId::get_for_contact()` from public API.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add secondary verified key.
|
||||
- Add info messages about implicitly added members.
|
||||
- Treat reset state as encryption not preferred.
|
||||
- Grow sleep durations on errors in Imap::fake_idle() ([#4424](https://github.com/deltachat/deltachat-core-rust/pull/4424)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Mark 1:1 chat as protected when joining a group.
|
||||
- Raise lower auto-download limit to 160k.
|
||||
- Remove `Reporting-UA` from read receipts.
|
||||
- Do not apply group changes to special chats. Avoid adding members to the trash chat.
|
||||
- imap: make `UidGrouper` robust against duplicate UIDs.
|
||||
- Do not return hidden chat from `dc_get_chat_id_by_contact_id`.
|
||||
- Smtp_loop(): Don't grow timeout if interrupted early ([#4833](https://github.com/deltachat/deltachat-core-rust/pull/4833)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: Do not FETCH right after `scan_folders()`.
|
||||
- deltachat-rpc-client: Use `itertools` instead of `Lock` for thread-safe request ID generation.
|
||||
|
||||
### Tests
|
||||
|
||||
- Remove unused `--liveconfig` option.
|
||||
- Test chatlist can load for corrupted chats ([#4979](https://github.com/deltachat/deltachat-core-rust/pull/4979)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update provider-db ([#4949](https://github.com/deltachat/deltachat-core-rust/pull/4949)).
|
||||
|
||||
## [1.129.1] - 2023-11-06
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update tokio-imap to fix Outlook STATUS parsing bug.
|
||||
- deltachat-rpc-client: Add the Lock around request ID.
|
||||
- `apply_group_changes`: Don't implicitly delete members locally, add absent ones instead ([#4934](https://github.com/deltachat/deltachat-core-rust/pull/4934)).
|
||||
- Partial messages do not change group state ([#4900](https://github.com/deltachat/deltachat-core-rust/pull/4900)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Group chats device synchronisation.
|
||||
|
||||
## [1.129.0] - 2023-11-06
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add JSON-RPC `get_chat_id_by_contact_id` API ([#4918](https://github.com/deltachat/deltachat-core-rust/pull/4918)).
|
||||
- [**breaking**] Remove deprecated `get_verifier_addr`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Sync chat `Blocked` state, chat visibility, chat mute duration and contact blocked status across devices ([#4817](https://github.com/deltachat/deltachat-core-rust/pull/4817)).
|
||||
- Add 'group created instructions' as info message ([#4916](https://github.com/deltachat/deltachat-core-rust/pull/4916)).
|
||||
- Add hardcoded fallback DNS cache.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Switch to `EncryptionPreference::Mutual` on a receipt of encrypted+signed message ([#4707](https://github.com/deltachat/deltachat-core-rust/pull/4707)).
|
||||
- imap: Check UIDNEXT with a STATUS command before going IDLE.
|
||||
- Allow to change verified key via "member added" message.
|
||||
- json-rpc: Return verifier even if the contact is not "verified" (Autocrypt key does not equal Secure-Join key).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Refine `Contact::get_verifier_id` and `Contact::is_verified` documentation ([#4922](https://github.com/deltachat/deltachat-core-rust/pull/4922)).
|
||||
- Contact profile view should not use `dc_contact_is_verified()`.
|
||||
- Remove documentation for non-existing `dc_accounts_new` `os_name` param.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove unused or useless code paths in Secure-Join ([#4897](https://github.com/deltachat/deltachat-core-rust/pull/4897)).
|
||||
- Improve error handling in Secure-Join code.
|
||||
- Add hostname to "no DNS resolution results" error message.
|
||||
- Accept `&str` instead of `Option<String>` in idle().
|
||||
|
||||
## [1.128.0] - 2023-11-02
|
||||
|
||||
### Build system
|
||||
- [**breaking**] Upgrade nodejs version to 18 ([#4903](https://github.com/deltachat/deltachat-core-rust/pull/4903)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.wait_for_incoming_msg_event()`.
|
||||
- Decrease ratelimit for .testrun.org subdomains.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail securejoin due to unrelated pending bobstate ([#4896](https://github.com/deltachat/deltachat-core-rust/pull/4896)).
|
||||
- Allow other verified group recipients to be unverified, only check the sender verification.
|
||||
- Remove not working attempt to recover from verified key changes.
|
||||
|
||||
## [1.127.2] - 2023-10-29
|
||||
|
||||
### API-Changes
|
||||
@@ -187,8 +36,6 @@
|
||||
- [**breaking**] Remove unused `dc_set_chat_protection()`
|
||||
- Hide `DcSecretKey` trait from the API.
|
||||
- Verified 1:1 chats ([#4315](https://github.com/deltachat/deltachat-core-rust/pull/4315)). Disabled by default, enable with `verified_one_on_one_chats` config.
|
||||
- Add api `chat::Chat::is_protection_broken`
|
||||
- Add `dc_chat_is_protection_broken()` C API.
|
||||
|
||||
### CI
|
||||
|
||||
@@ -3202,11 +3049,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.127.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.126.1...v1.127.0
|
||||
[1.127.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.0...v1.127.1
|
||||
[1.127.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.1...v1.127.2
|
||||
[1.128.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.127.2...v1.128.0
|
||||
[1.129.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.128.0...v1.129.0
|
||||
[1.129.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.0...v1.129.1
|
||||
[1.130.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.129.1...v1.130.0
|
||||
[1.131.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.130.0...v1.131.0
|
||||
[1.131.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.0...v1.131.1
|
||||
[1.131.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.1...v1.131.2
|
||||
[1.131.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.2...v1.131.3
|
||||
|
||||
455
Cargo.lock
generated
455
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.131.3"
|
||||
version = "1.127.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.70"
|
||||
@@ -72,7 +72,7 @@ quick-xml = "0.31"
|
||||
rand = "0.8"
|
||||
regex = "1.9"
|
||||
reqwest = { version = "0.11.20", features = ["json"] }
|
||||
rusqlite = { version = "0.30", features = ["sqlcipher"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1.0"
|
||||
@@ -90,7 +90,7 @@ tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
toml = "0.8"
|
||||
toml = "0.7"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.131.3"
|
||||
version = "1.127.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -2941,6 +2941,7 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
||||
* use dc_accounts_remove_account().
|
||||
*
|
||||
* @memberof dc_accounts_t
|
||||
* @param os_name
|
||||
* @param dir The directory to create the context-databases in.
|
||||
* If the directory does not exist,
|
||||
* dc_accounts_new() will try to create it.
|
||||
@@ -3731,22 +3732,8 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
||||
|
||||
/**
|
||||
* Check if a chat is protected.
|
||||
*
|
||||
* End-to-end encryption is guaranteed in protected chats
|
||||
* and only verified contacts
|
||||
* as determined by dc_contact_is_verified()
|
||||
* can be added to protected chats.
|
||||
*
|
||||
* Protected chats are created using dc_create_group_chat()
|
||||
* by setting the 'protect' parameter to 1.
|
||||
* 1:1 chats become protected or unprotected automatically
|
||||
* if `verified_one_on_one_chats` setting is enabled.
|
||||
*
|
||||
* UI should display a green checkmark
|
||||
* in the chat title,
|
||||
* in the chatlist item
|
||||
* and in the chat profile
|
||||
* if chat protection is enabled.
|
||||
* Protected chats contain only verified members and encryption is always enabled.
|
||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
@@ -4580,18 +4567,15 @@ int dc_msg_has_html (dc_msg_t* msg);
|
||||
* if they are larger than the limit set by the dc_set_config()-option `download_limit`.
|
||||
*
|
||||
* The function returns one of:
|
||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||
* and should be rendered as usual.
|
||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||
* In addition to the usual message rendering,
|
||||
* the UI shall show a download button that calls dc_download_full_msg()
|
||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
*
|
||||
* - @ref DC_DOWNLOAD_UNDECIPHERABLE - The message does not need any futher download action.
|
||||
* It was fully downloaded, but we failed to decrypt it.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
* - @ref DC_DOWNLOAD_DONE - The message does not need any further download action
|
||||
* and should be rendered as usual.
|
||||
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
|
||||
* In addition to the usual message rendering,
|
||||
* the UI shall show a download button that calls dc_download_full_msg()
|
||||
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_download_full_msg() and is still in progress.
|
||||
* If the download fails or succeeds,
|
||||
* the event @ref DC_EVENT_MSGS_CHANGED is emitted.
|
||||
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_download_full_msg() again.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
@@ -5049,16 +5033,10 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact
|
||||
* can be added to verified chats,
|
||||
* i.e. has a verified key
|
||||
* and Autocrypt key matches the verified key.
|
||||
* Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||
* and if the key has not changed since this verification.
|
||||
*
|
||||
* If contact is verified
|
||||
* UI should display green checkmark after the contact name
|
||||
* in contact list items,
|
||||
* in chat member list items
|
||||
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
|
||||
* The UI may draw a checkbox or something like that beside verified contacts.
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -5068,19 +5046,32 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
|
||||
int dc_contact_is_verified (dc_contact_t* contact);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID that verified a contact.
|
||||
* Return the address that verified a contact
|
||||
*
|
||||
* If the function returns non-zero result,
|
||||
* display green checkmark in the profile and "Introduced by ..." line
|
||||
* with the name and address of the contact
|
||||
* formatted by dc_contact_get_name_n_addr.
|
||||
* The UI may use this in addition to a checkmark showing the verification status.
|
||||
* In case of verification chains,
|
||||
* the last contact in the chain is shown.
|
||||
* This is because of privacy reasons, but also as it would not help the user
|
||||
* to see a unknown name here - where one can mostly always ask the shown name
|
||||
* as it is directly known.
|
||||
*
|
||||
* If this function returns a verifier,
|
||||
* this does not necessarily mean
|
||||
* you can add the contact to verified chats.
|
||||
* Use dc_contact_is_verified() to check
|
||||
* if a contact can be added to a verified chat instead.
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||
* information or the contact is not verified.
|
||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Return the `ContactId` that verified a contact
|
||||
*
|
||||
* The UI may use this in addition to a checkmark showing the verification status
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
@@ -6243,7 +6234,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
* @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)
|
||||
* 1000=vg-member-added/vc-contact-confirm received
|
||||
*/
|
||||
#define DC_EVENT_SECUREJOIN_JOINER_PROGRESS 2061
|
||||
|
||||
@@ -6436,27 +6426,22 @@ void dc_event_unref(dc_event_t* event);
|
||||
/**
|
||||
* Download not needed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_DONE 0
|
||||
#define DC_DOWNLOAD_DONE 0
|
||||
|
||||
/**
|
||||
* Download available, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_AVAILABLE 10
|
||||
#define DC_DOWNLOAD_AVAILABLE 10
|
||||
|
||||
/**
|
||||
* Download failed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_FAILURE 20
|
||||
|
||||
/**
|
||||
* Download not needed, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_UNDECIPHERABLE 30
|
||||
#define DC_DOWNLOAD_FAILURE 20
|
||||
|
||||
/**
|
||||
* Download in progress, see dc_msg_get_download_state() for details.
|
||||
*/
|
||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||
#define DC_DOWNLOAD_IN_PROGRESS 1000
|
||||
|
||||
|
||||
|
||||
@@ -7275,11 +7260,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in info messages.
|
||||
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||
|
||||
/// "Others will only see this group after you sent a first message."
|
||||
///
|
||||
/// Used as the first info messages in newly created groups.
|
||||
#define DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE 172
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -489,7 +489,7 @@ pub unsafe extern "C" fn dc_start_io(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
return;
|
||||
}
|
||||
let ctx = &mut *context;
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.start_io())
|
||||
}
|
||||
@@ -4119,6 +4119,23 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l
|
||||
.unwrap_or_default() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_addr(
|
||||
contact: *mut dc_contact_t,
|
||||
) -> *mut libc::c_char {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_get_verifier_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
let ctx = &*ffi_contact.context;
|
||||
block_on(ffi_contact.contact.get_verifier_addr(ctx))
|
||||
.context("failed to get verifier for contact")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
|
||||
if contact.is_null() {
|
||||
@@ -4929,8 +4946,8 @@ pub unsafe extern "C" fn dc_accounts_start_io(accounts: *mut dc_accounts_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
block_on(async move { accounts.write().await.start_io().await });
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.start_io().await });
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.131.3"
|
||||
version = "1.127.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -221,13 +221,13 @@ impl CommandApi {
|
||||
|
||||
/// Starts background tasks for all accounts.
|
||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.write().await.start_io().await;
|
||||
self.accounts.read().await.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stops background tasks for all accounts.
|
||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||
self.accounts.write().await.stop_io().await;
|
||||
self.accounts.read().await.stop_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ impl CommandApi {
|
||||
|
||||
/// Starts background tasks for a single account.
|
||||
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -383,7 +383,7 @@ impl CommandApi {
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let mut ctx = self.get_context(account_id).await?;
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
let result = ctx.configure().await;
|
||||
if result.is_err() {
|
||||
@@ -1391,19 +1391,6 @@ impl CommandApi {
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
|
||||
/// Returns the [`ChatId`] for the 1:1 chat with `contact_id` if it exists.
|
||||
///
|
||||
/// If it does not exist, `None` is returned.
|
||||
async fn get_chat_id_by_contact_id(
|
||||
&self,
|
||||
account_id: u32,
|
||||
contact_id: u32,
|
||||
) -> Result<Option<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::lookup_by_contact(&ctx, ContactId::new(contact_id)).await?;
|
||||
Ok(chat_id.map(|id| id.to_u32()))
|
||||
}
|
||||
|
||||
/// Returns all message IDs of the given types in a chat.
|
||||
/// Typically used to show a gallery.
|
||||
///
|
||||
|
||||
@@ -18,17 +18,6 @@ use super::contact::ContactObject;
|
||||
pub struct FullChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
@@ -139,17 +128,6 @@ impl FullChat {
|
||||
pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
|
||||
/// True if the chat is protected.
|
||||
///
|
||||
/// UI should display a green checkmark
|
||||
/// in the chat title,
|
||||
/// in the chat profile title and
|
||||
/// in the chatlist item
|
||||
/// if chat protection is enabled.
|
||||
/// UI should also display a green checkmark
|
||||
/// in the contact profile
|
||||
/// if 1:1 chat with this contact exists and is protected.
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
|
||||
@@ -19,30 +19,11 @@ pub struct ContactObject {
|
||||
profile_image: Option<String>, // BLOBS
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
|
||||
/// True if the contact can be added to verified groups.
|
||||
///
|
||||
/// If this is true
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items,
|
||||
/// in chat member list items
|
||||
/// and in profiles if no chat with the contact exist.
|
||||
is_verified: bool,
|
||||
|
||||
/// True if the contact profile title should have a green checkmark.
|
||||
///
|
||||
/// This indicates whether 1:1 chat has a green checkmark
|
||||
/// or will have a green checkmark if created.
|
||||
is_profile_verified: bool,
|
||||
|
||||
/// The ID of the contact that verified this contact.
|
||||
///
|
||||
/// If this is present,
|
||||
/// display a green checkmark and "Introduced by ..."
|
||||
/// string followed by the verifier contact name and address
|
||||
/// in the contact profile.
|
||||
/// the address that verified this contact
|
||||
verifier_addr: Option<String>,
|
||||
/// the id of the contact that verified this contact
|
||||
verifier_id: Option<u32>,
|
||||
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
@@ -58,12 +39,18 @@ impl ContactObject {
|
||||
None => None,
|
||||
};
|
||||
let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
|
||||
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||
|
||||
let verifier_id = contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.map(|contact_id| contact_id.to_u32());
|
||||
let (verifier_addr, verifier_id) = if is_verified {
|
||||
(
|
||||
contact.get_verifier_addr(context).await?,
|
||||
contact
|
||||
.get_verifier_id(context)
|
||||
.await?
|
||||
.map(|contact_id| contact_id.to_u32()),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
Ok(ContactObject {
|
||||
address: contact.get_addr().to_owned(),
|
||||
@@ -77,7 +64,7 @@ impl ContactObject {
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_verified,
|
||||
is_profile_verified,
|
||||
verifier_addr,
|
||||
verifier_id,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
|
||||
@@ -28,7 +28,7 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.layer(Extension(state.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
state.accounts.write().await.start_io().await;
|
||||
state.accounts.read().await.start_io().await;
|
||||
});
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/ws": "^7.2.4",
|
||||
"c8": "^7.10.0",
|
||||
"chai": "^4.3.4",
|
||||
@@ -16,6 +17,7 @@
|
||||
"esbuild": "^0.17.9",
|
||||
"http-server": "^14.1.1",
|
||||
"mocha": "^9.1.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.6.2",
|
||||
"typedoc": "^0.23.2",
|
||||
@@ -53,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.131.3"
|
||||
"version": "1.127.2"
|
||||
}
|
||||
|
||||
@@ -79,9 +79,6 @@ describe("basic tests", () => {
|
||||
accountId = await dc.rpc.addAccount();
|
||||
});
|
||||
it("should block and unblock contact", async function () {
|
||||
// Cannot send sync messages to self as we do not have a self address.
|
||||
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
|
||||
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId,
|
||||
"example@delta.chat",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { tmpdir } from "os";
|
||||
import { join, resolve } from "path";
|
||||
import { mkdtemp, rm } from "fs/promises";
|
||||
import { spawn, exec } from "child_process";
|
||||
import fetch from "node-fetch";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
export type RpcServerHandle = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.131.3"
|
||||
version = "1.127.2"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -11,7 +11,7 @@ deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.20"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.30"
|
||||
rusqlite = "0.29"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ enum ExitResult {
|
||||
|
||||
async fn handle_cmd(
|
||||
line: &str,
|
||||
mut ctx: Context,
|
||||
ctx: Context,
|
||||
selected_chat: &mut ChatId,
|
||||
) -> Result<ExitResult, Error> {
|
||||
let mut args = line.splitn(2, ' ');
|
||||
|
||||
@@ -25,7 +25,7 @@ $ pip install .
|
||||
## Testing
|
||||
|
||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||
2. Run `PATH="../target/debug:$PATH" tox`.
|
||||
|
||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from warnings import warn
|
||||
|
||||
from ._utils import AttrDict
|
||||
from .chat import Chat
|
||||
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId
|
||||
from .const import ChatlistFlag, ContactFlag, SpecialContactId
|
||||
from .contact import Contact
|
||||
from .message import Message
|
||||
|
||||
@@ -111,20 +111,6 @@ class Account:
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]:
|
||||
"""Return 1:1 chat for a contact if it exists."""
|
||||
if isinstance(contact, Contact):
|
||||
assert contact.account == self
|
||||
contact_id = contact.id
|
||||
elif isinstance(contact, int):
|
||||
contact_id = contact
|
||||
else:
|
||||
raise ValueError(f"{contact!r} is not a contact")
|
||||
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
|
||||
if chat_id:
|
||||
return Chat(self, chat_id)
|
||||
return None
|
||||
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
@@ -264,13 +250,6 @@ class Account:
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
"""Wait for incoming message event and return it."""
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
|
||||
@@ -21,9 +21,7 @@ class ACFactory:
|
||||
self.deltachat = deltachat
|
||||
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
account = self.deltachat.add_account()
|
||||
account.set_config("verified_one_on_one_chats", "1")
|
||||
return account
|
||||
return self.deltachat.add_account()
|
||||
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
@@ -61,16 +59,6 @@ class ACFactory:
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
|
||||
def resetup_account(self, ac: Account) -> Account:
|
||||
"""Resetup account from scratch, losing the encryption key."""
|
||||
ac.stop_io()
|
||||
ac_clone = self.get_unconfigured_account()
|
||||
for i in ["addr", "mail_pw"]:
|
||||
ac_clone.set_config(i, ac.get_config(i))
|
||||
ac.remove()
|
||||
ac_clone.configure()
|
||||
return ac_clone
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -6,7 +5,7 @@ import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Iterator, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
@@ -24,7 +23,7 @@ class Rpc:
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.process: subprocess.Popen
|
||||
self.id_iterator: Iterator[int]
|
||||
self.id: int
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
@@ -55,7 +54,7 @@ class Rpc:
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id_iterator = itertools.count(start=1)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
@@ -132,9 +131,7 @@ class Rpc:
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
event = event["event"]
|
||||
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||
queue.put(event)
|
||||
queue.put(event["event"])
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
@@ -146,12 +143,14 @@ class Rpc:
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
def method(*args) -> Any:
|
||||
request_id = next(self.id_iterator)
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": args,
|
||||
"id": request_id,
|
||||
"id": self.id,
|
||||
}
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
import logging
|
||||
|
||||
from deltachat_rpc_client import Chat, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_securejoin(acfactory):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = snapshot.chat
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# Chat stays being a contact request.
|
||||
assert bob_chat_alice.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
def test_qr_readreceipt(acfactory) -> None:
|
||||
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("Bob and Charlie setup contact with Alice")
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
charlie.secure_join(qr_code)
|
||||
|
||||
for joiner in [bob, charlie]:
|
||||
while True:
|
||||
event = joiner.wait_for_event()
|
||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
charlie_addr = charlie.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.add_contact(alice_contact_charlie)
|
||||
|
||||
# Promote a group.
|
||||
group.send_message(text="Hello")
|
||||
|
||||
logging.info("Bob and Charlie receive a group")
|
||||
|
||||
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
|
||||
bob_message = bob.get_message_by_id(bob_msg_id)
|
||||
bob_snapshot = bob_message.get_snapshot()
|
||||
assert bob_snapshot.text == "Hello"
|
||||
|
||||
# Charlie receives the same "Hello" message as Bob.
|
||||
charlie.wait_for_incoming_msg_event()
|
||||
|
||||
logging.info("Bob sends a message to the group")
|
||||
|
||||
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
|
||||
|
||||
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
|
||||
charlie_message = charlie.get_message_by_id(charlie_msg_id)
|
||||
charlie_snapshot = charlie_message.get_snapshot()
|
||||
assert charlie_snapshot.text == "Hi from Bob!"
|
||||
|
||||
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
logging.info("Charlie reads Bob's message")
|
||||
charlie_message.mark_seen()
|
||||
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
|
||||
break
|
||||
|
||||
# Receiving a read receipt from Charlie
|
||||
# should not unblock hidden chat with Charlie for Bob.
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
|
||||
def test_verified_group_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
|
||||
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
|
||||
assert ac1_contact.get_snapshot().is_verified
|
||||
|
||||
# ac2 can write messages to the group.
|
||||
snapshot.chat.send_text("Works again!")
|
||||
|
||||
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_chat_messages = snapshot.chat.get_messages()
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
|
||||
|
||||
def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("ac1 creates verified group")
|
||||
chat = ac1.create_group("Verified group", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
# ac1 has ac2 directly verified.
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
|
||||
|
||||
logging.info("ac3 joins verified group")
|
||||
ac3_chat = ac3.secure_join(qr_code)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac2 logs in on a new device")
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = ac3.wait_for_event()
|
||||
if event.kind == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
break
|
||||
|
||||
logging.info("ac3 sends a message to the group")
|
||||
assert len(ac3_chat.get_contacts()) == 3
|
||||
ac3_chat.send_text("Hi!")
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("Received message %s", snapshot.text)
|
||||
assert snapshot.text == "Hi!"
|
||||
|
||||
ac1.wait_for_incoming_msg_event() # Hi!
|
||||
|
||||
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac3_chat.remove_contact(ac3_contact_ac2)
|
||||
ac3_chat.add_contact(ac3_contact_ac2)
|
||||
|
||||
msg_id = ac2.wait_for_incoming_msg_event().msg_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "removed" in snapshot.text
|
||||
|
||||
event = ac2.wait_for_incoming_msg_event()
|
||||
msg_id = event.msg_id
|
||||
chat_id = event.chat_id
|
||||
message = ac2.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
logging.info("ac2 got event message: %s", snapshot.text)
|
||||
assert "added" in snapshot.text
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert "added" in snapshot.text
|
||||
|
||||
chat = Chat(ac2, chat_id)
|
||||
chat.send_text("Works again!")
|
||||
|
||||
msg_id = ac3.wait_for_incoming_msg_event().msg_id
|
||||
message = ac3.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Works again!"
|
||||
|
||||
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
|
||||
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
|
||||
assert ac1_contact_ac2_snapshot.is_verified
|
||||
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
|
||||
|
||||
# ac2 is now verified by ac3 for ac1
|
||||
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
|
||||
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
|
||||
@@ -377,3 +377,15 @@ def test_provider_info(rpc) -> None:
|
||||
rpc.set_config(account_id, "socks5_enabled", "1")
|
||||
provider_info = rpc.get_provider_info(account_id, "github.com")
|
||||
assert provider_info is None
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||
return
|
||||
|
||||
@@ -30,4 +30,4 @@ commands =
|
||||
[pytest]
|
||||
timeout = 300
|
||||
log_cli = true
|
||||
log_level = debug
|
||||
log_level = info
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.131.3"
|
||||
version = "1.127.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -27,16 +27,18 @@ skip = [
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hashbrown", version = "<0.14.0" },
|
||||
{ name = "indexmap", version = "<2.0.0" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.3.5" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "ring", version = "0.16.20" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
|
||||
@@ -40,7 +40,7 @@ npm install deltachat-node
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Nodejs >= `v18.0.0`
|
||||
- Nodejs >= `v16.0.0`
|
||||
- rustup (optional if you can't use the prebuilds)
|
||||
|
||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
||||
@@ -113,8 +113,8 @@ Then, in the `deltachat-desktop` repository, run:
|
||||
deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
|
||||
|
||||
```
|
||||
$ fnm install 19 --arch x64
|
||||
$ fnm use 19
|
||||
$ fnm install 17 --arch x64
|
||||
$ fnm use 17
|
||||
$ node -p process.arch
|
||||
# result should be x64
|
||||
$ rustup target add x86_64-apple-darwin
|
||||
@@ -127,8 +127,8 @@ $ npm run test
|
||||
If your node and electron are already build for arm64 you can also try building for arm:
|
||||
|
||||
```
|
||||
$ fnm install 18 --arch arm64
|
||||
$ fnm use 18
|
||||
$ fnm install 16 --arch arm64
|
||||
$ fnm use 16
|
||||
$ node -p process.arch
|
||||
# result should be arm64
|
||||
$ npm_config_arch=arm64 npm run build
|
||||
|
||||
@@ -28,7 +28,6 @@ module.exports = {
|
||||
DC_DOWNLOAD_DONE: 0,
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
@@ -240,7 +239,6 @@ module.exports = {
|
||||
DC_STR_MSGGRPNAME: 15,
|
||||
DC_STR_MSGLOCATIONDISABLED: 65,
|
||||
DC_STR_MSGLOCATIONENABLED: 64,
|
||||
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE: 172,
|
||||
DC_STR_NOMESSAGES: 1,
|
||||
DC_STR_NOT_CONNECTED: 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,
|
||||
|
||||
@@ -28,7 +28,6 @@ export enum C {
|
||||
DC_DOWNLOAD_DONE = 0,
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
@@ -240,7 +239,6 @@ export enum C {
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NEW_GROUP_SEND_FIRST_MESSAGE = 172,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { statSync } from 'fs'
|
||||
import { Context } from '../dist/context'
|
||||
import fetch from 'node-fetch'
|
||||
chai.use(chaiAsPromised)
|
||||
chai.config.truncateThreshold = 0 // Do not truncate assertion errors.
|
||||
|
||||
@@ -667,9 +668,9 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
|
||||
const lot = chatList.getSummary(0)
|
||||
strictEqual(lot.getId(), 0, 'lot has no id')
|
||||
strictEqual(lot.getState(), C.DC_STATE_IN_NOTICED, 'correct state')
|
||||
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
|
||||
|
||||
const text = 'Others will only see this group after you sent a first message.'
|
||||
const text = 'No messages.'
|
||||
context.createGroupChat('groupchat1111')
|
||||
chatList = context.getChatList(0, 'groupchat1111', null)
|
||||
strictEqual(
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
},
|
||||
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
|
||||
"typedocOptions": {
|
||||
"mode": "file",
|
||||
"out": "docs",
|
||||
"excludePrivate": true,
|
||||
"excludeNotExported": true,
|
||||
"defaultCategory": "index",
|
||||
"includeVersion": true,
|
||||
"entryPoints": ["lib/index.ts"]
|
||||
"includeVersion": true
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ E.g via <https://git-scm.com/download/win>
|
||||
|
||||
## install node
|
||||
|
||||
Download and install `v18` from <https://nodejs.org/en/>
|
||||
Download and install `v16` from <https://nodejs.org/en/>
|
||||
|
||||
## install rust
|
||||
|
||||
|
||||
26
package.json
26
package.json
@@ -2,25 +2,28 @@
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.6.1"
|
||||
"node-gyp-build": "^4.1.0"
|
||||
},
|
||||
"description": "node.js bindings for deltachat-core",
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/node": "^16.11.26",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"hallmark": "^2.0.0",
|
||||
"mocha": "^8.2.1",
|
||||
"node-gyp": "^10.0.0",
|
||||
"prebuildify": "^5.0.1",
|
||||
"prebuildify-ci": "^1.0.5",
|
||||
"prettier": "^3.0.3",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2"
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-gyp": "^9.0.0",
|
||||
"opn-cli": "^5.0.0",
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"node/scripts/*",
|
||||
@@ -46,15 +49,16 @@
|
||||
"build:core:rust": "node node/scripts/rebuild-core.js",
|
||||
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
|
||||
"download-prebuilds": "prebuildify-ci download",
|
||||
"hallmark": "hallmark --fix",
|
||||
"install": "node node/scripts/install.js",
|
||||
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
|
||||
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
|
||||
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
|
||||
"prebuildify": "cd node && prebuildify -t 18.0.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.131.3"
|
||||
"version": "1.127.2"
|
||||
}
|
||||
|
||||
@@ -571,11 +571,11 @@ class Account:
|
||||
return ScannedQRCode(lot)
|
||||
|
||||
def qr_setup_contact(self, qr):
|
||||
"""setup contact and return a `Chat` instance after contact is established.
|
||||
|
||||
This function triggers a network protocol in the background between
|
||||
the emitter of the QR code and this account.
|
||||
"""setup contact and return a Chat after contact is established.
|
||||
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned.
|
||||
:param qr: valid "setup contact" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
assert self.check_qr(qr).is_ask_verifycontact()
|
||||
@@ -585,11 +585,11 @@ class Account:
|
||||
return Chat(self, chat_id)
|
||||
|
||||
def qr_join_chat(self, qr):
|
||||
"""return a `Chat` instance for which the securejoin network
|
||||
protocol has been started.
|
||||
"""join a chat group through a QR code.
|
||||
|
||||
This function triggers a network protocol in the background between
|
||||
the emitter of the QR code and this account.
|
||||
Note that this function may block for a long time as messages are exchanged
|
||||
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
|
||||
is returned which is the chat that we just joined.
|
||||
|
||||
:param qr: valid "join-group" QR code (all other QR codes will result in an exception)
|
||||
"""
|
||||
|
||||
@@ -23,6 +23,12 @@ from .events import FFIEventLogger, FFIEventTracker
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
group.addoption(
|
||||
"--liveconfig",
|
||||
action="store",
|
||||
default=None,
|
||||
help="a file with >=2 lines where each line contains NAME=VALUE config settings for one account",
|
||||
)
|
||||
group.addoption(
|
||||
"--chatmail",
|
||||
action="store",
|
||||
@@ -52,6 +58,8 @@ def pytest_addoption(parser):
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
cfg = config.getoption("--liveconfig")
|
||||
|
||||
cfg = config.getoption("--chatmail")
|
||||
if not cfg:
|
||||
cfg = os.getenv("CHATMAIL_DOMAIN")
|
||||
@@ -125,10 +133,13 @@ def pytest_report_header(config, startdir):
|
||||
),
|
||||
]
|
||||
|
||||
chatmail_opt = config.getoption("--chatmail")
|
||||
if chatmail_opt:
|
||||
summary.append(f"Chatmail account provider: {chatmail_opt}")
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "?" in cfg:
|
||||
url, token = cfg.split("?", 1)
|
||||
summary.append(f"Liveconfig provider: {url}?<token omitted>")
|
||||
else:
|
||||
summary.append(f"Liveconfig file: {cfg}")
|
||||
return summary
|
||||
|
||||
|
||||
@@ -153,7 +164,11 @@ class TestProcess:
|
||||
def get_liveconfig_producer(self):
|
||||
"""provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can re-use already known live configs.
|
||||
Depending on the --liveconfig option this comes from
|
||||
a file with a line specifying each accounts config
|
||||
or a --chatmail domain.
|
||||
"""
|
||||
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
|
||||
chatmail_opt = self.pytestconfig.getoption("--chatmail")
|
||||
if chatmail_opt:
|
||||
# Use a chatmail instance.
|
||||
@@ -163,8 +178,7 @@ class TestProcess:
|
||||
try:
|
||||
yield self._configlist[index]
|
||||
except IndexError:
|
||||
part = "".join(random.choices("2345789acdefghjkmnpqrstuvwxyz", k=6))
|
||||
username = f"ci-{part}"
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
password = f"{username}${username}"
|
||||
addr = f"{username}@{domain}"
|
||||
config = {"addr": addr, "mail_pw": password}
|
||||
@@ -172,9 +186,20 @@ class TestProcess:
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
|
||||
elif liveconfig_opt:
|
||||
# Read a list of accounts from file.
|
||||
for line in open(liveconfig_opt):
|
||||
if line.strip() and not line.strip().startswith("#"):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self._configlist.append(d)
|
||||
|
||||
yield from iter(self._configlist)
|
||||
else:
|
||||
pytest.skip(
|
||||
"specify CHATMAIL_DOMAIN or --chatmail to provide live accounts",
|
||||
"specify --liveconfig, CHATMAIL_DOMAIN or --chatmail to provide live accounts",
|
||||
)
|
||||
|
||||
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
|
||||
@@ -521,7 +546,6 @@ class ACFactory:
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
configdict.setdefault("sync_msgs", False)
|
||||
ac.update_config(configdict)
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
return ac
|
||||
|
||||
@@ -140,10 +140,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_system_message()
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
assert any(
|
||||
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
for m in msg.chat.get_messages()
|
||||
)
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
@@ -584,7 +580,6 @@ def test_use_new_verified_group_after_going_online(acfactory, tmp_path, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
|
||||
chat2 = msg_in.chat
|
||||
assert chat2.is_protected()
|
||||
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
lp.sec("ac2_offl: sending message")
|
||||
msg_out = chat2.send_text("hello")
|
||||
@@ -652,15 +647,6 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert msg_in.is_system_message()
|
||||
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
|
||||
# We need to consume one event that has data2=0
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
assert ev.data2 == 0
|
||||
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
|
||||
@@ -367,7 +367,7 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(300000))}, "some test data")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
@@ -489,7 +489,7 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert len(messages) == 1
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
@@ -1445,7 +1445,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
message, then processes a partially downloaded message.
|
||||
- As a result, Bob does not see a reaction
|
||||
"""
|
||||
download_limit = 300000
|
||||
download_limit = 32768
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_addr = ac1.get_config("addr")
|
||||
chat = ac1.create_chat(ac2)
|
||||
@@ -1699,59 +1699,6 @@ def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
|
||||
assert not ch.is_protected()
|
||||
|
||||
|
||||
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory, lp):
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
lp.sec("ac3: verify with ac2")
|
||||
ac3.qr_setup_contact(ac2.get_setup_contact_qr())
|
||||
ac2._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# in order for ac2 to have pending bobstate with a verified group
|
||||
# we first create a fully joined verified group, and then start
|
||||
# joining a second time but interrupt it, to create pending bob state
|
||||
|
||||
lp.sec("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group_chat("ac1-shutoff group", verified=True)
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
# ensure ac1 can write and ac2 receives messages in verified chat
|
||||
ch1.send_text("ac1 says hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "ac1 says hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
ac2.qr_join_chat(ch1.get_join_qr())
|
||||
ac1.shutdown()
|
||||
lp.sec("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
|
||||
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
|
||||
assert ac3.get_contact(ac2).is_verified()
|
||||
assert ac2.get_contact(ac3).is_verified()
|
||||
|
||||
lp.sec("ac3: create a verified group VG with ac2")
|
||||
vg = ac3.create_group_chat("ac3-created", [ac2], verified=True)
|
||||
|
||||
# ensure ac2 receives message in VG
|
||||
vg.send_text("hello")
|
||||
while 1:
|
||||
msg = ac2.wait_next_incoming_message()
|
||||
if msg.text == "hello":
|
||||
assert msg.chat.is_protected()
|
||||
break
|
||||
|
||||
lp.sec("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
ac4.qr_join_chat(vg.get_join_qr())
|
||||
ac3._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
while 1:
|
||||
ev = ac2._evtracker.get()
|
||||
if "added by unrelated SecureJoin" in str(ev):
|
||||
return
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory, lp):
|
||||
"""Regression test for a bug intoduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
@@ -1957,7 +1904,7 @@ def test_system_group_msg_from_blocked_user(acfactory, lp):
|
||||
chat_on_ac2.send_text("This will arrive")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "This will arrive"
|
||||
message_texts = [m.text for m in chat_on_ac1.get_messages() if not m.is_system_message()]
|
||||
message_texts = [m.text for m in chat_on_ac1.get_messages()]
|
||||
assert len(message_texts) == 2
|
||||
assert "First group message" in message_texts
|
||||
assert "This will arrive" in message_texts
|
||||
|
||||
@@ -26,7 +26,7 @@ def wait_msgs_changed(account, msgs_list):
|
||||
break
|
||||
else:
|
||||
account.log(f"waiting mismatch data1={data1} data2={data2}")
|
||||
return ev.data2
|
||||
return ev.data1, ev.data2
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
@@ -68,17 +68,14 @@ class TestOnlineInCreation:
|
||||
assert prepared_original.is_out_preparing()
|
||||
wait_msgs_changed(ac1, [(chat.id, prepared_original.id)])
|
||||
|
||||
lp.sec("create a new group")
|
||||
chat2 = ac1.create_group_chat("newgroup")
|
||||
wait_msgs_changed(ac1, [(0, 0)])
|
||||
|
||||
lp.sec("add a contact to new group")
|
||||
chat2.add_contact(ac2)
|
||||
wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
|
||||
lp.sec("forward the message while still in creation")
|
||||
chat2 = ac1.create_group_chat("newgroup")
|
||||
chat2.add_contact(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why not chat id?
|
||||
ac1.forward_messages([prepared_original], chat2)
|
||||
forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
# XXX there might be two EVENT_MSGS_CHANGED and only one of them
|
||||
# is the one caused by forwarding
|
||||
_, forwarded_id = wait_msgs_changed(ac1, [(chat2.id, None)])
|
||||
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||
assert forwarded_msg.is_out_preparing()
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ from datetime import datetime, timedelta, timezone
|
||||
import pytest
|
||||
|
||||
import deltachat as dc
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
@@ -800,7 +802,6 @@ class TestOfflineChat:
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
@@ -810,6 +811,12 @@ class TestOfflineChat:
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
lp.sec("check message count of all messages")
|
||||
assert len(chat.get_messages()) == 4
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
|
||||
assert len(sysmessages) == 3
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref,
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-11-15
|
||||
2023-10-29
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=18f714cf73d0bdfb8b013fa344494ab80c92b477
|
||||
REV=3c8f7e846c915a183dc44536fb5480d1f25d7c42
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
@@ -258,8 +258,8 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
|
||||
pub async fn start_io(&mut self) {
|
||||
for account in self.accounts.values_mut() {
|
||||
pub async fn start_io(&self) {
|
||||
for account in self.accounts.values() {
|
||||
account.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
725
src/chat.rs
725
src/chat.rs
File diff suppressed because it is too large
Load Diff
@@ -761,40 +761,4 @@ mod tests {
|
||||
let summary = chats.get_summary(&t, 0, None).await.unwrap();
|
||||
assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_broken() {
|
||||
let t = TestContext::new_bob().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
|
||||
// obfuscated one chat
|
||||
t.sql
|
||||
.execute("UPDATE chats SET type=10 WHERE id=?", (chat_id1,))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// obfuscated chat can't be loaded
|
||||
assert!(Chat::load_from_db(&t, chat_id1).await.is_err());
|
||||
|
||||
// chatlist loads fine
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
|
||||
// only corrupted chat fails to create summary
|
||||
assert!(chats.get_summary(&t, 0, None).await.is_ok());
|
||||
assert!(chats.get_summary(&t, 1, None).await.is_ok());
|
||||
assert!(chats.get_summary(&t, 2, None).await.is_err());
|
||||
assert_eq!(chats.get_index_for_id(chat_id1).unwrap(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,10 +266,6 @@ pub enum Config {
|
||||
/// True if it is a bot account.
|
||||
Bot,
|
||||
|
||||
/// True when to skip initial start messages in groups.
|
||||
#[strum(props(default = "0"))]
|
||||
SkipStartMessages,
|
||||
|
||||
/// Whether we send a warning if the password is wrong (set to false when we send a warning
|
||||
/// because we do not want to send a second warning)
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -296,19 +292,13 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Whether to avoid using IMAP NOTIFY even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing prefetch without NOTIFY.
|
||||
#[strum(props(default = "0"))]
|
||||
DisableNotify,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
|
||||
#[strum(props(default = "1"))]
|
||||
#[strum(props(default = "0"))]
|
||||
SyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
|
||||
@@ -31,6 +31,7 @@ use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::stock_str;
|
||||
@@ -480,7 +481,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
ctx.set_config_bool(Config::FetchedExistingMsgs, false)
|
||||
.await?;
|
||||
ctx.scheduler.interrupt_inbox().await;
|
||||
ctx.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
|
||||
progress!(ctx, 940);
|
||||
update_device_chats_handle.await??;
|
||||
|
||||
133
src/contact.rs
133
src/contact.rs
@@ -19,7 +19,7 @@ use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{ChatId, ProtectionStatus};
|
||||
use crate::chat::ChatId;
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
@@ -32,7 +32,6 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, strip_rtlo_characters, time,
|
||||
EmailAddress,
|
||||
@@ -149,22 +148,6 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset gossip timestamp in all chats with this contact.
|
||||
pub(crate) async fn regossip_keys(&self, context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats
|
||||
SET gossiped_timestamp=0
|
||||
WHERE EXISTS (SELECT 1 FROM chats_contacts
|
||||
WHERE chats_contacts.chat_id=chats.id
|
||||
AND chats_contacts.contact_id=?)",
|
||||
(self,),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -476,12 +459,12 @@ impl Contact {
|
||||
|
||||
/// Block the given contact.
|
||||
pub async fn block(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_blocked(context, Sync, id, true).await
|
||||
set_block_contact(context, id, true).await
|
||||
}
|
||||
|
||||
/// Unblock the given contact.
|
||||
pub async fn unblock(context: &Context, id: ContactId) -> Result<()> {
|
||||
set_blocked(context, Sync, id, false).await
|
||||
set_block_contact(context, id, false).await
|
||||
}
|
||||
|
||||
/// Add a single contact as a result of an _explicit_ user action.
|
||||
@@ -511,7 +494,7 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
if blocked {
|
||||
set_blocked(context, Nosync, contact_id, false).await?;
|
||||
Contact::unblock(context, contact_id).await?;
|
||||
}
|
||||
|
||||
Ok(contact_id)
|
||||
@@ -540,24 +523,10 @@ impl Contact {
|
||||
///
|
||||
/// To validate an e-mail address independently of the contact database
|
||||
/// use `may_be_valid_addr()`.
|
||||
///
|
||||
/// Returns the contact ID of the contact belonging to the e-mail address or 0 if there is no
|
||||
/// contact that is or was introduced by an accepted contact.
|
||||
pub async fn lookup_id_by_addr(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
) -> Result<Option<ContactId>> {
|
||||
Self::lookup_id_by_addr_ex(context, addr, min_origin, Some(Blocked::Not)).await
|
||||
}
|
||||
|
||||
/// The same as `lookup_id_by_addr()`, but internal function. Currently also allows looking up
|
||||
/// not unblocked contacts.
|
||||
pub(crate) async fn lookup_id_by_addr_ex(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
min_origin: Origin,
|
||||
blocked: Option<Blocked>,
|
||||
) -> Result<Option<ContactId>> {
|
||||
if addr.is_empty() {
|
||||
bail!("lookup_id_by_addr: empty address");
|
||||
@@ -574,14 +543,8 @@ impl Contact {
|
||||
.query_get_value(
|
||||
"SELECT id FROM contacts \
|
||||
WHERE addr=?1 COLLATE NOCASE \
|
||||
AND id>?2 AND origin>=?3 AND (? OR blocked=?)",
|
||||
(
|
||||
&addr_normalized,
|
||||
ContactId::LAST_SPECIAL,
|
||||
min_origin as u32,
|
||||
blocked.is_none(),
|
||||
blocked.unwrap_or_default(),
|
||||
),
|
||||
AND id>?2 AND origin>=?3 AND blocked=0;",
|
||||
(&addr_normalized, ContactId::LAST_SPECIAL, min_origin as u32),
|
||||
)
|
||||
.await?;
|
||||
Ok(id)
|
||||
@@ -1267,20 +1230,10 @@ impl Contact {
|
||||
self.status.as_str()
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
/// and Autocrypt key matches the verified key.
|
||||
/// Check if a contact was verified. E.g. by a secure-join QR code scan
|
||||
/// and if the key has not changed since this verification.
|
||||
///
|
||||
/// If contact is verified
|
||||
/// UI should display green checkmark after the contact name
|
||||
/// in contact list items and
|
||||
/// in chat member list items.
|
||||
///
|
||||
/// In contact profile view, us this function only if there is no chat with the contact,
|
||||
/// otherwise use is_chat_protected().
|
||||
/// Use [Self::get_verifier_id] to display the verifier contact
|
||||
/// in the info section of the contact profile.
|
||||
/// The UI may draw a checkbox or something like that beside verified contacts.
|
||||
pub async fn is_verified(&self, context: &Context) -> Result<VerifiedStatus> {
|
||||
// We're always sort of secured-verified as we could verify the key on this device any time with the key
|
||||
// on this device
|
||||
@@ -1297,23 +1250,16 @@ impl Contact {
|
||||
Ok(VerifiedStatus::Unverified)
|
||||
}
|
||||
|
||||
/// Returns the `ContactId` that verified the contact.
|
||||
///
|
||||
/// If the function returns non-zero result,
|
||||
/// display green checkmark in the profile and "Introduced by ..." line
|
||||
/// with the name and address of the contact
|
||||
/// formatted by [Self::get_name_n_addr].
|
||||
///
|
||||
/// If this function returns a verifier,
|
||||
/// this does not necessarily mean
|
||||
/// you can add the contact to verified chats.
|
||||
/// Use [Self::is_verified] to check
|
||||
/// if a contact can be added to a verified chat instead.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = Peerstate::from_addr(context, self.get_addr())
|
||||
/// Returns the address that verified the contact.
|
||||
pub async fn get_verifier_addr(&self, context: &Context) -> Result<Option<String>> {
|
||||
Ok(Peerstate::from_addr(context, self.get_addr())
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))
|
||||
else {
|
||||
.and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned())))
|
||||
}
|
||||
|
||||
/// Returns the ContactId that verified the contact.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
@@ -1332,27 +1278,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the contact profile title should display a green checkmark.
|
||||
///
|
||||
/// This generally should be consistent with the 1:1 chat with the contact
|
||||
/// so 1:1 chat with the contact and the contact profile
|
||||
/// either both display the green checkmark or both don't display a green checkmark.
|
||||
///
|
||||
/// UI often knows beforehand if a chat exists and can also call
|
||||
/// `chat.is_protected()` (if there is a chat)
|
||||
/// or `contact.is_verified()` (if there is no chat) directly.
|
||||
/// This is often easier and also skips some database calls.
|
||||
pub async fn is_profile_verified(&self, context: &Context) -> Result<bool> {
|
||||
let contact_id = self.id;
|
||||
|
||||
if let Some(chat_id) = ChatId::lookup_by_contact(context, contact_id).await? {
|
||||
Ok(chat_id.is_protected(context).await? == ProtectionStatus::Protected)
|
||||
} else {
|
||||
// 1:1 chat does not exist.
|
||||
Ok(self.is_verified(context).await? == VerifiedStatus::BidirectVerified)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -1438,9 +1363,8 @@ fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
async fn set_block_contact(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
contact_id: ContactId,
|
||||
new_blocking: bool,
|
||||
) -> Result<()> {
|
||||
@@ -1449,6 +1373,7 @@ pub(crate) async fn set_blocked(
|
||||
"Can't block special contact {}",
|
||||
contact_id
|
||||
);
|
||||
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
|
||||
if contact.blocked != new_blocking {
|
||||
@@ -1490,23 +1415,9 @@ WHERE type=? AND id IN (
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(context, &contact.addr).await?
|
||||
{
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id.unblock(context).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
let action = match new_blocking {
|
||||
true => chat::SyncAction::Block,
|
||||
false => chat::SyncAction::Unblock,
|
||||
};
|
||||
context
|
||||
.add_sync_item(SyncData::AlterChat {
|
||||
id: chat::SyncId::ContactAddr(contact.addr.clone()),
|
||||
action,
|
||||
})
|
||||
.await?;
|
||||
context.send_sync_msg().await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -2799,6 +2710,7 @@ Hi."#;
|
||||
|
||||
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_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
// Receive a message from Bob to create a peerstate.
|
||||
@@ -2807,6 +2719,7 @@ Hi."#;
|
||||
alice.recv_msg(&sent_msg).await;
|
||||
|
||||
let contact = Contact::get_by_id(&alice, contact_id).await?;
|
||||
assert!(contact.get_verifier_addr(&alice).await?.is_none());
|
||||
assert!(contact.get_verifier_id(&alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::key::{load_self_public_key, DcKey as _};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::{self, MessageState, MsgId};
|
||||
use crate::quota::QuotaInfo;
|
||||
use crate::scheduler::SchedulerState;
|
||||
use crate::scheduler::{InterruptInfo, SchedulerState};
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::timesmearing::SmearedTimestamp;
|
||||
@@ -398,24 +398,11 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Starts the IO scheduler.
|
||||
pub async fn start_io(&mut self) {
|
||||
if !self.is_configured().await.unwrap_or_default() {
|
||||
pub async fn start_io(&self) {
|
||||
if let Ok(false) = self.is_configured().await {
|
||||
warn!(self, "can not start io on a context that is not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
if self
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.filter(|s| s.ends_with(".testrun.org"))
|
||||
.is_some()
|
||||
{
|
||||
let mut lock = self.ratelimit.write().await;
|
||||
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
|
||||
}
|
||||
}
|
||||
self.scheduler.start(self.clone()).await;
|
||||
}
|
||||
|
||||
@@ -437,7 +424,11 @@ impl Context {
|
||||
|
||||
pub(crate) async fn schedule_resync(&self) -> Result<()> {
|
||||
self.resync_request.store(true, Ordering::Relaxed);
|
||||
self.scheduler.interrupt_inbox().await;
|
||||
self.scheduler
|
||||
.interrupt_inbox(InterruptInfo {
|
||||
probe_network: false,
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -595,7 +586,6 @@ impl Context {
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let sync_msgs = self.get_config_int(Config::SyncMsgs).await?;
|
||||
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
|
||||
let disable_notify = self.get_config_bool(Config::DisableNotify).await?;
|
||||
|
||||
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
|
||||
|
||||
@@ -709,7 +699,6 @@ impl Context {
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("sync_msgs", sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("disable_notify", disable_notify.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
@@ -1313,7 +1302,6 @@ mod tests {
|
||||
"send_port",
|
||||
"send_security",
|
||||
"server_flags",
|
||||
"skip_start_messages",
|
||||
"smtp_certificate_checks",
|
||||
"socks5_host",
|
||||
"socks5_port",
|
||||
|
||||
@@ -12,16 +12,18 @@ use crate::context::Context;
|
||||
use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::time;
|
||||
use crate::{stock_str, EventType};
|
||||
|
||||
/// Download limits should not be used below `MIN_DOWNLOAD_LIMIT`.
|
||||
///
|
||||
/// For better UX, some messages as add-member, non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// should always be downloaded completely to handle them correctly,
|
||||
/// also in larger groups and if group and contact avatar are attached.
|
||||
/// Most of these cases are caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 163840;
|
||||
/// Some messages as non-delivery-reports (NDN) or read-receipts (MDN)
|
||||
/// need to be downloaded completely to handle them correctly,
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
@@ -92,7 +94,10 @@ impl MsgId {
|
||||
.sql
|
||||
.execute("INSERT INTO download (msg_id) VALUES (?)", (self,))
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
39
src/e2ee.rs
39
src/e2ee.rs
@@ -67,8 +67,13 @@ impl EncryptHelper {
|
||||
"peerstate for {:?} is {}", addr, peerstate.prefer_encrypt
|
||||
);
|
||||
match peerstate.prefer_encrypt {
|
||||
EncryptPreference::NoPreference | EncryptPreference::Reset => {}
|
||||
EncryptPreference::NoPreference => {}
|
||||
EncryptPreference::Mutual => prefer_encrypt_count += 1,
|
||||
EncryptPreference::Reset => {
|
||||
if !e2ee_guaranteed {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
None => {
|
||||
@@ -100,39 +105,16 @@ impl EncryptHelper {
|
||||
) -> Result<String> {
|
||||
let mut keyring: Vec<SignedPublicKey> = Vec::new();
|
||||
|
||||
let mut verifier_addresses: Vec<&str> = Vec::new();
|
||||
|
||||
for (peerstate, addr) in peerstates
|
||||
.iter()
|
||||
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
|
||||
.into_iter()
|
||||
.filter_map(|(state, addr)| state.map(|s| (s, addr)))
|
||||
{
|
||||
let key = peerstate
|
||||
.take_key(min_verified)
|
||||
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
}
|
||||
|
||||
// Encrypt to self.
|
||||
keyring.push(self.public_key.clone());
|
||||
|
||||
// Encrypt to secondary verified keys
|
||||
// if we also encrypt to the introducer ("verifier") of the key.
|
||||
if min_verified == PeerstateVerifiedStatus::BidirectVerified {
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let (Some(key), Some(verifier)) = (
|
||||
peerstate.secondary_verified_key.as_ref(),
|
||||
peerstate.secondary_verifier.as_deref(),
|
||||
) {
|
||||
if verifier_addresses.contains(&verifier) {
|
||||
keyring.push(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let raw_message = mail_to_encrypt.build().as_string().into_bytes();
|
||||
@@ -319,11 +301,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
vec![(Some(peerstate), addr)]
|
||||
}
|
||||
|
||||
@@ -250,7 +250,6 @@ pub enum EventType {
|
||||
/// 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)
|
||||
/// 1000=vg-member-added/vc-contact-confirm received
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
|
||||
188
src/imap.rs
188
src/imap.rs
@@ -35,6 +35,7 @@ use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
};
|
||||
use crate::scheduler::connectivity::ConnectivityStore;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
@@ -85,7 +86,7 @@ const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Imap {
|
||||
pub(crate) idle_interrupt_receiver: Receiver<()>,
|
||||
pub(crate) idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
config: ImapConfig,
|
||||
pub(crate) session: Option<Session>,
|
||||
login_failed_once: bool,
|
||||
@@ -194,7 +195,7 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
|
||||
|
||||
while let Some((next_rowid, next_uid, _)) =
|
||||
self.inner.next_if(|(_, next_uid, next_folder)| {
|
||||
next_folder == &folder && (*next_uid == end_uid + 1 || *next_uid == end_uid)
|
||||
next_folder == &folder && *next_uid == end_uid + 1
|
||||
})
|
||||
{
|
||||
end_uid = next_uid;
|
||||
@@ -227,7 +228,7 @@ impl Imap {
|
||||
socks5_config: Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
) -> Result<Self> {
|
||||
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
|
||||
bail!("Incomplete IMAP connection parameters");
|
||||
@@ -260,7 +261,7 @@ impl Imap {
|
||||
/// Creates new disconnected IMAP client using configured parameters.
|
||||
pub async fn new_configured(
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
) -> Result<Self> {
|
||||
if !context.is_configured().await? {
|
||||
bail!("IMAP Connect without configured params");
|
||||
@@ -448,22 +449,6 @@ impl Imap {
|
||||
self.session = None;
|
||||
}
|
||||
|
||||
/// Tries to setup NOTIFY.
|
||||
pub async fn setup_notify(&mut self, context: &Context) -> Result<()> {
|
||||
let session = self
|
||||
.session
|
||||
.as_mut()
|
||||
.context("no IMAP connection established")?;
|
||||
if session.can_notify() && !session.notify_set {
|
||||
let cmd = format!("NOTIFY SET (selected (Messagenew {PREFETCH_FLAGS} messageexpunge))");
|
||||
session.run_command_and_check_ok(cmd).await?;
|
||||
info!(context, "Enabled NOTIFY");
|
||||
session.notify_set = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// FETCH-MOVE-DELETE iteration.
|
||||
///
|
||||
/// Prefetches headers and downloads new message from the folder, moves messages away from the
|
||||
@@ -583,14 +568,9 @@ impl Imap {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Selects a folder and takes care of UIDVALIDITY changes.
|
||||
///
|
||||
/// When selecting a folder for the first time, sets the uid_next to the current
|
||||
/// Select a folder and take care of uidvalidity changes.
|
||||
/// Also, when selecting a folder for the first time, sets the uid_next to the current
|
||||
/// mailbox.uid_next so that no old emails are fetched.
|
||||
///
|
||||
/// Makes sure that UIDNEXT is known for `selected_mailbox`
|
||||
/// and errors out if UIDNEXT cannot be determined.
|
||||
///
|
||||
/// Returns Result<new_emails> (i.e. whether new emails arrived),
|
||||
/// if in doubt, returns new_emails=true so emails are fetched.
|
||||
pub(crate) async fn select_with_uidvalidity(
|
||||
@@ -611,37 +591,6 @@ impl Imap {
|
||||
let new_uid_validity = mailbox
|
||||
.uid_validity
|
||||
.with_context(|| format!("No UIDVALIDITY for folder {folder}"))?;
|
||||
let new_uid_next = if let Some(uid_next) = mailbox.uid_next {
|
||||
uid_next
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
|
||||
);
|
||||
|
||||
// RFC 3501 says STATUS command SHOULD NOT be used
|
||||
// on the currently selected mailbox because the same
|
||||
// information can be obtained by other means,
|
||||
// such as reading SELECT response.
|
||||
//
|
||||
// However, it also says that UIDNEXT is REQUIRED
|
||||
// in the SELECT response and if we are here,
|
||||
// it is actually not returned.
|
||||
//
|
||||
// In particular, Winmail Pro Mail Server 5.1.0616
|
||||
// never returns UIDNEXT in SELECT response,
|
||||
// but responds to "STATUS INBOX (UIDNEXT)" command.
|
||||
let status = session
|
||||
.inner
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
|
||||
|
||||
status
|
||||
.uid_next
|
||||
.with_context(|| format!("STATUS {folder} (UIDNEXT) did not return UIDNEXT"))?
|
||||
};
|
||||
mailbox.uid_next = Some(new_uid_next);
|
||||
|
||||
let old_uid_validity = get_uidvalidity(context, folder)
|
||||
.await
|
||||
@@ -657,16 +606,18 @@ impl Imap {
|
||||
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
|
||||
// new messages is only one command, just as a SELECT command)
|
||||
true
|
||||
} else {
|
||||
if new_uid_next < old_uid_next {
|
||||
} else if let Some(uid_next) = mailbox.uid_next {
|
||||
if uid_next < old_uid_next {
|
||||
warn!(
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uid_next(context, folder, uid_next).await?;
|
||||
context.schedule_resync().await?;
|
||||
}
|
||||
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
|
||||
uid_next != old_uid_next // If uid_next changed, there are new emails
|
||||
} else {
|
||||
true // We have no uid_next and if in doubt, return true
|
||||
};
|
||||
return Ok(new_emails);
|
||||
}
|
||||
@@ -676,6 +627,43 @@ impl Imap {
|
||||
|
||||
// ============== uid_validity has changed or is being set the first time. ==============
|
||||
|
||||
let new_uid_next = match mailbox.uid_next {
|
||||
Some(uid_next) => uid_next,
|
||||
None => {
|
||||
warn!(
|
||||
context,
|
||||
"SELECT response for IMAP folder {folder:?} has no UIDNEXT, fall back to STATUS command."
|
||||
);
|
||||
|
||||
// RFC 3501 says STATUS command SHOULD NOT be used
|
||||
// on the currently selected mailbox because the same
|
||||
// information can be obtained by other means,
|
||||
// such as reading SELECT response.
|
||||
//
|
||||
// However, it also says that UIDNEXT is REQUIRED
|
||||
// in the SELECT response and if we are here,
|
||||
// it is actually not returned.
|
||||
//
|
||||
// In particular, Winmail Pro Mail Server 5.1.0616
|
||||
// never returns UIDNEXT in SELECT response,
|
||||
// but responds to "SELECT INBOX (UIDNEXT)" command.
|
||||
let status = session
|
||||
.inner
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.context("STATUS (UIDNEXT) error for {folder:?}")?;
|
||||
|
||||
if let Some(uid_next) = status.uid_next {
|
||||
uid_next
|
||||
} else {
|
||||
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
|
||||
|
||||
// Set UIDNEXT to 1 as a last resort fallback.
|
||||
1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
||||
|
||||
@@ -736,9 +724,7 @@ impl Imap {
|
||||
.await
|
||||
.context("prefetch_existing_msgs")?
|
||||
} else {
|
||||
self.prefetch(context, old_uid_next)
|
||||
.await
|
||||
.context("prefetch")?
|
||||
self.prefetch(old_uid_next).await.context("prefetch")?
|
||||
};
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
@@ -881,28 +867,14 @@ impl Imap {
|
||||
uids_fetch_in_batch.push(uid);
|
||||
}
|
||||
|
||||
// Advance uid_next to the maximum of the largest known UID plus 1
|
||||
// and mailbox UIDNEXT.
|
||||
// Largest known UID is normally less than UIDNEXT,
|
||||
// but a message may have arrived between determining UIDNEXT
|
||||
// and executing the FETCH command.
|
||||
let mailbox_uid_next = self
|
||||
.session
|
||||
.as_ref()
|
||||
.context("No IMAP session")?
|
||||
.selected_mailbox
|
||||
.as_ref()
|
||||
.with_context(|| format!("Expected {folder:?} to be selected"))?
|
||||
.uid_next
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Expected UIDNEXT to be determined for {folder:?} by select_with_uidvalidity"
|
||||
)
|
||||
})?;
|
||||
let new_uid_next = max(
|
||||
max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0)) + 1,
|
||||
mailbox_uid_next,
|
||||
);
|
||||
// determine which uid_next to use to update to
|
||||
// receive_imf() returns an `Err` value only on recoverable errors, otherwise it just logs an error.
|
||||
// `largest_uid_processed` is the largest uid where receive_imf() did NOT return an error.
|
||||
|
||||
// So: Update the uid_next to the largest uid that did NOT recoverably fail. Not perfect because if there was
|
||||
// another message afterwards that succeeded, we will not retry. The upside is that we will not retry an infinite amount of times.
|
||||
let largest_uid_without_errors = max(largest_uid_fetched, largest_uid_skipped.unwrap_or(0));
|
||||
let new_uid_next = largest_uid_without_errors + 1;
|
||||
|
||||
if new_uid_next > old_uid_next {
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
@@ -1347,13 +1319,7 @@ impl Imap {
|
||||
|
||||
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
|
||||
/// in the order of ascending delivery time to the server (INTERNALDATE).
|
||||
async fn prefetch(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
uid_next: u32,
|
||||
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
self.setup_notify(context).await?;
|
||||
|
||||
async fn prefetch(&mut self, uid_next: u32) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
let session = self
|
||||
.session
|
||||
.as_mut()
|
||||
@@ -2313,7 +2279,10 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
|
||||
(message_id,),
|
||||
)
|
||||
.await?;
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2861,31 +2830,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uid_grouper() {
|
||||
// Input: sequence of (rowid: i64, uid: u32, target: String)
|
||||
// Output: sequence of (target: String, rowid_set: Vec<i64>, uid_set: String)
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(res, vec![("INBOX".to_string(), vec![1], "2".to_string())]);
|
||||
|
||||
let grouper = UidGrouper::from([(1, 2, "INBOX".to_string()), (2, 3, "INBOX".to_string())]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2], "2:3".to_string())]
|
||||
);
|
||||
|
||||
let grouper = UidGrouper::from([
|
||||
(1, 2, "INBOX".to_string()),
|
||||
(2, 2, "INBOX".to_string()),
|
||||
(3, 3, "INBOX".to_string()),
|
||||
]);
|
||||
let res: Vec<(String, Vec<i64>, String)> = grouper.into_iter().collect();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![("INBOX".to_string(), vec![1, 2, 3], "2:3".to_string())]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ pub(crate) struct Capabilities {
|
||||
/// <https://tools.ietf.org/html/rfc2177>
|
||||
pub can_idle: bool,
|
||||
|
||||
/// True if the server has NOTIFY capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc5465>
|
||||
pub can_notify: bool,
|
||||
|
||||
/// True if the server has MOVE capability as defined in
|
||||
/// <https://tools.ietf.org/html/rfc6851>
|
||||
pub can_move: bool,
|
||||
|
||||
@@ -56,7 +56,6 @@ async fn determine_capabilities(
|
||||
};
|
||||
let capabilities = Capabilities {
|
||||
can_idle: caps.has_str("IDLE"),
|
||||
can_notify: caps.has_str("NOTIFY"),
|
||||
can_move: caps.has_str("MOVE"),
|
||||
can_check_quota: caps.has_str("QUOTA"),
|
||||
can_condstore: caps.has_str("CONDSTORE"),
|
||||
|
||||
134
src/imap/idle.rs
134
src/imap/idle.rs
@@ -8,9 +8,9 @@ use futures_lite::FutureExt;
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::imap::{client::IMAP_TIMEOUT, get_uid_next, FolderMeaning};
|
||||
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
|
||||
use crate::log::LogExt;
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
|
||||
|
||||
@@ -18,43 +18,30 @@ impl Session {
|
||||
pub async fn idle(
|
||||
mut self,
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
folder: &str,
|
||||
) -> Result<Self> {
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
watch_folder: Option<String>,
|
||||
) -> Result<(Self, InterruptInfo)> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
if context.get_config_bool(Config::DisableIdle).await? {
|
||||
bail!("IMAP IDLE is disabled");
|
||||
}
|
||||
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
|
||||
let mut info = Default::default();
|
||||
|
||||
self.select_folder(context, watch_folder.as_deref()).await?;
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
return Ok(self);
|
||||
return Ok((self, info));
|
||||
}
|
||||
|
||||
// Despite checking for unsolicited EXISTS above,
|
||||
// we may have missed EXISTS if the message was
|
||||
// received when the folder was not selected.
|
||||
let status = self
|
||||
.status(folder, "(UIDNEXT)")
|
||||
.await
|
||||
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
|
||||
if let Some(uid_next) = status.uid_next {
|
||||
let expected_uid_next = get_uid_next(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("failed to get old UID NEXT for folder {folder}"))?;
|
||||
if uid_next > expected_uid_next {
|
||||
info!(
|
||||
context,
|
||||
"Skipping IDLE on {folder:?} because UIDNEXT {uid_next}>{expected_uid_next} indicates there are new messages."
|
||||
);
|
||||
return Ok(self);
|
||||
}
|
||||
} else {
|
||||
warn!(context, "STATUS {folder} (UIDNEXT) did not return UIDNEXT");
|
||||
// Go to IDLE anyway if STATUS is broken.
|
||||
}
|
||||
|
||||
if let Ok(()) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "skip idle, got interrupt");
|
||||
return Ok(self);
|
||||
if let Ok(info) = idle_interrupt_receiver.try_recv() {
|
||||
info!(context, "skip idle, got interrupt {:?}", info);
|
||||
return Ok((self, info));
|
||||
}
|
||||
|
||||
let mut handle = self.inner.idle();
|
||||
@@ -71,45 +58,59 @@ impl Session {
|
||||
|
||||
enum Event {
|
||||
IdleResponse(IdleResponse),
|
||||
Interrupt,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
|
||||
info!(context, "{folder}: Idle entering wait-on-remote state");
|
||||
let folder_name = watch_folder.as_deref().unwrap_or("None");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle entering wait-on-remote state", folder_name
|
||||
);
|
||||
let fut = idle_wait.map(|ev| ev.map(Event::IdleResponse)).race(async {
|
||||
idle_interrupt_receiver.recv().await.ok();
|
||||
let info = idle_interrupt_receiver.recv().await;
|
||||
|
||||
// cancel imap idle connection properly
|
||||
drop(interrupt);
|
||||
|
||||
Ok(Event::Interrupt)
|
||||
Ok(Event::Interrupt(info.unwrap_or_default()))
|
||||
});
|
||||
|
||||
match fut.await {
|
||||
Ok(Event::IdleResponse(IdleResponse::NewData(x))) => {
|
||||
info!(context, "{folder}: Idle has NewData {:?}", x);
|
||||
info!(context, "{}: Idle has NewData {:?}", folder_name, x);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::Timeout)) => {
|
||||
info!(context, "{folder}: Idle-wait timeout or interruption");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle-wait timeout or interruption", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::IdleResponse(IdleResponse::ManualInterrupt)) => {
|
||||
info!(context, "{folder}: Idle wait was interrupted manually");
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted manually", folder_name
|
||||
);
|
||||
}
|
||||
Ok(Event::Interrupt) => {
|
||||
info!(context, "{folder}: Idle wait was interrupted");
|
||||
Ok(Event::Interrupt(i)) => {
|
||||
info!(
|
||||
context,
|
||||
"{}: Idle wait was interrupted: {:?}", folder_name, &i
|
||||
);
|
||||
info = i;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "{folder}: Idle wait errored: {err:?}");
|
||||
warn!(context, "{}: Idle wait errored: {:?}", folder_name, err);
|
||||
}
|
||||
}
|
||||
|
||||
let mut session = tokio::time::timeout(Duration::from_secs(15), handle.done())
|
||||
.await
|
||||
.with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?
|
||||
.with_context(|| format!("{folder}: IMAP IDLE failed"))?;
|
||||
.with_context(|| format!("{folder_name}: IMAP IDLE protocol timed out"))?
|
||||
.with_context(|| format!("{folder_name}: IMAP IDLE failed"))?;
|
||||
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
|
||||
self.inner = session;
|
||||
|
||||
Ok(self)
|
||||
Ok((self, info))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ impl Imap {
|
||||
context: &Context,
|
||||
watch_folder: Option<String>,
|
||||
folder_meaning: FolderMeaning,
|
||||
) {
|
||||
) -> InterruptInfo {
|
||||
// Idle using polling. This is also needed if we're not yet configured -
|
||||
// in this case, we're waiting for a configure job (and an interrupt).
|
||||
|
||||
@@ -130,33 +131,32 @@ impl Imap {
|
||||
watch_folder
|
||||
} else {
|
||||
info!(context, "IMAP-fake-IDLE: no folder, waiting for interrupt");
|
||||
self.idle_interrupt_receiver.recv().await.ok();
|
||||
return;
|
||||
return self
|
||||
.idle_interrupt_receiver
|
||||
.recv()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
};
|
||||
info!(context, "IMAP-fake-IDLEing folder={:?}", watch_folder);
|
||||
|
||||
const TIMEOUT_INIT_MS: u64 = 60_000;
|
||||
let mut timeout_ms: u64 = TIMEOUT_INIT_MS;
|
||||
// check every minute if there are new messages
|
||||
// TODO: grow sleep durations / make them more flexible
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
||||
|
||||
enum Event {
|
||||
Tick,
|
||||
Interrupt,
|
||||
Interrupt(InterruptInfo),
|
||||
}
|
||||
// loop until we are interrupted or if we fetched something
|
||||
loop {
|
||||
let info = loop {
|
||||
use futures::future::FutureExt;
|
||||
use rand::Rng;
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(timeout_ms));
|
||||
timeout_ms = timeout_ms
|
||||
.saturating_add(rand::thread_rng().gen_range((timeout_ms / 2)..=timeout_ms));
|
||||
interval.tick().await; // The first tick completes immediately.
|
||||
match interval
|
||||
.tick()
|
||||
.map(|_| Event::Tick)
|
||||
.race(
|
||||
self.idle_interrupt_receiver
|
||||
.recv()
|
||||
.map(|_| Event::Interrupt),
|
||||
.map(|probe_network| Event::Interrupt(probe_network.unwrap_or_default())),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -178,7 +178,7 @@ impl Imap {
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break;
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
info!(context, "fake_idle is connected");
|
||||
@@ -192,9 +192,8 @@ impl Imap {
|
||||
{
|
||||
Ok(res) => {
|
||||
info!(context, "fetch_new_messages returned {:?}", res);
|
||||
timeout_ms = TIMEOUT_INIT_MS;
|
||||
if res {
|
||||
break;
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -203,12 +202,13 @@ impl Imap {
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Interrupt => {
|
||||
Event::Interrupt(info) => {
|
||||
// Interrupt
|
||||
info!(context, "Fake IDLE interrupted");
|
||||
break;
|
||||
break info;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
context,
|
||||
@@ -219,5 +219,7 @@ impl Imap {
|
||||
.as_millis() as f64
|
||||
/ 1000.,
|
||||
);
|
||||
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,6 @@ pub(crate) struct Session {
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
|
||||
/// True if NOTIFY SET command was executed in this session.
|
||||
pub notify_set: bool,
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
@@ -49,7 +46,6 @@ impl Session {
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
notify_set: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,10 +53,6 @@ impl Session {
|
||||
self.capabilities.can_idle
|
||||
}
|
||||
|
||||
pub fn can_notify(&self) -> bool {
|
||||
self.capabilities.can_notify
|
||||
}
|
||||
|
||||
pub fn can_move(&self) -> bool {
|
||||
self.capabilities.can_move
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::mimeparser::{parse_message_id, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::reaction::get_msg_reactions;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
use crate::tools::{
|
||||
@@ -1526,7 +1527,10 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
}
|
||||
|
||||
// Interrupt Inbox loop to start message deletion and run housekeeping.
|
||||
context.scheduler.interrupt_inbox().await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_inbox(InterruptInfo::new(false))
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1644,7 +1648,10 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
context
|
||||
.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
@@ -1811,14 +1818,6 @@ pub async fn estimate_deletion_cnt(
|
||||
pub(crate) async fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<Option<MsgId>> {
|
||||
rfc724_mid_exists_and(context, rfc724_mid, "1").await
|
||||
}
|
||||
|
||||
pub(crate) async fn rfc724_mid_exists_and(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
cond: &str,
|
||||
) -> Result<Option<MsgId>> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
@@ -1829,7 +1828,7 @@ pub(crate) async fn rfc724_mid_exists_and(
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
&("SELECT id FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
|
||||
"SELECT id FROM msgs WHERE rfc724_mid=?",
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::chat::Chat;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::context::{get_version_str, Context};
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::html::new_html_mimepart;
|
||||
@@ -316,15 +316,7 @@ impl<'a> MimeFactory<'a> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat } => {
|
||||
if chat.is_protected() {
|
||||
if self.msg.get_info_type() == SystemMessage::SecurejoinMessage {
|
||||
// Securejoin messages are supposed to verify a key.
|
||||
// In order to do this, it is necessary that they can be sent
|
||||
// to a key that is not yet verified.
|
||||
// This has to work independently of whether the chat is protected right now.
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
} else {
|
||||
PeerstateVerifiedStatus::BidirectVerified
|
||||
}
|
||||
PeerstateVerifiedStatus::BidirectVerified
|
||||
} else {
|
||||
PeerstateVerifiedStatus::Unverified
|
||||
}
|
||||
@@ -604,10 +596,9 @@ impl<'a> MimeFactory<'a> {
|
||||
|
||||
if let Loaded::Message { chat } = &self.loaded {
|
||||
if chat.typ == Chattype::Broadcast {
|
||||
let encoded_chat_name = encode_words(&chat.name);
|
||||
headers.protected.push(Header::new(
|
||||
"List-ID".into(),
|
||||
format!("{encoded_chat_name} <{}>", chat.grpid),
|
||||
format!("{} <{}>", chat.name, chat.grpid),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1366,12 +1357,14 @@ impl<'a> MimeFactory<'a> {
|
||||
);
|
||||
|
||||
// second body part: machine-readable, always REQUIRED by RFC 6522
|
||||
let version = get_version_str();
|
||||
let message_text2 = format!(
|
||||
"Original-Recipient: rfc822;{}\r\n\
|
||||
"Reporting-UA: Delta Chat {}\r\n\
|
||||
Original-Recipient: rfc822;{}\r\n\
|
||||
Final-Recipient: rfc822;{}\r\n\
|
||||
Original-Message-ID: <{}>\r\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
|
||||
self.from_addr, self.from_addr, self.msg.rfc724_mid
|
||||
version, self.from_addr, self.from_addr, self.msg.rfc724_mid
|
||||
);
|
||||
|
||||
let extension_fields = if additional_msg_ids.is_empty() {
|
||||
|
||||
@@ -59,7 +59,7 @@ pub(crate) struct MimeMessage {
|
||||
/// Message headers.
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
/// Addresses are normalized and lowercase
|
||||
/// Addresses are normalized and lowercased:
|
||||
pub recipients: Vec<SingleInfo>,
|
||||
|
||||
/// `From:` address.
|
||||
@@ -163,10 +163,10 @@ pub enum SystemMessage {
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged = 10,
|
||||
|
||||
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||
/// Chat protection is enabled.
|
||||
ChatProtectionEnabled = 11,
|
||||
|
||||
/// "%1$s sent a message from another device."
|
||||
/// Chat protection is disabled.
|
||||
ChatProtectionDisabled = 12,
|
||||
|
||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||
@@ -376,12 +376,6 @@ impl MimeMessage {
|
||||
if !encrypted {
|
||||
signatures.clear();
|
||||
}
|
||||
if let Some(peerstate) = &mut decryption_info.peerstate {
|
||||
if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-submitted is also set by holiday-notices so we also check `chat-version`
|
||||
let is_bot = headers.contains_key("auto-submitted") && headers.contains_key("chat-version");
|
||||
|
||||
23
src/net.rs
23
src/net.rs
@@ -1,11 +1,10 @@
|
||||
//! # Common network utilities.
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::pin::Pin;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use tokio::net::{lookup_host, TcpStream};
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
@@ -120,22 +119,6 @@ async fn lookup_host_with_cache(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resolved_addrs.is_empty() {
|
||||
// Load hardcoded cache if everything else fails.
|
||||
//
|
||||
// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
|
||||
// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
|
||||
//
|
||||
// In the future we may pre-resolve all provider database addresses
|
||||
// and build them in.
|
||||
if hostname == "mail.sangham.net" {
|
||||
resolved_addrs.push(SocketAddr::new(
|
||||
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
|
||||
port,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_addrs)
|
||||
@@ -195,9 +178,7 @@ pub(crate) async fn connect_tcp(
|
||||
let tcp_stream = match tcp_stream {
|
||||
Some(tcp_stream) => tcp_stream,
|
||||
None => {
|
||||
return Err(
|
||||
last_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}"))
|
||||
);
|
||||
return Err(last_error.unwrap_or_else(|| Error::msg("no DNS resolution results")));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
194
src/peerstate.rs
194
src/peerstate.rs
@@ -1,5 +1,7 @@
|
||||
//! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Error, Result};
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -38,7 +40,7 @@ pub enum PeerstateVerifiedStatus {
|
||||
}
|
||||
|
||||
/// Peerstate represents the state of an Autocrypt peer.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Peerstate {
|
||||
/// E-mail address of the contact.
|
||||
pub addr: String,
|
||||
@@ -80,24 +82,13 @@ pub struct Peerstate {
|
||||
/// Fingerprint of the verified public key.
|
||||
pub verified_key_fingerprint: Option<Fingerprint>,
|
||||
|
||||
/// The address that introduced this verified key.
|
||||
pub verifier: Option<String>,
|
||||
|
||||
/// Secondary public verified key of the contact.
|
||||
/// It could be a contact gossiped by another verified contact in a shared group
|
||||
/// or a key that was previously used as a verified key.
|
||||
pub secondary_verified_key: Option<SignedPublicKey>,
|
||||
|
||||
/// Fingerprint of the secondary verified public key.
|
||||
pub secondary_verified_key_fingerprint: Option<Fingerprint>,
|
||||
|
||||
/// The address that introduced secondary verified key.
|
||||
pub secondary_verifier: Option<String>,
|
||||
|
||||
/// True if it was detected
|
||||
/// that the fingerprint of the key used in chats with
|
||||
/// opportunistic encryption was changed after Peerstate creation.
|
||||
pub fingerprint_changed: bool,
|
||||
|
||||
/// The address that verified this contact
|
||||
pub verifier: Option<String>,
|
||||
}
|
||||
|
||||
impl Peerstate {
|
||||
@@ -115,11 +106,8 @@ impl Peerstate {
|
||||
gossip_timestamp: 0,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,11 +132,8 @@ impl Peerstate {
|
||||
gossip_timestamp: message_time,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,10 +141,7 @@ impl Peerstate {
|
||||
pub async fn from_addr(context: &Context, addr: &str) -> Result<Option<Peerstate>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint, \
|
||||
verifier, \
|
||||
secondary_verified_key, secondary_verified_key_fingerprint, \
|
||||
secondary_verifier \
|
||||
verified_key, verified_key_fingerprint, verifier \
|
||||
FROM acpeerstates \
|
||||
WHERE addr=? COLLATE NOCASE LIMIT 1;";
|
||||
Self::from_stmt(context, query, (addr,)).await
|
||||
@@ -172,10 +154,7 @@ impl Peerstate {
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint, \
|
||||
verifier, \
|
||||
secondary_verified_key, secondary_verified_key_fingerprint, \
|
||||
secondary_verifier \
|
||||
verified_key, verified_key_fingerprint, verifier \
|
||||
FROM acpeerstates \
|
||||
WHERE public_key_fingerprint=? \
|
||||
OR gossip_key_fingerprint=? \
|
||||
@@ -195,10 +174,7 @@ impl Peerstate {
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint, \
|
||||
verifier, \
|
||||
secondary_verified_key, secondary_verified_key_fingerprint, \
|
||||
secondary_verifier \
|
||||
verified_key, verified_key_fingerprint, verifier \
|
||||
FROM acpeerstates \
|
||||
WHERE verified_key_fingerprint=? \
|
||||
OR addr=? COLLATE NOCASE \
|
||||
@@ -215,6 +191,11 @@ impl Peerstate {
|
||||
let peerstate = context
|
||||
.sql
|
||||
.query_row_optional(query, params, |row| {
|
||||
// all the above queries start with this: SELECT
|
||||
// addr, last_seen, last_seen_autocrypt, prefer_encrypted,
|
||||
// public_key, gossip_timestamp, gossip_key, public_key_fingerprint,
|
||||
// gossip_key_fingerprint, verified_key, verified_key_fingerprint
|
||||
|
||||
let res = Peerstate {
|
||||
addr: row.get("addr")?,
|
||||
last_seen: row.get("last_seen")?,
|
||||
@@ -249,24 +230,11 @@ impl Peerstate {
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
fingerprint_changed: false,
|
||||
verifier: {
|
||||
let verifier: Option<String> = row.get("verifier")?;
|
||||
verifier.filter(|s| !s.is_empty())
|
||||
verifier.filter(|verifier| !verifier.is_empty())
|
||||
},
|
||||
secondary_verified_key: row
|
||||
.get("secondary_verified_key")
|
||||
.ok()
|
||||
.and_then(|blob: Vec<u8>| SignedPublicKey::from_slice(&blob).ok()),
|
||||
secondary_verified_key_fingerprint: row
|
||||
.get::<_, Option<String>>("secondary_verified_key_fingerprint")?
|
||||
.map(|s| s.parse::<Fingerprint>())
|
||||
.transpose()
|
||||
.unwrap_or_default(),
|
||||
secondary_verifier: {
|
||||
let secondary_verifier: Option<String> = row.get("secondary_verifier")?;
|
||||
secondary_verifier.filter(|s| !s.is_empty())
|
||||
},
|
||||
fingerprint_changed: false,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
@@ -453,56 +421,48 @@ impl Peerstate {
|
||||
/// Make sure to call `self.save_to_db` to save these changes
|
||||
/// Params:
|
||||
/// verifier:
|
||||
/// The address which introduces the given contact.
|
||||
/// If we are verifying the contact, use that contacts address.
|
||||
/// The address which verifies the given contact
|
||||
/// If we are verifying the contact, use that contacts address
|
||||
pub fn set_verified(
|
||||
&mut self,
|
||||
which_key: PeerstateKeyType,
|
||||
fingerprint: Fingerprint,
|
||||
verified: PeerstateVerifiedStatus,
|
||||
verifier: String,
|
||||
) -> Result<()> {
|
||||
match which_key {
|
||||
PeerstateKeyType::PublicKey => {
|
||||
if self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.public_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's public key fingerprint",
|
||||
)))
|
||||
if verified == PeerstateVerifiedStatus::BidirectVerified {
|
||||
match which_key {
|
||||
PeerstateKeyType::PublicKey => {
|
||||
if self.public_key_fingerprint.is_some()
|
||||
&& self.public_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.public_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's public key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
PeerstateKeyType::GossipKey => {
|
||||
if self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.gossip_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's gossip key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
PeerstateKeyType::GossipKey => {
|
||||
if self.gossip_key_fingerprint.is_some()
|
||||
&& self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint
|
||||
{
|
||||
self.verified_key = self.gossip_key.clone();
|
||||
self.verified_key_fingerprint = Some(fingerprint);
|
||||
self.verifier = Some(verifier);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"{fingerprint} is not peer's gossip key fingerprint",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets current gossiped key as the secondary verified key.
|
||||
///
|
||||
/// If gossiped key is the same as the current verified key,
|
||||
/// do nothing to avoid overwriting secondary verified key
|
||||
/// which may be different.
|
||||
pub fn set_secondary_verified_key_from_gossip(&mut self, verifier: String) {
|
||||
if self.gossip_key_fingerprint != self.verified_key_fingerprint {
|
||||
self.secondary_verified_key = self.gossip_key.clone();
|
||||
self.secondary_verified_key_fingerprint = self.gossip_key_fingerprint.clone();
|
||||
self.secondary_verifier = Some(verifier);
|
||||
} else {
|
||||
Err(Error::msg("BidirectVerified required"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,12 +480,9 @@ impl Peerstate {
|
||||
gossip_key_fingerprint,
|
||||
verified_key,
|
||||
verified_key_fingerprint,
|
||||
verifier,
|
||||
secondary_verified_key,
|
||||
secondary_verified_key_fingerprint,
|
||||
secondary_verifier,
|
||||
addr)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
addr,
|
||||
verifier)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET
|
||||
last_seen = excluded.last_seen,
|
||||
@@ -538,10 +495,7 @@ impl Peerstate {
|
||||
gossip_key_fingerprint = excluded.gossip_key_fingerprint,
|
||||
verified_key = excluded.verified_key,
|
||||
verified_key_fingerprint = excluded.verified_key_fingerprint,
|
||||
verifier = excluded.verifier,
|
||||
secondary_verified_key = excluded.secondary_verified_key,
|
||||
secondary_verified_key_fingerprint = excluded.secondary_verified_key_fingerprint,
|
||||
secondary_verifier = excluded.secondary_verifier",
|
||||
verifier = excluded.verifier",
|
||||
(
|
||||
self.last_seen,
|
||||
self.last_seen_autocrypt,
|
||||
@@ -553,19 +507,23 @@ impl Peerstate {
|
||||
self.gossip_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()),
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
self.secondary_verified_key.as_ref().map(|k| k.to_bytes()),
|
||||
self.secondary_verified_key_fingerprint
|
||||
.as_ref()
|
||||
.map(|fp| fp.hex()),
|
||||
self.secondary_verifier.as_deref().unwrap_or(""),
|
||||
&self.addr,
|
||||
self.verifier.as_deref().unwrap_or(""),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if verified key is contained in the given set of fingerprints.
|
||||
pub fn has_verified_key(&self, fingerprints: &HashSet<Fingerprint>) -> bool {
|
||||
if let Some(vkc) = &self.verified_key_fingerprint {
|
||||
fingerprints.contains(vkc) && self.verified_key.is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the address that verified the contact
|
||||
pub fn get_verifier(&self) -> Option<&str> {
|
||||
self.verifier.as_deref()
|
||||
@@ -816,11 +774,8 @@ mod tests {
|
||||
gossip_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verified_key: Some(pub_key.clone()),
|
||||
verified_key_fingerprint: Some(pub_key.fingerprint()),
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -859,11 +814,8 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -895,11 +847,8 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
|
||||
assert!(
|
||||
@@ -961,11 +910,8 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
|
||||
peerstate.apply_header(&header, 100);
|
||||
|
||||
@@ -222,99 +222,6 @@ static P_BUZON_UY: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c1.testrun.org.md: c1.testrun.org
|
||||
static P_C1_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c1.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c1-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c2.testrun.org.md: c2.testrun.org
|
||||
static P_C2_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c2.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c2-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c3.testrun.org.md: c3.testrun.org
|
||||
static P_C3_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c3.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c3-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
static P_CHELLO_AT: Provider = Provider {
|
||||
id: "chello.at",
|
||||
@@ -960,37 +867,6 @@ static P_NAVER: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nine.testrun.org.md: nine.testrun.org
|
||||
static P_NINE_TESTRUN_ORG: Provider = Provider {
|
||||
id: "nine.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/nine-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nubo.coop.md: nubo.coop
|
||||
static P_NUBO_COOP: Provider = Provider {
|
||||
id: "nubo.coop",
|
||||
@@ -1619,9 +1495,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("delta.blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("xfinity.com", &P_COMCAST),
|
||||
("comcast.net", &P_COMCAST),
|
||||
@@ -1835,7 +1708,6 @@ pub(crate) static PROVIDER_DATA: Lazy<HashMap<&'static str, &'static Provider>>
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver.com", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("hotmail.com", &P_OUTLOOK_COM),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
@@ -1988,9 +1860,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
@@ -2019,7 +1888,6 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
("ouvaton.coop", &P_OUVATON_COOP),
|
||||
@@ -2050,4 +1918,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 11, 5).unwrap());
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 2, 20).unwrap());
|
||||
|
||||
@@ -1052,11 +1052,8 @@ mod tests {
|
||||
gossip_key_fingerprint: None,
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
assert!(
|
||||
peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(),
|
||||
|
||||
@@ -26,18 +26,16 @@ use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
|
||||
Viewtype,
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{
|
||||
buf_compress, extract_grpid_from_rfc724_mid, smeared_time, strip_rtlo_characters,
|
||||
};
|
||||
@@ -227,8 +225,6 @@ pub(crate) async fn receive_imf_inner(
|
||||
.and_then(|value| mailparse::dateparse(value).ok())
|
||||
.map_or(rcvd_timestamp, |value| min(value, rcvd_timestamp + 60));
|
||||
|
||||
update_verified_keys(context, &mut mime_parser, from_id).await?;
|
||||
|
||||
// Add parts
|
||||
let received_msg = add_parts(
|
||||
context,
|
||||
@@ -285,7 +281,9 @@ pub(crate) async fn receive_imf_inner(
|
||||
if let Some(ref sync_items) = mime_parser.sync_items {
|
||||
if from_id == ContactId::SELF {
|
||||
if mime_parser.was_encrypted() {
|
||||
context.execute_sync_items(sync_items).await;
|
||||
if let Err(err) = context.execute_sync_items(sync_items).await {
|
||||
warn!(context, "receive_imf cannot execute sync items: {err:#}.");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Sync items are not encrypted.");
|
||||
}
|
||||
@@ -457,7 +455,6 @@ async fn add_parts(
|
||||
let mut chat_id_blocked = Blocked::Not;
|
||||
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = (Vec::new(), None);
|
||||
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
|
||||
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
|
||||
}
|
||||
@@ -505,6 +502,7 @@ async fn add_parts(
|
||||
// - incoming messages introduce a chat only for known contacts if they are sent by a messenger
|
||||
// (of course, the user can add other chats manually later)
|
||||
let to_id: ContactId;
|
||||
|
||||
let state: MessageState;
|
||||
let mut needs_delete_job = false;
|
||||
if incoming {
|
||||
@@ -516,23 +514,25 @@ async fn add_parts(
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id)
|
||||
.await
|
||||
.context("error in Secure-Join message handling")?
|
||||
{
|
||||
securejoin::HandshakeMessage::Done => {
|
||||
match handle_securejoin_handshake(context, mime_parser, from_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
needs_delete_job = true;
|
||||
securejoin_seen = true;
|
||||
}
|
||||
securejoin::HandshakeMessage::Ignore => {
|
||||
Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
securejoin_seen = true;
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
securejoin_seen = false;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join message handling: {err:#}.");
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
securejoin_seen = true;
|
||||
}
|
||||
}
|
||||
// Peerstate could be updated by handling the Securejoin handshake.
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
@@ -554,11 +554,6 @@ async fn add_parts(
|
||||
markseen_on_imap_table(context, rfc724_mid).await.ok();
|
||||
}
|
||||
|
||||
if chat_id.is_none() && is_mdn {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// try to assign to a chat based on In-Reply-To/References:
|
||||
|
||||
@@ -655,16 +650,15 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
group_changes_msgs = apply_group_changes(
|
||||
better_msg = better_msg.or(apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
group_chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?;
|
||||
.await?);
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
@@ -814,17 +808,19 @@ async fn add_parts(
|
||||
|
||||
// handshake may mark contacts as verified and must be processed before chats are created
|
||||
if mime_parser.get_header(HeaderDef::SecureJoin).is_some() {
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id)
|
||||
.await
|
||||
.context("error in Secure-Join watching")?
|
||||
{
|
||||
securejoin::HandshakeMessage::Done | securejoin::HandshakeMessage::Ignore => {
|
||||
match observe_securejoin_on_other_device(context, mime_parser, to_id).await {
|
||||
Ok(securejoin::HandshakeMessage::Done)
|
||||
| Ok(securejoin::HandshakeMessage::Ignore) => {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
securejoin::HandshakeMessage::Propagate => {
|
||||
Ok(securejoin::HandshakeMessage::Propagate) => {
|
||||
// process messages as "member added" normally
|
||||
chat_id = None;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Error in Secure-Join watching: {err:#}.");
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
}
|
||||
}
|
||||
} else if mime_parser.sync_items.is_some() && self_sent {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
@@ -891,23 +887,22 @@ async fn add_parts(
|
||||
// automatically unblock chat when the user sends a message
|
||||
if chat_id_blocked != Blocked::Not {
|
||||
if let Some(chat_id) = chat_id {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id_blocked = Blocked::Not;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
group_changes_msgs = apply_group_changes(
|
||||
better_msg = better_msg.or(apply_group_changes(
|
||||
context,
|
||||
mime_parser,
|
||||
sent_timestamp,
|
||||
chat_id,
|
||||
from_id,
|
||||
to_ids,
|
||||
is_partial_download.is_some(),
|
||||
)
|
||||
.await?;
|
||||
.await?);
|
||||
}
|
||||
|
||||
if chat_id.is_none() && self_sent {
|
||||
@@ -924,27 +919,11 @@ async fn add_parts(
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
if Blocked::Not != chat_id_blocked {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id.unblock(context).await?;
|
||||
// Not assigning `chat_id_blocked = Blocked::Not` to avoid unused_assignments warning.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if chat_id.is_none() {
|
||||
// Check if the message belongs to a broadcast list.
|
||||
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
|
||||
let listid = mailinglist_header_listid(mailinglist_header)?;
|
||||
chat_id = Some(
|
||||
if let Some((id, ..)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
||||
id
|
||||
} else {
|
||||
let name =
|
||||
compute_mailinglist_name(mailinglist_header, &listid, mime_parser);
|
||||
chat::create_broadcast_list_ex(context, Nosync, listid, name).await?
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fetching_existing_messages && mime_parser.decrypting_failed {
|
||||
@@ -1137,29 +1116,7 @@ async fn add_parts(
|
||||
|
||||
let mut created_db_entries = Vec::with_capacity(mime_parser.parts.len());
|
||||
|
||||
if let Some(msg) = group_changes_msgs.1 {
|
||||
match &better_msg {
|
||||
None => better_msg = Some(msg),
|
||||
Some(_) => group_changes_msgs.0.push(msg),
|
||||
}
|
||||
}
|
||||
|
||||
for group_changes_msg in group_changes_msgs.0 {
|
||||
// Currently all additional group changes messages are "Member added".
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
&group_changes_msg,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
sort_timestamp,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
for part in &mime_parser.parts {
|
||||
for part in &mut mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
set_msg_reaction(
|
||||
@@ -1700,10 +1657,20 @@ async fn create_or_lookup_group(
|
||||
members.push(from_id);
|
||||
}
|
||||
members.extend(to_ids);
|
||||
members.sort_unstable();
|
||||
members.dedup();
|
||||
chat::add_to_chat_contacts_table(context, new_chat_id, &members).await?;
|
||||
|
||||
// once, we have protected-chats explained in UI, we can uncomment the following lines.
|
||||
// ("verified groups" did not add a message anyway)
|
||||
//
|
||||
//if create_protected == ProtectionStatus::Protected {
|
||||
// set from_id=0 as it is not clear that the sender of this random group message
|
||||
// actually really has enabled chat-protection at some point.
|
||||
//new_chat_id
|
||||
// .add_protection_msg(context, ProtectionStatus::Protected, false, 0)
|
||||
// .await?;
|
||||
//}
|
||||
|
||||
context.emit_event(EventType::ChatModified(new_chat_id));
|
||||
}
|
||||
|
||||
@@ -1728,7 +1695,6 @@ async fn create_or_lookup_group(
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
///
|
||||
/// Optionally returns better message to replace the original system message.
|
||||
/// is_partial_download: whether the message is not fully downloaded.
|
||||
async fn apply_group_changes(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
@@ -1736,21 +1702,15 @@ async fn apply_group_changes(
|
||||
chat_id: ChatId,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
is_partial_download: bool,
|
||||
) -> Result<(Vec<String>, Option<String>)> {
|
||||
if chat_id.is_special() {
|
||||
// Do not apply group changes to the trash chat.
|
||||
return Ok((Vec::new(), None));
|
||||
}
|
||||
) -> Result<Option<String>> {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Group {
|
||||
return Ok((Vec::new(), None));
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
let self_added =
|
||||
@@ -1760,14 +1720,12 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
let mut chat_contacts =
|
||||
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let is_from_in_chat =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = !is_partial_download
|
||||
&& is_from_in_chat
|
||||
let allow_member_list_changes = is_from_in_chat
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::MemberListTimestamp, sent_timestamp)
|
||||
.await?;
|
||||
@@ -1781,9 +1739,7 @@ async fn apply_group_changes(
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists_and(context, reply_to, "download_state=0")
|
||||
.await?
|
||||
.is_none(),
|
||||
Some(reply_to) => rfc724_mid_exists(context, reply_to).await?.is_none(),
|
||||
None => false,
|
||||
}
|
||||
} && {
|
||||
@@ -1807,12 +1763,7 @@ async fn apply_group_changes(
|
||||
|
||||
if !chat.is_protected() {
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
smeared_time(context),
|
||||
Some(from_id),
|
||||
)
|
||||
.inner_set_protection(context, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -1905,43 +1856,33 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
if !recreate_member_list {
|
||||
// Don't delete any members locally, but instead add absent ones to provide group
|
||||
// membership consistency for all members:
|
||||
// - Classical MUA users usually don't intend to remove users from an email thread, so
|
||||
// if they removed a recipient then it was probably by accident.
|
||||
// - DC users could miss new member additions and then better to handle this in the same
|
||||
// way as for classical MUA messages. Moreover, if we remove a member implicitly, they
|
||||
// will never know that and continue to think they're still here.
|
||||
// But it shouldn't be a big problem if somebody missed a member removal, because they
|
||||
// will likely recreate the member list from the next received message. The problem
|
||||
// occurs only if that "somebody" managed to reply earlier. Really, it's a problem for
|
||||
// big groups with high message rate, but let it be for now.
|
||||
let mut diff: HashSet<ContactId> =
|
||||
new_members.difference(&chat_contacts).copied().collect();
|
||||
new_members = chat_contacts.clone();
|
||||
new_members.extend(diff.clone());
|
||||
if let Some(added_id) = added_id {
|
||||
diff.remove(&added_id);
|
||||
}
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit addition of {diff:?} to chat {chat_id}.");
|
||||
}
|
||||
group_changes_msgs.reserve(diff.len());
|
||||
for contact_id in diff {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
group_changes_msgs.push(
|
||||
stock_str::msg_add_member_local(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ContactId::UNDEFINED,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
let diff: HashSet<ContactId> =
|
||||
chat_contacts.difference(&new_members).copied().collect();
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
// This is what provides group membership consistency: we remove group members
|
||||
// locally if we see a discrepancy with the "To" list in the received message as it
|
||||
// is better for privacy than adding absent members locally. But it shouldn't be a
|
||||
// big problem if somebody missed a member addition, because they will likely
|
||||
// recreate the member list from the next received message. The problem occurs only
|
||||
// if that "somebody" managed to reply earlier. Really, it's a problem for big
|
||||
// groups with high message rate, but let it be for now.
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit removal of {diff:?} from chat {chat_id}.");
|
||||
}
|
||||
new_members = chat_contacts.difference(&diff).copied().collect();
|
||||
} else {
|
||||
new_members.extend(diff);
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
if let Some(added_id) = added_id {
|
||||
new_members.insert(added_id);
|
||||
}
|
||||
if recreate_member_list {
|
||||
info!(
|
||||
context,
|
||||
@@ -1950,7 +1891,21 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
chat::update_chat_contacts_table(context, chat_id, &new_members).await?;
|
||||
let new_members_ref = &new_members;
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
|
||||
for contact_id in new_members_ref {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
@@ -1990,22 +1945,11 @@ async fn apply_group_changes(
|
||||
if send_event_chat_modified {
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
}
|
||||
Ok((group_changes_msgs, better_msg))
|
||||
Ok(better_msg)
|
||||
}
|
||||
|
||||
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
|
||||
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
||||
Ok(match LIST_ID_REGEX.captures(list_id_header) {
|
||||
Some(cap) => cap.get(2).context("no match??")?.as_str().trim(),
|
||||
None => list_id_header
|
||||
.trim()
|
||||
.trim_start_matches('<')
|
||||
.trim_end_matches('>'),
|
||||
}
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Create or lookup a mailing list chat.
|
||||
///
|
||||
/// `list_id_header` contains the Id that must be used for the mailing list
|
||||
@@ -2015,13 +1959,21 @@ fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
||||
///
|
||||
/// `mime_parser` is the corresponding message
|
||||
/// and is used to figure out the mailing list name from different header fields.
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
async fn create_or_lookup_mailinglist(
|
||||
context: &Context,
|
||||
allow_creation: bool,
|
||||
list_id_header: &str,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
let listid = mailinglist_header_listid(list_id_header)?;
|
||||
let listid = match LIST_ID_REGEX.captures(list_id_header) {
|
||||
Some(cap) => cap[2].trim().to_string(),
|
||||
None => list_id_header
|
||||
.trim()
|
||||
.trim_start_matches('<')
|
||||
.trim_end_matches('>')
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
if let Some((chat_id, _, blocked)) = chat::get_chat_id_by_grpid(context, &listid).await? {
|
||||
return Ok(Some((chat_id, blocked)));
|
||||
@@ -2293,72 +2245,10 @@ enum VerifiedEncryption {
|
||||
NotVerified(String), // The string contains the reason why it's not verified
|
||||
}
|
||||
|
||||
/// Moves secondary verified key to primary verified key
|
||||
/// if the message is signed with a secondary verified key.
|
||||
/// Removes secondary verified key if the message is signed with primary key.
|
||||
async fn update_verified_keys(
|
||||
context: &Context,
|
||||
mimeparser: &mut MimeMessage,
|
||||
from_id: ContactId,
|
||||
) -> Result<Option<String>> {
|
||||
if from_id == ContactId::SELF {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if !mimeparser.was_encrypted() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(peerstate) = &mut mimeparser.decryption_info.peerstate else {
|
||||
// No peerstate means no verified keys.
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let signed_with_primary_verified_key = peerstate
|
||||
.verified_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|fp| mimeparser.signatures.contains(fp))
|
||||
.is_some();
|
||||
let signed_with_secondary_verified_key = peerstate
|
||||
.secondary_verified_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|fp| mimeparser.signatures.contains(fp))
|
||||
.is_some();
|
||||
|
||||
if signed_with_primary_verified_key {
|
||||
// Remove secondary key if it exists.
|
||||
if peerstate.secondary_verified_key.is_some()
|
||||
|| peerstate.secondary_verified_key_fingerprint.is_some()
|
||||
|| peerstate.secondary_verifier.is_some()
|
||||
{
|
||||
peerstate.secondary_verified_key = None;
|
||||
peerstate.secondary_verified_key_fingerprint = None;
|
||||
peerstate.secondary_verifier = None;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
|
||||
// No need to notify about secondary key removal.
|
||||
Ok(None)
|
||||
} else if signed_with_secondary_verified_key {
|
||||
peerstate.verified_key = peerstate.secondary_verified_key.take();
|
||||
peerstate.verified_key_fingerprint = peerstate.secondary_verified_key_fingerprint.take();
|
||||
peerstate.verifier = peerstate.secondary_verifier.take();
|
||||
peerstate.fingerprint_changed = true;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
// Primary verified key changed.
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether the message is allowed to appear in a protected chat.
|
||||
///
|
||||
/// This means that it is encrypted, signed with a verified key,
|
||||
/// and if it's a group, all the recipients are verified.
|
||||
///
|
||||
/// Also propagates gossiped keys to verified if needed.
|
||||
async fn has_verified_encryption(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
@@ -2395,13 +2285,7 @@ async fn has_verified_encryption(
|
||||
));
|
||||
};
|
||||
|
||||
let signed_with_verified_key = peerstate
|
||||
.verified_key_fingerprint
|
||||
.as_ref()
|
||||
.filter(|fp| mimeparser.signatures.contains(fp))
|
||||
.is_some();
|
||||
|
||||
if !signed_with_verified_key {
|
||||
if !peerstate.has_verified_key(&mimeparser.signatures) {
|
||||
return Ok(NotVerified(
|
||||
"The message was sent with non-verified encryption".to_string(),
|
||||
));
|
||||
@@ -2419,16 +2303,6 @@ async fn has_verified_encryption(
|
||||
return Ok(Verified);
|
||||
}
|
||||
|
||||
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
async fn mark_recipients_as_verified(
|
||||
context: &Context,
|
||||
from_id: ContactId,
|
||||
to_ids: Vec<ContactId>,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let rows = context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -2452,48 +2326,49 @@ async fn mark_recipients_as_verified(
|
||||
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
|
||||
for (to_addr, is_verified) in rows {
|
||||
for (to_addr, mut is_verified) in rows {
|
||||
info!(
|
||||
context,
|
||||
"has_verified_encryption: {:?} self={:?}.",
|
||||
to_addr,
|
||||
context.is_self_addr(&to_addr).await
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &to_addr).await?;
|
||||
|
||||
// mark gossiped keys (if any) as verified
|
||||
if mimeparser.gossiped_addr.contains(&to_addr.to_lowercase()) {
|
||||
if let Some(mut peerstate) = Peerstate::from_addr(context, &to_addr).await? {
|
||||
// If we're here, we know the gossip key is verified.
|
||||
//
|
||||
// Use the gossip-key as verified-key if there is no verified-key.
|
||||
//
|
||||
// Store gossip key as secondary verified key if there is a verified key and
|
||||
// gossiped key is different.
|
||||
//
|
||||
// See <https://github.com/nextleap-project/countermitm/issues/46>
|
||||
// and <https://github.com/deltachat/deltachat-core-rust/issues/4541> for discussion.
|
||||
let verifier_addr = contact.get_addr().to_owned();
|
||||
if !is_verified {
|
||||
info!(context, "{verifier_addr} has verified {to_addr}.");
|
||||
if let Some(fp) = peerstate.gossip_key_fingerprint.clone() {
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fp, verifier_addr)?;
|
||||
if let Some(mut peerstate) = peerstate {
|
||||
// if we're here, we know the gossip key is verified:
|
||||
// - use the gossip-key as verified-key if there is no verified-key
|
||||
// - OR if the verified-key does not match public-key or gossip-key
|
||||
// (otherwise a verified key can _only_ be updated through QR scan which might be annoying,
|
||||
// see <https://github.com/nextleap-project/countermitm/issues/46> for a discussion about this point)
|
||||
if !is_verified
|
||||
|| peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint
|
||||
&& peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint
|
||||
{
|
||||
info!(context, "{} has verified {}.", contact.get_addr(), to_addr);
|
||||
let fp = peerstate.gossip_key_fingerprint.clone();
|
||||
if let Some(fp) = fp {
|
||||
peerstate.set_verified(
|
||||
PeerstateKeyType::GossipKey,
|
||||
fp,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
contact.get_addr().to_owned(),
|
||||
)?;
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
|
||||
if !is_verified {
|
||||
let (to_contact_id, _) = Contact::add_or_lookup(
|
||||
context,
|
||||
"",
|
||||
ContactAddress::new(&to_addr)?,
|
||||
Origin::Hidden,
|
||||
)
|
||||
.await?;
|
||||
ChatId::set_protection_for_contact(context, to_contact_id).await?;
|
||||
}
|
||||
is_verified = true;
|
||||
}
|
||||
} else {
|
||||
// The contact already has a verified key.
|
||||
// Store gossiped key as the secondary verified key.
|
||||
peerstate.set_secondary_verified_key_from_gossip(verifier_addr);
|
||||
peerstate.save_to_db(&context.sql).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_verified {
|
||||
return Ok(NotVerified(format!(
|
||||
"{to_addr} is not a member of this protected chat member list {to_ids:?}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
/// Returns the last message referenced from `References` header if it is in the database.
|
||||
|
||||
@@ -3106,6 +3106,19 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
|
||||
async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <message@example.org>\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
hello foo\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
@@ -3118,40 +3131,28 @@ async fn test_thunderbird_autocrypt() -> Result<()> {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
|
||||
async fn test_thunderbird_encrypted_with_pubkey_attached() -> Result<()> {
|
||||
let t = TestContext::new_bob().await;
|
||||
|
||||
let raw =
|
||||
include_bytes!("../../test-data/message/thunderbird_encrypted_signed_with_pubkey.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: alice@example.org\n\
|
||||
To: bob@example.net\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <message@example.org>\n\
|
||||
Date: Thu, 2 Nov 2023 02:20:28 -0300\n\
|
||||
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
|
||||
\n\
|
||||
unencrypted\n",
|
||||
hello foo\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Reset);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_with_pubkey_attached.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert!(peerstate.public_key.is_some());
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Ok(())
|
||||
@@ -3175,6 +3176,13 @@ async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> {
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted.eml");
|
||||
receive_imf(&t, raw, false).await?;
|
||||
let peerstate = Peerstate::from_addr(&t, "alice@example.org")
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3411,24 +3419,14 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bob didn't receive the addition of Fiona, but Alice mustn't remove Fiona from the members
|
||||
// list back. Instead, Bob must add Fiona from the next Alice's message to make their group
|
||||
// members view consistent.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
// Bob didn't receive the addition of Fiona, so Alice must remove Fiona from the members list
|
||||
// back to make their group members view consistent.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
send_text_msg(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
"Finally add Fiona please".to_string(),
|
||||
)
|
||||
.await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3512,11 +3510,8 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Alice didn't receive Bob's leave message, so Bob must readd themselves otherwise other
|
||||
// members would think Bob is still here while they aren't, and then retry to leave if they
|
||||
// think that Alice didn't re-add them on purpose (which is possible if Alice uses a classical
|
||||
// MUA).
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
// Alice didn't receive Bobs leave message, but bob shouldn't readded himself just because of that.
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3686,34 +3681,6 @@ async fn test_mua_can_readd() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_member_left_does_not_create_chat() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// Bob only received a message of Alice leaving the group.
|
||||
// This should not create the group.
|
||||
//
|
||||
// The reason is to avoid recreating deleted chats,
|
||||
// especially the chats that were created due to "split group" bugs
|
||||
// which some members simply deleted and some members left,
|
||||
// recreating the chat for others.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
assert!(bob_chat_id.is_trash());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -3727,14 +3694,11 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "second message".to_string()).await?;
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
assert!(!bob_chat_id.is_special());
|
||||
|
||||
// Bob missed the message adding them, but must recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3894,113 +3858,3 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_partial_group_consistency() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 2);
|
||||
|
||||
// Get initial timestamp.
|
||||
let timestamp = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Bob receives partial message.
|
||||
let msg_id = receive_imf_inner(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: <bob@example.net>, <charlie@example.com>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp2 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Partial download does not change the member list.
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(timestamp, timestamp2);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
// Alice sends normal message to bob, adding fiona.
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
Contact::create(&alice, "fiona", "fiona@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
let timestamp3 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// Receiving a message after a partial download recreates the member list because we treat
|
||||
// such messages as if we have not seen them.
|
||||
assert_ne!(timestamp, timestamp3);
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
assert_eq!(contacts.len(), 3);
|
||||
|
||||
// Bob fully reives the partial message.
|
||||
let msg_id = receive_imf_inner(
|
||||
&bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Subject: subject\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\
|
||||
Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
|
||||
let msg = Message::load_from_db(&bob, msg_id.msg_ids[0]).await?;
|
||||
let timestamp4 = bob_chat_id
|
||||
.get_param(&bob)
|
||||
.await?
|
||||
.get_i64(Param::MemberListTimestamp)
|
||||
.unwrap();
|
||||
|
||||
// After full download, the old message should not change group state.
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(timestamp3, timestamp4);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?, contacts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
190
src/scheduler.rs
190
src/scheduler.rs
@@ -1,4 +1,3 @@
|
||||
use std::cmp;
|
||||
use std::iter::{self, once};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -7,7 +6,6 @@ use anyhow::{bail, Context as _, Error, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use futures::future::try_join_all;
|
||||
use futures_lite::FutureExt;
|
||||
use rand::Rng;
|
||||
use tokio::sync::{oneshot, RwLock, RwLockWriteGuard};
|
||||
use tokio::task;
|
||||
|
||||
@@ -235,17 +233,17 @@ impl SchedulerState {
|
||||
connectivity::maybe_network_lost(context, stores).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_inbox(&self) {
|
||||
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.interrupt_inbox();
|
||||
scheduler.interrupt_inbox(info);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self) {
|
||||
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
let inner = self.inner.read().await;
|
||||
if let InnerSchedulerState::Started(ref scheduler) = *inner {
|
||||
scheduler.interrupt_smtp();
|
||||
scheduler.interrupt_smtp(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -465,15 +463,18 @@ async fn inbox_loop(
|
||||
/// handling all the errors. In case of an error, it is logged, but not propagated upwards. If
|
||||
/// critical operation fails such as fetching new messages fails, connection is reset via
|
||||
/// `trigger_reconnect`, so a fresh one can be opened.
|
||||
async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: FolderMeaning) {
|
||||
async fn fetch_idle(
|
||||
ctx: &Context,
|
||||
connection: &mut Imap,
|
||||
folder_meaning: FolderMeaning,
|
||||
) -> InterruptInfo {
|
||||
let folder_config = match folder_meaning.to_config() {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
error!(ctx, "Bad folder meaning: {}", folder_meaning);
|
||||
connection
|
||||
return connection
|
||||
.fake_idle(ctx, None, FolderMeaning::Unknown)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let folder = match ctx.get_config(folder_config).await {
|
||||
@@ -483,10 +484,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
ctx,
|
||||
"Can not watch {} folder, failed to retrieve config: {:#}", folder_config, err
|
||||
);
|
||||
connection
|
||||
return connection
|
||||
.fake_idle(ctx, None, FolderMeaning::Unknown)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -495,10 +495,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
} else {
|
||||
connection.connectivity.set_not_configured(ctx).await;
|
||||
info!(ctx, "Can not watch {} folder, not set", folder_config);
|
||||
connection
|
||||
return connection
|
||||
.fake_idle(ctx, None, FolderMeaning::Unknown)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
// connect and fake idle if unable to connect
|
||||
@@ -509,10 +508,9 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
{
|
||||
warn!(ctx, "{:#}", err);
|
||||
connection.trigger_reconnect(ctx);
|
||||
connection
|
||||
return connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
if folder_config == Config::ConfiguredInboxFolder {
|
||||
@@ -536,7 +534,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
{
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
return;
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
|
||||
// Mark expired messages for deletion. Marked messages will be deleted from the server
|
||||
@@ -575,7 +573,7 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
{
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
return;
|
||||
return InterruptInfo::new(false);
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
@@ -593,56 +591,55 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_meaning: Folder
|
||||
connection.connectivity.set_connected(ctx).await;
|
||||
|
||||
ctx.emit_event(EventType::ImapInboxIdle);
|
||||
let Some(session) = connection.session.take() else {
|
||||
if let Some(session) = connection.session.take() {
|
||||
if !session.can_idle() {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
return connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
}
|
||||
|
||||
if ctx
|
||||
.get_config_bool(Config::DisableIdle)
|
||||
.await
|
||||
.context("Failed to get disable_idle config")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
return connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
}
|
||||
|
||||
info!(ctx, "IMAP session supports IDLE, using it.");
|
||||
match session
|
||||
.idle(
|
||||
ctx,
|
||||
connection.idle_interrupt_receiver.clone(),
|
||||
Some(watch_folder),
|
||||
)
|
||||
.await
|
||||
.context("idle")
|
||||
{
|
||||
Ok((session, info)) => {
|
||||
connection.session = Some(session);
|
||||
info
|
||||
}
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
InterruptInfo::new(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(ctx, "No IMAP session, going to fake idle.");
|
||||
connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
|
||||
if !session.can_idle() {
|
||||
info!(
|
||||
ctx,
|
||||
"IMAP session does not support IDLE, going to fake idle."
|
||||
);
|
||||
connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
if ctx
|
||||
.get_config_bool(Config::DisableIdle)
|
||||
.await
|
||||
.context("Failed to get disable_idle config")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
info!(ctx, "IMAP session supports IDLE, using it.");
|
||||
match session
|
||||
.idle(
|
||||
ctx,
|
||||
connection.idle_interrupt_receiver.clone(),
|
||||
&watch_folder,
|
||||
)
|
||||
.await
|
||||
.context("idle")
|
||||
{
|
||||
Ok(session) => {
|
||||
connection.session = Some(session);
|
||||
}
|
||||
Err(err) => {
|
||||
connection.trigger_reconnect(ctx);
|
||||
warn!(ctx, "{:#}", err);
|
||||
}
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,9 +706,8 @@ async fn smtp_loop(
|
||||
loop {
|
||||
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}", err);
|
||||
timeout = Some(timeout.unwrap_or(30));
|
||||
timeout = Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
|
||||
} else {
|
||||
timeout = None;
|
||||
let duration_until_can_send = ctx.ratelimit.read().await.until_can_send();
|
||||
if !duration_until_can_send.is_zero() {
|
||||
info!(
|
||||
@@ -719,9 +715,14 @@ async fn smtp_loop(
|
||||
"smtp got rate limited, waiting for {} until can send again",
|
||||
duration_to_str(duration_until_can_send)
|
||||
);
|
||||
tokio::time::sleep(duration_until_can_send).await;
|
||||
tokio::time::timeout(duration_until_can_send, async {
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
continue;
|
||||
}
|
||||
timeout = None;
|
||||
}
|
||||
|
||||
// Fake Idle
|
||||
@@ -735,23 +736,17 @@ async fn smtp_loop(
|
||||
// sending is retried (at the latest) after the timeout. If sending fails
|
||||
// again, we increase the timeout exponentially, in order not to do lots of
|
||||
// unnecessary retries.
|
||||
if let Some(t) = timeout {
|
||||
let now = tokio::time::Instant::now();
|
||||
if let Some(timeout) = timeout {
|
||||
info!(
|
||||
ctx,
|
||||
"smtp has messages to retry, planning to retry {} seconds later", t,
|
||||
"smtp has messages to retry, planning to retry {} seconds later", timeout
|
||||
);
|
||||
let duration = std::time::Duration::from_secs(t);
|
||||
let duration = std::time::Duration::from_secs(timeout);
|
||||
tokio::time::timeout(duration, async {
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let slept = (tokio::time::Instant::now() - now).as_secs();
|
||||
timeout = Some(cmp::max(
|
||||
t,
|
||||
slept.saturating_add(rand::thread_rng().gen_range((slept / 2)..=slept)),
|
||||
));
|
||||
} else {
|
||||
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default();
|
||||
@@ -865,24 +860,24 @@ impl Scheduler {
|
||||
|
||||
fn maybe_network(&self) {
|
||||
for b in self.boxes() {
|
||||
b.conn_state.interrupt();
|
||||
b.conn_state.interrupt(InterruptInfo::new(true));
|
||||
}
|
||||
self.interrupt_smtp();
|
||||
self.interrupt_smtp(InterruptInfo::new(true));
|
||||
}
|
||||
|
||||
fn maybe_network_lost(&self) {
|
||||
for b in self.boxes() {
|
||||
b.conn_state.interrupt();
|
||||
b.conn_state.interrupt(InterruptInfo::new(false));
|
||||
}
|
||||
self.interrupt_smtp();
|
||||
self.interrupt_smtp(InterruptInfo::new(false));
|
||||
}
|
||||
|
||||
fn interrupt_inbox(&self) {
|
||||
self.inbox.conn_state.interrupt();
|
||||
fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
self.inbox.conn_state.interrupt(info);
|
||||
}
|
||||
|
||||
fn interrupt_smtp(&self) {
|
||||
self.smtp.interrupt();
|
||||
fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
self.smtp.interrupt(info);
|
||||
}
|
||||
|
||||
fn interrupt_ephemeral_task(&self) {
|
||||
@@ -932,7 +927,7 @@ struct ConnectionState {
|
||||
/// Channel to interrupt the whole connection.
|
||||
stop_sender: Sender<()>,
|
||||
/// Channel to interrupt idle.
|
||||
idle_interrupt_sender: Sender<()>,
|
||||
idle_interrupt_sender: Sender<InterruptInfo>,
|
||||
/// Mutex to pass connectivity info between IMAP/SMTP threads and the API
|
||||
connectivity: ConnectivityStore,
|
||||
}
|
||||
@@ -948,9 +943,9 @@ impl ConnectionState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn interrupt(&self) {
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
// Use try_send to avoid blocking on interrupts.
|
||||
self.idle_interrupt_sender.try_send(()).ok();
|
||||
self.idle_interrupt_sender.try_send(info).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -982,8 +977,8 @@ impl SmtpConnectionState {
|
||||
}
|
||||
|
||||
/// Interrupt any form of idle.
|
||||
fn interrupt(&self) {
|
||||
self.state.interrupt();
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info);
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
@@ -996,7 +991,7 @@ impl SmtpConnectionState {
|
||||
struct SmtpConnectionHandlers {
|
||||
connection: Smtp,
|
||||
stop_receiver: Receiver<()>,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
idle_interrupt_receiver: Receiver<InterruptInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -1027,8 +1022,8 @@ impl ImapConnectionState {
|
||||
}
|
||||
|
||||
/// Interrupt any form of idle.
|
||||
fn interrupt(&self) {
|
||||
self.state.interrupt();
|
||||
fn interrupt(&self, info: InterruptInfo) {
|
||||
self.state.interrupt(info);
|
||||
}
|
||||
|
||||
/// Shutdown this connection completely.
|
||||
@@ -1043,3 +1038,14 @@ struct ImapConnectionHandlers {
|
||||
connection: Imap,
|
||||
stop_receiver: Receiver<()>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct InterruptInfo {
|
||||
pub probe_network: bool,
|
||||
}
|
||||
|
||||
impl InterruptInfo {
|
||||
pub fn new(probe_network: bool) -> Self {
|
||||
Self { probe_network }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use core::fmt;
|
||||
use std::cmp::min;
|
||||
use std::{iter::once, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use humansize::{format_size, BINARY};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
@@ -299,10 +299,6 @@ impl Context {
|
||||
.yellow {
|
||||
background-color: #fdc625;
|
||||
}
|
||||
.not-started-error {
|
||||
font-size: 2em;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>"#
|
||||
@@ -322,8 +318,7 @@ impl Context {
|
||||
sched.smtp.state.connectivity.clone(),
|
||||
),
|
||||
_ => {
|
||||
ret += "<div class=\"not-started-error\">Error: IO Not Started</div><p>Please report this issue to the app developer.</p>\n</body></html>\n";
|
||||
return Ok(ret);
|
||||
return Err(anyhow!("Not started"));
|
||||
}
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -8,8 +8,8 @@ use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::constants::{Blocked, Chattype};
|
||||
use crate::contact::{Contact, ContactId, Origin, VerifiedStatus};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::ensure_secret_key_exists;
|
||||
use crate::events::EventType;
|
||||
@@ -18,10 +18,9 @@ use crate::key::{load_self_public_key, DcKey, Fingerprint};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, SystemMessage};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::qr::check_qr;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::token;
|
||||
use crate::tools::time;
|
||||
|
||||
@@ -37,15 +36,17 @@ use crate::token::Namespace;
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
|
||||
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
|
||||
debug_assert!(
|
||||
progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
context.emit_event(EventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
});
|
||||
macro_rules! inviter_progress {
|
||||
($context:tt, $contact_id:expr, $progress:expr) => {
|
||||
assert!(
|
||||
$progress >= 0 && $progress <= 1000,
|
||||
"value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success"
|
||||
);
|
||||
$context.emit_event($crate::events::EventType::SecurejoinInviterProgress {
|
||||
contact_id: $contact_id,
|
||||
progress: $progress,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a Secure Join QR code.
|
||||
@@ -317,7 +318,7 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
info!(context, "Secure-join requested.",);
|
||||
|
||||
inviter_progress(context, contact_id, 300);
|
||||
inviter_progress!(context, contact_id, 300);
|
||||
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
@@ -414,9 +415,10 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?
|
||||
.get_addr()
|
||||
.to_owned();
|
||||
let fingerprint_found =
|
||||
mark_peer_as_verified(context, fingerprint.clone(), contact_addr).await?;
|
||||
if !fingerprint_found {
|
||||
if mark_peer_as_verified(context, fingerprint.clone(), contact_addr)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -426,11 +428,10 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
contact_id.regossip_keys(context).await?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::SecurejoinInvited).await?;
|
||||
info!(context, "Auth verified.",);
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress(context, contact_id, 600);
|
||||
inviter_progress!(context, contact_id, 600);
|
||||
if join_vg {
|
||||
// the vg-member-added message is special:
|
||||
// this is a normal Chat-Group-Member-Added message
|
||||
@@ -445,19 +446,17 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
match chat::get_chat_id_by_grpid(context, field_grpid).await? {
|
||||
Some((group_chat_id, _, _)) => {
|
||||
secure_connection_established(context, contact_id, group_chat_id).await?;
|
||||
chat::add_contact_to_chat_ex(
|
||||
context,
|
||||
Nosync,
|
||||
group_chat_id,
|
||||
contact_id,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
if let Err(err) =
|
||||
chat::add_contact_to_chat_ex(context, group_chat_id, contact_id, true)
|
||||
.await
|
||||
{
|
||||
error!(context, "failed to add contact: {}", err);
|
||||
}
|
||||
}
|
||||
None => bail!("Chat {} not found", &field_grpid),
|
||||
}
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
} else {
|
||||
// Alice -> Bob
|
||||
secure_connection_established(
|
||||
@@ -475,52 +474,53 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
.await
|
||||
.context("failed sending vc-contact-confirm message")?;
|
||||
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
|
||||
}
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
"vc-contact-confirm" => match BobState::from_db(&context.sql).await? {
|
||||
Some(bobstate) => bob::handle_contact_confirm(context, bobstate, mime_message).await,
|
||||
None => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
|
||||
"vg-member-added" => {
|
||||
let Some(member_added) = mime_message
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
else {
|
||||
warn!(
|
||||
context,
|
||||
"vg-member-added without Chat-Group-Member-Added header"
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
};
|
||||
if !context.is_self_addr(member_added).await? {
|
||||
info!(
|
||||
context,
|
||||
"Member {member_added} added by unrelated SecureJoin process"
|
||||
);
|
||||
return Ok(HandshakeMessage::Propagate);
|
||||
}
|
||||
"vg-member-added" | "vc-contact-confirm" => {
|
||||
/*=======================================================
|
||||
==== Bob - the joiner's side ====
|
||||
==== Step 7 in "Setup verified contact" protocol ====
|
||||
=======================================================*/
|
||||
match BobState::from_db(&context.sql).await? {
|
||||
Some(bobstate) => {
|
||||
bob::handle_contact_confirm(context, bobstate, mime_message).await
|
||||
}
|
||||
None => Ok(HandshakeMessage::Propagate),
|
||||
None => match join_vg {
|
||||
true => Ok(HandshakeMessage::Propagate),
|
||||
false => Ok(HandshakeMessage::Ignore),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
"vg-member-added-received" | "vc-contact-confirm-received" => {
|
||||
/*==========================================================
|
||||
==== Alice - the inviter side ====
|
||||
==== Step 8 in "Out-of-band verified groups" protocol ====
|
||||
==========================================================*/
|
||||
|
||||
Ok(HandshakeMessage::Done) // "Done" deletes the message
|
||||
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
|
||||
if contact.is_verified(context).await? == VerifiedStatus::Unverified {
|
||||
warn!(context, "{} invalid.", step);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if join_vg {
|
||||
let field_grpid = mime_message
|
||||
.get_header(HeaderDef::SecureJoinGroup)
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or_else(|| "");
|
||||
if let Err(err) = chat::get_chat_id_by_grpid(context, field_grpid).await {
|
||||
warn!(context, "Failed to lookup chat_id from grpid: {}", err);
|
||||
return Err(
|
||||
err.context(format!("Chat for group {} not found", &field_grpid))
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(HandshakeMessage::Ignore) // "Done" deletes the message and breaks multi-device
|
||||
} else {
|
||||
warn!(context, "{} invalid.", step);
|
||||
Ok(HandshakeMessage::Ignore)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
warn!(context, "invalid step: {}", step);
|
||||
@@ -614,18 +614,30 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
peerstate.set_verified(PeerstateKeyType::GossipKey, fingerprint, addr)?;
|
||||
if let Err(err) = peerstate.set_verified(
|
||||
PeerstateKeyType::GossipKey,
|
||||
fingerprint,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
addr,
|
||||
) {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
&format!("Could not mark peer as verified at step {step}: {err}"),
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
|
||||
ChatId::set_protection_for_contact(context, contact_id).await?;
|
||||
} else if let Some(fingerprint) =
|
||||
mime_message.get_header(HeaderDef::SecureJoinFingerprint)
|
||||
{
|
||||
// FIXME: Old versions of DC send this header instead of gossips. Remove this
|
||||
// eventually.
|
||||
let fingerprint = fingerprint.parse()?;
|
||||
let fingerprint_found = mark_peer_as_verified(
|
||||
if mark_peer_as_verified(
|
||||
context,
|
||||
fingerprint,
|
||||
Contact::get_by_id(context, contact_id)
|
||||
@@ -633,8 +645,9 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
.get_addr()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
if !fingerprint_found {
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
@@ -659,10 +672,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if step.as_str() == "vg-member-added" {
|
||||
inviter_progress(context, contact_id, 800);
|
||||
inviter_progress!(context, contact_id, 800);
|
||||
}
|
||||
if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" {
|
||||
inviter_progress(context, contact_id, 1000);
|
||||
inviter_progress!(context, contact_id, 1000);
|
||||
}
|
||||
if step.as_str() == "vg-request-with-auth" || step.as_str() == "vc-request-with-auth" {
|
||||
// This actually reflects what happens on the first device (which does the secure
|
||||
@@ -685,21 +698,24 @@ async fn secure_connection_established(
|
||||
contact_id: ContactId,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
if context
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
{
|
||||
let private_chat_id = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Yes)
|
||||
.await?
|
||||
.id;
|
||||
private_chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(contact_id),
|
||||
)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ == Chattype::Single {
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
time(),
|
||||
Some(contact_id),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
Ok(())
|
||||
@@ -721,21 +737,27 @@ async fn could_not_establish_secure_connection(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to mark peer with provided key fingerprint as verified.
|
||||
///
|
||||
/// Returns true if such key was found, false otherwise.
|
||||
async fn mark_peer_as_verified(
|
||||
context: &Context,
|
||||
fingerprint: Fingerprint,
|
||||
verifier: String,
|
||||
) -> Result<bool> {
|
||||
let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
peerstate.set_verified(PeerstateKeyType::PublicKey, fingerprint, verifier)?;
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
Ok(true)
|
||||
) -> Result<()> {
|
||||
if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? {
|
||||
if let Err(err) = peerstate.set_verified(
|
||||
PeerstateKeyType::PublicKey,
|
||||
fingerprint,
|
||||
PeerstateVerifiedStatus::BidirectVerified,
|
||||
verifier,
|
||||
) {
|
||||
error!(context, "Could not mark peer as verified: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
peerstate.prefer_encrypt = EncryptPreference::Mutual;
|
||||
peerstate.save_to_db(&context.sql).await.unwrap_or_default();
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("no peerstate in db for fingerprint {}", fingerprint.hex());
|
||||
}
|
||||
}
|
||||
|
||||
/* ******************************************************************************
|
||||
@@ -772,7 +794,6 @@ mod tests {
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactAddress;
|
||||
use crate::contact::VerifiedStatus;
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -922,10 +943,25 @@ mod tests {
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg = get_chat_msg(&alice, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(msg_ids.len(), 2);
|
||||
|
||||
let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap();
|
||||
assert!(msg0.is_info());
|
||||
assert!(msg0.get_text().contains("bob@example.net verified"));
|
||||
|
||||
let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap();
|
||||
assert!(msg1.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
assert_eq!(msg1.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
@@ -961,10 +997,24 @@ mod tests {
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
{
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg = get_chat_msg(&bob, chat.get_id(), 0, 1).await;
|
||||
assert!(msg.is_info());
|
||||
let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id())
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.filter_map(|item| match item {
|
||||
chat::ChatItem::Message { msg_id } => Some(msg_id),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap();
|
||||
assert!(msg0.is_info());
|
||||
assert!(msg0.get_text().contains("alice@example.org verified"));
|
||||
|
||||
let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap();
|
||||
assert!(msg1.is_info());
|
||||
let expected_text = chat_protection_enabled(&bob).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
assert_eq!(msg1.get_text(), expected_text);
|
||||
}
|
||||
|
||||
// Check Bob sent the final message
|
||||
@@ -1004,11 +1054,8 @@ mod tests {
|
||||
gossip_key_fingerprint: Some(alice_pubkey.fingerprint()),
|
||||
verified_key: None,
|
||||
verified_key_fingerprint: None,
|
||||
verifier: None,
|
||||
secondary_verified_key: None,
|
||||
secondary_verified_key_fingerprint: None,
|
||||
secondary_verifier: None,
|
||||
fingerprint_changed: false,
|
||||
verifier: None,
|
||||
};
|
||||
peerstate.save_to_db(&bob.ctx.sql).await?;
|
||||
|
||||
@@ -1263,11 +1310,11 @@ mod tests {
|
||||
);
|
||||
// There should be 3 messages in the chat:
|
||||
// - The ChatProtectionEnabled message
|
||||
// - bob@example.net verified
|
||||
// - You added member bob@example.net
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 0, 2).await;
|
||||
let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await;
|
||||
assert!(msg.is_info());
|
||||
let expected_text = chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.get_text(), expected_text);
|
||||
assert!(msg.get_text().contains("bob@example.net verified"));
|
||||
}
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
@@ -1303,6 +1350,27 @@ mod tests {
|
||||
println!("msg {msg_id} text: {text}");
|
||||
}
|
||||
}
|
||||
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter();
|
||||
loop {
|
||||
match msg_iter.next() {
|
||||
Some(chat::ChatItem::Message { msg_id }) => {
|
||||
let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap();
|
||||
let text = msg.get_text();
|
||||
match text.contains("alice@example.org verified") {
|
||||
true => {
|
||||
assert!(msg.is_info());
|
||||
break;
|
||||
}
|
||||
false => continue,
|
||||
}
|
||||
}
|
||||
Some(_) => continue,
|
||||
None => panic!("Verified message not found in Bob's group chat"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
|
||||
@@ -15,7 +15,6 @@ use crate::contact::Contact;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
@@ -111,7 +110,7 @@ pub(super) async fn handle_auth_required(
|
||||
/// Handles `vc-contact-confirm` and `vg-member-added` handshake messages.
|
||||
///
|
||||
/// # Bob - the joiner's side
|
||||
/// ## Step 7 in the "Setup Contact protocol"
|
||||
/// ## Step 4 in the "Setup Contact protocol"
|
||||
pub(super) async fn handle_contact_confirm(
|
||||
context: &Context,
|
||||
mut bobstate: BobState,
|
||||
@@ -132,7 +131,6 @@ pub(super) async fn handle_contact_confirm(
|
||||
// verify both contacts (this could be a bug/security issue, see
|
||||
// e.g. https://github.com/deltachat/deltachat-core-rust/issues/1177).
|
||||
bobstate.notify_peer_verified(context).await?;
|
||||
bobstate.emit_progress(context, JoinerProgress::Succeeded);
|
||||
Ok(retval)
|
||||
}
|
||||
Some(_) => {
|
||||
@@ -181,7 +179,7 @@ impl BobState {
|
||||
} => {
|
||||
let group_chat_id = match chat::get_chat_id_by_grpid(context, grpid).await? {
|
||||
Some((chat_id, _protected, _blocked)) => {
|
||||
chat_id.unblock_ex(context, Nosync).await?;
|
||||
chat_id.unblock(context).await?;
|
||||
chat_id
|
||||
}
|
||||
None => {
|
||||
@@ -222,13 +220,16 @@ impl BobState {
|
||||
/// This creates an info message in the chat being joined.
|
||||
async fn notify_peer_verified(&self, context: &Context) -> Result<()> {
|
||||
let contact = Contact::get_by_id(context, self.invite().contact_id()).await?;
|
||||
let msg = stock_str::contact_verified(context, &contact).await;
|
||||
let chat_id = self.joining_chat_id(context).await?;
|
||||
chat::add_info_msg(context, chat_id, &msg, time()).await?;
|
||||
|
||||
if context
|
||||
.get_config_bool(Config::VerifiedOneOnOneChats)
|
||||
.await?
|
||||
&& chat_id == self.alice_chat()
|
||||
{
|
||||
self.alice_chat()
|
||||
chat_id
|
||||
.set_protection(
|
||||
context,
|
||||
ProtectionStatus::Protected,
|
||||
@@ -254,8 +255,8 @@ enum JoinerProgress {
|
||||
///
|
||||
/// Typically shows as "alice@addr verified, introducing myself."
|
||||
RequestWithAuthSent,
|
||||
/// Completed securejoin.
|
||||
Succeeded,
|
||||
// /// Completed securejoin.
|
||||
// Succeeded,
|
||||
}
|
||||
|
||||
impl From<JoinerProgress> for usize {
|
||||
@@ -263,7 +264,7 @@ impl From<JoinerProgress> for usize {
|
||||
match progress {
|
||||
JoinerProgress::Error => 0,
|
||||
JoinerProgress::RequestWithAuthSent => 400,
|
||||
JoinerProgress::Succeeded => 1000,
|
||||
// JoinerProgress::Succeeded => 1000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! The [`BobState`] is only directly used to initially create it when starting the
|
||||
//! protocol.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Error, Result};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use super::qrinvite::QrInvite;
|
||||
@@ -335,6 +335,36 @@ impl BobState {
|
||||
context,
|
||||
"Bob Step 7 - handling vc-contact-confirm/vg-member-added message"
|
||||
);
|
||||
let vg_expect_encrypted = match self.invite {
|
||||
QrInvite::Contact { .. } => {
|
||||
// setup-contact is always encrypted
|
||||
true
|
||||
}
|
||||
QrInvite::Group { ref grpid, .. } => {
|
||||
// This is buggy, result will always be
|
||||
// false since the group is created by receive_imf for
|
||||
// the very handshake message we're handling now. But
|
||||
// only after we have returned. It does not impact
|
||||
// the security invariants of secure-join however.
|
||||
|
||||
chat::get_chat_id_by_grpid(context, grpid)
|
||||
.await?
|
||||
.map_or(false, |(_chat_id, is_protected, _blocked)| is_protected)
|
||||
// when joining a non-verified group
|
||||
// the vg-member-added message may be unencrypted
|
||||
// when not all group members have keys or prefer encryption.
|
||||
// So only expect encryption if this is a verified group
|
||||
}
|
||||
};
|
||||
if vg_expect_encrypted
|
||||
&& !encrypted_and_signed(context, mime_message, Some(self.invite.fingerprint()))
|
||||
{
|
||||
self.update_next(&context.sql, SecureJoinStep::Terminated)
|
||||
.await?;
|
||||
return Ok(Some(BobHandshakeStage::Terminated(
|
||||
"Contact confirm message not encrypted",
|
||||
)));
|
||||
}
|
||||
mark_peer_as_verified(
|
||||
context,
|
||||
self.invite.fingerprint().clone(),
|
||||
@@ -345,6 +375,17 @@ impl BobState {
|
||||
.await?;
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
|
||||
if let QrInvite::Group { .. } = self.invite {
|
||||
let member_added = mime_message
|
||||
.get_header(HeaderDef::ChatGroupMemberAdded)
|
||||
.map(|s| s.as_str())
|
||||
.ok_or_else(|| Error::msg("Missing Chat-Group-Member-Added header"))?;
|
||||
if !context.is_self_addr(member_added).await? {
|
||||
info!(context, "Message belongs to a different handshake (scaled up contact anyway to allow creation of group).");
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
self.send_handshake_message(context, BobHandshakeMsg::ContactConfirmReceived)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
|
||||
@@ -750,19 +750,6 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 104 {
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE acpeerstates
|
||||
ADD COLUMN secondary_verified_key;
|
||||
ALTER TABLE acpeerstates
|
||||
ADD COLUMN secondary_verified_key_fingerprint TEXT DEFAULT '';
|
||||
ALTER TABLE acpeerstates
|
||||
ADD COLUMN secondary_verifier TEXT DEFAULT ''",
|
||||
104,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -413,12 +413,6 @@ pub enum StockMessage {
|
||||
|
||||
#[strum(props(fallback = "%1$s sent a message from another device."))]
|
||||
ChatProtectionDisabled = 171,
|
||||
|
||||
#[strum(props(fallback = "Others will only see this group after you sent a first message."))]
|
||||
NewGroupSendFirstMessage = 172,
|
||||
|
||||
#[strum(props(fallback = "Member %1$s added."))]
|
||||
MsgAddMember = 173,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -606,7 +600,6 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId
|
||||
}
|
||||
|
||||
/// Stock string: `I added member %1$s.`.
|
||||
/// This one is for sending in group chats.
|
||||
///
|
||||
/// The `added_member_addr` parameter should be an email address and is looked up in the
|
||||
/// contacts to combine with the authorized display name.
|
||||
@@ -641,11 +634,7 @@ pub(crate) async fn msg_add_member_local(
|
||||
.unwrap_or_else(|_| addr.to_string()),
|
||||
_ => addr.to_string(),
|
||||
};
|
||||
if by_contact == ContactId::UNDEFINED {
|
||||
translated(context, StockMessage::MsgAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
} else if by_contact == ContactId::SELF {
|
||||
if by_contact == ContactId::SELF {
|
||||
translated(context, StockMessage::MsgYouAddMember)
|
||||
.await
|
||||
.replace1(whom)
|
||||
@@ -824,7 +813,6 @@ pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &C
|
||||
}
|
||||
|
||||
/// Stock string: `%1$s verified.`.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
|
||||
let addr = &contact.get_name_n_addr();
|
||||
translated(context, StockMessage::ContactVerified)
|
||||
@@ -1296,11 +1284,6 @@ pub(crate) async fn aeap_explanation_and_link(
|
||||
.replace2(new_addr)
|
||||
}
|
||||
|
||||
/// Stock string: `Others will only see this group after you sent a first message.`.
|
||||
pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
|
||||
translated(context, StockMessage::NewGroupSendFirstMessage).await
|
||||
}
|
||||
|
||||
/// Text to put in the [`Qr::Backup`] rendered SVG image.
|
||||
///
|
||||
/// The default is "Scan to set up second device for <account name (account addr)>". The
|
||||
|
||||
217
src/sync.rs
217
src/sync.rs
@@ -5,35 +5,18 @@ use lettre_email::mime::{self};
|
||||
use lettre_email::PartBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::{self, Chat, ChatId};
|
||||
use crate::chat::{Chat, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::sync::SyncData::{AddQrToken, AlterChat, DeleteQrToken};
|
||||
use crate::sync::SyncData::{AddQrToken, DeleteQrToken};
|
||||
use crate::token::Namespace;
|
||||
use crate::tools::time;
|
||||
use crate::{stock_str, token};
|
||||
|
||||
/// Whether to send device sync messages. Aimed for usage in the internal API.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Sync {
|
||||
Nosync,
|
||||
Sync,
|
||||
}
|
||||
|
||||
impl From<Sync> for bool {
|
||||
fn from(sync: Sync) -> bool {
|
||||
match sync {
|
||||
Sync::Nosync => false,
|
||||
Sync::Sync => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::{chat, stock_str, token};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct QrTokenData {
|
||||
@@ -46,24 +29,12 @@ pub(crate) struct QrTokenData {
|
||||
pub(crate) enum SyncData {
|
||||
AddQrToken(QrTokenData),
|
||||
DeleteQrToken(QrTokenData),
|
||||
AlterChat {
|
||||
id: chat::SyncId,
|
||||
action: chat::SyncAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum SyncDataOrUnknown {
|
||||
SyncData(SyncData),
|
||||
Unknown(serde_json::Value),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SyncItem {
|
||||
timestamp: i64,
|
||||
|
||||
data: SyncDataOrUnknown,
|
||||
data: SyncData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -71,17 +42,8 @@ pub(crate) struct SyncItems {
|
||||
items: Vec<SyncItem>,
|
||||
}
|
||||
|
||||
impl From<SyncData> for SyncDataOrUnknown {
|
||||
fn from(sync_data: SyncData) -> Self {
|
||||
Self::SyncData(sync_data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Adds an item to the list of items that should be synchronized to other devices.
|
||||
///
|
||||
/// NB: Private and `pub(crate)` functions shouldn't call this unless `Sync::Sync` is explicitly
|
||||
/// passed to them. This way it's always clear whether the code performs synchronisation.
|
||||
pub(crate) async fn add_sync_item(&self, data: SyncData) -> Result<()> {
|
||||
self.add_sync_item_with_timestamp(data, time()).await
|
||||
}
|
||||
@@ -93,10 +55,7 @@ impl Context {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let item = SyncItem {
|
||||
timestamp,
|
||||
data: data.into(),
|
||||
};
|
||||
let item = SyncItem { timestamp, data };
|
||||
let item = serde_json::to_string(&item)?;
|
||||
self.sql
|
||||
.execute("INSERT INTO multi_device_sync (item) VALUES(?);", (item,))
|
||||
@@ -245,71 +204,58 @@ impl Context {
|
||||
Ok(sync_items)
|
||||
}
|
||||
|
||||
/// Executes sync items sent by other device.
|
||||
/// Execute sync items.
|
||||
///
|
||||
/// CAVE: When changing the code to handle other sync items,
|
||||
/// take care that does not result in calls to `add_sync_item()`
|
||||
/// as otherwise we would add in a dead-loop between two devices
|
||||
/// sending message back and forth.
|
||||
///
|
||||
/// If an error is returned, the caller shall not try over because some sync items could be
|
||||
/// already executed. Sync items are considered independent and executed in the given order but
|
||||
/// regardless of whether executing of the previous items succeeded.
|
||||
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) {
|
||||
/// If an error is returned, the caller shall not try over.
|
||||
/// Therefore, errors should only be returned on database errors or so.
|
||||
/// If eg. just an item cannot be deleted,
|
||||
/// that should not hold off the other items to be executed.
|
||||
pub(crate) async fn execute_sync_items(&self, items: &SyncItems) -> Result<()> {
|
||||
info!(self, "executing {} sync item(s)", items.items.len());
|
||||
for item in &items.items {
|
||||
match &item.data {
|
||||
SyncDataOrUnknown::SyncData(data) => match data {
|
||||
AddQrToken(token) => self.add_qr_token(token).await,
|
||||
DeleteQrToken(token) => self.delete_qr_token(token).await,
|
||||
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
|
||||
},
|
||||
SyncDataOrUnknown::Unknown(data) => {
|
||||
warn!(self, "Ignored unknown sync item: {data}.");
|
||||
Ok(())
|
||||
AddQrToken(token) => {
|
||||
let chat_id = if let Some(grpid) = &token.grpid {
|
||||
if let Some((chat_id, _, _)) =
|
||||
chat::get_chat_id_by_grpid(self, grpid).await?
|
||||
{
|
||||
Some(chat_id)
|
||||
} else {
|
||||
warn!(
|
||||
self,
|
||||
"Ignoring token for nonexistent/deleted group '{}'.", grpid
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber)
|
||||
.await?;
|
||||
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
|
||||
}
|
||||
DeleteQrToken(token) => {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
}
|
||||
}
|
||||
.log_err(self)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
async fn add_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
let chat_id = if let Some(grpid) = &token.grpid {
|
||||
if let Some((chat_id, _, _)) = chat::get_chat_id_by_grpid(self, grpid).await? {
|
||||
Some(chat_id)
|
||||
} else {
|
||||
warn!(
|
||||
self,
|
||||
"Ignoring token for nonexistent/deleted group '{}'.", grpid
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
token::save(self, Namespace::InviteNumber, chat_id, &token.invitenumber).await?;
|
||||
token::save(self, Namespace::Auth, chat_id, &token.auth).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> {
|
||||
token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?;
|
||||
token::delete(self, Namespace::Auth, &token.auth).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::Chat;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::contact::{Contact, Origin};
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::token::Namespace;
|
||||
|
||||
@@ -331,20 +277,6 @@ mod tests {
|
||||
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
// Having one test on `SyncData::AlterChat` is sufficient here as
|
||||
// `chat::SyncAction::SetMuted` introduces enums inside items and `SystemTime`. Let's avoid
|
||||
// in-depth testing of the serialiser here which is an external crate.
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AlterChat {
|
||||
id: chat::SyncId::ContactAddr("bob@example.net".to_string()),
|
||||
action: chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999),
|
||||
)),
|
||||
},
|
||||
1631781315,
|
||||
)
|
||||
.await?;
|
||||
|
||||
t.add_sync_item_with_timestamp(
|
||||
SyncData::AddQrToken(QrTokenData {
|
||||
invitenumber: "testinvite".to_string(),
|
||||
@@ -368,7 +300,6 @@ mod tests {
|
||||
assert_eq!(
|
||||
serialized,
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"testinvite","auth":"testauth","grpid":"group123"}}},
|
||||
{"timestamp":1631781317,"data":{"DeleteQrToken":{"invitenumber":"123!?\":.;{}","auth":"456","grpid":null}}}
|
||||
]}"#
|
||||
@@ -379,7 +310,7 @@ mod tests {
|
||||
assert!(t.build_sync_json().await?.is_none());
|
||||
|
||||
let sync_items = t.parse_sync_items(serialized)?;
|
||||
assert_eq!(sync_items.items.len(), 3);
|
||||
assert_eq!(sync_items.items.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -406,44 +337,36 @@ mod tests {
|
||||
|
||||
assert!(t.parse_sync_items(r#"{"badname":[]}"#.to_string()).is_err());
|
||||
|
||||
for bad_item_example in [
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#,
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#, // `123` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#, // `true` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#, // `[]` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#, // `{}` is invalid for `String`
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#, // missing field
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#, // Unknown enum value
|
||||
] {
|
||||
let sync_items = t.parse_sync_items(bad_item_example.to_string()).unwrap();
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
assert!(matches!(sync_items.items[0].timestamp, 1631781316));
|
||||
assert!(matches!(
|
||||
sync_items.items[0].data,
|
||||
SyncDataOrUnknown::Unknown(_)
|
||||
));
|
||||
}
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"BadItem":{"invitenumber":"in","auth":"a","grpid":null}}}]}"#
|
||||
.to_string(),
|
||||
)
|
||||
.is_err());
|
||||
|
||||
// Test enums inside items and SystemTime
|
||||
let sync_items = t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":{"SetMuted":{"Until":{"secs_since_epoch":42,"nanos_since_epoch":999000000}}}}}}]}"#.to_string(),
|
||||
)?;
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
let SyncDataOrUnknown::SyncData(AlterChat { id, action }) =
|
||||
&sync_items.items.get(0).unwrap().data
|
||||
else {
|
||||
bail!("bad item");
|
||||
};
|
||||
assert_eq!(
|
||||
*id,
|
||||
chat::SyncId::ContactAddr("bob@example.net".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
*action,
|
||||
chat::SyncAction::SetMuted(chat::MuteDuration::Until(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_millis(42999)
|
||||
))
|
||||
);
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":123}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `123` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":true}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `true` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":[]}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `[]` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":{}}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // `{}` is invalid for `String`
|
||||
|
||||
assert!(t.parse_sync_items(
|
||||
r#"{"items":[{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","grpid":null}}}]}"#.to_string(),
|
||||
)
|
||||
.is_err()); // missing field
|
||||
|
||||
// empty item list is okay
|
||||
assert_eq!(
|
||||
@@ -473,9 +396,7 @@ mod tests {
|
||||
)?;
|
||||
|
||||
assert_eq!(sync_items.items.len(), 1);
|
||||
if let SyncDataOrUnknown::SyncData(AddQrToken(token)) =
|
||||
&sync_items.items.get(0).unwrap().data
|
||||
{
|
||||
if let AddQrToken(token) = &sync_items.items.get(0).unwrap().data {
|
||||
assert_eq!(token.invitenumber, "in");
|
||||
assert_eq!(token.auth, "yip");
|
||||
assert_eq!(token.grpid, None);
|
||||
@@ -502,7 +423,6 @@ mod tests {
|
||||
let sync_items = t
|
||||
.parse_sync_items(
|
||||
r#"{"items":[
|
||||
{"timestamp":1631781315,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Block"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"yip-in","auth":"a"}}},
|
||||
{"timestamp":1631781316,"data":{"DeleteQrToken":{"invitenumber":"in","auth":"delete unexistent, shall continue"}}},
|
||||
{"timestamp":1631781316,"data":{"AddQrToken":{"invitenumber":"in","auth":"yip-auth"}}},
|
||||
@@ -513,13 +433,8 @@ mod tests {
|
||||
.to_string(),
|
||||
)
|
||||
?;
|
||||
t.execute_sync_items(&sync_items).await;
|
||||
t.execute_sync_items(&sync_items).await?;
|
||||
|
||||
assert!(
|
||||
Contact::lookup_id_by_addr(&t, "bob@example.net", Origin::Unknown)
|
||||
.await?
|
||||
.is_none()
|
||||
);
|
||||
assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await);
|
||||
assert!(token::exists(&t, Namespace::Auth, "yip-auth").await);
|
||||
assert!(!token::exists(&t, Namespace::Auth, "non-existent").await);
|
||||
|
||||
@@ -22,8 +22,8 @@ use tokio::sync::RwLock;
|
||||
use tokio::{fs, task};
|
||||
|
||||
use crate::chat::{
|
||||
self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, ChatIdBlocked,
|
||||
MessageListOptions, ProtectionStatus,
|
||||
self, add_to_chat_contacts_table, create_group_chat, Chat, ChatId, MessageListOptions,
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
@@ -374,11 +374,6 @@ impl TestContext {
|
||||
}
|
||||
});
|
||||
|
||||
ctx.set_config(Config::SkipStartMessages, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(Config::SyncMsgs, Some("0")).await.unwrap();
|
||||
|
||||
Self {
|
||||
ctx,
|
||||
dir,
|
||||
@@ -514,7 +509,12 @@ impl TestContext {
|
||||
.await
|
||||
.expect("receive_imf() seems not to have added a new message to the db");
|
||||
|
||||
let msg = Message::load_from_db(self, *received.msg_ids.last().unwrap())
|
||||
assert_eq!(
|
||||
received.msg_ids.len(),
|
||||
1,
|
||||
"recv_msg() can currently only receive messages with exactly one part"
|
||||
);
|
||||
let msg = Message::load_from_db(self, received.msg_ids[0])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -592,17 +592,14 @@ impl TestContext {
|
||||
}
|
||||
|
||||
/// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist.
|
||||
/// May return a blocked chat.
|
||||
///
|
||||
/// This first creates a contact using the configured details on the other account, then
|
||||
/// gets the 1:1 chat with this contact.
|
||||
pub async fn get_chat(&self, other: &TestContext) -> Chat {
|
||||
let contact = self.add_or_lookup_contact(other).await;
|
||||
|
||||
let chat_id = ChatIdBlocked::lookup_by_contact(&self.ctx, contact.id)
|
||||
let chat_id = ChatId::lookup_by_contact(&self.ctx, contact.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|chat_id_blocked| chat_id_blocked.id)
|
||||
.expect(
|
||||
"There is no chat with this contact. \
|
||||
Hint: Use create_chat() instead of get_chat() if this is expected.",
|
||||
@@ -714,9 +711,7 @@ impl TestContext {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let Ok(sel_chat) = Chat::load_from_db(self, chat_id).await else {
|
||||
return String::from("Can't load chat\n");
|
||||
};
|
||||
let sel_chat = Chat::load_from_db(self, chat_id).await.unwrap();
|
||||
let members = chat::get_chat_contacts(self, sel_chat.id).await.unwrap();
|
||||
let subtitle = if sel_chat.is_device_talk() {
|
||||
"device-talk".to_string()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chat::{Chat, ProtectionStatus};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_GCL_FOR_FORWARDING;
|
||||
@@ -126,22 +126,24 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
VerifiedStatus::BidirectVerified
|
||||
);
|
||||
|
||||
// Alice should have a hidden protected chat with Fiona
|
||||
// As soon as Alice creates a chat with Fiona, it should directly be protected
|
||||
{
|
||||
let chat = alice.get_chat(&fiona).await;
|
||||
let chat = alice.create_chat(&fiona).await;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg = get_chat_msg(&alice, chat.id, 0, 1).await;
|
||||
let msg = alice.get_last_msg().await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&alice).await;
|
||||
assert_eq!(msg.text, expected_text);
|
||||
}
|
||||
|
||||
// Fiona should have a hidden protected chat with Alice
|
||||
// Fiona should also see the chat as protected
|
||||
{
|
||||
let chat = fiona.get_chat(&alice).await;
|
||||
let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await;
|
||||
let alice_fiona_id = rcvd.chat_id;
|
||||
let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 1).await;
|
||||
let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await;
|
||||
let expected_text = stock_str::chat_protection_enabled(&fiona).await;
|
||||
assert_eq!(msg0.text, expected_text);
|
||||
}
|
||||
@@ -163,15 +165,6 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_protection_broken());
|
||||
|
||||
let msg1 = get_chat_msg(&alice, chat.id, 0, 3).await;
|
||||
assert_eq!(msg1.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 1, 3).await;
|
||||
assert_eq!(msg2.get_info_type(), SystemMessage::ChatProtectionDisabled);
|
||||
|
||||
let msg2 = get_chat_msg(&alice, chat.id, 2, 3).await;
|
||||
assert_eq!(msg2.text, "I have a new device");
|
||||
|
||||
// After recreating the chat, it should still be unprotected
|
||||
chat.id.delete(&alice).await?;
|
||||
|
||||
@@ -712,39 +705,6 @@ async fn test_break_protection_then_verify_again() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test for the following bug:
|
||||
///
|
||||
/// - Scan your chat partner's QR Code
|
||||
/// - They change devices
|
||||
/// - Scan their QR code again
|
||||
///
|
||||
/// -> The re-verification fails.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_verify_then_verify_again() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
enable_verified_oneonone_chats(&[&alice, &bob]).await;
|
||||
|
||||
mark_as_verified(&alice, &bob).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
|
||||
alice.create_chat(&bob).await;
|
||||
assert_verified(&alice, &bob, ProtectionStatus::Protected).await;
|
||||
|
||||
tcm.section("Bob reinstalls DC");
|
||||
drop(bob);
|
||||
let bob_new = tcm.unconfigured().await;
|
||||
enable_verified_oneonone_chats(&[&bob_new]).await;
|
||||
bob_new.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob_new).await?;
|
||||
|
||||
tcm.execute_securejoin(&bob_new, &alice).await;
|
||||
assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Regression test:
|
||||
/// - Verify a contact
|
||||
/// - The contact stops using DC and sends a message from a classical MUA instead
|
||||
|
||||
@@ -37,6 +37,7 @@ use crate::mimefactory::wrapped_base64_encode;
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::tools::strip_rtlo_characters;
|
||||
use crate::tools::{create_smeared_timestamp, get_abs_path};
|
||||
|
||||
@@ -484,7 +485,9 @@ impl Context {
|
||||
DO UPDATE SET last_serial=excluded.last_serial, descr=excluded.descr",
|
||||
(instance.id, status_update_serial, status_update_serial, descr),
|
||||
).await?;
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
self.scheduler
|
||||
.interrupt_smtp(InterruptInfo::new(false))
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#11): I created a group [FRESH]
|
||||
Msg#11: (Contact#Contact#11): Member Fiona (fiona@example.net) added by alice@example.org. [FRESH][INFO]
|
||||
Msg#12: Me (Contact#Contact#Self): You removed member Fiona (fiona@example.net). [INFO] o
|
||||
Msg#13: info (Contact#Contact#Info): Member Fiona (fiona@example.net) added. [NOTICED][INFO]
|
||||
Msg#14: (Contact#Contact#11): Welcome, Fiona! [FRESH]
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -1,7 +0,0 @@
|
||||
Group#Chat#10: Group chat [4 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
|
||||
Msg#12: info (Contact#Contact#Info): Member Me (bob@example.net) added. [NOTICED][INFO]
|
||||
Msg#13: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
|
||||
--------------------------------------------------------------------------------
|
||||
80
test-data/message/thunderbird_encrypted.eml
Normal file
80
test-data/message/thunderbird_encrypted.eml
Normal file
@@ -0,0 +1,80 @@
|
||||
From - Fri, 13 Oct 2023 05:36:46 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <19925052-21a0-530a-cb93-dd05d227ece2@example.org>
|
||||
Date: Fri, 13 Oct 2023 02:36:46 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------RT0xHdRXXmnS0UAhLpOJ60ps"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------RT0xHdRXXmnS0UAhLpOJ60ps
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------RT0xHdRXXmnS0UAhLpOJ60ps
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
PbZLmbNpqVOJMuSJZZA=
|
||||
=JH2e
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------RT0xHdRXXmnS0UAhLpOJ60ps--
|
||||
@@ -1,80 +0,0 @@
|
||||
From - Thu, 02 Nov 2023 05:25:44 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <619b4cc5-1b76-2bfc-02ee-631b774bcb4a@example.org>
|
||||
Date: Thu, 2 Nov 2023 02:25:44 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------ODMSY6iRxlSk4uG4zIxjzLbw"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
d+EkgKXHfQVfohZhSg==
|
||||
=u1yY
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------ODMSY6iRxlSk4uG4zIxjzLbw--
|
||||
@@ -1,165 +0,0 @@
|
||||
From - Thu, 02 Nov 2023 05:20:27 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <956fad6d-206e-67af-2443-3ea5819418ff@example.org>
|
||||
Date: Thu, 2 Nov 2023 02:20:27 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
Autocrypt: addr=alice@example.org; keydata=
|
||||
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
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------vm4cgC4UBxQhCLFg6Vv0nrTd"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=UKyr
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------vm4cgC4UBxQhCLFg6Vv0nrTd--
|
||||
165
test-data/message/thunderbird_encrypted_with_pubkey_attached.eml
Normal file
165
test-data/message/thunderbird_encrypted_with_pubkey_attached.eml
Normal file
@@ -0,0 +1,165 @@
|
||||
From - Tue, 31 Oct 2023 05:04:31 GMT
|
||||
X-Mozilla-Status: 0801
|
||||
X-Mozilla-Status2: 00000000
|
||||
Message-ID: <6e2cfe71-c22f-14ef-b900-5b3b803e1d1c@example.org>
|
||||
Date: Tue, 31 Oct 2023 02:04:31 -0300
|
||||
MIME-Version: 1.0
|
||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101
|
||||
Thunderbird/102.13.0
|
||||
Content-Language: en-US
|
||||
To: bob@example.net
|
||||
From: Alice <alice@example.org>
|
||||
Autocrypt: addr=alice@example.org; keydata=
|
||||
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
|
||||
/qMLjKwBpKEd/w==
|
||||
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
|
||||
attachmentreminder=0; deliveryformat=0
|
||||
X-Identity-Key: id3
|
||||
Fcc: imap://alice%40example.org@in.example.org/Sent
|
||||
Subject: ...
|
||||
Content-Type: multipart/encrypted;
|
||||
protocol="application/pgp-encrypted";
|
||||
boundary="------------KGk5Y8jKHbRMNVbglgZHajLT"
|
||||
|
||||
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
|
||||
--------------KGk5Y8jKHbRMNVbglgZHajLT
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
--------------KGk5Y8jKHbRMNVbglgZHajLT
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc"
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
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
|
||||
=Rw/C
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--------------KGk5Y8jKHbRMNVbglgZHajLT--
|
||||
Reference in New Issue
Block a user