Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
99a57c7084 refactor(deltachat-jsonrpc): enable verbatimModuleSyntax 2023-11-13 09:24:55 +00:00
47 changed files with 603 additions and 834 deletions

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.74.0
RUSTUP_TOOLCHAIN: 1.73.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -76,11 +76,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.74.0
rust: 1.73.0
- os: windows-latest
rust: 1.74.0
rust: 1.73.0
- os: macos-latest
rust: 1.74.0
rust: 1.73.0
# Minimum Supported Rust Version = 1.70.0
- os: ubuntu-latest

View File

@@ -1,95 +1,5 @@
# Changelog
## [1.131.5] - 2023-11-20
### API-Changes
- deltachat-rpc-client: Add `Message.get_sender_contact()`.
- Turn `ContactAddress` into an owned type.
### Features / Changes
- Lowercase addresses in Autocrypt and Autocrypt-Gossip headers.
- Lowercase the address in member added/removed messages.
- Lowercase `addr` when it is set.
- Do not replace the message with an error in square brackets when the sender is not a member of the protected group.
### Fixes
- `Chat::sync_contacts()`: Fetch contact addresses in a single query.
- `Chat::rename_ex()`: Sync improved chat name to other devices.
- Recognize `Chat-Group-Member-Added` of self case-insensitively.
- Compare verifier addr to peerstate addr case-insensitively.
### Tests
- Port [Secure-Join](https://securejoin.readthedocs.io/) tests to JSON-RPC.
### CI
- Test with Rust 1.74.
## [1.131.4] - 2023-11-16
### Documentation
- Document DC_DOWNLOAD_UNDECIPHERABLE.
### Fixes
- Always add "Member added" as system message.
## [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
@@ -3246,9 +3156,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[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
[1.131.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.3...v1.131.4
[1.131.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.131.4...v1.131.5

View File

@@ -86,11 +86,6 @@ For example:
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.

88
Cargo.lock generated
View File

@@ -203,7 +203,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37875bd9915b7d67c2f117ea2c30a0989874d0b2cb694fe25403c85763c0c9e"
dependencies = [
"concurrent-queue",
"event-listener 3.1.0",
"event-listener 3.0.1",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
@@ -224,9 +224,9 @@ dependencies = [
[[package]]
name = "async-imap"
version = "0.9.4"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d736a74edf6c327b53dd9c932eae834253470ac5f0c55770e7e133bcbf986362"
checksum = "2e542b1682eba6b85a721daef0c58e79319ffd0c678565c07ac57c8071c548b5"
dependencies = [
"async-channel 2.1.0",
"base64 0.21.5",
@@ -931,9 +931,9 @@ dependencies = [
[[package]]
name = "crypto-bigint"
version = "0.5.4"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a"
checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124"
dependencies = [
"generic-array",
"rand_core 0.6.4",
@@ -1087,7 +1087,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.131.5"
version = "1.130.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1165,7 +1165,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "1.131.5"
version = "1.130.0"
dependencies = [
"anyhow",
"async-channel 2.1.0",
@@ -1189,7 +1189,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "1.131.5"
version = "1.130.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1204,7 +1204,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "1.131.5"
version = "1.130.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.131.5"
version = "1.130.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1583,7 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914"
dependencies = [
"base16ct 0.2.0",
"crypto-bigint 0.5.4",
"crypto-bigint 0.5.3",
"digest 0.10.7",
"ff 0.13.0",
"generic-array",
@@ -1798,9 +1798,9 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "event-listener"
version = "3.1.0"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2"
checksum = "01cec0252c2afff729ee6f00e903d479fba81784c8e2bd77447673471fdfaea1"
dependencies = [
"concurrent-queue",
"parking",
@@ -1813,7 +1813,7 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96b852f1345da36d551b9473fa1e2b1eb5c5195585c6c018118bc92a8d91160"
dependencies = [
"event-listener 3.1.0",
"event-listener 3.0.1",
"pin-project-lite",
]
@@ -1899,9 +1899,9 @@ dependencies = [
[[package]]
name = "fiat-crypto"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a56f0780318174bad1c127063fd0c5fdfb35398e3cd79ffaab931a6c79df80"
checksum = "f69037fe1b785e84986b4f2cbcf647381876a00671d25ceef715d7812dd7e1dd"
[[package]]
name = "filetime"
@@ -2150,9 +2150,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.22"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
"bytes",
"fnv",
@@ -2160,7 +2160,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap",
"indexmap 1.9.3",
"slab",
"tokio",
"tokio-util",
@@ -2173,6 +2173,12 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.2"
@@ -2189,7 +2195,7 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown",
"hashbrown 0.14.2",
]
[[package]]
@@ -2295,9 +2301,9 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.11"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb"
checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150"
dependencies = [
"bytes",
"fnv",
@@ -2466,6 +2472,16 @@ dependencies = [
"nom",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
]
[[package]]
name = "indexmap"
version = "2.1.0"
@@ -2473,7 +2489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.14.2",
]
[[package]]
@@ -4058,9 +4074,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.24"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
"bitflags 2.4.1",
"errno",
@@ -4727,9 +4743,9 @@ dependencies = [
[[package]]
name = "termcolor"
version = "1.4.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
@@ -4998,7 +5014,7 @@ version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap",
"indexmap 2.1.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -5078,9 +5094,9 @@ dependencies = [
[[package]]
name = "tracing-log"
version = "0.2.0"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2"
dependencies = [
"log",
"once_cell",
@@ -5089,9 +5105,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -5678,18 +5694,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.7.26"
version = "0.7.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.26"
version = "0.7.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.131.5"
version = "1.130.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.70"

View File

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

View File

@@ -2561,7 +2561,7 @@ dc_lot_t* dc_check_qr (dc_context_t* context, const char*
* the Verified-Group-Invite protocol is offered in the QR code;
* works for protected groups as well as for normal groups.
* If set to 0, the Setup-Contact protocol is offered in the QR code.
* See https://securejoin.readthedocs.io/en/latest/new.html
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
* @return The text that should go to the QR code,
* On errors, an empty QR code is returned, NULL is never returned.
@@ -2597,7 +2597,7 @@ char* dc_get_securejoin_qr_svg (dc_context_t* context, uint32_
*
* Subsequent calls of dc_join_securejoin() will abort previous, unfinished handshakes.
*
* See https://securejoin.readthedocs.io/en/latest/new.html
* See https://countermitm.readthedocs.io/en/latest/new.html
* for details about both protocols.
*
* @memberof dc_context_t
@@ -4580,18 +4580,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.
@@ -6436,27 +6433,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

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.131.5"
version = "1.130.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"

View File

@@ -678,7 +678,7 @@ impl CommandApi {
/// the Verified-Group-Invite protocol is offered in the QR code;
/// works for protected groups as well as for normal groups.
/// If not set, the Setup-Contact protocol is offered in the QR code.
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// return format: `[code, svg]`
@@ -707,7 +707,7 @@ impl CommandApi {
///
/// Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
///
/// See https://securejoin.readthedocs.io/en/latest/new.html
/// See https://countermitm.readthedocs.io/en/latest/new.html
/// for details about both protocols.
///
/// **qr**: The text of the scanned QR code. Typically, the same string as given

View File

@@ -1,4 +1,5 @@
import { DcEvent, DeltaChat } from "../deltachat.js";
import type { DcEvent } from "../deltachat.js";
import { DeltaChat } from "../deltachat.js";
var SELECTED_ACCOUNT = 0;

View File

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

View File

@@ -1,8 +1,9 @@
import * as T from "../generated/types.js";
import { EventType } from "../generated/types.js";
import type { EventType } from "../generated/types.js";
import * as RPC from "../generated/jsonrpc.js";
import { RawClient } from "../generated/client.js";
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
import type { Request } from "yerpc";
import { WebsocketTransport, BaseTransport } from "yerpc";
import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {

View File

@@ -4,7 +4,8 @@ import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import { RpcServerHandle, startServer } from "./test_base.js";
import type { RpcServerHandle } from "./test_base.js";
import { startServer } from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;

View File

@@ -1,6 +1,8 @@
import { assert, expect } from "chai";
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import type { DcEvent } from "../deltachat.js";
import type { RpcServerHandle } from "./test_base.js";
import { createTempUser, startServer } from "./test_base.js";
const EVENT_TIMEOUT = 20000;

View File

@@ -10,6 +10,7 @@
"target": "ES2017",
"module": "es2020",
"declaration": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noImplicitAny": true,

View File

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

View File

@@ -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,
@@ -218,7 +204,7 @@ class Account:
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code.
"""
@@ -271,18 +257,6 @@ class Account:
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
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

@@ -42,10 +42,6 @@ class Message:
return AttrDict(reactions)
return None
def get_sender_contact(self) -> Contact:
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])

View File

@@ -9,14 +9,20 @@ def test_qr_setup_contact(acfactory) -> None:
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
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
bob.wait_for_securejoin_joiner_success()
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"))
@@ -29,31 +35,24 @@ def test_qr_securejoin(acfactory):
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
# Check that at least some of the handshake messages are deleted.
for ac in [alice, bob]:
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
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
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
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"))
@@ -61,97 +60,7 @@ def test_qr_securejoin(acfactory):
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]:
joiner.wait_for_securejoin_joiner_success()
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."""
def test_verified_group_recovery(acfactory, rpc) -> None:
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -161,7 +70,10 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
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"))
@@ -169,7 +81,10 @@ def test_verified_group_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
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)
@@ -178,44 +93,69 @@ def test_verified_group_recovery(acfactory) -> None:
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
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()
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 contact cannot be verified by ac2 because ac3 did not gossip ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert not ac1_contact.get_snapshot().is_verified
ac3_contact_id_ac1 = rpc.lookup_contact_id_by_addr(ac3.id, ac1.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_id_ac1)
ac3_chat.add_contact(ac3_contact_id_ac1)
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!"
logging.info("ac2 got event message: %s", snapshot.text)
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
# 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!")
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
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!"
ac1.wait_for_incoming_msg_event() # Hi!
ac1.wait_for_incoming_msg_event() # Member removed
ac1.wait_for_incoming_msg_event() # Member added
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
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-1].get_snapshot().text == f"Changed setup for {ac2_addr}"
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")
@@ -225,7 +165,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
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"))
@@ -233,7 +176,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
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)
@@ -242,7 +188,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
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
@@ -298,134 +247,3 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
# 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_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
ev = ac2.wait_for_event()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group("Group for joining", protect=True)
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
logging.info("receiving first message")
ac2.wait_for_incoming_msg_event() # member added message
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
ac1.configure()
ac1.start_io()
logging.info("sending second message")
msg_out = chat.send_text("changed address").get_snapshot()
logging.info("receiving second message")
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]

View File

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

View File

@@ -27,6 +27,8 @@ 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" },

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,

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,

View File

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

View File

@@ -9,7 +9,7 @@ import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat import account_hookimpl, Message, Chat
from deltachat.tracker import ImexTracker
@@ -1658,6 +1658,128 @@ def test_ac_setup_message_twice(acfactory, lp):
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
def test_qr_setup_contact(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
qr = ac1.get_setup_contact_qr()
lp.sec("ac2: start QR-code based setup contact protocol")
ch = ac2.qr_setup_contact(qr)
assert ch.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1])
def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats)
lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin")
chat = ac1.create_group_chat("hello")
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ch = ac2.qr_join_chat(qr)
lp.sec("ac2: qr_join_chat() returned")
assert ch.id >= 10
# check that at least some of the handshake messages are deleted
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac1._evtracker.wait_securejoin_inviter_progress(1000)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr"))
# ac1 reloads the chat.
chat = Chat(chat.account, chat.id)
assert not chat.is_protected()
# ac2 reloads the chat.
ch = Chat(ch.account, ch.id)
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.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
@@ -2405,6 +2527,47 @@ def test_delete_deltachat_folder(acfactory):
assert "DeltaChat" in ac1.direct_imap.list_folders()
def test_aeap_flow_verified(acfactory, lp):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
chat2 = ac2.qr_join_chat(qr)
assert chat2.id >= 10
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("sending first message")
msg_out = chat.send_text("old address")
lp.sec("receiving first message")
ac2._evtracker.wait_next_incoming_message() # member added message
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_1.text == msg_out.text
lp.sec("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
configtracker = ac1.configure()
configtracker.wait_finish()
ac1.start_io()
lp.sec("sending second message")
msg_out = chat.send_text("changed address")
lp.sec("receiving second message")
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
assert msg_in_2.text == msg_out.text
assert msg_in_2.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
assert len(msg_in_2.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
def test_archived_muted_chat(acfactory, lp):
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.

View File

@@ -1 +1 @@
2023-11-20
2023-11-10

View File

@@ -67,7 +67,7 @@ impl Aheader {
impl fmt::Display for Aheader {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "addr={};", self.addr.to_lowercase())?;
write!(fmt, "addr={};", self.addr)?;
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
@@ -262,16 +262,5 @@ mod tests {
)
)
.contains("prefer-encrypt"));
// Always lowercase the address in the header.
assert!(format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
)
)
.contains("test@example.com"));
}
}

View File

@@ -993,9 +993,8 @@ impl ChatId {
AND y.contact_id > 9
AND x.chat_id=?
AND y.chat_id<>x.chat_id
AND y.chat_id>?
GROUP BY y.chat_id",
(self, DC_CHAT_ID_LAST_SPECIAL),
(self,),
|row| {
let chat_id: ChatId = row.get(0)?;
let intersection: f64 = row.get(1)?;
@@ -1935,18 +1934,11 @@ impl Chat {
/// Sends a `SyncAction` synchronising chat contacts to other devices.
pub(crate) async fn sync_contacts(&self, context: &Context) -> Result<()> {
let addrs = context
.sql
.query_map(
"SELECT c.addr \
FROM contacts c INNER JOIN chats_contacts cc \
ON c.id=cc.contact_id \
WHERE cc.chat_id=?",
(self.id,),
|row| row.get::<_, String>(0),
|addrs| addrs.collect::<Result<Vec<_>, _>>().map_err(Into::into),
)
.await?;
let mut addrs = Vec::new();
for contact_id in get_chat_contacts(context, self.id).await? {
let contact = Contact::get_by_id(context, contact_id).await?;
addrs.push(contact.get_addr().to_string());
}
self.sync(context, SyncAction::SetContacts(addrs)).await
}
@@ -2447,13 +2439,10 @@ async fn prepare_msg_common(
// Check if the chat can be sent to.
if let Some(reason) = chat.why_cant_send(context).await? {
if matches!(
reason,
CantSendReason::ProtectionBroken | CantSendReason::ContactRequest
) && msg.param.get_cmd() == SystemMessage::SecurejoinMessage
if reason == CantSendReason::ProtectionBroken
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
// Send out the message, the securejoin message is supposed to repair the verification.
// If the chat is a contact request, let the user accept it later.
// Send out the message, the securejoin message is supposed to repair the verification
} else {
bail!("cannot send to {chat_id}: {reason}");
}
@@ -3461,8 +3450,8 @@ pub(crate) async fn add_contact_to_chat_ex(
if chat.typ == Chattype::Group && chat.is_promoted() {
msg.viewtype = Viewtype::Text;
let contact_addr = contact.get_addr().to_lowercase();
msg.text = stock_str::msg_add_member_local(context, &contact_addr, ContactId::SELF).await;
let contact_addr = contact.get_addr();
msg.text = stock_str::msg_add_member_local(context, contact_addr, ContactId::SELF).await;
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
msg.param.set(Param::Arg, contact_addr);
msg.param.set_int(Param::Arg2, from_handshake.into());
@@ -3633,7 +3622,7 @@ pub async fn remove_contact_from_chat(
.await;
}
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
msg.param.set(Param::Arg, contact.get_addr());
msg.id = send_msg(context, chat_id, &mut msg).await?;
} else {
sync = Sync;
@@ -3692,6 +3681,7 @@ async fn rename_ex(
chat_id: ChatId,
new_name: &str,
) -> Result<()> {
let sync_name = new_name;
let new_name = improve_single_line_input(new_name);
/* the function only sets the names of group chats; normal chats get their names from the contacts */
let mut success = false;
@@ -3743,8 +3733,8 @@ async fn rename_ex(
if !success {
bail!("Failed to set name");
}
if sync.into() && chat.name != new_name {
let sync_name = new_name.to_string();
if sync.into() {
let sync_name = sync_name.to_string();
chat.sync(context, SyncAction::Rename(sync_name))
.await
.log_err(context)
@@ -4233,7 +4223,7 @@ async fn set_contacts_by_addrs(context: &Context, id: ChatId, addrs: &[String])
let mut contacts = HashSet::new();
for addr in addrs {
let contact_addr = ContactAddress::new(addr)?;
let contact = Contact::add_or_lookup(context, "", &contact_addr, Origin::Hidden)
let contact = Contact::add_or_lookup(context, "", contact_addr, Origin::Hidden)
.await?
.0;
contacts.insert(contact);
@@ -4686,37 +4676,6 @@ mod tests {
Ok(())
}
/// Test that if a message implicitly adds a member, both messages appear.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_msg_with_implicit_member_add() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_contact_id =
Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
let fiona_addr = "fiona@example.net";
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
let alice_chat_id =
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
let sent_msg = alice.send_text(alice_chat_id, "I created a group").await;
let bob_received_msg = bob.recv_msg(&sent_msg).await;
let bob_chat_id = bob_received_msg.get_chat_id();
bob_chat_id.accept(&bob).await?;
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
let sent_msg = alice.pop_sent_msg().await;
bob.recv_msg(&sent_msg).await;
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
bob.recv_msg(&sent_msg).await;
bob.golden_test_chat(bob_chat_id, "chat_test_msg_with_implicit_member_add")
.await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_modify_chat_multi_device() -> Result<()> {
let a1 = TestContext::new_alice().await;
@@ -5591,7 +5550,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("foo@bar.org")?,
ContactAddress::new("foo@bar.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -6602,7 +6561,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("foo@bar.org")?,
ContactAddress::new("foo@bar.org")?,
Origin::ManuallyCreated,
)
.await?;

View File

@@ -513,11 +513,6 @@ impl Context {
);
self.sql.set_raw_config(key.as_ref(), value).await?;
}
Config::Addr => {
self.sql
.set_raw_config(key.as_ref(), value.map(|s| s.to_lowercase()).as_deref())
.await?;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;
}
@@ -667,21 +662,6 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_addr() {
let t = TestContext::new().await;
// Test that uppercase address get lowercased.
assert!(t
.set_config(Config::Addr, Some("Foobar@eXample.oRg"))
.await
.is_ok());
assert_eq!(
t.get_config(Config::Addr).await.unwrap().unwrap(),
"foobar@example.org"
);
}
/// Tests that "bot" config can only be set to "0" or "1".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_config_bot() {

View File

@@ -43,43 +43,43 @@ use crate::{chat, stock_str};
const SEEN_RECENTLY_SECONDS: i64 = 600;
/// Valid contact address.
#[derive(Debug, Clone)]
pub(crate) struct ContactAddress(String);
#[derive(Debug, Clone, Copy)]
pub(crate) struct ContactAddress<'a>(&'a str);
impl Deref for ContactAddress {
impl Deref for ContactAddress<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
self.0
}
}
impl AsRef<str> for ContactAddress {
impl AsRef<str> for ContactAddress<'_> {
fn as_ref(&self) -> &str {
&self.0
self.0
}
}
impl fmt::Display for ContactAddress {
impl fmt::Display for ContactAddress<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ContactAddress {
impl<'a> ContactAddress<'a> {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &str) -> Result<Self> {
pub fn new(s: &'a str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
if !may_be_valid_addr(addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr.to_string()))
Ok(Self(addr))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
impl rusqlite::types::ToSql for ContactAddress<'_> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
@@ -149,22 +149,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 {
@@ -500,7 +484,7 @@ impl Contact {
let addr = ContactAddress::new(&addr)?;
let (contact_id, sth_modified) =
Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated)
Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated)
.await
.context("add_or_lookup")?;
let blocked = Contact::is_blocked_load(context, contact_id).await?;
@@ -565,7 +549,7 @@ impl Contact {
let addr_normalized = addr_normalize(addr);
if context.is_self_addr(&addr_normalized).await? {
if context.is_self_addr(addr_normalized).await? {
return Ok(Some(ContactId::SELF));
}
@@ -615,7 +599,7 @@ impl Contact {
pub(crate) async fn add_or_lookup(
context: &Context,
name: &str,
addr: &ContactAddress,
addr: ContactAddress<'_>,
mut origin: Origin,
) -> Result<(ContactId, Modifier)> {
let mut sth_modified = Modifier::None;
@@ -623,7 +607,7 @@ impl Contact {
ensure!(!addr.is_empty(), "Can not add_or_lookup empty address");
ensure!(origin != Origin::Unknown, "Missing valid origin");
if context.is_self_addr(addr).await? {
if context.is_self_addr(&addr).await? {
return Ok((ContactId::SELF, sth_modified));
}
@@ -778,7 +762,7 @@ impl Contact {
} else {
"".to_string()
},
&addr,
addr,
origin,
if update_authname {
name.to_string()
@@ -823,7 +807,7 @@ impl Contact {
let name = normalize_name(&name);
match ContactAddress::new(&addr) {
Ok(addr) => {
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await {
Ok((_, modified)) => {
if modified != Modifier::None {
modify_cnt += 1
@@ -1317,7 +1301,7 @@ impl Contact {
return Ok(None);
};
if addr_cmp(&verifier_addr, &self.addr) {
if verifier_addr == self.addr {
// Contact is directly verified via QR code.
return Ok(Some(ContactId::SELF));
}
@@ -1405,13 +1389,12 @@ pub fn may_be_valid_addr(addr: &str) -> bool {
res.is_ok()
}
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
/// Returns address with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> &str {
let norm = addr.trim();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(&norm).to_string()
norm.get(7..).unwrap_or(norm)
} else {
norm
}
@@ -1666,8 +1649,8 @@ fn cat_fingerprint(
/// Compares two email addresses, normalizing them beforehand.
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
let norm1 = addr_normalize(addr1).to_lowercase();
let norm2 = addr_normalize(addr2).to_lowercase();
norm1 == norm2
}
@@ -1865,7 +1848,10 @@ mod tests {
fn test_normalize_addr() {
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
assert_eq!(addr_normalize(" hello@world.com "), "hello@world.com");
assert_eq!(addr_normalize("John@Doe.com"), "john@doe.com");
// normalisation preserves case to allow user-defined spelling.
// however, case is ignored on addr_cmp()
assert_ne!(addr_normalize("John@Doe.com"), "john@doe.com");
}
#[test]
@@ -1891,7 +1877,7 @@ mod tests {
let (id, _modified) = Contact::add_or_lookup(
&context.ctx,
"bob",
&ContactAddress::new("user@example.org")?,
ContactAddress::new("user@example.org")?,
Origin::IncomingReplyTo,
)
.await?;
@@ -1919,7 +1905,7 @@ mod tests {
let (contact_bob_id, modified) = Contact::add_or_lookup(
&context.ctx,
"someone",
&ContactAddress::new("user@example.org")?,
ContactAddress::new("user@example.org")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1984,7 +1970,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bla foo",
&ContactAddress::new("one@eins.org").unwrap(),
ContactAddress::new("one@eins.org").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2003,7 +1989,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"Real one",
&ContactAddress::new(" one@eins.org ").unwrap(),
ContactAddress::new(" one@eins.org ").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2019,7 +2005,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("three@drei.sam").unwrap(),
ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2036,7 +2022,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"m. serious",
&ContactAddress::new("three@drei.sam").unwrap(),
ContactAddress::new("three@drei.sam").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2051,7 +2037,7 @@ mod tests {
let (contact_id_test, sth_modified) = Contact::add_or_lookup(
&t,
"schnucki",
&ContactAddress::new("three@drei.sam").unwrap(),
ContactAddress::new("three@drei.sam").unwrap(),
Origin::ManuallyCreated,
)
.await
@@ -2067,7 +2053,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("alice@w.de").unwrap(),
ContactAddress::new("alice@w.de").unwrap(),
Origin::IncomingUnknownTo,
)
.await
@@ -2209,7 +2195,7 @@ mod tests {
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2288,7 +2274,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob1",
&ContactAddress::new("bob@example.org").unwrap(),
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2304,7 +2290,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob2",
&ContactAddress::new("bob@example.org").unwrap(),
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2330,7 +2316,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"bob4",
&ContactAddress::new("bob@example.org").unwrap(),
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2359,7 +2345,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire1",
&ContactAddress::new("claire@example.org").unwrap(),
ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2375,7 +2361,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"claire2",
&ContactAddress::new("claire@example.org").unwrap(),
ContactAddress::new("claire@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2400,7 +2386,7 @@ mod tests {
let (contact_id, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
&ContactAddress::new("bob@example.org")?,
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2412,7 +2398,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Not Bob",
&ContactAddress::new("bob@example.org")?,
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownTo,
)
.await?;
@@ -2425,7 +2411,7 @@ mod tests {
let (contact_id_same, sth_modified) = Contact::add_or_lookup(
&t,
"Bob",
&ContactAddress::new("bob@example.org")?,
ContactAddress::new("bob@example.org")?,
Origin::IncomingUnknownFrom,
)
.await?;
@@ -2454,7 +2440,7 @@ mod tests {
Contact::add_or_lookup(
&t,
"dave2",
&ContactAddress::new("dave@example.org").unwrap(),
ContactAddress::new("dave@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2575,7 +2561,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -2738,7 +2724,7 @@ CCCB 5AA9 F6E1 141C 9431
let (contact_id, _) = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;

View File

@@ -413,8 +413,7 @@ impl Context {
.is_some()
{
let mut lock = self.ratelimit.write().await;
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
*lock = Ratelimit::new(Duration::new(40, 0), 6.0);
}
}
self.scheduler.start(self.clone()).await;

View File

@@ -619,11 +619,11 @@ impl Imap {
.inner
.status(folder, "(UIDNEXT)")
.await
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
.context("STATUS (UIDNEXT) error for {folder:?}")?;
status
.uid_next
.with_context(|| format!("STATUS {folder} (UIDNEXT) did not return UIDNEXT"))?
.context("STATUS {folder} (UIDNEXT) did not return UIDNEXT")?
};
mailbox.uid_next = Some(new_uid_next);
@@ -2519,7 +2519,7 @@ async fn add_all_recipients_as_contacts(
let (_, modified) = Contact::add_or_lookup(
context,
&display_name_normalized,
&recipient_addr,
recipient_addr,
Origin::OutgoingTo,
)
.await?;

View File

@@ -35,7 +35,7 @@ impl Session {
let status = self
.status(folder, "(UIDNEXT)")
.await
.with_context(|| format!("STATUS (UIDNEXT) error for {folder:?}"))?;
.context("STATUS (UIDNEXT) error for {folder:?}")?;
if let Some(uid_next) = status.uid_next {
let expected_uid_next = get_uid_next(context, folder)
.await

View File

@@ -1919,7 +1919,7 @@ mod tests {
let contact_id = Contact::add_or_lookup(
&t,
"Dave",
&ContactAddress::new("dave@example.com").unwrap(),
ContactAddress::new("dave@example.com").unwrap(),
Origin::ManuallyCreated,
)
.await

View File

@@ -648,7 +648,7 @@ impl Peerstate {
let (new_contact_id, _) = Contact::add_or_lookup(
context,
"",
&new_addr,
new_addr,
Origin::IncomingUnknownFrom,
)
.await?;

View File

@@ -365,10 +365,9 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan)
.await
.with_context(|| format!("failed to add or lookup contact for address {addr:?}"))?;
if let (Some(grpid), Some(grpname)) = (grpid, grpname) {
if context
@@ -433,7 +432,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
if let Some(peerstate) = peerstate {
let peerstate_addr = ContactAddress::new(&peerstate.addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, &name, &peerstate_addr, Origin::UnhandledQrScan)
Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan)
.await
.context("add_or_lookup")?;
ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request)
@@ -778,7 +777,7 @@ impl Qr {
) -> Result<Self> {
let addr = ContactAddress::new(addr)?;
let (contact_id, _) =
Contact::add_or_lookup(context, name, &addr, Origin::UnhandledQrScan).await?;
Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?;
Ok(Qr::Addr { contact_id, draft })
}
}
@@ -789,7 +788,7 @@ fn normalize_address(addr: &str) -> Result<String> {
let new_addr = percent_decode_str(addr).decode_utf8()?;
let new_addr = addr_normalize(&new_addr);
ensure!(may_be_valid_addr(&new_addr), "Bad e-mail address");
ensure!(may_be_valid_addr(new_addr), "Bad e-mail address");
Ok(new_addr.to_string())
}

View File

@@ -392,7 +392,7 @@ Can we chat at 1pm pacific, today?"
let bob_id = Contact::add_or_lookup(
&alice,
"",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?

View File

@@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus};
use crate::config::Config;
use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH};
use crate::contact::{
addr_cmp, may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin,
};
use crate::context::Context;
use crate::debug_logging::maybe_set_logging_xdc_inner;
@@ -227,7 +227,8 @@ 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?;
let updated_verified_key_addr =
update_verified_keys(context, &mut mime_parser, from_id).await?;
// Add parts
let received_msg = add_parts(
@@ -280,6 +281,11 @@ pub(crate) async fn receive_imf_inner(
MsgId::new_unset()
};
if let Some(addr) = updated_verified_key_addr {
let msg = stock_str::contact_setup_changed(context, &addr).await;
chat::add_info_msg(context, chat_id, &msg, received_msg.sort_timestamp).await?;
}
save_locations(context, &mime_parser, chat_id, from_id, insert_msg_id).await?;
if let Some(ref sync_items) = mime_parser.sync_items {
@@ -457,7 +463,7 @@ async fn add_parts(
let mut chat_id_blocked = Blocked::Not;
let mut better_msg = None;
let mut group_changes_msgs = (Vec::new(), None);
let mut group_changes_msgs = Vec::new();
if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled {
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
}
@@ -554,11 +560,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:
@@ -636,9 +637,14 @@ async fn add_parts(
if let Some(group_chat_id) = chat_id {
if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? {
let chat = Chat::load_from_db(context, group_chat_id).await?;
if chat.is_protected() && chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
if chat.is_protected() {
if chat.typ == Chattype::Single {
// Just assign the message to the 1:1 chat with the actual sender instead.
chat_id = None;
} else {
let s = stock_str::unknown_sender_for_chat(context).await;
mime_parser.replace_msg_by_error(&s);
}
} else {
// In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~`
// to the sender's name, indicating to the user that he/she is not part of the group.
@@ -646,12 +652,6 @@ async fn add_parts(
let name: &str = from.display_name.as_ref().unwrap_or(&from.addr);
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
if chat.is_protected() {
// In protected chat, also mark the message with an error.
let s = stock_str::unknown_sender_for_chat(context).await;
part.error = Some(s);
}
}
}
}
@@ -1138,29 +1138,9 @@ 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 {
group_changes_msgs = group_changes_msgs.into_iter().rev().collect();
let mut parts = mime_parser.parts.iter_mut().peekable();
while let Some(part) = parts.peek() {
if part.is_reaction {
let reaction_str = simplify::remove_footers(part.msg.as_str());
set_msg_reaction(
@@ -1193,7 +1173,10 @@ async fn add_parts(
}
let mut txt_raw = "".to_string();
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
let group_changes_msg = group_changes_msgs.pop();
let (msg, typ): (&str, Viewtype) = if let Some(msg) = &group_changes_msg {
(msg, Viewtype::Text)
} else if let Some(better_msg) = &better_msg {
(better_msg, Viewtype::Text)
} else {
(&part.msg, part.typ)
@@ -1321,6 +1304,10 @@ RETURNING id
debug_assert!(!row_id.is_special());
created_db_entries.push(row_id);
if group_changes_msg.is_none() || (group_changes_msgs.is_empty() && better_msg.is_none()) {
parts.next();
}
}
// check all parts whether they contain a new logging webxdc
@@ -1738,25 +1725,24 @@ async fn apply_group_changes(
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
) -> Result<(Vec<String>, Option<String>)> {
) -> Result<Vec<String>> {
if chat_id.is_special() {
// Do not apply group changes to the trash chat.
return Ok((Vec::new(), None));
return Ok(Vec::new());
}
let mut chat = Chat::load_from_db(context, chat_id).await?;
if chat.typ != Chattype::Group {
return Ok((Vec::new(), None));
return Ok(Vec::new());
}
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 =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
context.get_primary_self_addr().await? == *added_addr
} else {
false
};
@@ -1821,11 +1807,11 @@ async fn apply_group_changes(
if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?;
better_msg = if removed_id == Some(from_id) {
Some(stock_str::msg_group_left_local(context, from_id).await)
group_changes_msgs.push(if removed_id == Some(from_id) {
stock_str::msg_group_left_local(context, from_id).await
} else {
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
};
stock_str::msg_del_member_local(context, removed_addr, from_id).await
});
if removed_id.is_some() {
if !allow_member_list_changes {
@@ -1838,7 +1824,8 @@ async fn apply_group_changes(
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
group_changes_msgs
.push(stock_str::msg_add_member_local(context, added_addr, from_id).await);
if allow_member_list_changes {
if !recreate_member_list {
@@ -1879,21 +1866,20 @@ async fn apply_group_changes(
send_event_chat_modified = true;
}
better_msg = Some(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
group_changes_msgs
.push(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
} else if let Some(value) = mime_parser.get_header(HeaderDef::ChatContent) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg = match avatar_action {
AvatarAction::Delete => {
Some(stock_str::msg_grp_img_deleted(context, from_id).await)
}
group_changes_msgs.push(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
Some(stock_str::msg_grp_img_changed(context, from_id).await)
stock_str::msg_grp_img_changed(context, from_id).await
}
};
});
}
}
}
@@ -1991,7 +1977,7 @@ async fn apply_group_changes(
if send_event_chat_modified {
context.emit_event(EventType::ChatModified(chat_id));
}
Ok((group_changes_msgs, better_msg))
Ok(group_changes_msgs)
}
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
@@ -2177,7 +2163,7 @@ async fn apply_mailinglist_changes(
return Ok(());
}
};
let (contact_id, _) = Contact::add_or_lookup(context, "", &list_post, Origin::Hidden).await?;
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
let mut contact = Contact::get_by_id(context, contact_id).await?;
if contact.param.get(Param::ListId) != Some(listid) {
contact.param.set(Param::ListId, listid);
@@ -2297,6 +2283,9 @@ enum VerifiedEncryption {
/// 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.
///
/// Returns address of the peerstate if the primary verified key was updated,
/// the caller then needs to add "Setup changed" notification somewhere.
async fn update_verified_keys(
context: &Context,
mimeparser: &mut MimeMessage,
@@ -2344,11 +2333,10 @@ async fn update_verified_keys(
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)
Ok(Some(peerstate.addr.clone()))
} else {
Ok(None)
}
@@ -2477,7 +2465,7 @@ async fn mark_recipients_as_verified(
let (to_contact_id, _) = Contact::add_or_lookup(
context,
"",
&ContactAddress::new(&to_addr)?,
ContactAddress::new(&to_addr)?,
Origin::Hidden,
)
.await?;
@@ -2615,7 +2603,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_contact_by_addr(
context: &Context,
display_name: Option<&str>,
addr: ContactAddress,
addr: ContactAddress<'_>,
origin: Origin,
) -> Result<ContactId> {
if context.is_self_addr(&addr).await? {
@@ -2624,7 +2612,7 @@ async fn add_or_lookup_contact_by_addr(
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
let (contact_id, _modified) =
Contact::add_or_lookup(context, &display_name_normalized, &addr, origin).await?;
Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?;
Ok(contact_id)
}

View File

@@ -431,7 +431,7 @@ async fn test_escaped_recipients() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"Carl",
&ContactAddress::new("carl@host.tld").unwrap(),
ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -477,7 +477,7 @@ async fn test_cc_to_contact() {
let carl_contact_id = Contact::add_or_lookup(
&t,
"garabage",
&ContactAddress::new("carl@host.tld").unwrap(),
ContactAddress::new("carl@host.tld").unwrap(),
Origin::IncomingUnknownFrom,
)
.await
@@ -2003,7 +2003,7 @@ async fn test_duplicate_message() -> Result<()> {
let bob_contact_id = Contact::add_or_lookup(
&alice,
"Bob",
&ContactAddress::new("bob@example.org").unwrap(),
ContactAddress::new("bob@example.org").unwrap(),
Origin::IncomingUnknownFrom,
)
.await?
@@ -2060,7 +2060,7 @@ async fn test_ignore_footer_status_from_mailinglist() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("bob@example.net").unwrap(),
ContactAddress::new("bob@example.net").unwrap(),
Origin::IncomingUnknownCc,
)
.await?
@@ -2139,7 +2139,7 @@ async fn test_ignore_old_status_updates() -> Result<()> {
let bob_id = Contact::add_or_lookup(
&t,
"",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::AddressBook,
)
.await?
@@ -2623,17 +2623,19 @@ async fn test_incoming_contact_request() -> Result<()> {
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
assert!(chat.is_contact_request());
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
Ok(())
loop {
let event = t
.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. }))
.await;
match event {
EventType::IncomingMsg { chat_id, msg_id } => {
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.id, msg_id);
return Ok(());
}
_ => unreachable!(),
}
_ => unreachable!(),
}
}

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

@@ -1,4 +1,4 @@
//! Verified contact protocol implementation as [specified by countermitm project](https://securejoin.readthedocs.io/en/latest/new.html#setup-contact-protocol).
//! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol).
use std::convert::TryFrom;
@@ -426,7 +426,6 @@ 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)));
@@ -685,6 +684,9 @@ 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?
@@ -770,7 +772,7 @@ fn encrypted_and_signed(
mod tests {
use super::*;
use crate::chat;
use crate::chat::{remove_contact_from_chat, ProtectionStatus};
use crate::chat::ProtectionStatus;
use crate::chatlist::Chatlist;
use crate::constants::Chattype;
use crate::contact::ContactAddress;
@@ -922,10 +924,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 +978,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
@@ -1058,7 +1089,7 @@ mod tests {
let (contact_bob_id, _modified) = Contact::add_or_lookup(
&alice.ctx,
"Bob",
&ContactAddress::new("bob@example.net")?,
ContactAddress::new("bob@example.net")?,
Origin::ManuallyCreated,
)
.await?;
@@ -1263,11 +1294,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 +1334,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;
@@ -1352,32 +1404,4 @@ First thread."#;
assert!(get_securejoin_qr(&alice, Some(chat_id)).await.is_err());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unknown_sender() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
tcm.execute_securejoin(&alice, &bob).await;
let alice_chat_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group with Bob", &[&bob])
.await;
let sent = alice.send_text(alice_chat_id, "Hi!").await;
let bob_chat_id = bob.recv_msg(&sent).await.chat_id;
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
// The message from Bob is delivered late, Bob is already removed.
let msg = alice.recv_msg(&sent).await;
assert_eq!(msg.text, "Hi hi!");
assert_eq!(msg.error.unwrap(), "Unknown sender for this chat.");
Ok(())
}
}

View File

@@ -222,7 +222,9 @@ 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)

View File

@@ -149,7 +149,7 @@ pub enum StockMessage {
however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
WelcomeMessage = 71,
#[strum(props(fallback = "Unknown sender for this chat."))]
#[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))]
UnknownSenderForChat = 72,
#[strum(props(fallback = "Message from %1$s"))]
@@ -824,7 +824,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)
@@ -930,7 +929,7 @@ pub(crate) async fn welcome_message(context: &Context) -> String {
translated(context, StockMessage::WelcomeMessage).await
}
/// Stock string: `Unknown sender for this chat.`.
/// Stock string: `Unknown sender for this chat. See 'info' for more details.`.
pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
translated(context, StockMessage::UnknownSenderForChat).await
}

View File

@@ -52,18 +52,10 @@ pub(crate) enum SyncData {
},
}
#[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,12 +63,6 @@ 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.
///
@@ -93,10 +79,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,))
@@ -259,15 +242,9 @@ impl Context {
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) => self.add_qr_token(token).await,
DeleteQrToken(token) => self.delete_qr_token(token).await,
AlterChat { id, action } => self.sync_alter_chat(id, action).await,
}
.log_err(self)
.ok();
@@ -406,32 +383,48 @@ 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());
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
assert!(t.parse_sync_items(
r#"{"items":[{"timestamp":1631781318,"data":{"AlterChat":{"id":{"ContactAddr":"bob@example.net"},"action":"Burn"}}}]}"#.to_string(),
)
.is_err()); // Unknown enum value
// 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 {
let AlterChat { id, action } = &sync_items.items.get(0).unwrap().data else {
bail!("bad item");
};
assert_eq!(
@@ -473,9 +466,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);

View File

@@ -580,7 +580,7 @@ impl TestContext {
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
// origin when creating this contact.
let (contact_id, modified) =
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
Contact::add_or_lookup(self, &name, addr, Origin::MailinglistAddress)
.await
.expect("add_or_lookup");
match modified {

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

@@ -2,6 +2,6 @@ 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]
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
Msg#13: (Contact#Contact#10): Member Me (bob@example.net) added. [FRESH][INFO]
--------------------------------------------------------------------------------