Compare commits

..

2 Commits

Author SHA1 Message Date
iequidoo
7a697d4316 try to reproduce (#4707) 2023-10-31 02:12:33 -03:00
iequidoo
dfdedf073d try to reproduce (#4707) 2023-10-31 02:11:30 -03:00
86 changed files with 1638 additions and 3576 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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"] }

View File

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

View File

@@ -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
/**
* @}
*/

View File

@@ -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]

View File

@@ -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"

View File

@@ -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.
///

View File

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

View File

@@ -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(),

View File

@@ -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));

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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 = {

View File

@@ -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"] }

View File

@@ -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, ' ');

View File

@@ -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.

View File

@@ -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(

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -30,4 +30,4 @@ commands =
[pytest]
timeout = 300
log_cli = true
log_level = debug
log_level = info

View File

@@ -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"

View File

@@ -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" },

View File

@@ -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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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)
"""

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -1 +1 @@
2023-11-15
2023-10-29

View File

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

View File

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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??;

View File

@@ -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(())

View File

@@ -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",

View File

@@ -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(())

View File

@@ -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)]
}

View File

@@ -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,
},

View File

@@ -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())]
);
}
}

View File

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

View File

@@ -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"),

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)?;

View File

@@ -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() {

View File

@@ -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");

View File

@@ -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")));
}
};

View File

@@ -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);

View File

@@ -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());

View File

@@ -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(),

View File

@@ -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.

View File

@@ -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(())
}

View File

@@ -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 }
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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,
}
}
}

View File

@@ -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(|_| {

View File

@@ -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?

View File

@@ -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

View File

@@ -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);

View File

@@ -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()

View File

@@ -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

View File

@@ -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(())
}

View File

@@ -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]
--------------------------------------------------------------------------------

View File

@@ -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]
--------------------------------------------------------------------------------

View 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--

View File

@@ -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--

View File

@@ -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--

View 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=
xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E/jFiKZWj
1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37mdw+DKs0YvNIlc+A
RjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsrtS2JgJ+tLSYUvNJeMJXm/cDL
XKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJF0De11/7G62dHNHuhmtgRLsTN4Q372Q9
KNdYEFLHaN91jEzyD/+aHNskATxtcGhppI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yf
VAyA69t5fctQRb4+bTwL+sS9KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiL
vYUfdNJstAqvLf04mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3E
q8e6TbOY7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT
AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcFFQgJCgsF
FgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nnYq5pOuHGUndZ7jYK
cOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdtHoulEG4RPGVboDaY9tuMOL3/
GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJ
x15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQl
nfISJ17GBLmH1YxmPPZ3CRHC6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJ
YskyNndtv0iaNRT7YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ
8myMwA+ybfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw
eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVFuPGrSdRU
06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXFpgtZUNV3iGOSOcSE
LldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6FdKYxVd4NBVH9abZ7t8Tm4qC
urZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBgU3q5W+wrtZ56kI9mxJec62KHpyLZ0rTE
xEAeVbChUJOo11vUtJfTrDhI6lhqyr72o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTS
UxOz60xNggEfDVtfgfjBZrBbiHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h
8l019MYmGadpQgbuA4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfu
g2fuRf258Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY
AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXwByRZ5Hri
EOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxCjWrnUDvvJEwP1W3j
UXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ9eLjEM++rbOD4vWbYXRwaDiH
LetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvgPxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYT
XhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+bQw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajj
Wy7b9TuT38t1HArv4m/LyVuBHiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pK
MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa
j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa
/qMLjKwBpKEd/w==
X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0;
attachmentreminder=0; deliveryformat=0
X-Identity-Key: id3
Fcc: imap://alice%40example.org@in.example.org/Sent
Subject: ...
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--