diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1564c7a44..815e7ef0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -256,7 +256,7 @@ jobs: path: target/debug - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} @@ -303,7 +303,7 @@ jobs: persist-credentials: false - name: Install python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} diff --git a/.github/workflows/deltachat-rpc-server.yml b/.github/workflows/deltachat-rpc-server.yml index 981c0c391..b3e175f60 100644 --- a/.github/workflows/deltachat-rpc-server.yml +++ b/.github/workflows/deltachat-rpc-server.yml @@ -224,7 +224,7 @@ jobs: # Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py - name: Install python 3.12 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.12 @@ -289,7 +289,7 @@ jobs: with: show-progress: false persist-credentials: false - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.11" @@ -401,7 +401,7 @@ jobs: deltachat-rpc-server/npm-package/*.tgz # Configure Node.js for publishing. - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/jsonrpc-client-npm-package.yml b/.github/workflows/jsonrpc-client-npm-package.yml index 0c43dbb7b..2aecdc9d3 100644 --- a/.github/workflows/jsonrpc-client-npm-package.yml +++ b/.github/workflows/jsonrpc-client-npm-package.yml @@ -19,7 +19,7 @@ jobs: show-progress: false persist-credentials: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 20 registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index edccae690..45db64efa 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -21,7 +21,7 @@ jobs: show-progress: false persist-credentials: false - name: Use Node.js 18.x - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: 18.x - name: Add Rust cache diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index 2615914e0..62e9412d4 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -78,7 +78,7 @@ jobs: persist-credentials: false fetch-depth: 0 # Fetch history to calculate VCS version number. - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v5 with: node-version: '18' - name: npm install diff --git a/CHANGELOG.md b/CHANGELOG.md index 13fff2b5e..099e1d05f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Changelog +## [2.13.0] - 2025-09-09 + +### API-Changes + +- [**breaking**] Remove `is_profile_verified` APIs. +- [**breaking**] Remove deprecated `is_protection_broken`. +- [**breaking**] Remove `e2ee_enabled` preference. + +### Features / Changes + +- Add call ringing API ([#6650](https://github.com/chatmail/core/pull/6650), [#7174](https://github.com/chatmail/core/pull/7174), [#7175](https://github.com/chatmail/core/pull/7175), [#7179](https://github.com/chatmail/core/pull/7179)) +- Warn for outdated versions after 6 months instead of 1 year ([#7144](https://github.com/chatmail/core/pull/7144)). +- Do not set "unknown sender for this chat" error. +- Do not replace messages with an error on verification failure. +- Support receiving Autocrypt-Gossip with `_verified` attribute. +- Withdraw all QR codes when one is withdrawn. + +### Fixes + +- Don't reverify contacts by SELF on receipt of a message from another device. +- Don't verify contacts by others having an unknown verifier. +- Update verifier_id if it's "unknown" and new verifier has known verifier. +- Mark message as failed if it can't be sent ([#7143](https://github.com/chatmail/core/pull/7143)). +- Add "Messages are end-to-end encrypted." to non-protected groups. + +### Documentation + +- Fix for SecurejoinInviterProgress with progress == 600. +- STYLE.md: Prefer BTreeMap and BTreeSet over hash variants. + +### Miscellaneous Tasks + +- Update provider database. +- Update dependencies. + +### Refactor + +- Check that verifier is verified in turn. +- Remove unused `EncryptPreference::Reset`. +- Remove `Aheader::new`. + +### Tests + +- Add another TimeShiftFalsePositiveNote ([#7142](https://github.com/chatmail/core/pull/7142)). +- Add TestContext.create_chat_id. + ## [2.12.0] - 2025-08-26 ### API-Changes diff --git a/Cargo.lock b/Cargo.lock index a4ca86b31..2b64fcf4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,7 +1296,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "2.12.0" +version = "2.13.0" dependencies = [ "anyhow", "async-broadcast", @@ -1337,7 +1337,7 @@ dependencies = [ "mail-builder", "mailparse", "mime", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "num-derive", "num-traits", "num_cpus", @@ -1406,7 +1406,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "2.12.0" +version = "2.13.0" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -1428,13 +1428,13 @@ dependencies = [ [[package]] name = "deltachat-repl" -version = "2.12.0" +version = "2.13.0" dependencies = [ "anyhow", "deltachat", "dirs", "log", - "nu-ansi-term 0.46.0", + "nu-ansi-term", "qr2term", "rusqlite", "rustyline", @@ -1444,7 +1444,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "2.12.0" +version = "2.13.0" dependencies = [ "anyhow", "deltachat", @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "2.12.0" +version = "2.13.0" dependencies = [ "anyhow", "deltachat", @@ -3729,16 +3729,6 @@ dependencies = [ "serde", ] -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -3977,12 +3967,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -6335,7 +6319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term 0.50.1", + "nu-ansi-term", "once_cell", "regex-automata", "sharded-slab", diff --git a/Cargo.toml b/Cargo.toml index a7fdf3546..384019313 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat" -version = "2.12.0" +version = "2.13.0" edition = "2024" license = "MPL-2.0" rust-version = "1.85" @@ -191,7 +191,7 @@ futures-lite = "2.6.1" libc = "0.2" log = "0.4" mailparse = "0.16.1" -nu-ansi-term = "0.46" +nu-ansi-term = "0.50" num-traits = "0.2" rand = "0.8" regex = "1.10" diff --git a/STYLE.md b/STYLE.md index 08a01f792..c67f77102 100644 --- a/STYLE.md +++ b/STYLE.md @@ -112,6 +112,18 @@ Follow for `.expect` message style. +## BTreeMap vs HashMap + +Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html) +over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html) +and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html) +over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html) +as iterating over these structures returns items in deterministic order. + +Non-deterministic code may result in difficult to reproduce bugs, +flaky tests, regression tests that miss bugs +or different behavior on different devices when processing the same messages. + ## Logging For logging, use `info!`, `warn!` and `error!` macros. diff --git a/benches/benchmark_decrypting.rs b/benches/benchmark_decrypting.rs index 2cad44f9d..159b628f3 100644 --- a/benches/benchmark_decrypting.rs +++ b/benches/benchmark_decrypting.rs @@ -57,9 +57,7 @@ async fn create_context() -> Context { .set_config(Config::ConfiguredAddr, Some("bob@example.net")) .await .unwrap(); - let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")) - .unwrap() - .0; + let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap(); let public = secret.signed_public_key(); let key_pair = KeyPair { public, secret }; store_self_keypair(&context, &key_pair) diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index a8aa0fc56..e49faeeb6 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat_ffi" -version = "2.12.0" +version = "2.13.0" description = "Deltachat FFI" edition = "2018" readme = "README.md" diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index dee7ded8a..7e0d811b6 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -415,7 +415,6 @@ char* dc_get_blobdir (const dc_context_t* context); * As for `displayname` and `selfstatus`, also the avatar is sent to the recipients. * To save traffic, however, the avatar is attached only as needed * and also recoded to a reasonable size. - * - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default) * - `mdns_enabled` = 0=do not send or request read receipts, * 1=send and request read receipts * default=send and request read receipts, only send but not request if `bot` is set @@ -1217,21 +1216,21 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c /** * Start an outgoing call. - * This sends a message with all relevant information to the callee, + * This sends a message of type #DC_MSG_CALL with all relevant information to the callee, * who will get informed by an #DC_EVENT_INCOMING_CALL event and rings. * * Possible actions during ringing: * * - caller cancels the call using dc_end_call(): - * callee receives #DC_EVENT_CALL_ENDED + * callee receives #DC_EVENT_CALL_ENDED and has a "Missed Call" * * - callee accepts using dc_accept_incoming_call(): * caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED. * callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts * - * - callee rejects using dc_end_call(): - * caller receives #DC_EVENT_CALL_ENDED after 1 minute timeout. - * callee's other devices receive #DC_EVENT_CALL_ENDED + * - callee declines using dc_end_call(): + * caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call". + * callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Cancelled Call", * * - callee is already in a call: * in this case, UI may decide to show a notification instead of ringing. @@ -1241,7 +1240,9 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c * after 1 minute without action, * caller and callee receive #DC_EVENT_CALL_ENDED * to prevent endless ringing of callee - * in case caller got offline without being able to send cancellation message + * in case caller got offline without being able to send cancellation message. + * for caller, this is a "Cancelled Call"; + * for callee, this is a "Missed Call" * * Actions during the call: * @@ -1277,13 +1278,15 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch * All affected devices will receive * either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED. * + * If the call is already accepted or ended, nothing happens. + * * @memberof dc_context_t * @param context The context object. * @param msg_id The ID of the call to accept. * This is the ID reported by #DC_EVENT_INCOMING_CALL * and equals to the ID of the corresponding info message. * @param accept_call_info any data that other devices receive - * in #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED. + * in #DC_EVENT_OUTGOING_CALL_ACCEPTED. * @return 1=success, 0=error */ int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info); @@ -1292,17 +1295,13 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch /** * End incoming or outgoing call. * - * From the view of the caller, a "cancellation", - * from the view of callee, a "rejection". + * For unaccepted calls ended by the caller, this is a "cancellation". + * Unaccepted calls ended by the callee are a "decline". * If the call was accepted, this is a "hangup". * - * For accepted calls, - * all participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED. - * For not accepted calls, only the caller will inform the callee. + * All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED. * - * If the callee rejects, the caller will get a timeout or give up at some point - - * same as for all other reasons the call cannot be established: Device not in reach, device muted, connectivity etc. - * This is to protect privacy of the callee, avoiding to check if callee is online. + * If the call is already ended, nothing happens. * * @memberof dc_context_t * @param context The context object. @@ -4621,8 +4620,6 @@ int dc_msg_is_info (const dc_msg_t* msg); * and also offer a way to fix the encryption, eg. by a button offering a QR scan * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` * - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted" - * - DC_INFO_OUTGOING_CALL (60) - Info-message refers to an outgoing call - * - DC_INFO_INCOMING_CALL (65) - Info-message refers to an incoming call * * For the messages that refer to a CONTACT, * dc_msg_get_info_contact_id() returns the contact ID. @@ -4679,8 +4676,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg); #define DC_INFO_INVALID_UNENCRYPTED_MAIL 13 #define DC_INFO_WEBXDC_INFO_MESSAGE 32 #define DC_INFO_CHAT_E2EE 50 -#define DC_INFO_OUTGOING_CALL 60 -#define DC_INFO_INCOMING_CALL 65 /** @@ -5717,6 +5712,12 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); #define DC_MSG_VIDEOCHAT_INVITATION 70 +/** + * Message indicating an incoming or outgoing call. + */ +#define DC_MSG_CALL 71 + + /** * The message is a webxdc instance. * @@ -6723,29 +6724,27 @@ void dc_event_unref(dc_event_t* event); * or show a notification if there is already a call in some profile. * * Together with this event, - * an info-message is added to the corresponding chat. - * The info-message, however, is _not_ additionally notified using #DC_EVENT_INCOMING_MSG, - * if needed, this has to be done by the UI explicitly. + * a message of type #DC_MSG_CALL is added to the corresponding chat; + * this message is announced and updated by the usual even as #DC_EVENT_MSGS_CHANGED. * * If user takes action, dc_accept_incoming_call() or dc_end_call() should be called. * * Otherwise, ringing should end on #DC_EVENT_CALL_ENDED * or #DC_EVENT_INCOMING_CALL_ACCEPTED * - * @param data1 (int) msg_id ID of the info-message referring to the call. + * @param data1 (int) msg_id ID of the message referring to the call. * @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call() */ #define DC_EVENT_INCOMING_CALL 2550 /** - * The callee accepted an incoming call on another device using dc_accept_incoming_call(). + * The callee accepted an incoming call on this or another device using dc_accept_incoming_call(). * The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time. * * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. * - * @param data1 (int) msg_id ID of the info-message referring to the call - * @param data2 (char*) accept_call_info, text passed to dc_place_outgoing_call() + * @param data1 (int) msg_id ID of the message referring to the call */ #define DC_EVENT_INCOMING_CALL_ACCEPTED 2560 @@ -6755,7 +6754,7 @@ void dc_event_unref(dc_event_t* event); * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. * - * @param data1 (int) msg_id ID of the info-message referring to the call + * @param data1 (int) msg_id ID of the message referring to the call * @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call() */ #define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570 @@ -6767,7 +6766,7 @@ void dc_event_unref(dc_event_t* event); * The event is sent unconditionally when the corresponding message is received. * UI should only take action in case call UI was opened before, otherwise the event should be ignored. * - * @param data1 (int) msg_id ID of the info-message referring to the call + * @param data1 (int) msg_id ID of the message referring to the call */ #define DC_EVENT_CALL_ENDED 2580 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index ddae6de52..9dc2d0680 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -779,6 +779,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut | EventType::ChatlistChanged | EventType::AccountsChanged | EventType::AccountsItemChanged + | EventType::IncomingCallAccepted { .. } | EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(), EventType::IncomingCall { place_call_info, .. @@ -786,10 +787,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut let data2 = place_call_info.to_c_string().unwrap_or_default(); data2.into_raw() } - EventType::IncomingCallAccepted { - accept_call_info, .. - } - | EventType::OutgoingCallAccepted { + EventType::OutgoingCallAccepted { accept_call_info, .. } => { let data2 = accept_call_info.to_c_string().unwrap_or_default(); diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 3133c6b08..0e77b6b54 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-jsonrpc" -version = "2.12.0" +version = "2.13.0" description = "DeltaChat JSON-RPC API" edition = "2021" license = "MPL-2.0" diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 2113a9d7a..746eee72f 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -430,8 +430,6 @@ pub enum EventType { IncomingCallAccepted { /// ID of the info message referring to the call. msg_id: u32, - /// User-defined info passed to dc_accept_incoming_call() - accept_call_info: String, }, /// Outgoing call accepted. @@ -604,12 +602,8 @@ impl From for EventType { msg_id: msg_id.to_u32(), place_call_info, }, - CoreEventType::IncomingCallAccepted { - msg_id, - accept_call_info, - } => IncomingCallAccepted { + CoreEventType::IncomingCallAccepted { msg_id } => IncomingCallAccepted { msg_id: msg_id.to_u32(), - accept_call_info, }, CoreEventType::OutgoingCallAccepted { msg_id, diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 398a6b018..0369c43e3 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -324,6 +324,9 @@ pub enum MessageViewtype { /// Message is an invitation to a videochat. VideochatInvitation, + /// Message is a call. + Call, + /// Message is an webxdc instance. Webxdc, @@ -346,6 +349,7 @@ impl From for MessageViewtype { Viewtype::Video => MessageViewtype::Video, Viewtype::File => MessageViewtype::File, Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation, + Viewtype::Call => MessageViewtype::Call, Viewtype::Webxdc => MessageViewtype::Webxdc, Viewtype::Vcard => MessageViewtype::Vcard, } @@ -365,6 +369,7 @@ impl From for Viewtype { MessageViewtype::Video => Viewtype::Video, MessageViewtype::File => Viewtype::File, MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation, + MessageViewtype::Call => Viewtype::Call, MessageViewtype::Webxdc => Viewtype::Webxdc, MessageViewtype::Vcard => Viewtype::Vcard, } @@ -438,8 +443,6 @@ pub enum SystemMessageType { /// This message contains a users iroh node address. IrohNodeAddr, - OutgoingCall, - IncomingCall, CallAccepted, CallEnded, } @@ -468,8 +471,6 @@ impl From for SystemMessageType { SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr, SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait, SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout, - SystemMessage::OutgoingCall => SystemMessageType::OutgoingCall, - SystemMessage::IncomingCall => SystemMessageType::IncomingCall, SystemMessage::CallAccepted => SystemMessageType::CallAccepted, SystemMessage::CallEnded => SystemMessageType::CallEnded, } diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 865365282..56ca32738 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -54,5 +54,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "2.12.0" + "version": "2.13.0" } diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 4b578702f..0f8950a83 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-repl" -version = "2.12.0" +version = "2.13.0" license = "MPL-2.0" edition = "2021" repository = "https://github.com/chatmail/core" diff --git a/deltachat-rpc-client/pyproject.toml b/deltachat-rpc-client/pyproject.toml index 4e06b5479..87ba827fb 100644 --- a/deltachat-rpc-client/pyproject.toml +++ b/deltachat-rpc-client/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat-rpc-client" -version = "2.12.0" +version = "2.13.0" description = "Python client for Delta Chat core JSON-RPC interface" classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index 48e19b920..4893b95c5 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deltachat-rpc-server" -version = "2.12.0" +version = "2.13.0" description = "DeltaChat JSON-RPC server" edition = "2021" readme = "README.md" diff --git a/deltachat-rpc-server/npm-package/package.json b/deltachat-rpc-server/npm-package/package.json index b1611b8e0..f4c07b041 100644 --- a/deltachat-rpc-server/npm-package/package.json +++ b/deltachat-rpc-server/npm-package/package.json @@ -15,5 +15,5 @@ }, "type": "module", "types": "index.d.ts", - "version": "2.12.0" + "version": "2.13.0" } diff --git a/deny.toml b/deny.toml index d46630e0a..42d5ab5c5 100644 --- a/deny.toml +++ b/deny.toml @@ -33,7 +33,6 @@ skip = [ { name = "lru", version = "0.12.3" }, { name = "netlink-packet-route", version = "0.17.1" }, { name = "nom", version = "7.1.3" }, - { name = "nu-ansi-term", version = "0.46.0" }, { name = "rand_chacha", version = "0.3.1" }, { name = "rand_core", version = "0.6.4" }, { name = "rand", version = "0.8.5" }, diff --git a/python/pyproject.toml b/python/pyproject.toml index 8049bec07..702c763a2 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "deltachat" -version = "2.12.0" +version = "2.13.0" description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat" readme = "README.rst" requires-python = ">=3.8" diff --git a/release-date.in b/release-date.in index fd0ed26ad..72764f116 100644 --- a/release-date.in +++ b/release-date.in @@ -1 +1 @@ -2025-08-26 \ No newline at end of file +2025-09-09 \ No newline at end of file diff --git a/scripts/update-provider-database.sh b/scripts/update-provider-database.sh index a9d4e7268..eaed2d97f 100755 --- a/scripts/update-provider-database.sh +++ b/scripts/update-provider-database.sh @@ -6,7 +6,7 @@ set -euo pipefail export TZ=UTC # Provider database revision. -REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9 +REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e CORE_ROOT="$PWD" TMP="$(mktemp -d)" diff --git a/src/aheader.rs b/src/aheader.rs index c0e26a3ec..86941fbeb 100644 --- a/src/aheader.rs +++ b/src/aheader.rs @@ -17,7 +17,6 @@ pub enum EncryptPreference { #[default] NoPreference = 0, Mutual = 1, - Reset = 20, } impl fmt::Display for EncryptPreference { @@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference { match *self { EncryptPreference::Mutual => write!(fmt, "mutual"), EncryptPreference::NoPreference => write!(fmt, "nopreference"), - EncryptPreference::Reset => write!(fmt, "reset"), } } } @@ -48,21 +46,13 @@ pub struct Aheader { pub addr: String, pub public_key: SignedPublicKey, pub prefer_encrypt: EncryptPreference, -} -impl Aheader { - /// Creates new autocrypt header - pub fn new( - addr: String, - public_key: SignedPublicKey, - prefer_encrypt: EncryptPreference, - ) -> Self { - Aheader { - addr, - public_key, - prefer_encrypt, - } - } + // Whether `_verified` attribute is present. + // + // `_verified` attribute is an extension to `Autocrypt-Gossip` + // header that is used to tell that the sender + // marked this key as verified. + pub verified: bool, } impl fmt::Display for Aheader { @@ -71,6 +61,9 @@ impl fmt::Display for Aheader { if self.prefer_encrypt == EncryptPreference::Mutual { write!(fmt, " prefer-encrypt=mutual;")?; } + if self.verified { + write!(fmt, " _verified=1;")?; + } // adds a whitespace every 78 characters, this allows // email crate to wrap the lines according to RFC 5322 @@ -125,6 +118,8 @@ impl FromStr for Aheader { .and_then(|raw| raw.parse().ok()) .unwrap_or_default(); + let verified = attributes.remove("_verified").is_some(); + // Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored // Autocrypt-Level0: unknown attribute, treat the header as invalid if attributes.keys().any(|k| !k.starts_with('_')) { @@ -135,6 +130,7 @@ impl FromStr for Aheader { addr, public_key, prefer_encrypt, + verified, }) } } @@ -152,10 +148,11 @@ mod tests { assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual); + assert_eq!(h.verified, false); Ok(()) } - // EncryptPreference::Reset is an internal value, parser should never return it + // Non-standard values of prefer-encrypt such as `reset` are treated as no preference. #[test] fn test_from_str_reset() -> Result<()> { let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}"); @@ -245,11 +242,12 @@ mod tests { assert!( format!( "{}", - Aheader::new( - "test@example.com".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::Mutual - ) + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false + } ) .contains("prefer-encrypt=mutual;") ); @@ -260,11 +258,12 @@ mod tests { assert!( !format!( "{}", - Aheader::new( - "test@example.com".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::NoPreference - ) + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::NoPreference, + verified: false + } ) .contains("prefer-encrypt") ); @@ -273,13 +272,27 @@ mod tests { assert!( format!( "{}", - Aheader::new( - "TeSt@eXaMpLe.cOm".to_string(), - SignedPublicKey::from_base64(RAWKEY).unwrap(), - EncryptPreference::Mutual - ) + Aheader { + addr: "TeSt@eXaMpLe.cOm".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::Mutual, + verified: false + } ) .contains("test@example.com") ); + + assert!( + format!( + "{}", + Aheader { + addr: "test@example.com".to_string(), + public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), + prefer_encrypt: EncryptPreference::NoPreference, + verified: true + } + ) + .contains("_verified") + ); } } diff --git a/src/calls.rs b/src/calls.rs index b59740e34..5b14fa408 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -1,16 +1,17 @@ //! # Handle calls. //! -//! Internally, calls are bound to the user-visible info message initializing the call. -//! This means, the "Call ID" is a "Message ID" currently - similar to webxdc. +//! Internally, calls are bound a user-visible message initializing the call. +//! This means, the "Call ID" is a "Message ID" - similar to Webxdc IDs. use crate::chat::{Chat, ChatId, send_msg}; use crate::constants::Chattype; +use crate::contact::ContactId; use crate::context::Context; use crate::events::EventType; use crate::headerdef::HeaderDef; -use crate::message::{self, Message, MsgId, Viewtype, rfc724_mid_exists}; +use crate::log::info; +use crate::message::{self, Message, MsgId, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; -use crate::sync::SyncData; use crate::tools::time; use anyhow::{Result, ensure}; use std::time::Duration; @@ -28,29 +29,30 @@ use tokio::time::sleep; /// as the callee won't start the call afterwards. const RINGING_SECONDS: i64 = 60; +/// For persisting parameters in the call, we use Param::Arg* +const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg; +const CALL_ENDED_TIMESTAMP: Param = Param::Arg4; + /// Information about the status of a call. #[derive(Debug, Default)] pub struct CallInfo { - /// Incoming or outgoing call? - pub is_incoming: bool, - - /// Was an incoming call accepted on this device? - /// For privacy reasons, only for accepted incoming calls, callee sends a message to caller on `end_call()`. - /// On other devices and for outgoing calls, `is_accepted` is never set. - pub is_accepted: bool, - /// User-defined text as given to place_outgoing_call() pub place_call_info: String, /// User-defined text as given to accept_incoming_call() pub accept_call_info: String, - /// Info message referring to the call. + /// Message referring to the call. + /// Data are persisted along with the message using Param::Arg* pub msg: Message, } impl CallInfo { - fn is_stale_call(&self) -> bool { + fn is_incoming(&self) -> bool { + self.msg.from_id != ContactId::SELF + } + + fn is_stale(&self) -> bool { self.remaining_ring_seconds() <= 0 } @@ -69,6 +71,60 @@ impl CallInfo { .await?; Ok(()) } + + async fn update_text_duration(&self, context: &Context) -> Result<()> { + let minutes = self.get_duration_seconds() / 60; + let duration = match minutes { + 0 => "<1 minute".to_string(), + 1 => "1 minute".to_string(), + n => format!("{} minutes", n), + }; + + if self.is_incoming() { + self.update_text(context, &format!("Incoming call\n{duration}")) + .await?; + } else { + self.update_text(context, &format!("Outgoing call\n{duration}")) + .await?; + } + Ok(()) + } + + /// Mark calls as accepted. + /// This is needed for all devices where a stale-timer runs, to prevent accepted calls being terminated as stale. + async fn mark_as_accepted(&mut self, context: &Context) -> Result<()> { + self.msg.param.set_i64(CALL_ACCEPTED_TIMESTAMP, time()); + self.msg.update_param(context).await?; + Ok(()) + } + + fn is_accepted(&self) -> bool { + self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP) + } + + async fn mark_as_ended(&mut self, context: &Context) -> Result<()> { + self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time()); + self.msg.update_param(context).await?; + Ok(()) + } + + fn is_ended(&self) -> bool { + self.msg.param.exists(CALL_ENDED_TIMESTAMP) + } + + fn get_duration_seconds(&self) -> i64 { + if let (Some(start), Some(end)) = ( + self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP), + self.msg.param.get_i64(CALL_ENDED_TIMESTAMP), + ) { + let seconds = end - start; + if seconds <= 0 { + return 1; + } + return seconds; + } + 0 + } } impl Context { @@ -82,11 +138,10 @@ impl Context { ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); let mut call = Message { - viewtype: Viewtype::Text, - text: "Calling...".into(), + viewtype: Viewtype::Call, + text: "Outgoing call".into(), ..Default::default() }; - call.param.set_cmd(SystemMessage::OutgoingCall); call.param.set(Param::WebrtcRoom, &place_call_info); call.id = send_msg(self, chat_id, &mut call).await?; @@ -107,60 +162,70 @@ impl Context { accept_call_info: String, ) -> Result<()> { let mut call: CallInfo = self.load_call_by_id(call_id).await?; - ensure!(call.is_incoming); + ensure!(call.is_incoming()); + if call.is_accepted() || call.is_ended() { + info!(self, "Call already accepted/ended"); + return Ok(()); + } + call.mark_as_accepted(self).await?; let chat = Chat::load_from_db(self, call.msg.chat_id).await?; if chat.is_contact_request() { chat.id.accept(self).await?; } - call.msg - .mark_call_as_accepted(self, accept_call_info.to_string()) - .await?; - // send an acceptance message around: to the caller as well as to the other devices of the callee let mut msg = Message { viewtype: Viewtype::Text, - text: "Call accepted".into(), + text: "[Call accepted]".into(), ..Default::default() }; msg.param.set_cmd(SystemMessage::CallAccepted); + msg.hidden = true; msg.param .set(Param::WebrtcAccepted, accept_call_info.to_string()); msg.set_quote(self, Some(&call.msg)).await?; msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; self.emit_event(EventType::IncomingCallAccepted { msg_id: call.msg.id, - accept_call_info, }); + self.emit_msgs_changed(call.msg.chat_id, call_id); Ok(()) } - /// Cancel, reject or hangup an incoming or outgoing call. + /// Cancel, decline or hangup an incoming or outgoing call. pub async fn end_call(&self, call_id: MsgId) -> Result<()> { - let call: CallInfo = self.load_call_by_id(call_id).await?; - - if call.is_accepted || !call.is_incoming { - let mut msg = Message { - viewtype: Viewtype::Text, - text: "Call ended".into(), - ..Default::default() - }; - msg.param.set_cmd(SystemMessage::CallEnded); - msg.set_quote(self, Some(&call.msg)).await?; - msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; - } else if call.is_incoming { - // to protect privacy, we do not send a message to others from callee for unaccepted calls - self.add_sync_item(SyncData::RejectIncomingCall { - msg: call.msg.rfc724_mid, - }) - .await?; - self.scheduler.interrupt_inbox().await; + let mut call: CallInfo = self.load_call_by_id(call_id).await?; + if call.is_ended() { + info!(self, "Call already ended"); + return Ok(()); } + call.mark_as_ended(self).await?; + + if !call.is_accepted() { + if call.is_incoming() { + call.update_text(self, "Declined call").await?; + } else { + call.update_text(self, "Cancelled call").await?; + } + } else { + call.update_text_duration(self).await?; + } + + let mut msg = Message { + viewtype: Viewtype::Text, + text: "[Call ended]".into(), + ..Default::default() + }; + msg.param.set_cmd(SystemMessage::CallEnded); + msg.hidden = true; + msg.set_quote(self, Some(&call.msg)).await?; + msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; self.emit_event(EventType::CallEnded { msg_id: call.msg.id, }); + self.emit_msgs_changed(call.msg.chat_id, call_id); Ok(()) } @@ -170,8 +235,15 @@ impl Context { call_id: MsgId, ) -> Result<()> { sleep(Duration::from_secs(wait)).await; - let call = context.load_call_by_id(call_id).await?; - if !call.is_accepted { + let mut call = context.load_call_by_id(call_id).await?; + if !call.is_accepted() && !call.is_ended() { + call.mark_as_ended(&context).await?; + if call.is_incoming() { + call.update_text(&context, "Missed call").await?; + } else { + call.update_text(&context, "Cancelled call").await?; + } + context.emit_msgs_changed(call.msg.chat_id, call_id); context.emit_event(EventType::CallEnded { msg_id: call.msg.id, }); @@ -181,70 +253,94 @@ impl Context { pub(crate) async fn handle_call_msg( &self, - mime_message: &MimeMessage, call_id: MsgId, + mime_message: &MimeMessage, + from_id: ContactId, ) -> Result<()> { - match mime_message.is_system_message { - SystemMessage::IncomingCall => { - let call = self.load_call_by_id(call_id).await?; - if call.is_incoming { - if call.is_stale_call() { - call.update_text(self, "Missed call").await?; - self.emit_incoming_msg(call.msg.chat_id, call_id); - } else { - self.emit_msgs_changed(call.msg.chat_id, call_id); - self.emit_event(EventType::IncomingCall { - msg_id: call.msg.id, - place_call_info: call.place_call_info.to_string(), - }); - let wait = call.remaining_ring_seconds(); - task::spawn(Context::emit_end_call_if_unaccepted( - self.clone(), - wait.try_into()?, - call.msg.id, - )); + if mime_message.is_call() { + let call = self.load_call_by_id(call_id).await?; + if call.is_incoming() { + if call.is_stale() { + call.update_text(self, "Missed call").await?; + self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call + } else { + call.update_text(self, "Incoming call").await?; + self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified + self.emit_event(EventType::IncomingCall { + msg_id: call.msg.id, + place_call_info: call.place_call_info.to_string(), + }); + let wait = call.remaining_ring_seconds(); + task::spawn(Context::emit_end_call_if_unaccepted( + self.clone(), + wait.try_into()?, + call.msg.id, + )); + } + } else { + call.update_text(self, "Outgoing call").await?; + self.emit_msgs_changed(call.msg.chat_id, call_id); + } + } else { + match mime_message.is_system_message { + SystemMessage::CallAccepted => { + let mut call = self.load_call_by_id(call_id).await?; + if call.is_ended() || call.is_accepted() { + info!(self, "CallAccepted received for accepted/ended call"); + return Ok(()); } - } else { - self.emit_msgs_changed(call.msg.chat_id, call_id); - } - } - SystemMessage::CallAccepted => { - let call = self.load_call_by_id(call_id).await?; - self.emit_msgs_changed(call.msg.chat_id, call_id); - if call.is_incoming { - self.emit_event(EventType::IncomingCallAccepted { - msg_id: call.msg.id, - accept_call_info: call.accept_call_info, - }); - } else { - let accept_call_info = mime_message - .get_header(HeaderDef::ChatWebrtcAccepted) - .unwrap_or_default(); - call.msg - .clone() - .mark_call_as_accepted(self, accept_call_info.to_string()) - .await?; - self.emit_event(EventType::OutgoingCallAccepted { - msg_id: call.msg.id, - accept_call_info: accept_call_info.to_string(), - }); - } - } - SystemMessage::CallEnded => { - let call = self.load_call_by_id(call_id).await?; - self.emit_msgs_changed(call.msg.chat_id, call_id); - self.emit_event(EventType::CallEnded { - msg_id: call.msg.id, - }); - } - _ => {} - } - Ok(()) - } - pub(crate) async fn sync_call_rejection(&self, rfc724_mid: &str) -> Result<()> { - if let Some((msg_id, _)) = rfc724_mid_exists(self, rfc724_mid).await? { - self.emit_event(EventType::CallEnded { msg_id }); + call.mark_as_accepted(self).await?; + self.emit_msgs_changed(call.msg.chat_id, call_id); + if call.is_incoming() { + self.emit_event(EventType::IncomingCallAccepted { + msg_id: call.msg.id, + }); + } else { + let accept_call_info = mime_message + .get_header(HeaderDef::ChatWebrtcAccepted) + .unwrap_or_default(); + self.emit_event(EventType::OutgoingCallAccepted { + msg_id: call.msg.id, + accept_call_info: accept_call_info.to_string(), + }); + } + } + SystemMessage::CallEnded => { + let mut call = self.load_call_by_id(call_id).await?; + if call.is_ended() { + // may happen eg. if a a message is missed + info!(self, "CallEnded received for ended call"); + return Ok(()); + } + + call.mark_as_ended(self).await?; + if !call.is_accepted() { + if call.is_incoming() { + if from_id == ContactId::SELF { + call.update_text(self, "Declined call").await?; + } else { + call.update_text(self, "Missed call").await?; + } + } else { + // outgoing + if from_id == ContactId::SELF { + call.update_text(self, "Cancelled call").await?; + } else { + call.update_text(self, "Declined call").await?; + } + } + } else { + call.update_text_duration(self).await?; + } + + self.emit_msgs_changed(call.msg.chat_id, call_id); + self.emit_event(EventType::CallEnded { + msg_id: call.msg.id, + }); + } + _ => {} + } } Ok(()) } @@ -255,14 +351,9 @@ impl Context { } fn load_call_by_message(&self, call: Message) -> Result { - ensure!( - call.get_info_type() == SystemMessage::IncomingCall - || call.get_info_type() == SystemMessage::OutgoingCall - ); + ensure!(call.viewtype == Viewtype::Call); Ok(CallInfo { - is_incoming: call.get_info_type() == SystemMessage::IncomingCall, - is_accepted: call.is_call_accepted()?, place_call_info: call .param .get(Param::WebrtcRoom) @@ -278,30 +369,5 @@ impl Context { } } -impl Message { - async fn mark_call_as_accepted( - &mut self, - context: &Context, - accept_call_info: String, - ) -> Result<()> { - ensure!( - self.get_info_type() == SystemMessage::IncomingCall - || self.get_info_type() == SystemMessage::OutgoingCall - ); - self.param.set_int(Param::Arg, 1); - self.param.set(Param::WebrtcAccepted, accept_call_info); - self.update_param(context).await?; - Ok(()) - } - - fn is_call_accepted(&self) -> Result { - ensure!( - self.get_info_type() == SystemMessage::IncomingCall - || self.get_info_type() == SystemMessage::OutgoingCall - ); - Ok(self.param.get_int(Param::Arg) == Some(1)) - } -} - #[cfg(test)] mod calls_tests; diff --git a/src/calls/calls_tests.rs b/src/calls/calls_tests.rs index 44a76a29d..d51f2ce28 100644 --- a/src/calls/calls_tests.rs +++ b/src/calls/calls_tests.rs @@ -1,17 +1,23 @@ use super::*; use crate::config::Config; -use crate::test_utils::{TestContext, TestContextManager, sync}; +use crate::test_utils::{TestContext, TestContextManager}; struct CallSetup { pub alice: TestContext, pub alice2: TestContext, pub alice_call: Message, + pub alice2_call: Message, pub bob: TestContext, pub bob2: TestContext, pub bob_call: Message, pub bob2_call: Message, } +async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()> { + assert_eq!(Message::load_from_db(t, call_id).await?.text, text); + Ok(()) +} + async fn setup_call() -> Result { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; @@ -26,18 +32,20 @@ async fn setup_call() -> Result { // Alice's other device sees the same message as an outgoing call. let alice_chat = alice.create_chat(&bob).await; let test_msg_id = alice - .place_outgoing_call(alice_chat.id, "place_info".to_string()) + .place_outgoing_call(alice_chat.id, "place-info-123".to_string()) .await?; let sent1 = alice.pop_sent_msg().await; assert_eq!(sent1.sender_msg_id, test_msg_id); let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?; let alice2_call = alice2.recv_msg(&sent1).await; for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] { - assert!(m.is_info()); - assert_eq!(m.get_info_type(), SystemMessage::OutgoingCall); + assert!(!m.is_info()); + assert_eq!(m.viewtype, Viewtype::Call); let info = t.load_call_by_id(m.id).await?; - assert!(!info.is_accepted); - assert_eq!(info.place_call_info, "place_info"); + assert!(!info.is_incoming()); + assert!(!info.is_accepted()); + assert_eq!(info.place_call_info, "place-info-123"); + assert_text(t, m.id, "Outgoing call").await?; } // Bob receives the message referring to the call on two devices; @@ -45,20 +53,23 @@ async fn setup_call() -> Result { let bob_call = bob.recv_msg(&sent1).await; let bob2_call = bob2.recv_msg(&sent1).await; for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] { - assert!(m.is_info()); - assert_eq!(m.get_info_type(), SystemMessage::IncomingCall); + assert!(!m.is_info()); + assert_eq!(m.viewtype, Viewtype::Call); t.evtracker .get_matching(|evt| matches!(evt, EventType::IncomingCall { .. })) .await; let info = t.load_call_by_id(m.id).await?; - assert!(!info.is_accepted); - assert_eq!(info.place_call_info, "place_info"); + assert!(info.is_incoming()); + assert!(!info.is_accepted()); + assert_eq!(info.place_call_info, "place-info-123"); + assert_text(t, m.id, "Incoming call").await?; } Ok(CallSetup { alice, alice2, alice_call, + alice2_call, bob, bob2, bob_call, @@ -71,55 +82,53 @@ async fn accept_call() -> Result { alice, alice2, alice_call, + alice2_call, bob, bob2, bob_call, bob2_call, } = setup_call().await?; - // Bob accepts the incoming call, this does not add an additional message to the chat - bob.accept_incoming_call(bob_call.id, "accepted_info".to_string()) + // Bob accepts the incoming call + bob.accept_incoming_call(bob_call.id, "accept-info-456".to_string()) .await?; + assert_text(&bob, bob_call.id, "Incoming call").await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) .await; let sent2 = bob.pop_sent_msg().await; let info = bob.load_call_by_id(bob_call.id).await?; - assert!(info.is_accepted); - assert_eq!(info.place_call_info, "place_info"); - assert_eq!(info.accept_call_info, "accepted_info"); + assert!(info.is_accepted()); + assert_eq!(info.place_call_info, "place-info-123"); - let bob_accept_msg = bob2.recv_msg(&sent2).await; - assert!(bob_accept_msg.is_info()); - assert_eq!(bob_accept_msg.get_info_type(), SystemMessage::CallAccepted); + bob2.recv_msg_trash(&sent2).await; + assert_text(&bob, bob_call.id, "Incoming call").await?; bob2.evtracker .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) .await; let info = bob2.load_call_by_id(bob2_call.id).await?; - assert!(!info.is_accepted); // "accepted" is only true on the device that does the call + assert!(info.is_accepted()); // Alice receives the acceptance message - let alice_accept_msg = alice.recv_msg(&sent2).await; - assert!(alice_accept_msg.is_info()); - assert_eq!( - alice_accept_msg.get_info_type(), - SystemMessage::CallAccepted - ); - alice + alice.recv_msg_trash(&sent2).await; + assert_text(&alice, alice_call.id, "Outgoing call").await?; + let ev = alice .evtracker .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) .await; - let info = alice.load_call_by_id(alice_call.id).await?; - assert!(info.is_accepted); - assert_eq!(info.place_call_info, "place_info"); - assert_eq!(info.accept_call_info, "accepted_info"); - - let alice2_accept_msg = alice2.recv_msg(&sent2).await; - assert!(alice2_accept_msg.is_info()); assert_eq!( - alice2_accept_msg.get_info_type(), - SystemMessage::CallAccepted + ev, + EventType::OutgoingCallAccepted { + msg_id: alice2_call.id, + accept_call_info: "accept-info-456".to_string() + } ); + let info = alice.load_call_by_id(alice_call.id).await?; + assert!(info.is_accepted()); + assert_eq!(info.place_call_info, "place-info-123"); + + alice2.recv_msg_trash(&sent2).await; + assert_text(&alice2, alice2_call.id, "Outgoing call").await?; alice2 .evtracker .get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. })) @@ -129,6 +138,7 @@ async fn accept_call() -> Result { alice, alice2, alice_call, + alice2_call, bob, bob2, bob_call, @@ -136,46 +146,45 @@ async fn accept_call() -> Result { }) } -fn assert_is_call_ended_info_msg(msg: Message) { - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::CallEnded); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_accept_call_callee_ends() -> Result<()> { // Alice calls Bob, Bob accepts let CallSetup { alice, + alice_call, alice2, + alice2_call, bob, bob2, bob_call, + bob2_call, .. } = accept_call().await?; // Bob has accepted the call and also ends it bob.end_call(bob_call.id).await?; + assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = bob.pop_sent_msg().await; - let bob2_end_call_msg = bob2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(bob2_end_call_msg); + bob2.recv_msg_trash(&sent3).await; + assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?; bob2.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; // Alice receives the ending message - let alice_end_call_msg = alice.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(alice_end_call_msg); + alice.recv_msg_trash(&sent3).await; + assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?; alice .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; - let alice2_end_call_msg = alice2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(alice2_end_call_msg); + alice2.recv_msg_trash(&sent3).await; + assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?; alice2 .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) @@ -189,37 +198,41 @@ async fn test_accept_call_caller_ends() -> Result<()> { // Alice calls Bob, Bob accepts let CallSetup { alice, + alice_call, alice2, + alice2_call, bob, bob2, bob_call, + bob2_call, .. } = accept_call().await?; // Bob has accepted the call but Alice ends it - alice.end_call(bob_call.id).await?; + alice.end_call(alice_call.id).await?; + assert_text(&alice, alice_call.id, "Outgoing call\n<1 minute").await?; alice .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = alice.pop_sent_msg().await; - let alice2_end_call_msg = alice2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(alice2_end_call_msg); + alice2.recv_msg_trash(&sent3).await; + assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?; alice2 .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; // Bob receives the ending message - let bob_end_call_msg = bob.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(bob_end_call_msg); + bob.recv_msg_trash(&sent3).await; + assert_text(&bob, bob_call.id, "Incoming call\n<1 minute").await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; - let bob2_end_call_msg = bob2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(bob2_end_call_msg); + bob2.recv_msg_trash(&sent3).await; + assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?; bob2.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; @@ -231,25 +244,47 @@ async fn test_accept_call_caller_ends() -> Result<()> { async fn test_callee_rejects_call() -> Result<()> { // Alice calls Bob let CallSetup { + alice, + alice2, + alice_call, + alice2_call, bob, bob2, bob_call, + bob2_call, .. } = setup_call().await?; - // Bob does not want to talk with Alice. - // To protect Bob's privacy, no message is sent to Alice (who will time out). - // To let Bob close the call window on all devices, a sync message is used instead. + // Bob has accepted Alice before, but does not want to talk with Alice + bob_call.chat_id.accept(&bob).await?; bob.end_call(bob_call.id).await?; + assert_text(&bob, bob_call.id, "Declined call").await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + let sent3 = bob.pop_sent_msg().await; - sync(&bob, &bob2).await; + bob2.recv_msg_trash(&sent3).await; + assert_text(&bob2, bob2_call.id, "Declined call").await?; bob2.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; + // Alice receives decline message + alice.recv_msg_trash(&sent3).await; + assert_text(&alice, alice_call.id, "Declined call").await?; + alice + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + + alice2.recv_msg_trash(&sent3).await; + assert_text(&alice2, alice2_call.id, "Declined call").await?; + alice2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) + .await; + Ok(()) } @@ -260,35 +295,39 @@ async fn test_caller_cancels_call() -> Result<()> { alice, alice2, alice_call, + alice2_call, bob, bob2, + bob_call, + bob2_call, .. } = setup_call().await?; // Alice changes their mind before Bob picks up alice.end_call(alice_call.id).await?; + assert_text(&alice, alice_call.id, "Cancelled call").await?; alice .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; let sent3 = alice.pop_sent_msg().await; - let alice2_call_ended_msg = alice2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(alice2_call_ended_msg); + alice2.recv_msg_trash(&sent3).await; + assert_text(&alice2, alice2_call.id, "Cancelled call").await?; alice2 .evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; // Bob receives the ending message - let bob_call_ended_msg = bob.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(bob_call_ended_msg); + bob.recv_msg_trash(&sent3).await; + assert_text(&bob, bob_call.id, "Missed call").await?; bob.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; - let bob2_call_ended_msg = bob2.recv_msg(&sent3).await; - assert_is_call_ended_info_msg(bob2_call_ended_msg); + bob2.recv_msg_trash(&sent3).await; + assert_text(&bob2, bob2_call.id, "Missed call").await?; bob2.evtracker .get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .await; @@ -306,7 +345,7 @@ async fn test_is_stale_call() -> Result<()> { }, ..Default::default() }; - assert!(!call_info.is_stale_call()); + assert!(!call_info.is_stale()); let remaining_seconds = call_info.remaining_ring_seconds(); assert!(remaining_seconds == RINGING_SECONDS || remaining_seconds == RINGING_SECONDS - 1); @@ -318,7 +357,7 @@ async fn test_is_stale_call() -> Result<()> { }, ..Default::default() }; - assert!(!call_info.is_stale_call()); + assert!(!call_info.is_stale()); let remaining_seconds = call_info.remaining_ring_seconds(); assert!(remaining_seconds == RINGING_SECONDS - 5 || remaining_seconds == RINGING_SECONDS - 6); @@ -330,28 +369,32 @@ async fn test_is_stale_call() -> Result<()> { }, ..Default::default() }; - assert!(call_info.is_stale_call()); + assert!(call_info.is_stale()); assert_eq!(call_info.remaining_ring_seconds(), 0); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_mark_call_as_accepted() -> Result<()> { +async fn test_mark_calls() -> Result<()> { let CallSetup { alice, alice_call, .. } = setup_call().await?; - assert!(!alice_call.is_call_accepted()?); - let mut alice_call = Message::load_from_db(&alice, alice_call.id).await?; - assert!(!alice_call.is_call_accepted()?); - alice_call - .mark_call_as_accepted(&alice, "accepted_info".to_string()) - .await?; - assert!(alice_call.is_call_accepted()?); + let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?; + assert!(!call_info.is_accepted()); + assert!(!call_info.is_ended()); + call_info.mark_as_accepted(&alice).await?; + assert!(call_info.is_accepted()); + assert!(!call_info.is_ended()); - let alice_call = Message::load_from_db(&alice, alice_call.id).await?; - assert!(alice_call.is_call_accepted()?); + let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?; + assert!(call_info.is_accepted()); + assert!(!call_info.is_ended()); + + call_info.mark_as_ended(&alice).await?; + assert!(call_info.is_accepted()); + assert!(call_info.is_ended()); Ok(()) } diff --git a/src/chat.rs b/src/chat.rs index a6e325bca..f9d95fc9f 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2717,7 +2717,10 @@ impl ChatIdBlocked { } async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { - if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation { + if msg.viewtype == Viewtype::Text + || msg.viewtype == Viewtype::VideochatInvitation + || msg.viewtype == Viewtype::Call + { // the caller should check if the message text is empty } else if msg.viewtype.has_file() { let viewtype_orig = msg.viewtype; @@ -3204,6 +3207,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin original_msg.viewtype != Viewtype::VideochatInvitation, "Cannot edit videochat invitations" ); + ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls"); ensure!( !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings "Cannot add text" diff --git a/src/config.rs b/src/config.rs index b16382cd7..02ad57374 100644 --- a/src/config.rs +++ b/src/config.rs @@ -151,10 +151,6 @@ pub enum Config { /// setting up a second device, or receiving a sync message. BccSelf, - /// True if encryption is preferred according to Autocrypt standard. - #[strum(props(default = "1"))] - E2eeEnabled, - /// True if Message Delivery Notifications (read receipts) should /// be sent and requested. #[strum(props(default = "1"))] @@ -705,7 +701,6 @@ impl Context { Config::Socks5Enabled | Config::ProxyEnabled | Config::BccSelf - | Config::E2eeEnabled | Config::MdnsEnabled | Config::SentboxWatch | Config::MvboxMove diff --git a/src/context.rs b/src/context.rs index ba085f63e..c94780a8d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -833,7 +833,6 @@ impl Context { .query_get_value("PRAGMA journal_mode;", ()) .await? .unwrap_or_else(|| "unknown".to_string()); - let e2ee_enabled = self.get_config_int(Config::E2eeEnabled).await?; let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?; let bcc_self = self.get_config_int(Config::BccSelf).await?; let sync_msgs = self.get_config_int(Config::SyncMsgs).await?; @@ -967,7 +966,6 @@ impl Context { res.insert("configured_mvbox_folder", configured_mvbox_folder); res.insert("configured_trash_folder", configured_trash_folder); res.insert("mdns_enabled", mdns_enabled.to_string()); - res.insert("e2ee_enabled", e2ee_enabled.to_string()); res.insert("bcc_self", bcc_self.to_string()); res.insert("sync_msgs", sync_msgs.to_string()); res.insert("disable_idle", disable_idle.to_string()); diff --git a/src/e2ee.rs b/src/e2ee.rs index 69046de12..1f5f902bb 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -4,10 +4,8 @@ use std::io::Cursor; use anyhow::Result; use mail_builder::mime::MimePart; -use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; -use crate::config::Config; use crate::context::Context; use crate::key::{SignedPublicKey, load_self_public_key, load_self_secret_key}; use crate::pgp; @@ -21,9 +19,7 @@ pub struct EncryptHelper { impl EncryptHelper { pub async fn new(context: &Context) -> Result { - let prefer_encrypt = - EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?) - .unwrap_or_default(); + let prefer_encrypt = EncryptPreference::Mutual; let addr = context.get_primary_self_addr().await?; let public_key = load_self_public_key(context).await?; @@ -35,9 +31,12 @@ impl EncryptHelper { } pub fn get_aheader(&self) -> Aheader { - let pk = self.public_key.clone(); - let addr = self.addr.to_string(); - Aheader::new(addr, pk, self.prefer_encrypt) + Aheader { + addr: self.addr.clone(), + public_key: self.public_key.clone(), + prefer_encrypt: self.prefer_encrypt, + verified: false, + } } /// Tries to encrypt the passed in `mail`. diff --git a/src/events/payload.rs b/src/events/payload.rs index fb0d972a3..6bddadf79 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -388,8 +388,6 @@ pub enum EventType { IncomingCallAccepted { /// ID of the message referring to the call. msg_id: MsgId, - /// User-defined info as passed to accept_incoming_call() - accept_call_info: String, }, /// Outgoing call accepted. diff --git a/src/imex.rs b/src/imex.rs index 3ccdc4615..56f018acb 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -140,32 +140,8 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result { } async fn set_self_key(context: &Context, armored: &str) -> Result<()> { - // try hard to only modify key-state - let (private_key, header) = SignedSecretKey::from_asc(armored)?; + let private_key = SignedSecretKey::from_asc(armored)?; let public_key = private_key.split_public_key()?; - if let Some(preferencrypt) = header.get("Autocrypt-Prefer-Encrypt") { - let e2ee_enabled = match preferencrypt.as_str() { - "nopreference" => 0, - "mutual" => 1, - _ => { - bail!("invalid Autocrypt-Prefer-Encrypt header: {:?}", header); - } - }; - context - .sql - .set_raw_config_int("e2ee_enabled", e2ee_enabled) - .await?; - } else { - // `Autocrypt-Prefer-Encrypt` is not included - // in keys exported to file. - // - // `Autocrypt-Prefer-Encrypt` also SHOULD be sent - // in Autocrypt Setup Message according to Autocrypt specification, - // but K-9 6.802 does not include this header. - // - // We keep current setting in this case. - info!(context, "No Autocrypt-Prefer-Encrypt header."); - }; let keypair = pgp::KeyPair { public: public_key, diff --git a/src/imex/key_transfer.rs b/src/imex/key_transfer.rs index 84bce5598..c995094fa 100644 --- a/src/imex/key_transfer.rs +++ b/src/imex/key_transfer.rs @@ -93,10 +93,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result None, - true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), - }; + let ac_headers = Some(("Autocrypt-Prefer-Encrypt", "mutual")); let private_key_asc = private_key.to_asc(ac_headers); let encr = pgp::symm_encrypt_setup_file(passphrase, private_key_asc.into_bytes()) .await? diff --git a/src/internals_for_benchmarks.rs b/src/internals_for_benchmarks.rs index 5ada6e0db..d08bd0842 100644 --- a/src/internals_for_benchmarks.rs +++ b/src/internals_for_benchmarks.rs @@ -3,7 +3,6 @@ use anyhow::Result; use deltachat_contact_tools::EmailAddress; -use std::collections::BTreeMap; use crate::chat::ChatId; use crate::context::Context; @@ -14,7 +13,7 @@ pub use crate::pgp; use self::pgp::KeyPair; -pub fn key_from_asc(data: &str) -> Result<(key::SignedSecretKey, BTreeMap)> { +pub fn key_from_asc(data: &str) -> Result { key::SignedSecretKey::from_asc(data) } diff --git a/src/key.rs b/src/key.rs index 7d01812d2..e9ca6c595 100644 --- a/src/key.rs +++ b/src/key.rs @@ -71,31 +71,17 @@ pub(crate) trait DcKey: Serialize + Deserializable + Clone { } /// Create a key from an ASCII-armored string. - /// - /// Returns the key and a map of any headers which might have been set in - /// the ASCII-armored representation. - fn from_asc(data: &str) -> Result<(Self, BTreeMap)> { + fn from_asc(data: &str) -> Result { let bytes = data.as_bytes(); let res = Self::from_armor_single(Cursor::new(bytes)); - let (key, headers) = match res { + let (key, _headers) = match res { Err(pgp::errors::Error::NoMatchingPacket { .. }) => match Self::is_private() { true => bail!("No private key packet found"), false => bail!("No public key packet found"), }, _ => res.context("rPGP error")?, }; - let headers = headers - .into_iter() - .map(|(key, values)| { - ( - key.trim().to_lowercase(), - values - .last() - .map_or_else(String::new, |s| s.trim().to_string()), - ) - }) - .collect(); - Ok((key, headers)) + Ok(key) } /// Serialise the key as bytes. @@ -446,7 +432,7 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) -> /// to avoid generating the key in tests. /// Use import/export APIs instead. pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Result<()> { - let secret = SignedSecretKey::from_asc(secret_data)?.0; + let secret = SignedSecretKey::from_asc(secret_data)?; let public = secret.split_public_key()?; let keypair = KeyPair { public, secret }; store_self_keypair(context, &keypair).await?; @@ -532,7 +518,7 @@ mod tests { #[test] fn test_from_armored_string() { - let (private_key, _) = SignedSecretKey::from_asc( + let private_key = SignedSecretKey::from_asc( "-----BEGIN PGP PRIVATE KEY BLOCK----- xcLYBF0fgz4BCADnRUV52V4xhSsU56ZaAn3+3oG86MZhXy4X8w14WZZDf0VJGeTh @@ -600,17 +586,13 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD fn test_asc_roundtrip() { let key = KEYPAIR.public.clone(); let asc = key.to_asc(Some(("spam", "ham"))); - let (key2, hdrs) = SignedPublicKey::from_asc(&asc).unwrap(); + let key2 = SignedPublicKey::from_asc(&asc).unwrap(); assert_eq!(key, key2); - assert_eq!(hdrs.len(), 1); - assert_eq!(hdrs.get("spam"), Some(&String::from("ham"))); let key = KEYPAIR.secret.clone(); let asc = key.to_asc(Some(("spam", "ham"))); - let (key2, hdrs) = SignedSecretKey::from_asc(&asc).unwrap(); + let key2 = SignedSecretKey::from_asc(&asc).unwrap(); assert_eq!(key, key2); - assert_eq!(hdrs.len(), 1); - assert_eq!(hdrs.get("spam"), Some(&String::from("ham"))); } #[test] diff --git a/src/message.rs b/src/message.rs index a9aca8aaa..a5e5a218b 100644 --- a/src/message.rs +++ b/src/message.rs @@ -973,8 +973,6 @@ impl Message { | SystemMessage::WebxdcStatusUpdate | SystemMessage::WebxdcInfoMessage | SystemMessage::IrohNodeAddr - | SystemMessage::OutgoingCall - | SystemMessage::IncomingCall | SystemMessage::CallAccepted | SystemMessage::CallEnded | SystemMessage::Unknown => Ok(None), @@ -2293,6 +2291,9 @@ pub enum Viewtype { /// Message is an invitation to a videochat. VideochatInvitation = 70, + /// Message is an incoming or outgoing call. + Call = 71, + /// Message is an webxdc instance. Webxdc = 80, @@ -2316,6 +2317,7 @@ impl Viewtype { Viewtype::Video => true, Viewtype::File => true, Viewtype::VideochatInvitation => false, + Viewtype::Call => false, Viewtype::Webxdc => true, Viewtype::Vcard => true, } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 3a1e15177..f02670326 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashSet}; use std::io::Cursor; -use anyhow::{Context as _, Result, anyhow, bail}; +use anyhow::{Context as _, Result, bail}; use base64::Engine as _; use data_encoding::BASE32_NOPAD; use deltachat_contact_tools::sanitize_bidi_characters; @@ -1132,13 +1132,14 @@ impl MimeFactory { continue; } - let header = Aheader::new( - addr.clone(), - key.clone(), + let header = Aheader { + addr: addr.clone(), + public_key: key.clone(), // Autocrypt 1.1.0 specification says that // `prefer-encrypt` attribute SHOULD NOT be included. - EncryptPreference::NoPreference, - ) + prefer_encrypt: EncryptPreference::NoPreference, + verified: false, + } .to_string(); message = message.header( @@ -1614,15 +1615,6 @@ impl MimeFactory { .into(), )); } - SystemMessage::OutgoingCall => { - headers.push(( - "Chat-Content", - mail_builder::headers::raw::Raw::new("call").into(), - )); - } - SystemMessage::IncomingCall => { - return Err(anyhow!("Unexpected incoming call rendering.")); - } SystemMessage::CallAccepted => { headers.push(( "Chat-Content", @@ -1659,6 +1651,14 @@ impl MimeFactory { "Chat-Content", mail_builder::headers::raw::Raw::new("videochat-invitation").into(), )); + } else if msg.viewtype == Viewtype::Call { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("call").into(), + )); + placeholdertext = Some( + "[This is a 'Call'. The sender uses an experiment not supported on your version yet]".to_string(), + ); } if msg.param.exists(Param::WebrtcRoom) { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 7c0e5f4ee..5b31fc43c 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,7 +1,7 @@ //! # MIME message parsing module. use std::cmp::min; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::Path; use std::str; use std::str::FromStr; @@ -36,6 +36,17 @@ use crate::tools::{ use crate::{chatlist_events, location, stock_str, tools}; use crate::{constants, token}; +/// Public key extracted from `Autocrypt-Gossip` +/// header with associated information. +#[derive(Debug)] +pub struct GossipedKey { + /// Public key extracted from `keydata` attribute. + pub public_key: SignedPublicKey, + + /// True if `Autocrypt-Gossip` has a `_verified` attribute. + pub verified: bool, +} + /// A parsed MIME message. /// /// This represents the relevant information of a parsed MIME message @@ -85,7 +96,7 @@ pub(crate) struct MimeMessage { /// The addresses for which there was a gossip header /// and their respective gossiped keys. - pub gossiped_keys: HashMap, + pub gossiped_keys: BTreeMap, /// Fingerprint of the key in the Autocrypt header. /// @@ -217,17 +228,7 @@ pub enum SystemMessage { /// "Messages are end-to-end encrypted." ChatE2ee = 50, - /// This system message represents an outgoing call. - /// This message is visible to the user as an "info" message. - OutgoingCall = 60, - - /// This system message represents an incoming call. - /// This message is visible to the user as an "info" message. - IncomingCall = 65, - /// Message indicating that a call was accepted. - /// While the 1:1 call may be established elsewhere, - /// the message is still needed for a multidevice setup, so that other devices stop ringing. CallAccepted = 66, /// Message indicating that a call was ended. @@ -705,12 +706,6 @@ impl MimeMessage { self.is_system_message = SystemMessage::ChatProtectionDisabled; } else if value == "group-avatar-changed" { self.is_system_message = SystemMessage::GroupImageChanged; - } else if value == "call" { - self.is_system_message = if self.incoming { - SystemMessage::IncomingCall - } else { - SystemMessage::OutgoingCall - }; } else if value == "call-accepted" { self.is_system_message = SystemMessage::CallAccepted; } else if value == "call-ended" { @@ -751,6 +746,8 @@ impl MimeMessage { if let Some(room) = room { if content == "videochat-invitation" { part.typ = Viewtype::VideochatInvitation; + } else if content == "call" { + part.typ = Viewtype::Call } part.param.set(Param::WebrtcRoom, room); } else if let Some(accepted) = accepted { @@ -780,7 +777,10 @@ impl MimeMessage { | Viewtype::Vcard | Viewtype::File | Viewtype::Webxdc => true, - Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false, + Viewtype::Unknown + | Viewtype::Text + | Viewtype::VideochatInvitation + | Viewtype::Call => false, }) { let mut parts = std::mem::take(&mut self.parts); @@ -1532,7 +1532,7 @@ impl MimeMessage { ); return Ok(false); } - Ok((key, _)) => key, + Ok(key) => key, }; if let Err(err) = key.verify() { warn!(context, "Attached PGP key verification failed: {err:#}."); @@ -1595,6 +1595,13 @@ impl MimeMessage { } } + /// Check if a message is a call. + pub(crate) fn is_call(&self) -> bool { + self.parts + .first() + .is_some_and(|part| part.typ == Viewtype::Call) + } + pub fn replace_msg_by_error(&mut self, error_msg: &str) { self.is_system_message = SystemMessage::Unknown; if let Some(part) = self.parts.first_mut() { @@ -1989,9 +1996,9 @@ async fn parse_gossip_headers( from: &str, recipients: &[SingleInfo], gossip_headers: Vec, -) -> Result> { +) -> Result> { // XXX split the parsing from the modification part - let mut gossiped_keys: HashMap = Default::default(); + let mut gossiped_keys: BTreeMap = Default::default(); for value in &gossip_headers { let header = match value.parse::() { @@ -2033,7 +2040,12 @@ async fn parse_gossip_headers( ) .await?; - gossiped_keys.insert(header.addr.to_lowercase(), header.public_key); + let gossiped_key = GossipedKey { + public_key: header.public_key, + + verified: header.verified, + }; + gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key); } Ok(gossiped_keys) diff --git a/src/provider/data.rs b/src/provider/data.rs index 9979be93d..1e328b9ff 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -13,8 +13,8 @@ use std::sync::LazyLock; // 163.md: 163.com static P_163: Provider = Provider { id: "163", - status: Status::Ok, - before_login_hint: "", + status: Status::Preparation, + before_login_hint: "Enable \"POP3/SMTP/IMAP\" on the website, add a third-party auth code and use that as the login password", after_login_hint: "", overview_page: "https://providers.delta.chat/163", server: &[ @@ -98,7 +98,7 @@ static P_ALIYUN: Provider = Provider { static P_AOL: Provider = Provider { id: "aol", status: Status::Preparation, - before_login_hint: "To log in to AOL with Delta Chat, you need to set up an app password in the AOL web interface.", + before_login_hint: "To log in to AOL, you need to set up an app password in the AOL web interface.", after_login_hint: "", overview_page: "https://providers.delta.chat/aol", server: &[ @@ -432,7 +432,7 @@ static P_EXAMPLE_COM: Provider = Provider { id: "example.com", status: Status::Broken, before_login_hint: "Hush this provider doesn't exist!", - after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!", + after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider, take a look at providers.delta.chat!", overview_page: "https://providers.delta.chat/example-com", server: &[ Server { @@ -459,7 +459,7 @@ static P_EXAMPLE_COM: Provider = Provider { static P_FASTMAIL: Provider = Provider { id: "fastmail", status: Status::Preparation, - before_login_hint: "You must create an app-specific password for Delta Chat before you can log in.", + before_login_hint: "You must create an app-specific password before you can log in.", after_login_hint: "", overview_page: "https://providers.delta.chat/fastmail", server: &[ @@ -526,7 +526,7 @@ static P_FIVE_CHAT: Provider = Provider { static P_FREENET_DE: Provider = Provider { id: "freenet.de", status: Status::Preparation, - before_login_hint: "Um deine freenet.de E-Mail-Adresse mit Delta Chat zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.", + before_login_hint: "Um deine freenet.de E-Mail-Adresse zu benutzen, musst du erst auf der freenet.de-Webseite \"POP3/IMAP/SMTP\" aktivieren.", after_login_hint: "", overview_page: "https://providers.delta.chat/freenet-de", server: &[ @@ -647,10 +647,6 @@ static P_HERMES_RADIO: Provider = Provider { key: Config::MdnsEnabled, value: "0", }, - ConfigDefault { - key: Config::E2eeEnabled, - value: "0", - }, ConfigDefault { key: Config::ShowEmails, value: "2", @@ -663,7 +659,7 @@ static P_HERMES_RADIO: Provider = Provider { static P_HEY_COM: Provider = Provider { id: "hey.com", status: Status::Broken, - before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to hey.com.", + before_login_hint: "hey.com does not offer the standard IMAP e-mail protocol, so you cannot log in to hey.com.", after_login_hint: "", overview_page: "https://providers.delta.chat/hey-com", server: &[], @@ -702,7 +698,7 @@ static P_I3_NET: Provider = Provider { static P_ICLOUD: Provider = Provider { id: "icloud", status: Status::Preparation, - before_login_hint: "You must create an app-specific password for Delta Chat before login.", + before_login_hint: "You must create an app-specific password before login.", after_login_hint: "", overview_page: "https://providers.delta.chat/icloud", server: &[ @@ -787,7 +783,7 @@ static P_KONTENT_COM: Provider = Provider { static P_MAIL_COM: Provider = Provider { id: "mail.com", status: Status::Preparation, - before_login_hint: "To log in with Delta Chat, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.", + before_login_hint: "To log in, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.", after_login_hint: "", overview_page: "https://providers.delta.chat/mail-com", server: &[], @@ -828,7 +824,7 @@ static P_MAIL_DE: Provider = Provider { static P_MAIL_RU: Provider = Provider { id: "mail.ru", status: Status::Preparation, - before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с Delta Chat.", + before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru, чтобы mail.ru работал с chatmail.", after_login_hint: "", overview_page: "https://providers.delta.chat/mail-ru", server: &[ @@ -1222,8 +1218,8 @@ static P_NUBO_COOP: Provider = Provider { // outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de static P_OUTLOOK_COM: Provider = Provider { id: "outlook.com", - status: Status::Ok, - before_login_hint: "", + status: Status::Broken, + before_login_hint: "Unfortunately, Outlook does not allow using passwords anymore, per-app-passwords are currently not working.", after_login_hint: "", overview_page: "https://providers.delta.chat/outlook-com", server: &[ @@ -1321,8 +1317,8 @@ static P_POSTEO: Provider = Provider { static P_PROTONMAIL: Provider = Provider { id: "protonmail", status: Status::Broken, - before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Protonmail.", - after_login_hint: "To use Delta Chat with Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.", + before_login_hint: "Protonmail does not offer the standard IMAP e-mail protocol, so you cannot log in with to Protonmail.", + after_login_hint: "To use Protonmail, the IMAP bridge must be running in the background. If you have connectivity issues, double check whether it works as expected.", overview_page: "https://providers.delta.chat/protonmail", server: &[], opt: ProviderOptions::new(), @@ -1362,7 +1358,7 @@ static P_PURELYMAIL_COM: Provider = Provider { static P_QQ: Provider = Provider { id: "qq", status: Status::Preparation, - before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password for Delta Chat are required.", + before_login_hint: "Manually enabling IMAP/SMTP and creating an app-specific password are required.", after_login_hint: "", overview_page: "https://providers.delta.chat/qq", server: &[ @@ -1390,7 +1386,7 @@ static P_QQ: Provider = Provider { static P_RAMBLER_RU: Provider = Provider { id: "rambler.ru", status: Status::Preparation, - before_login_hint: "Чтобы войти в Рамблер/почта через Delta Chat, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru", + before_login_hint: "Чтобы войти в Рамблер/почта, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru", after_login_hint: "", overview_page: "https://providers.delta.chat/rambler-ru", server: &[ @@ -1566,7 +1562,7 @@ static P_SYSTEMLI_ORG: Provider = Provider { static P_T_ONLINE: Provider = Provider { id: "t-online", status: Status::Preparation, - before_login_hint: "To use Delta Chat with a T-Online email address, you need to create an app password in the web interface.", + before_login_hint: "To use a T-Online email address, you need to create an app password in the web interface.", after_login_hint: "", overview_page: "https://providers.delta.chat/t-online", server: &[ @@ -1677,7 +1673,7 @@ static P_TISCALI_IT: Provider = Provider { static P_TUTANOTA: Provider = Provider { id: "tutanota", status: Status::Broken, - before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in with Delta Chat to Tutanota.", + before_login_hint: "Tutanota does not offer the standard IMAP e-mail protocol, so you cannot log in to Tutanota.", after_login_hint: "", overview_page: "https://providers.delta.chat/tutanota", server: &[], @@ -1787,7 +1783,7 @@ static P_VIVALDI: Provider = Provider { static P_VK_COM: Provider = Provider { id: "vk.com", status: Status::Preparation, - before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.", + before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с chatmail.", after_login_hint: "", overview_page: "https://providers.delta.chat/vk-com", server: &[ @@ -1906,7 +1902,7 @@ static P_WKPB_DE: Provider = Provider { static P_YAHOO: Provider = Provider { id: "yahoo", status: Status::Preparation, - before_login_hint: "To use Delta Chat with your Yahoo email address you have to create an \"App-Password\" in the account security screen.", + before_login_hint: "To use your Yahoo email address you have to create an \"App-Password\" in the account security screen.", after_login_hint: "", overview_page: "https://providers.delta.chat/yahoo", server: &[ @@ -2662,4 +2658,4 @@ pub(crate) static PROVIDER_IDS: LazyLock = - LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap()); + LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2025, 9, 4).unwrap()); diff --git a/src/push.rs b/src/push.rs index ffd31cd9c..8b4bcc3ca 100644 --- a/src/push.rs +++ b/src/push.rs @@ -74,7 +74,7 @@ fn pad_device_token(s: &str) -> String { /// /// The result is base64-encoded and not ASCII armored to avoid dealing with newlines. pub(crate) fn encrypt_device_token(device_token: &str) -> Result { - let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?.0; + let public_key = pgp::composed::SignedPublicKey::from_asc(NOTIFIERS_PUBLIC_KEY)?; let encryption_subkey = public_key .public_subkeys .first() diff --git a/src/qr.rs b/src/qr.rs index 766352feb..0a1d1e6e2 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -808,19 +808,18 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> { authcode, .. } => { - token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?; - token::delete(context, token::Namespace::Auth, &authcode).await?; + token::delete(context, "").await?; context .sync_qr_code_token_deletion(invitenumber, authcode) .await?; } Qr::WithdrawVerifyGroup { + grpid, invitenumber, authcode, .. } => { - token::delete(context, token::Namespace::InviteNumber, &invitenumber).await?; - token::delete(context, token::Namespace::Auth, &authcode).await?; + token::delete(context, &grpid).await?; context .sync_qr_code_token_deletion(invitenumber, authcode) .await?; diff --git a/src/qr/qr_tests.rs b/src/qr/qr_tests.rs index 01cb0edbe..307cbbf5d 100644 --- a/src/qr/qr_tests.rs +++ b/src/qr/qr_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::chat::{ProtectionStatus, create_group_chat}; use crate::config::Config; use crate::securejoin::get_securejoin_qr; -use crate::test_utils::{TestContext, TestContextManager}; +use crate::test_utils::{TestContext, TestContextManager, sync}; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_http() -> Result<()> { @@ -509,6 +509,77 @@ async fn test_withdraw_verifygroup() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_withdraw_multidevice() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let alice2 = &tcm.alice().await; + + alice.set_config_bool(Config::SyncMsgs, true).await?; + alice2.set_config_bool(Config::SyncMsgs, true).await?; + + // Alice creates two QR codes on the first device: + // group QR code and contact QR code. + let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?; + let chat2_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group 2").await?; + let contact_qr = get_securejoin_qr(alice, None).await?; + let group_qr = get_securejoin_qr(alice, Some(chat_id)).await?; + let group2_qr = get_securejoin_qr(alice, Some(chat2_id)).await?; + + assert!(matches!( + check_qr(alice, &contact_qr).await?, + Qr::WithdrawVerifyContact { .. } + )); + assert!(matches!( + check_qr(alice, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + + // Sync group QR codes. + sync(alice, alice2).await; + assert!(matches!( + check_qr(alice2, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + assert!(matches!( + check_qr(alice2, &group2_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + + // Alice creates a contact QR code on second device + // and withdraws it. + let contact_qr2 = get_securejoin_qr(alice2, None).await?; + set_config_from_qr(alice2, &contact_qr2).await?; + assert!(matches!( + check_qr(alice2, &contact_qr2).await?, + Qr::ReviveVerifyContact { .. } + )); + + // Alice also withdraws second group QR code on second device. + set_config_from_qr(alice2, &group2_qr).await?; + + // Sync messages are sent from Alice's second device to first device. + sync(alice2, alice).await; + + // Now first device has reset all contact QR codes + // and second group QR code, + // but first group QR code is still valid. + assert!(matches!( + check_qr(alice, &contact_qr2).await?, + Qr::ReviveVerifyContact { .. } + )); + assert!(matches!( + check_qr(alice, &group_qr).await?, + Qr::WithdrawVerifyGroup { .. } + )); + assert!(matches!( + check_qr(alice, &group2_qr).await?, + Qr::ReviveVerifyGroup { .. } + )); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decode_and_apply_dclogin() -> Result<()> { let ctx = TestContext::new().await; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index cacadb218..ab71ed362 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1,6 +1,6 @@ //! Internet Message Format reception pipeline. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, HashSet}; use std::iter; use std::sync::LazyLock; @@ -27,7 +27,7 @@ use crate::ephemeral::{Timer as EphemeralTimer, stock_ephemeral_timer_changed}; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table}; -use crate::key::{DcKey, Fingerprint, SignedPublicKey}; +use crate::key::{DcKey, Fingerprint}; use crate::key::{self_fingerprint, self_fingerprint_opt}; use crate::log::LogExt; use crate::log::{info, warn}; @@ -35,7 +35,7 @@ use crate::logged_debug_assert; use crate::message::{ self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists, }; -use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids}; +use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids}; use crate::param::{Param, Params}; use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub}; use crate::reaction::{Reaction, set_msg_reaction}; @@ -838,7 +838,7 @@ pub(crate) async fn receive_imf_inner( context .sql .transaction(move |transaction| { - let fingerprint = gossiped_key.dc_fingerprint().hex(); + let fingerprint = gossiped_key.public_key.dc_fingerprint().hex(); transaction.execute( "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) VALUES (?, ?, ?) @@ -1003,8 +1003,10 @@ pub(crate) async fn receive_imf_inner( } } - if mime_parser.is_system_message == SystemMessage::IncomingCall { - context.handle_call_msg(&mime_parser, insert_msg_id).await?; + if mime_parser.is_call() { + context + .handle_call_msg(insert_msg_id, &mime_parser, from_id) + .await?; } else if received_msg.hidden { // No need to emit an event about the changed message } else if let Some(replace_chat_id) = replace_chat_id { @@ -1156,6 +1158,11 @@ async fn decide_chat_assignment( { info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); true + } else if mime_parser.is_system_message == SystemMessage::CallAccepted + || mime_parser.is_system_message == SystemMessage::CallEnded + { + info!(context, "Call state changed (TRASH)."); + true } else if mime_parser.decrypting_failed && !mime_parser.incoming { // Outgoing undecryptable message. let last_time = context @@ -1998,7 +2005,9 @@ async fn add_parts( if let Some(call) = message::get_by_rfc724_mids(context, &parse_message_ids(field)).await? { - context.handle_call_msg(mime_parser, call.get_id()).await?; + context + .handle_call_msg(call.get_id(), mime_parser, from_id) + .await?; } else { warn!(context, "Call: Cannot load parent.") } @@ -2931,7 +2940,7 @@ async fn apply_group_changes( // just like we have ChatGroupMemberRemovedFpr. // The result of the error is that info message // may contain display name of the wrong contact. - let fingerprint = key.dc_fingerprint().hex(); + let fingerprint = key.public_key.dc_fingerprint().hex(); if let Some(contact_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? { @@ -3750,10 +3759,28 @@ async fn mark_recipients_as_verified( to_ids: &[Option], mimeparser: &MimeMessage, ) -> Result<()> { + let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF); + for gossiped_key in mimeparser + .gossiped_keys + .values() + .filter(|gossiped_key| gossiped_key.verified) + { + let fingerprint = gossiped_key.public_key.dc_fingerprint().hex(); + let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else { + continue; + }; + + if to_id == ContactId::SELF || to_id == from_id { + continue; + } + + mark_contact_id_as_verified(context, to_id, verifier_id).await?; + ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?; + } + if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { return Ok(()); } - let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF); for to_id in to_ids.iter().filter_map(|&x| x) { if to_id == ContactId::SELF || to_id == from_id { continue; @@ -3846,7 +3873,7 @@ async fn add_or_lookup_contacts_by_address_list( async fn add_or_lookup_key_contacts( context: &Context, address_list: &[SingleInfo], - gossiped_keys: &HashMap, + gossiped_keys: &BTreeMap, fingerprints: &[Fingerprint], origin: Origin, ) -> Result>> { @@ -3862,7 +3889,7 @@ async fn add_or_lookup_key_contacts( // Iterator has not ran out of fingerprints yet. fp.hex() } else if let Some(key) = gossiped_keys.get(addr) { - key.dc_fingerprint().hex() + key.public_key.dc_fingerprint().hex() } else if context.is_self_addr(addr).await? { contact_ids.push(Some(ContactId::SELF)); continue; diff --git a/src/securejoin.rs b/src/securejoin.rs index 86788f684..12b9156ea 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -308,7 +308,9 @@ pub(crate) async fn handle_securejoin_handshake( let mut self_found = false; let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint(); for (addr, key) in &mime_message.gossiped_keys { - if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? { + if key.public_key.dc_fingerprint() == self_fingerprint + && context.is_self_addr(addr).await? + { self_found = true; break; } @@ -596,7 +598,7 @@ pub(crate) async fn observe_securejoin_on_other_device( return Ok(HandshakeMessage::Ignore); }; - if key.dc_fingerprint() != contact_fingerprint { + if key.public_key.dc_fingerprint() != contact_fingerprint { // Fingerprint does not match, ignore. warn!(context, "Fingerprint does not match."); return Ok(HandshakeMessage::Ignore); diff --git a/src/securejoin/securejoin_tests.rs b/src/securejoin/securejoin_tests.rs index 75c9c99c7..6339a53ac 100644 --- a/src/securejoin/securejoin_tests.rs +++ b/src/securejoin/securejoin_tests.rs @@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat}; use crate::chatlist::Chatlist; use crate::constants::Chattype; use crate::key::self_fingerprint; +use crate::mimeparser::GossipedKey; use crate::receive_imf::receive_imf; use crate::stock_str::{self, messages_e2e_encrypted}; use crate::test_utils::{ @@ -186,7 +187,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) { ); if case == SetupContactCase::WrongAliceGossip { - let wrong_pubkey = load_self_public_key(&bob).await.unwrap(); + let wrong_pubkey = GossipedKey { + public_key: load_self_public_key(&bob).await.unwrap(), + verified: false, + }; let alice_pubkey = msg .gossiped_keys .insert(alice_addr.to_string(), wrong_pubkey) diff --git a/src/summary.rs b/src/summary.rs index 6601e8982..7392b395e 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -97,7 +97,7 @@ impl Summary { let prefix = if msg.state == MessageState::OutDraft { Some(SummaryPrefix::Draft(stock_str::draft(context).await)) } else if msg.from_id == ContactId::SELF { - if msg.is_info() { + if msg.is_info() || msg.viewtype == Viewtype::Call { None } else { Some(SummaryPrefix::Me(stock_str::self_msg(context).await)) @@ -233,6 +233,16 @@ impl Message { type_file = self.param.get(Param::Summary1).map(|s| s.to_string()); append_text = true; } + Viewtype::Call => { + emoji = Some("📞"); + type_name = Some(if self.from_id == ContactId::SELF { + "Outgoing call".to_string() + } else { + "Incoming call".to_string() + }); + type_file = None; + append_text = false + } Viewtype::Text | Viewtype::Unknown => { emoji = None; if self.param.get_cmd() == SystemMessage::LocationOnly { diff --git a/src/sync.rs b/src/sync.rs index 3e3b83c58..1f7acf31d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -71,9 +71,6 @@ pub(crate) enum SyncData { DeleteMessages { msgs: Vec, // RFC724 id (i.e. "Message-Id" header) }, - RejectIncomingCall { - msg: String, // RFC724 id (i.e. "Message-Id" header) - }, } #[derive(Debug, Serialize, Deserialize)] @@ -267,7 +264,6 @@ impl Context { SyncData::Config { key, val } => self.sync_config(key, val).await, SyncData::SaveMessage { src, dest } => self.save_message(src, dest).await, SyncData::DeleteMessages { msgs } => self.sync_message_deletion(msgs).await, - SyncData::RejectIncomingCall { msg } => self.sync_call_rejection(msg).await, }, SyncDataOrUnknown::Unknown(data) => { warn!(self, "Ignored unknown sync item: {data}."); @@ -297,8 +293,15 @@ impl Context { } async fn delete_qr_token(&self, token: &QrTokenData) -> Result<()> { - token::delete(self, Namespace::InviteNumber, &token.invitenumber).await?; - token::delete(self, Namespace::Auth, &token.auth).await?; + self.sql + .execute( + "DELETE FROM tokens + WHERE foreign_key IN + (SELECT foreign_key FROM tokens + WHERE token=? OR token=?)", + (&token.invitenumber, &token.auth), + ) + .await?; Ok(()) } @@ -569,8 +572,8 @@ mod tests { .await? .is_none() ); - assert!(token::exists(&t, Namespace::InviteNumber, "yip-in").await?); - assert!(token::exists(&t, Namespace::Auth, "yip-auth").await?); + assert!(!token::exists(&t, Namespace::InviteNumber, "yip-in").await?); + assert!(!token::exists(&t, Namespace::Auth, "yip-auth").await?); assert!(!token::exists(&t, Namespace::Auth, "non-existent").await?); assert!(!token::exists(&t, Namespace::Auth, "directly deleted").await?); diff --git a/src/test_utils.rs b/src/test_utils.rs index fd8df9fa8..2e5571b62 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1282,9 +1282,8 @@ impl SentMessage<'_> { /// /// The keypair was created using the crate::key::tests::gen_key test. pub fn alice_keypair() -> KeyPair { - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")) - .unwrap() - .0; + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")).unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1293,9 +1292,8 @@ pub fn alice_keypair() -> KeyPair { /// /// Like [alice_keypair] but a different key and identity. pub fn bob_keypair() -> KeyPair { - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")) - .unwrap() - .0; + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1306,8 +1304,7 @@ pub fn bob_keypair() -> KeyPair { pub fn charlie_keypair() -> KeyPair { let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/charlie-secret.asc")) - .unwrap() - .0; + .unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1316,9 +1313,8 @@ pub fn charlie_keypair() -> KeyPair { /// /// Like [alice_keypair] but a different key and identity. pub fn dom_keypair() -> KeyPair { - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")) - .unwrap() - .0; + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/dom-secret.asc")).unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1327,9 +1323,8 @@ pub fn dom_keypair() -> KeyPair { /// /// Like [alice_keypair] but a different key and identity. pub fn elena_keypair() -> KeyPair { - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")) - .unwrap() - .0; + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/elena-secret.asc")).unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1338,9 +1333,8 @@ pub fn elena_keypair() -> KeyPair { /// /// Like [alice_keypair] but a different key and identity. pub fn fiona_keypair() -> KeyPair { - let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")) - .unwrap() - .0; + let secret = + key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap(); let public = secret.split_public_key().unwrap(); KeyPair { public, secret } } @@ -1476,8 +1470,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) { pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) { alice0.send_sync_msg().await.unwrap(); let sync_msg = alice0.pop_sent_sync_msg().await; - let no_msg = alice1.recv_msg_opt(&sync_msg).await; - assert!(no_msg.is_none()); + alice1.recv_msg_trash(&sync_msg).await; } /// Pretty-print an event to stdout diff --git a/src/token.rs b/src/token.rs index 70b11e48d..a89bedcbd 100644 --- a/src/token.rs +++ b/src/token.rs @@ -119,13 +119,14 @@ pub async fn auth_foreign_key(context: &Context, token: &str) -> Result Result<()> { +/// Resets all tokens corresponding to the `foreign_key`. +/// +/// `foreign_key` is a group ID to reset all group tokens +/// or empty string to reset all setup contact tokens. +pub async fn delete(context: &Context, foreign_key: &str) -> Result<()> { context .sql - .execute( - "DELETE FROM tokens WHERE namespc=? AND token=?;", - (namespace, token), - ) + .execute("DELETE FROM tokens WHERE foreign_key=?", (foreign_key,)) .await?; Ok(()) }