Compare commits

...
Author SHA1 Message Date
iequidoo e283d34449 test: Message in blocked chat arrives as InSeen
It's strange that this wasn't covered by any test.
2026-01-25 06:10:35 -03:00
iequidoo 65823fcfc6 test: Encrypted incoming message goes to encrypted 1:1 chat even if references messages in ad-hoc group
This is an important thing forgotten to be checked in 332527089.

Also there's another test which currently doesn't work as we want: outgoing encrypted messages
continue to arrive to ad-hoc group even if we already have contact's key. This should be fixed by
sending and receiving Intended Recipient Fingerprint subpackets.

The good thing is that apparently there are no scenarios requiring the contact to update their
software, the user should just update own devices.
2026-01-25 02:50:46 -03:00
B. Petersen 448c0d2268 feat: use more fitting encryption info message 2026-01-24 08:45:39 +01:00
iequidoo 3325270896 fix: Don't add SELF to unencrypted chat created from encrypted message (#7661)
I.e. create a non-replyable ad-hoc group in such cases. Unencrypted replies to encrypted messages
are a security issue and "Composing a Reply Message" from RFC 9787 and "Replying and Forwarding
Guidance" from RFC 9788 forbid such replies.
2026-01-24 02:45:53 -03:00
iequidoo b563064b26 fix: apply_group_changes(): Check whether From is key-contact
If From is an address-contact, it mustn't be able to modify an encrypted group. If From is a
key-contact, it mustn't be added to members of an unencrypted group.
2026-01-24 02:45:53 -03:00
iequidoo 8d32d3ae0c feat: receive_imf: Log reasoning for chat assignment 2026-01-24 02:45:53 -03:00
iequidoo c5f19f67a9 fix: Make self-contact a key-contact even if key isn't generated yet 2026-01-24 02:45:53 -03:00
link2xt baeb31b5fa chore(release): prepare for 2.39.0 2026-01-23 21:52:40 +00:00
link2xt 5d3bc00fd5 docs(RELEASE.md): add section about dealing with failed releases 2026-01-23 21:43:02 +00:00
link2xt 424928b660 docs(RELEASE.md): push preparation commit to the main branch before tagging 2026-01-23 21:43:02 +00:00
72374 1b8c732611 fix: Do not additionally reduce the resolution of images that fit into the resolution-limit and are larger than the file-size-limit (#7760)
See: https://support.delta.chat/t/high-quality-avatar/1052/8

This removes an additional resolution-reduction for images that fit
within the resolution-limit.

The additional reduction of the image-resolution was added in
https://github.com/chatmail/core/commit/b7864f232bbac4c585e75f1a3073d1175727e2f9.
It seems that back then only resolution-reduction was used to make the
file-size of images smaller, and the purpose was to skip an unnecessary
encoding-step.

However, encoding images with [jpeg-quality set to
`75`](https://github.com/72374/core/blob/a6b2a54e46157dd6694146bb9f109eb2c60462ef/src/blob.rs#L392),
as it is currently done, can significantly reduce the file-size, even if
the resolution is the same. As the resolution of the image that will be
encoded is rather low when it fits within the resolution-limit, further
reducing it can significantly reduce the quality of the image.
Thus it is better to not skip the encoding-step at the original
resolution.
2026-01-23 18:25:25 +01:00
bjoern 2531dfea1d chore: cleanup deprecated functions/defines (#7763)
this PR cleans up with some easy, deprecated stuff.

i roughly checked that they are no longer in use - by these checks,
other deprecated function were kept, eg.
dc_provider_new_from_email_with_dns() and dc_chat_is_protected() is
still used by deltatouch - to not add noise there, we remove them here
on the next cleanup ...

for DC_STR_*, however, if they are in used, the corresponding lines
should just be removed

this will cleanup https://c.delta.chat/deprecated.html as well
2026-01-23 15:25:48 +01:00
link2xt 9003b248aa chore: merge v2.38.0 into main branch
Release preparation commit was not pushed to main.
2026-01-23 09:09:59 +00:00
link2xt 35875f9b32 ci: update Rust to 1.93.0 2026-01-23 09:03:11 +00:00
Hocuri 008e6c4af3 chore(release): prepare for 2.38.0 2026-01-22 21:32:31 +01:00
Nico de HaenandHocuri a6baba1852 fix: forward message with file (#7755)
resolves #7724: When forwarding a message with file to another profile, the file was not copied to the target $blobdir and so the forwarded message missed it

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2026-01-22 20:15:26 +00:00
Hocuri a6b2a54e46 fix: Prevent possible infinite loop with invalid smtp row (#7746)
If `Message::load_from_db_optional()` or `set_msg_failed()` fails, we
shouldn't early-return. Because it's important that the line
`execute("DELETE FROM smtp WHERE id=?", (rowid,))` is executed in order
to prevent an infinite loop, if one of these functions fails.
2026-01-21 16:46:17 +01:00
Stefano Volpe 99aa99eb5b api: public re-export of Connectivity (#7737) 2026-01-20 18:46:00 -03:00
iequidoo 566395f1fa fix: Emit MsgsChanged instead of MsgsNoticed on self-MDN if chat still has fresh messages
Otherwise if the user reads messages being offline and then the device comes online, sent MDNs will
remove all notifications from other devices even if new messages have arrived. Notifications not
removed at all look more acceptable.
2026-01-20 18:34:10 -03:00
Simon LauxandWofWca 4ccd3cb665 api(rust and jsonrpc): marknoticed_all_chats method to mark all chats as notices, including muted ones. (#7709)
made for solving

https://github.com/deltachat/deltachat-desktop/issues/5891#issuecomment-3687566470

will also be more efficient, because desktop currently loads all fresh
messages to find out which chats to mark as noticed.
https://github.com/deltachat/deltachat-desktop/blob/76d32bfc9364d53e1ffea40fcf4a60ad9a8482ee/packages/frontend/src/components/AccountListSidebar/AccountItem.tsx#L334


# progress
- [x] implementation
- [x] write a test
- [x] make a pr to use it in desktop
https://github.com/deltachat/deltachat-desktop/pull/5923
- [x] address review comments

---------

Co-authored-by: WofWca <wofwca@protonmail.com>
2026-01-20 08:52:59 +00:00
Hocuri f5e1e2678b fix: Make it possible to leave and immediately delete a chat (#7744)
Without this PR, if you leave and immediately delete a chat, the leave
message won't be sent.

This is needed for
https://github.com/deltachat/deltachat-android/issues/4158.
2026-01-19 15:07:19 +01:00
Hocuri c3a5e3ac0d feat: In teamprofiles, don't mark chat as read on outgoing message (#7717)
Fix https://github.com/chatmail/core/issues/7704
2026-01-19 11:39:06 +00:00
Simon Laux b2f31c8148 api(rust, jsonrpc): add get_message_read_receipt_count method (#7732)
closes #7728
2026-01-19 11:37:10 +00:00
Hocuri 29c57ad065 fix: Don't remember old channel members in the database (#7716)
This PR fixes a bug that old channel members were remembered in the
database even after they left the channel. Concretely, they remained in
the `past_members` table that's only meant for groups.

Though it was not a bad bug; we're anyways not cleaning up old contacts.
2026-01-19 11:35:01 +01:00
Simon Laux 82a0d6b0ab fix: more reliable parsing of dclogin: links with ip address as host (#7734)
Also adds a test for it.

closes #7733
2026-01-18 16:11:59 +00:00
link2xt 5ff323ce15 feat(pgp): use preferred hash algorithm for signing instead of hardcoded SHA256
There is no difference for RSA and Ed25519,
the only signing keys that we generate.
The both use SHA256:
<https://github.com/rpgp/rpgp/blob/7e3b6c0af21e4a45c2a2dcba67f0b649e0b6e456/src/types/params/public.rs#L231-L234>

The only difference is for the possible future PQC signing keys
and imported NIST P-512 and NIST P-384 keys.
2026-01-18 03:59:12 +00:00
iequidoo a67a5299bf Send and apply MDNs to self (#7005)
We currently synchronize "seen" status of messages by setting `\Seen` flag on IMAP and then looking
for new `\Seen` flags using `CONDSTORE` IMAP extension. This approach has multiple disadvantages:
- It requires that the server supports `CONDSTORE` extension. For example Maddy does not support
  CONDSTORE yet: https://github.com/foxcpp/maddy/issues/727
- It leaks the seen status to the server without any encryption.
- It requires more than just store-and-forward queues and prevents replacing IMAP with simpler
  protocols like POP3 or UUCP or some HTTP-based API for queue polling.

A simpler approach is to send MDNs to self when `Config::BccSelf` (aka multidevice) is enabled,
regardless of whether the message requested and MDN. If MDN was requested and we have MDNs enabled,
then also send to the message sender, but MDN to self is sent regardless of whether read receipts
are actually enabled.

`sync_seen_flags()` and `CONDSTORE` check is better completely removed, maybe after one
release. `store_seen_flags_on_imap()` can be kept for unencrypted non-chat messages.

One potential problem with sending MDNs is that it may trigger ratelimits on some providers and
count as another recipient.
2026-01-17 20:54:35 -03:00
link2xt 659d21aa9d docs: fix formatting of indoc! link 2026-01-17 14:40:17 +00:00
link2xt 8f604e74ec fix: do not resolve ICE server hostnames during IMAP loop
Hostname resolution may timeout if DNS servers are not responding.
It is also not necessary to resolve fallback ICE server hostnames
if the user is not going to use calls.
2026-01-17 12:55:21 +00:00
42 changed files with 1018 additions and 574 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.92.0
RUST_VERSION: 1.93.0
# Minimum Supported Rust Version
MSRV: 1.88.0
+76
View File
@@ -1,5 +1,80 @@
# Changelog
## [2.39.0] - 2026-01-23
### CI
- Update Rust to 1.93.0.
### Documentation
- RELEASE.md: Push preparation commit to the main branch before tagging.
- RELEASE.md: Add section about dealing with failed releases.
### Fixes
- Forward message with file ([#7755](https://github.com/chatmail/core/pull/7755)).
- Do not additionally reduce the resolution of images that fit into the resolution-limit and are larger than the file-size-limit ([#7760](https://github.com/chatmail/core/pull/7760)).
### Miscellaneous Tasks
- Merge v2.38.0 into main branch.
- Cleanup deprecated functions/defines ([#7763](https://github.com/chatmail/core/pull/7763)).
## [2.38.0] - 2026-01-22
### API-Changes
- [**breaking**] Jsonrpc: remove `contacts` from `FullChat`. To migrate load contacts on demand via `get_contacts_by_ids` using `FullChat.contactIds` ([#7282](https://github.com/chatmail/core/pull/7282)).
- jsonrpc: Add run_until parameter for bots ([#7688](https://github.com/chatmail/core/pull/7688)).
- rust, jsonrpc: Add `get_message_read_receipt_count` method ([#7732](https://github.com/chatmail/core/pull/7732)).
- rust and jsonrpc: Marknoticed_all_chats method to mark all chats as notices, including muted ones. ([#7709](https://github.com/chatmail/core/pull/7709)).
- Public re-export of Connectivity ([#7737](https://github.com/chatmail/core/pull/7737)).
### Documentation
- Fix chat types.
- Set_config_from_qr() configures context for "DCACCOUNT:" and "DCLOGIN:" QRs ([#7450](https://github.com/chatmail/core/pull/7450)).
- Fix formatting of `indoc!` link.
### Features / Changes
- Pre-messages / next version of download on demand ([#7371](https://github.com/chatmail/core/pull/7371)).
- Connectivity view: move quota up and combine with IMAP state. ([#7653](https://github.com/chatmail/core/pull/7653)).
- Execute sync message before checking for primary transport update.
- Disable partial search by contact address.
- Don't put text into post-message ([#7714](https://github.com/chatmail/core/pull/7714)).
- Don't scale up Origin of multiple and broadcast recipients when sending a message.
- pgp: Use preferred hash algorithm for signing instead of hardcoded SHA256.
- In teamprofiles, don't mark chat as read on outgoing message ([#7717](https://github.com/chatmail/core/pull/7717)).
- Send and apply MDNs to self ([#7005](https://github.com/chatmail/core/pull/7005))
### Fixes
- Do not show contact address in message info ([#7695](https://github.com/chatmail/core/pull/7695)).
- Take transport_id into account when marking messages with \Seen flags.
- Send bcc-self messages to all own relays ([#7656](https://github.com/chatmail/core/pull/7656)).
- Only emit TransportsModified if transports are really modified.
- Logging errors in deltachat-rpc-server during startup ([#7707](https://github.com/chatmail/core/pull/7707)).
- Use only lowercase letters for stats id ([#7700](https://github.com/chatmail/core/pull/7700)).
- Hide incoming broadcasts in `DC_GCL_FOR_FORWARDING` ([#7726](https://github.com/chatmail/core/pull/7726)).
- Do not resolve ICE server hostnames during IMAP loop.
- More reliable parsing of `dclogin:` links with ip address as host ([#7734](https://github.com/chatmail/core/pull/7734)).
- Don't remember old channel members in the database ([#7716](https://github.com/chatmail/core/pull/7716)).
- Make it possible to leave and immediately delete a chat ([#7744](https://github.com/chatmail/core/pull/7744)).
- Emit MsgsChanged instead of MsgsNoticed on self-MDN if chat still has fresh messages.
- Prevent possible infinite loop with invalid `smtp` row ([#7746](https://github.com/chatmail/core/pull/7746)).
- Sync broadcast subscribers list ([#7578](https://github.com/chatmail/core/pull/7578))
### Refactor
- Don't use `concat!` in sql statements ([#7720](https://github.com/chatmail/core/pull/7720)).
### Tests
- Port test_dont_move_sync_msgs to JSON-RPC ([#7676](https://github.com/chatmail/core/pull/7676)).
- rpc-client: Replace remaining print()s with `logging` ([#6082](https://github.com/chatmail/core/pull/6082)).
## [2.37.0] - 2026-01-08
### API-Changes
@@ -7543,3 +7618,4 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.35.0]: https://github.com/chatmail/core/compare/v2.34.0..v2.35.0
[2.36.0]: https://github.com/chatmail/core/compare/v2.35.0..v2.36.0
[2.37.0]: https://github.com/chatmail/core/compare/v2.36.0..v2.37.0
[2.38.0]: https://github.com/chatmail/core/compare/v2.37.0..v2.38.0
Generated
+5 -5
View File
@@ -1303,7 +1303,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.37.0"
version = "2.39.0"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -1413,7 +1413,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.37.0"
version = "2.39.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1434,7 +1434,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.37.0"
version = "2.39.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1450,7 +1450,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.37.0"
version = "2.39.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1479,7 +1479,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.37.0"
version = "2.39.0"
dependencies = [
"anyhow",
"deltachat",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.37.0"
version = "2.39.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.88"
+13 -4
View File
@@ -1,4 +1,4 @@
# Releasing a new version of DeltaChat core
# Releasing a new version of chatmail core
For example, to release version 1.116.0 of the core, do the following steps.
@@ -14,8 +14,17 @@ For example, to release version 1.116.0 of the core, do the following steps.
5. Commit the changes as `chore(release): prepare for 1.116.0`.
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
6. Tag the release: `git tag --annotate v1.116.0`.
6. Push the commit to the `main` branch.
7. Push the release tag: `git push origin v1.116.0`.
7. Once the commit is on the `main` branch and passed CI, tag the release: `git tag --annotate v1.116.0`.
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
8. Push the release tag: `git push origin v1.116.0`.
9. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
## Dealing with failed releases
Once you make a GitHub release,
CI will try to build and publish [PyPI](https://pypi.org/) and [npm](https://www.npmjs.com/) packages.
If this fails for some reason, do not modify the failed tag, do not delete it and do not force-push to the `main` branch.
Fix the build process and tag a new release instead.
+1 -1
View File
@@ -21,7 +21,7 @@ text TEXT DEFAULT '' NOT NULL -- message text
```
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
or [`indoc!](https://docs.rs/indoc).
or [`indoc!`](https://docs.rs/indoc).
Do not escape newlines like this:
```
sql.execute(
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "2.37.0"
version = "2.39.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
+2 -256
View File
@@ -1563,7 +1563,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context);
* Mark all messages in a chat as _noticed_.
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
* but are still waiting for being marked as "seen" using dc_markseen_msgs()
* (IMAP/MDNs is not done for noticed messages).
* (read receipts aren't sent for noticed messages).
*
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
* See also dc_markseen_msgs().
@@ -2220,10 +2220,6 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
// Deprecated 2025-05-20, setting this flag is a no-op.
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
#define DC_GCL_ADD_SELF 0x02
#define DC_GCL_ADDRESS 0x04
@@ -2296,17 +2292,6 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
dc_array_t* dc_get_contacts (dc_context_t* context, uint32_t flags, const char* query);
/**
* Get the number of blocked contacts.
*
* @deprecated Deprecated 2021-02-22, use dc_array_get_cnt() on dc_get_blocked_contacts() instead.
* @memberof dc_context_t
* @param context The context object.
* @return The number of blocked contacts.
*/
int dc_get_blocked_cnt (dc_context_t* context);
/**
* Get blocked contacts.
*
@@ -2579,7 +2564,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_BACKUP 251 // deprecated
#define DC_QR_BACKUP2 252
#define DC_QR_BACKUP_TOO_NEW 255
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
@@ -4675,7 +4659,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
@@ -7022,61 +7005,16 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_FILE 12
/// "Group name changed from %1$s to %2$s."
///
/// Used in status messages for group name changes.
/// - %1$s will be replaced by the old group name
/// - %2$s will be replaced by the new group name
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPNAME 15
/// "Group image changed."
///
/// Used in status messages for group images changes.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGRPIMGCHANGED 16
/// "Member %1$s added."
///
/// Used in status messages for added members.
/// - %1$s will be replaced by the name of the added member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGADDMEMBER 17
/// "Member %1$s removed."
///
/// Used in status messages for removed members.
/// - %1$s will be replaced by the name of the removed member
///
/// @deprecated 2022-09-10
#define DC_STR_MSGDELMEMBER 18
/// "Group left."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_MSGGROUPLEFT 19
/// "GIF"
///
/// Used in summaries.
#define DC_STR_GIF 23
/// @deprecated 2025-07, this string is no longer needed.
#define DC_STR_ENCRYPTEDMSG 24
/// "End-to-end encryption available."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
/// @deprecated 2026-01-23
#define DC_STR_E2E_AVAILABLE 25
/// @deprecated Deprecated 2021-02-07, this string is no longer needed.
#define DC_STR_ENCR_TRANSP 27
/// "No encryption."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
@@ -7087,90 +7025,23 @@ void dc_event_unref(dc_event_t* event);
/// Used to build the string returned by dc_get_contact_encrinfo().
#define DC_STR_FINGERPRINTS 30
/// "Message opened"
///
/// Used in subjects of outgoing read receipts.
///
/// @deprecated Deprecated 2024-07-26
#define DC_STR_READRCPT 31
/// "The message '%1$s' you sent was displayed on the screen of the recipient."
///
/// Used as message text of outgoing read receipts.
/// - %1$s will be replaced by the subject of the displayed message
///
/// @deprecated Deprecated 2024-06-23
#define DC_STR_READRCPT_MAILBODY 32
/// @deprecated Deprecated, this string is no longer needed.
#define DC_STR_MSGGRPIMGDELETED 33
/// "End-to-end encryption preferred."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
/// @deprecated 2025-06-05
#define DC_STR_E2E_PREFERRED 34
/// "%1$s verified"
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the verified contact
#define DC_STR_CONTACT_VERIFIED 35
/// "Cannot establish guaranteed end-to-end encryption with %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact that cannot be verified
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_NOT_VERIFIED 36
/// "Changed setup for %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact with the changed setup
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_SETUP_CHANGED 37
/// "Archived chats"
///
/// Used as the name for the corresponding chatlist entry.
#define DC_STR_ARCHIVEDCHATS 40
/// "Autocrypt Setup Message"
///
/// @deprecated 2025-04
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
///
/// @deprecated 2025-04
#define DC_STR_AC_SETUP_MSG_BODY 43
/// "Cannot login as %1$s."
///
/// Used in error strings.
/// - %1$s will be replaced by the failing login name
#define DC_STR_CANNOT_LOGIN 60
/// "%1$s by %2$s"
///
/// Used to concretize actions,
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
/// - %2$s will be replaced by the name of the user taking that action
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYUSER 62
/// "%1$s by me"
///
/// Used to concretize actions.
/// - %1$s will be replaced by an action
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
///
/// @deprecated 2022-09-10
#define DC_STR_MSGACTIONBYME 63
/// "Location streaming enabled."
///
/// Used in status messages.
@@ -7211,13 +7082,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as message text for the message added to the device chat after successful login.
#define DC_STR_WELCOME_MESSAGE 71
/// "Unknown sender for this chat. See 'info' for more details."
///
/// Use as message text if assigning the message to a chat is not totally correct.
///
/// @deprecated 2025-08-18
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
/// "Message from %1$s"
///
/// Used in subjects of outgoing messages in one-to-one chats.
@@ -7231,53 +7095,6 @@ void dc_event_unref(dc_event_t* event);
/// - %1$s will be replaced by the name of the contact the message cannot be sent to
#define DC_STR_FAILED_SENDING_TO 74
/// "Message deletion timer is disabled."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DISABLED 75
/// "Message deletion timer is set to %1$s s."
///
/// Used in status messages when the other constants
/// (#DC_STR_EPHEMERAL_MINUTE, #DC_STR_EPHEMERAL_HOUR and so on) do not match the timer.
/// - %1$s will be replaced by the number of seconds the timer is set to
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_SECONDS 76
/// "Message deletion timer is set to 1 minute."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_MINUTE 77
/// "Message deletion timer is set to 1 hour."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_HOUR 78
/// "Message deletion timer is set to 1 day."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_DAY 79
/// "Message deletion timer is set to 1 week."
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
#define DC_STR_EPHEMERAL_WEEK 80
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
/// "Error: %1$s"
///
/// Used in error strings.
@@ -7311,42 +7128,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as device message text.
#define DC_STR_SELF_DELETED_MSG_BODY 91
/// "Message deletion timer is set to %1$s minutes."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_MINUTES and DC_STR_MSG_EPHEMERAL_TIMER_MINUTES_BY.
#define DC_STR_EPHEMERAL_MINUTES 93
/// "Message deletion timer is set to %1$s hours."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_HOURS and DC_STR_MSG_EPHEMERAL_TIMER_HOURS_BY.
#define DC_STR_EPHEMERAL_HOURS 94
/// "Message deletion timer is set to %1$s days."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_DAYS and DC_STR_MSG_EPHEMERAL_TIMER_DAYS_BY.
#define DC_STR_EPHEMERAL_DAYS 95
/// "Message deletion timer is set to %1$s weeks."
///
/// Used in status messages.
///
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
///
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_WEEKS and DC_STR_MSG_EPHEMERAL_TIMER_WEEKS_BY.
#define DC_STR_EPHEMERAL_WEEKS 96
/// "Forwarded"
///
/// Used in message summary text for notifications and chatlist.
@@ -7359,9 +7140,6 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the percentage used
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/// @deprecated Deprecated 2025-11-12, this string is no longer needed.
#define DC_STR_PARTIAL_DOWNLOAD_MSG_BODY 99
/// "Multi Device Synchronization"
///
/// Used in subjects of outgoing sync messages.
@@ -7387,10 +7165,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as a headline in the connectivity view.
#define DC_STR_OUTGOING_MESSAGES 104
/// @deprecated Deprecated 2022-04-16, this string is no longer needed.
#define DC_STR_ONE_MOMENT 106
/// "Connected"
///
/// Used as status in the connectivity view.
@@ -7481,12 +7255,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as status in the connectivity view.
#define DC_STR_NOT_CONNECTED 121
/// "%1$s changed their address from %2$s to %3$s"
///
/// Used as an info message to chats with contacts that changed their address.
/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed group name from \"%1$s\" to \"%2$s\"."
///
/// `%1$s` will be replaced by the old group name.
@@ -7601,17 +7369,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
/// "You set message deletion timer to 1 minute."
///
/// @deprecated 2025-11-14, this string is no longer needed
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
/// "Message deletion timer is set to 1 minute by %1$s."
///
/// `%1$s` will be replaced by name of the contact.
/// @deprecated 2025-11-14, this string is no longer needed
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
/// "You set message deletion timer to 1 hour."
///
/// Used in status messages.
@@ -7781,17 +7538,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
///
/// @deprecated 2025-03
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "The contact must be online to proceed. This process will continue automatically in background."
///
/// Used as info message.
/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
-16
View File
@@ -2260,22 +2260,6 @@ pub unsafe extern "C" fn dc_get_contacts(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_blocked_cnt(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_blocked_cnt()");
return 0;
}
let ctx = &*context;
block_on(async move {
Contact::get_all_blocked(ctx)
.await
.unwrap_or_log_default(ctx, "failed to get blocked count")
.len() as libc::c_int
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_blocked_contacts(
context: *mut dc_context_t,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.37.0"
version = "2.39.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"
+31 -5
View File
@@ -11,8 +11,8 @@ use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs,
get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem,
MessageListOptions,
get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat,
ChatId, ChatItem, MessageListOptions,
};
use deltachat::chatlist::Chatlist;
use deltachat::config::{get_all_ui_config_keys, Config};
@@ -23,8 +23,8 @@ use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::{
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
MessageState, MsgId, Viewtype,
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipt_count, get_msg_read_receipts,
markseen_msgs, Message, MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -1164,10 +1164,24 @@ impl CommandApi {
Ok(None)
}
/// Mark all messages in all chats as _noticed_.
/// Skips messages from blocked contacts, but does not skip messages in muted chats.
///
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
/// but are still waiting for being marked as "seen" using markseen_msgs()
/// (read receipts aren't sent for noticed messages).
///
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
/// See also markseen_msgs().
pub async fn marknoticed_all_chats(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
marknoticed_all_chats(&ctx).await
}
/// Mark all messages in a chat as _noticed_.
/// _Noticed_ messages are no longer _fresh_ and do not count as being unseen
/// but are still waiting for being marked as "seen" using markseen_msgs()
/// (IMAP/MDNs is not done for noticed messages).
/// (read receipts aren't sent for noticed messages).
///
/// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
/// See also markseen_msgs().
@@ -1434,6 +1448,18 @@ impl CommandApi {
MessageInfo::from_msg_id(&ctx, MsgId::new(message_id)).await
}
/// Returns count of read receipts on message.
///
/// This view count is meant as a feedback measure for the channel owner only.
async fn get_message_read_receipt_count(
&self,
account_id: u32,
message_id: u32,
) -> Result<usize> {
let ctx = self.get_context(account_id).await?;
get_msg_read_receipt_count(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
async fn get_message_read_receipts(
&self,
+1 -1
View File
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.37.0"
"version": "2.39.0"
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.37.0"
version = "2.39.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.37.0"
version = "2.39.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
@@ -44,6 +44,14 @@ class Message:
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
return [AttrDict(read_receipt) for read_receipt in read_receipts]
def get_read_receipt_count(self) -> int:
"""
Returns count of read receipts on message.
This view count is meant as a feedback measure for the channel owner only.
"""
return self._rpc.get_message_read_receipt_count(self.account.id, self.id)
def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
@@ -22,7 +22,7 @@ from .rpc import Rpc
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
Currently this is "Messages are end-to-end encrypted."
"""
@@ -911,6 +911,47 @@ def test_markseen_contact_request(acfactory):
assert message2.get_snapshot().state == MessageState.IN_SEEN
@pytest.mark.parametrize("team_profile", [True, False])
def test_no_markseen_in_team_profile(team_profile, acfactory):
"""
Test that seen status is synchronized iff `team_profile` isn't set.
"""
alice, bob = acfactory.get_online_accounts(2)
if team_profile:
bob.set_config("team_profile", "1")
# Bob sets up a second device.
bob2 = bob.clone()
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)
bob_chat_alice = bob.create_chat(alice)
bob2.create_chat(alice)
alice_chat_bob.send_text("Hello Bob!")
message = bob.wait_for_incoming_msg()
message2 = bob2.wait_for_incoming_msg()
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
# Send a message and wait until it arrives
# in order to wait until Bob2 gets the markseen message.
# This also tests that outgoing messages
# don't mark preceeding messages as seen in team profiles.
bob_chat_alice.send_text("Outgoing message")
while True:
outgoing = bob2.wait_for_msg(EventType.MSGS_CHANGED)
if outgoing.id != 0:
break
assert outgoing.get_snapshot().text == "Outgoing message"
if team_profile:
assert message2.get_snapshot().state == MessageState.IN_FRESH
else:
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acfactory):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
@@ -930,6 +971,9 @@ def test_read_receipt(acfactory):
assert len(read_receipts) == 1
assert read_receipts[0].contact_id == alice_contact_bob.id
read_receipt_cnt = read_msg.get_read_receipt_count()
assert read_receipt_cnt == 1
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
@@ -1133,6 +1177,30 @@ def test_leave_broadcast(acfactory, all_devices_online):
check_account(bob2, bob2.create_contact(alice), inviter_side=False)
def test_leave_and_delete_group(acfactory, log):
alice, bob = acfactory.get_online_accounts(2)
log.section("Alice creates a group")
alice_chat = alice.create_group("Group")
alice_chat.add_contact(bob)
assert len(alice_chat.get_contacts()) == 2 # Alice and Bob
alice_chat.send_text("hello")
log.section("Bob sees the group, and leaves and deletes it")
msg = bob.wait_for_incoming_msg().get_snapshot()
assert msg.text == "hello"
msg.chat.accept()
msg.chat.leave()
# Bob deletes the chat. This must not prevent the leave message from being sent.
msg.chat.delete()
log.section("Alice receives the delete message")
# After Bob left, only Alice will be left in the group:
while len(alice_chat.get_contacts()) != 1:
alice.wait_for_event(EventType.CHAT_MODIFIED)
def test_immediate_autodelete(acfactory, direct_imap, log):
ac1, ac2 = acfactory.get_online_accounts(2)
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "2.37.0"
version = "2.39.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.37.0"
"version": "2.39.0"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.37.0"
version = "2.39.0"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
-3
View File
@@ -258,9 +258,6 @@ class TestOfflineChat:
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
+1 -1
View File
@@ -1 +1 @@
2026-01-08
2026-01-23
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.92.0
RUST_VERSION=1.93.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
-10
View File
@@ -1,6 +1,5 @@
//! # Blob directory management.
use core::cmp::max;
use std::io::{Cursor, Seek};
use std::iter::FusedIterator;
use std::mem;
@@ -425,15 +424,6 @@ impl<'a> BlobObject<'a> {
});
if do_scale {
if !exceeds_wh {
img_wh = max(img.width(), img.height());
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
// crate when recoding, so don't scale them down.
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
img_wh = img_wh * 2 / 3;
}
}
loop {
if mem::take(&mut add_white_bg) {
self::add_white_bg(&mut img);
+106 -62
View File
@@ -617,33 +617,7 @@ struct IceServer {
pub credential: Option<String>,
}
/// Creates JSON with ICE servers.
async fn create_ice_servers(
context: &Context,
hostname: &str,
port: u16,
username: &str,
password: &str,
) -> Result<String> {
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, port, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let ice_server = IceServer {
urls,
username: Some(username.to_string()),
credential: Some(password.to_string()),
};
let json = serde_json::to_string(&[ice_server])?;
Ok(json)
}
/// Creates JSON with ICE servers from a line received over IMAP METADATA.
/// Creates ICE servers from a line received over IMAP METADATA.
///
/// IMAP METADATA returns a line such as
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
@@ -653,20 +627,107 @@ async fn create_ice_servers(
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
/// is the password.
pub(crate) async fn create_ice_servers_from_metadata(
context: &Context,
metadata: &str,
) -> Result<(i64, String)> {
) -> Result<(i64, Vec<UnresolvedIceServer>)> {
let (hostname, rest) = metadata.split_once(':').context("Missing hostname")?;
let (port, rest) = rest.split_once(':').context("Missing port")?;
let port = u16::from_str(port).context("Failed to parse the port")?;
let (ts, password) = rest.split_once(':').context("Missing timestamp")?;
let expiration_timestamp = i64::from_str(ts).context("Failed to parse the timestamp")?;
let ice_servers = create_ice_servers(context, hostname, port, ts, password).await?;
let ice_servers = vec![UnresolvedIceServer::Turn {
hostname: hostname.to_string(),
port,
username: ts.to_string(),
credential: password.to_string(),
}];
Ok((expiration_timestamp, ice_servers))
}
/// STUN or TURN server with unresolved DNS name.
#[derive(Debug, Clone)]
pub(crate) enum UnresolvedIceServer {
/// STUN server.
Stun { hostname: String, port: u16 },
/// TURN server with the username and password.
Turn {
hostname: String,
port: u16,
username: String,
credential: String,
},
}
/// Resolves domain names of ICE servers.
///
/// On failure to resolve, logs the error
/// and skips the server, but does not fail.
pub(crate) async fn resolve_ice_servers(
context: &Context,
unresolved_ice_servers: Vec<UnresolvedIceServer>,
) -> Result<String> {
let mut result: Vec<IceServer> = Vec::new();
// Do not use cache because there is no TLS.
let load_cache = false;
for unresolved_ice_server in unresolved_ice_servers {
match unresolved_ice_server {
UnresolvedIceServer::Stun { hostname, port } => {
match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
Ok(addrs) => {
let urls: Vec<String> = addrs
.into_iter()
.map(|addr| format!("stun:{addr}"))
.collect();
let stun_server = IceServer {
urls,
username: None,
credential: None,
};
result.push(stun_server);
}
Err(err) => {
warn!(
context,
"Failed to resolve STUN {hostname}:{port}: {err:#}."
);
}
}
}
UnresolvedIceServer::Turn {
hostname,
port,
username,
credential,
} => match lookup_host_with_cache(context, &hostname, port, "", load_cache).await {
Ok(addrs) => {
let urls: Vec<String> = addrs
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let turn_server = IceServer {
urls,
username: Some(username),
credential: Some(credential),
};
result.push(turn_server);
}
Err(err) => {
warn!(
context,
"Failed to resolve TURN {hostname}:{port}: {err:#}."
);
}
},
}
}
let json = serde_json::to_string(&result)?;
Ok(json)
}
/// Creates JSON with ICE servers when no TURN servers are known.
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
pub(crate) fn create_fallback_ice_servers() -> Vec<UnresolvedIceServer> {
// Do not use public STUN server from https://stunprotocol.org/.
// It changes the hostname every year
// (e.g. stunserver2025.stunprotocol.org
@@ -674,36 +735,18 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
// because of bandwidth costs:
// <https://github.com/jselbie/stunserver/issues/50>
let hostname = "nine.testrun.org";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("stun:{addr}"))
.collect();
let stun_server = IceServer {
urls,
username: None,
credential: None,
};
let hostname = "turn.delta.chat";
// Do not use cache because there is no TLS.
let load_cache = false;
let urls: Vec<String> = lookup_host_with_cache(context, hostname, STUN_PORT, "", load_cache)
.await?
.into_iter()
.map(|addr| format!("turn:{addr}"))
.collect();
let turn_server = IceServer {
urls,
username: Some("public".to_string()),
credential: Some("o4tR7yG4rG2slhXqRUf9zgmHz".to_string()),
};
let json = serde_json::to_string(&[stun_server, turn_server])?;
Ok(json)
vec![
UnresolvedIceServer::Stun {
hostname: "nine.testrun.org".to_string(),
port: STUN_PORT,
},
UnresolvedIceServer::Turn {
hostname: "turn.delta.chat".to_string(),
port: STUN_PORT,
username: "public".to_string(),
credential: "o4tR7yG4rG2slhXqRUf9zgmHz".to_string(),
},
]
}
/// Returns JSON with ICE servers.
@@ -717,7 +760,8 @@ pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<Str
/// <https://github.com/deltachat/deltachat-desktop/issues/5447>.
pub async fn ice_servers(context: &Context) -> Result<String> {
if let Some(ref metadata) = *context.metadata.read().await {
Ok(metadata.ice_servers.clone())
let ice_servers = resolve_ice_servers(context, metadata.ice_servers.clone()).await?;
Ok(ice_servers)
} else {
Ok("[]".to_string())
}
+54 -42
View File
@@ -19,6 +19,7 @@ use strum_macros::EnumIter;
use crate::blob::BlobObject;
use crate::chatlist::Chatlist;
use crate::chatlist_events;
use crate::color::str_to_color;
use crate::config::Config;
use crate::constants::{
@@ -42,7 +43,7 @@ use crate::mimefactory::{MimeFactory, RenderedEmail};
use crate::mimeparser::SystemMessage;
use crate::param::{Param, Params};
use crate::receive_imf::ReceivedMsg;
use crate::smtp::send_msg_to_smtp;
use crate::smtp::{self, send_msg_to_smtp};
use crate::stock_str;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::{
@@ -51,7 +52,6 @@ use crate::tools::{
gm2local_offset, normalize_text, smeared_time, time, truncate_msg_text,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{chatlist_events, imap};
pub(crate) const PARAM_BROADCAST_SECRET: Param = Param::Arg3;
@@ -635,10 +635,6 @@ impl ChatId {
"UPDATE imap SET target=? WHERE rfc724_mid IN (SELECT pre_rfc724_mid FROM msgs WHERE chat_id=? AND pre_rfc724_mid!='')",
(&delete_msgs_target, self,),
)?;
transaction.execute(
"DELETE FROM smtp WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
)?;
transaction.execute(
"DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=?)",
(self,),
@@ -1156,7 +1152,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
return Ok(stock_str::encr_none(context).await);
}
let mut ret = stock_str::e2e_available(context).await + "\n";
let mut ret = stock_str::messages_e2e_encrypted(context).await + "\n";
for &contact_id in get_chat_contacts(context, self)
.await?
@@ -2135,10 +2131,11 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
}
/// Whether the chat is pinned or archived.
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter)]
#[derive(Debug, Copy, Eq, PartialEq, Clone, Serialize, Deserialize, EnumIter, Default)]
#[repr(i8)]
pub enum ChatVisibility {
/// Chat is neither archived nor pinned.
#[default]
Normal = 0,
/// Chat is archived.
@@ -2838,32 +2835,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let from = context.get_primary_self_addr().await?;
let lowercase_from = from.to_lowercase();
// Send BCC to self if it is enabled.
//
// Previous versions of Delta Chat did not send BCC self
// if DeleteServerAfter was set to immediately delete messages
// from the server. This is not the case anymore
// because BCC-self messages are also used to detect
// that message was sent if SMTP server is slow to respond
// and connection is frequently lost
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
// disabled by default is fine.
//
// `from` must be the last addr, see `receive_imf_inner()` why.
recipients.retain(|x| x.to_lowercase() != lowercase_from);
if (context.get_config_bool(Config::BccSelf).await?
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
if context.get_config_bool(Config::BccSelf).await?
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage
{
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
// messages.
if needs_encryption {
for addr in context.get_secondary_self_addrs().await? {
recipients.push(addr);
}
}
recipients.push(from);
smtp::add_self_recipients(context, &mut recipients, needs_encryption).await?;
}
// Default Webxdc integrations are hidden messages and must not be sent out
@@ -3230,6 +3206,36 @@ pub async fn get_chat_msgs_ex(
Ok(items)
}
/// Marks all unread messages in all chats as noticed.
/// Ignores messages from blocked contacts, but does not ignore messages in muted chats.
pub async fn marknoticed_all_chats(context: &Context) -> Result<()> {
// The sql statement here is similar to the one in get_fresh_msgs
let list = context
.sql
.query_map_vec(
"SELECT DISTINCT(c.id)
FROM msgs m
INNER JOIN chats c
ON m.chat_id=c.id
WHERE m.state=?
AND m.hidden=0
AND m.chat_id>9
AND c.blocked=0;",
(MessageState::InFresh,),
|row| {
let msg_id: ChatId = row.get(0)?;
Ok(msg_id)
},
)
.await?;
for chat_id in list {
marknoticed_chat(context, chat_id).await?;
}
Ok(())
}
/// Marks all messages in the chat as noticed.
/// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed.
pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> {
@@ -3291,7 +3297,7 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
let hidden_messages = context
.sql
.query_map_vec(
"SELECT id, rfc724_mid FROM msgs
"SELECT id FROM msgs
WHERE state=?
AND hidden=1
AND chat_id=?
@@ -3299,16 +3305,11 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
(MessageState::InFresh, chat_id), // No need to check for InNoticed messages, because reactions are never InNoticed
|row| {
let msg_id: MsgId = row.get(0)?;
let rfc724_mid: String = row.get(1)?;
Ok((msg_id, rfc724_mid))
Ok(msg_id)
},
)
.await?;
for (msg_id, rfc724_mid) in &hidden_messages {
message::update_msg_state(context, *msg_id, MessageState::InSeen).await?;
imap::markseen_on_imap_table(context, rfc724_mid).await?;
}
message::markseen_msgs(context, hidden_messages).await?;
if noticed_msgs_count == 0 {
return Ok(());
}
@@ -3330,6 +3331,10 @@ pub(crate) async fn mark_old_messages_as_noticed(
context: &Context,
mut msgs: Vec<ReceivedMsg>,
) -> Result<()> {
if context.get_config_bool(Config::TeamProfile).await? {
return Ok(());
}
msgs.retain(|m| m.state.is_outgoing());
if msgs.is_empty() {
return Ok(());
@@ -4112,7 +4117,7 @@ pub async fn remove_contact_from_chat(
} else {
let mut sync = Nosync;
if chat.is_promoted() {
if chat.is_promoted() && chat.typ != Chattype::OutBroadcast {
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
} else {
remove_from_chat_contacts_table_without_trace(context, chat_id, contact_id).await?;
@@ -4375,7 +4380,14 @@ pub async fn forward_msgs_2ctx(
}
let param = &mut param;
msg.param.steal(param, Param::File);
// When forwarding between different accounts, blob files must be physically copied
// because each account has its own blob directory.
if let Some(src_path) = param.get_file_path(ctx_src)? {
let new_blob = BlobObject::create_and_deduplicate(ctx_dst, &src_path, &src_path)
.context("Failed to copy blob file to destination account")?;
msg.param.set(Param::File, new_blob.as_name());
}
msg.param.steal(param, Param::Filename);
msg.param.steal(param, Param::Width);
msg.param.steal(param, Param::Height);
+192 -5
View File
@@ -1,6 +1,7 @@
use std::sync::Arc;
use super::*;
use crate::Event;
use crate::chatlist::get_archived_cnt;
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
use crate::ephemeral::Timer;
@@ -1240,6 +1241,96 @@ async fn test_unarchive_if_muted() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_marknoticed_all_chats() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.section("alice: create chats & promote them by sending a message");
let alice_chat_normal = alice
.create_group_with_members("Chat (normal)", &[alice, bob])
.await;
send_text_msg(alice, alice_chat_normal, "Hi".to_string()).await?;
let alice_chat_muted = alice
.create_group_with_members("Chat (muted)", &[alice, bob])
.await;
send_text_msg(alice, alice_chat_muted, "Hi".to_string()).await?;
set_muted(&alice.ctx, alice_chat_muted, MuteDuration::Forever).await?;
let alice_chat_archived_and_muted = alice
.create_group_with_members("Chat (archived and muted)", &[alice, bob])
.await;
send_text_msg(alice, alice_chat_archived_and_muted, "Hi".to_string()).await?;
set_muted(
&alice.ctx,
alice_chat_archived_and_muted,
MuteDuration::Forever,
)
.await?;
alice_chat_archived_and_muted
.set_visibility(&alice.ctx, ChatVisibility::Archived)
.await?;
tcm.section("bob: receive messages, accept all chats and send a reply to each messsage");
while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await {
let bob_message = bob.recv_msg(&sent_msg).await;
let bob_chat_id = bob_message.chat_id;
bob_chat_id.accept(bob).await?;
send_text_msg(bob, bob_chat_id, "reply".to_string()).await?;
}
tcm.section("alice: receive replies from bob");
while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await {
alice.recv_msg(&sent_msg).await;
}
// ensure chats have unread messages
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 1);
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 1);
assert_eq!(
alice_chat_archived_and_muted
.get_fresh_msg_cnt(alice)
.await?,
1
);
tcm.section("alice: mark as read");
alice.evtracker.clear_events();
marknoticed_all_chats(alice).await?;
tcm.section("alice: check that chats are no longer unread and that chatlist update events were received");
assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 0);
assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 0);
assert_eq!(
alice_chat_archived_and_muted
.get_fresh_msg_cnt(alice)
.await?,
0
);
let emitted_events = alice.evtracker.take_events();
for event in &[
EventType::ChatlistItemChanged {
chat_id: Some(alice_chat_normal),
},
EventType::ChatlistItemChanged {
chat_id: Some(alice_chat_muted),
},
EventType::ChatlistItemChanged {
chat_id: Some(alice_chat_archived_and_muted),
},
EventType::ChatlistItemChanged {
chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK),
},
] {
assert!(emitted_events.iter().any(|Event { typ, .. }| typ == event));
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_archive_fresh_msgs() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -3344,6 +3435,11 @@ async fn test_remove_member_from_broadcast() -> Result<()> {
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
// Alice must not remember old members,
// because we would like to remember the minimum information possible
let past_contacts = get_past_chat_contacts(alice, alice_chat_id).await?;
assert_eq!(past_contacts.len(), 0);
let remove_msg = alice.pop_sent_msg().await;
let rcvd = bob.recv_msg(&remove_msg).await;
assert_eq!(rcvd.text, "Member Me removed by alice@example.org.");
@@ -3720,13 +3816,13 @@ async fn test_chat_get_encryption_info() -> Result<()> {
let chat_id = create_group(alice, "Group").await?;
assert_eq!(
chat_id.get_encryption_info(alice).await?,
"End-to-end encryption available"
"Messages are end-to-end encrypted."
);
add_contact_to_chat(alice, chat_id, contact_bob).await?;
assert_eq!(
chat_id.get_encryption_info(alice).await?,
"End-to-end encryption available\n\
"Messages are end-to-end encrypted.\n\
\n\
bob@example.net\n\
CCCB 5AA9 F6E1 141C 9431\n\
@@ -3736,7 +3832,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
add_contact_to_chat(alice, chat_id, contact_fiona).await?;
assert_eq!(
chat_id.get_encryption_info(alice).await?,
"End-to-end encryption available\n\
"Messages are end-to-end encrypted.\n\
\n\
fiona@example.net\n\
C8BA 50BF 4AC1 2FAF 38D7\n\
@@ -3750,13 +3846,13 @@ async fn test_chat_get_encryption_info() -> Result<()> {
let email_chat = alice.create_email_chat(bob).await;
assert_eq!(
email_chat.id.get_encryption_info(alice).await?,
"No encryption"
"No encryption."
);
alice.sql.execute("DELETE FROM public_keys", ()).await?;
assert_eq!(
chat_id.get_encryption_info(alice).await?,
"End-to-end encryption available\n\
"Messages are end-to-end encrypted.\n\
\n\
fiona@example.net\n\
(key missing)\n\
@@ -5395,6 +5491,97 @@ async fn test_forward_msgs_2ctx() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_msgs_2ctx_with_file() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
// First, establish a chat between Alice and Bob to have the chat IDs
let alice_chat = alice.create_chat(bob).await;
let alice_initial = alice.send_text(alice_chat.id, "hi").await;
let bob_alice_msg = bob.recv_msg(&alice_initial).await;
let bob_chat_id = bob_alice_msg.chat_id;
bob_chat_id.accept(bob).await?;
// Alice sends a message with an attached file to her self-chat
let alice_self_chat = alice.get_self_chat().await;
let file_bytes = b"test file content";
let file = alice.get_blobdir().join("test.txt");
tokio::fs::write(&file, file_bytes).await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file_and_deduplicate(alice, &file, Some("test.txt"), Some("text/plain"))?;
msg.set_text("Here's a file".to_string());
alice.send_msg(alice_self_chat.id, &mut msg).await;
let alice_self_msg = alice.get_last_msg().await;
// Verify the file exists in Alice's blobdir
assert_eq!(alice_self_msg.viewtype, Viewtype::File);
let alice_original_file_path = alice_self_msg.get_file(alice).unwrap();
let alice_original_content = tokio::fs::read(&alice_original_file_path).await?;
assert_eq!(alice_original_content, file_bytes);
// Alice forwards the message to Bob using forward_msgs_2ctx
forward_msgs_2ctx(alice, &[alice_self_msg.id], bob, bob_chat_id).await?;
// Bob should have the forwarded message with the file in his database
let bob_msg = bob.get_last_msg().await;
assert_eq!(bob_msg.viewtype, Viewtype::File);
assert!(bob_msg.is_forwarded());
assert_eq!(bob_msg.text, "Here's a file");
assert_eq!(bob_msg.from_id, ContactId::SELF);
// Verify Bob has the file in his blobdir with correct content
let bob_file_path = bob_msg.get_file(bob).unwrap();
let bob_file_content = tokio::fs::read(&bob_file_path).await?;
assert_eq!(bob_file_content, file_bytes);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_msgs_2ctx_missing_blob() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat = alice.create_chat(bob).await;
let alice_initial = alice.send_text(alice_chat.id, "hi").await;
let bob_alice_msg = bob.recv_msg(&alice_initial).await;
let bob_chat_id = bob_alice_msg.chat_id;
bob_chat_id.accept(bob).await?;
// Alice sends a file to her self-chat
let alice_self_chat = alice.get_self_chat().await;
let file_bytes = b"test content";
let file = alice.get_blobdir().join("test.txt");
tokio::fs::write(&file, file_bytes).await?;
let mut msg = Message::new(Viewtype::File);
msg.set_file_and_deduplicate(alice, &file, Some("test.txt"), Some("text/plain"))?;
msg.set_text("File message".to_string());
alice.send_msg(alice_self_chat.id, &mut msg).await;
let alice_self_msg = alice.get_last_msg().await;
// Delete the blob file from Alice's blobdir to simulate a missing file
let alice_file_path = alice_self_msg.get_file(alice).unwrap();
tokio::fs::remove_file(&alice_file_path).await?;
// Alice tries to forward the message - this should fail with an error
let result = forward_msgs_2ctx(alice, &[alice_self_msg.id], bob, bob_chat_id).await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to copy blob file")
);
Ok(())
}
/// Tests that in multi-device setup
/// second device learns the key of a contact
/// via Autocrypt-Gossip in 1:1 chats.
+3 -15
View File
@@ -1299,18 +1299,6 @@ WHERE addr=?
Ok(())
}
/// Returns number of blocked contacts.
pub async fn get_blocked_cnt(context: &Context) -> Result<usize> {
let count = context
.sql
.count(
"SELECT COUNT(*) FROM contacts WHERE id>? AND blocked!=0",
(ContactId::LAST_SPECIAL,),
)
.await?;
Ok(count)
}
/// Get blocked contacts.
pub async fn get_all_blocked(context: &Context) -> Result<Vec<ContactId>> {
Contact::update_blocked_mailinglist_contacts(context)
@@ -1354,13 +1342,13 @@ WHERE addr=?
let fingerprint_other = fingerprint_other.to_string();
let stock_message = if contact.public_key(context).await?.is_some() {
stock_str::e2e_available(context).await
stock_str::messages_e2e_encrypted(context).await
} else {
stock_str::encr_none(context).await
};
let finger_prints = stock_str::finger_prints(context).await;
let mut ret = format!("{stock_message}.\n{finger_prints}:");
let mut ret = format!("{stock_message}\n{finger_prints}:");
let fingerprint_self = load_self_public_key(context)
.await?
@@ -1466,7 +1454,7 @@ WHERE addr=?
/// Returns true if the contact is a key-contact.
/// Otherwise it is an addresss-contact.
pub fn is_key_contact(&self) -> bool {
self.fingerprint.is_some()
self.fingerprint.is_some() || self.id == ContactId::SELF
}
/// Returns OpenPGP fingerprint of a contact.
+2 -2
View File
@@ -823,7 +823,7 @@ async fn test_contact_get_encrinfo() -> Result<()> {
let address_contact_bob_id = alice.add_or_lookup_address_contact_id(bob).await;
let encrinfo = Contact::get_encrinfo(alice, address_contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
assert_eq!(encrinfo, "No encryption.");
let contact = Contact::get_by_id(alice, address_contact_bob_id).await?;
assert!(!contact.e2ee_avail(alice).await?);
@@ -832,7 +832,7 @@ async fn test_contact_get_encrinfo() -> Result<()> {
let encrinfo = Contact::get_encrinfo(alice, contact_bob_id).await?;
assert_eq!(
encrinfo,
"End-to-end encryption available.
"Messages are end-to-end encrypted.
Fingerprints:
Me (alice@example.org):
+2
View File
@@ -36,6 +36,8 @@ use crate::tools::{self, duration_to_str, time, time_elapsed};
use crate::transport::ConfiguredLoginParam;
use crate::{chatlist_events, stats};
pub use crate::scheduler::connectivity::Connectivity;
/// Builder for the [`Context`].
///
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
+13 -14
View File
@@ -23,7 +23,9 @@ use num_traits::FromPrimitive;
use ratelimit::Ratelimit;
use url::Url;
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
use crate::calls::{
UnresolvedIceServer, create_fallback_ice_servers, create_ice_servers_from_metadata,
};
use crate::chat::{self, ChatId, ChatIdBlocked, add_device_msg};
use crate::chatlist_events;
use crate::config::Config;
@@ -134,16 +136,15 @@ pub(crate) struct ServerMetadata {
pub iroh_relay: Option<Url>,
/// JSON with ICE servers for WebRTC calls
/// and the expiration timestamp.
///
/// If JSON is about to expire, new TURN credentials
/// should be fetched from the server
/// to be ready for WebRTC calls.
pub ice_servers: String,
/// ICE servers for WebRTC calls.
pub ice_servers: Vec<UnresolvedIceServer>,
/// Timestamp when ICE servers are considered
/// expired and should be updated.
///
/// If ICE servers are about to expire, new TURN credentials
/// should be fetched from the server
/// to be ready for WebRTC calls.
pub ice_servers_expiration_timestamp: i64,
}
@@ -1139,7 +1140,6 @@ impl Session {
/// Stores pending `\Seen` flags for messages in `imap_markseen` table.
pub(crate) async fn store_seen_flags_on_imap(&mut self, context: &Context) -> Result<()> {
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
@@ -1215,7 +1215,6 @@ impl Session {
}
if context.get_config_bool(Config::TeamProfile).await? {
info!(context, "Team profile, skipping seen flag synchronization.");
return Ok(());
}
@@ -1552,7 +1551,7 @@ impl Session {
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
match create_ice_servers_from_metadata(&value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
@@ -1569,7 +1568,7 @@ impl Session {
info!(context, "Will use fallback ICE servers.");
// Set expiration timestamp 7 days in the future so we don't request it again.
old_metadata.ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
old_metadata.ice_servers = create_fallback_ice_servers(context).await?;
old_metadata.ice_servers = create_fallback_ice_servers();
}
return Ok(());
}
@@ -1616,7 +1615,7 @@ impl Session {
}
"/shared/vendor/deltachat/turn" => {
if let Some(value) = m.value {
match create_ice_servers_from_metadata(context, &value).await {
match create_ice_servers_from_metadata(&value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
ice_servers_expiration_timestamp = parsed_timestamp;
ice_servers = Some(parsed_ice_servers);
@@ -1635,7 +1634,7 @@ impl Session {
} else {
// Set expiration timestamp 7 days in the future so we don't request it again.
ice_servers_expiration_timestamp = time() + 3600 * 24 * 7;
create_fallback_ice_servers(context).await?
create_fallback_ice_servers()
};
*lock = Some(ServerMetadata {
+35 -7
View File
@@ -452,6 +452,7 @@ pub struct Message {
pub(crate) is_dc_message: MessengerMessage,
pub(crate) original_msg_id: MsgId,
pub(crate) mime_modified: bool,
pub(crate) chat_visibility: ChatVisibility,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
pub(crate) error: Option<String>,
@@ -525,6 +526,7 @@ impl Message {
m.param AS param,
m.hidden AS hidden,
m.location_id AS location,
c.archived AS visibility,
c.blocked AS blocked
FROM msgs m
LEFT JOIN chats c ON c.id=m.chat_id
@@ -583,6 +585,7 @@ impl Message {
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
hidden: row.get("hidden")?,
location_id: row.get("location")?,
chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
chat_blocked: row
.get::<_, Option<Blocked>>("blocked")?
.unwrap_or_default(),
@@ -1503,6 +1506,16 @@ pub async fn get_msg_read_receipts(
.await
}
/// Returns count of read receipts on message.
///
/// This view count is meant as a feedback measure for the channel owner only.
pub async fn get_msg_read_receipt_count(context: &Context, msg_id: MsgId) -> Result<usize> {
context
.sql
.count("SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?", (msg_id,))
.await
}
pub(crate) fn guess_msgtype_from_suffix(msg: &Message) -> Option<(Viewtype, &'static str)> {
msg.param
.get(Param::Filename)
@@ -1837,6 +1850,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.param AS param,
m.from_id AS from_id,
m.rfc724_mid AS rfc724_mid,
m.hidden AS hidden,
c.archived AS archived,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
@@ -1848,6 +1862,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let hidden: bool = row.get("hidden")?;
let visibility: ChatVisibility = row.get("archived")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
@@ -1859,6 +1874,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
param,
from_id,
rfc724_mid,
hidden,
visibility,
blocked.unwrap_or_default(),
),
@@ -1891,6 +1907,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
curr_param,
curr_from_id,
curr_rfc724_mid,
curr_hidden,
curr_visibility,
curr_blocked,
),
@@ -1903,8 +1920,9 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
markseen_on_imap_table(context, &curr_rfc724_mid).await?;
// Read receipts for system messages are never sent. These messages have no place to
// display received read receipt anyway. And since their text is locally generated,
// Read receipts for system messages are never sent to contacts.
// These messages have no place to display received read receipt
// anyway. And since their text is locally generated,
// quoting them is dangerous as it may contain contact names. E.g., for original message
// "Group left by me", a read receipt will quote "Group left by <name>", and the name can
// be a display name stored in address book rather than the name sent in the From field by
@@ -1912,25 +1930,35 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
//
// We also don't send read receipts for contact requests.
// Read receipts will not be sent even after accepting the chat.
if curr_blocked == Blocked::Not
let to_id = if curr_blocked == Blocked::Not
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
&& curr_param.get_cmd() == SystemMessage::Unknown
&& context.should_send_mdns().await?
{
Some(curr_from_id)
} else if context.get_config_bool(Config::BccSelf).await? {
Some(ContactId::SELF)
} else {
None
};
if let Some(to_id) = to_id {
context
.sql
.execute(
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
(id, curr_from_id, curr_rfc724_mid),
(id, to_id, curr_rfc724_mid),
)
.await
.context("failed to insert into smtp_mdns")?;
context.scheduler.interrupt_smtp().await;
}
updated_chat_ids.insert(curr_chat_id);
if !curr_hidden {
updated_chat_ids.insert(curr_chat_id);
}
}
archived_chats_maybe_noticed |=
curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
archived_chats_maybe_noticed |= curr_state == MessageState::InFresh
&& !curr_hidden
&& curr_visibility == ChatVisibility::Archived;
}
for updated_chat_id in updated_chat_ids {
+35 -9
View File
@@ -106,6 +106,7 @@ pub struct MimeFactory {
/// addresses and OpenPGP keys
/// to use for encryption.
///
/// If `Some`, encrypt to self also.
/// `None` if the message is not encrypted.
encryption_pubkeys: Option<Vec<(String, SignedPublicKey)>>,
@@ -234,7 +235,6 @@ impl MimeFactory {
encryption_pubkeys = if msg.param.get_bool(Param::ForcePlaintext).unwrap_or(false) {
None
} else {
// Encrypt, but only to self.
Some(Vec::new())
};
} else if chat.is_mailing_list() {
@@ -247,6 +247,37 @@ impl MimeFactory {
// Do not encrypt messages to mailing lists.
encryption_pubkeys = None;
} else if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
let fp = fp?;
// In a broadcast channel, only send member-added/removed messages
// to the affected member
let (authname, addr) = context
.sql
.query_row(
"SELECT authname, addr FROM contacts WHERE fingerprint=?",
(fp,),
|row| {
let authname: String = row.get(0)?;
let addr: String = row.get(1)?;
Ok((authname, addr))
},
)
.await?;
let public_key_bytes: Vec<_> = context
.sql
.query_get_value(
"SELECT public_key FROM public_keys WHERE fingerprint=?",
(fp,),
)
.await?
.context("Can't send member addition/removal: missing key")?;
recipients.push(addr.clone());
to.push((authname, addr.clone()));
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
encryption_pubkeys = Some(vec![(addr, public_key)]);
} else {
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
msg.param.get(Param::Arg)
@@ -306,13 +337,6 @@ impl MimeFactory {
for row in rows {
let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
// In a broadcast channel, only send member-added/removed messages
// to the affected member:
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
&& fp? != fingerprint {
continue;
}
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
Some(SignedPublicKey::from_slice(public_key_bytes)?)
} else {
@@ -539,7 +563,9 @@ impl MimeFactory {
let timestamp = create_smeared_timestamp(context);
let addr = contact.get_addr().to_string();
let encryption_pubkeys = if contact.is_key_contact() {
let encryption_pubkeys = if from_id == ContactId::SELF {
Some(Vec::new())
} else if contact.is_key_contact() {
if let Some(key) = contact.public_key(context).await? {
Some(vec![(addr.clone(), key)])
} else {
+3 -9
View File
@@ -2159,9 +2159,9 @@ pub(crate) struct Report {
///
/// It MUST be present if the original message has a Message-ID according to RFC 8098.
/// In case we can't find it (shouldn't happen), this is None.
original_message_id: Option<String>,
pub original_message_id: Option<String>,
/// Additional-Message-IDs
additional_message_ids: Vec<String>,
pub additional_message_ids: Vec<String>,
}
/// Delivery Status Notification (RFC 3464, RFC 6533)
@@ -2468,13 +2468,7 @@ async fn handle_mdn(
timestamp_sent: i64,
) -> Result<()> {
if from_id == ContactId::SELF {
warn!(
context,
"Ignoring MDN sent to self, this is a bug on the sender device."
);
// This is not an error on our side,
// we successfully ignored an invalid MDN and return `Ok`.
// MDNs to self are handled in receive_imf_inner().
return Ok(());
}
+9 -7
View File
@@ -17,7 +17,9 @@ use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
use pgp::crypto::sym::SymmetricKeyAlgorithm;
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
use pgp::types::{CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, StringToKey};
use pgp::types::{
CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, SecretKeyTrait as _, StringToKey,
};
use rand_old::{Rng as _, thread_rng};
use tokio::runtime::Handle;
@@ -31,9 +33,6 @@ pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
/// Preferred symmetric encryption algorithm.
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
/// Preferred cryptographic hash.
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::Sha256;
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
///
/// Returns (type, headers, base64 encoded body).
@@ -205,7 +204,8 @@ pub async fn pk_encrypt(
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
@@ -228,7 +228,8 @@ pub async fn pk_encrypt(
}
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
@@ -453,7 +454,8 @@ pub async fn symm_encrypt_message(
);
msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
let hash_algorithm = private_key_for_signing.hash_alg();
msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
+31 -5
View File
@@ -59,14 +59,12 @@ pub enum LoginOptions {
/// scheme: `dclogin://user@host/?p=password&v=1[&options]`
/// read more about the scheme at <https://github.com/deltachat/interface/blob/master/uri-schemes.md#DCLOGIN>
pub(super) fn decode_login(qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).with_context(|| format!("Malformed url: {qr:?}"))?;
let qr = qr.replacen("://", ":", 1);
let url_without_scheme = qr
let url = url::Url::parse(&qr).with_context(|| format!("Malformed url: {qr:?}"))?;
let payload = qr
.get(DCLOGIN_SCHEME.len()..)
.context("invalid DCLOGIN payload E1")?;
let payload = url_without_scheme
.strip_prefix("//")
.unwrap_or(url_without_scheme);
let addr = payload
.split(['?', '/'])
@@ -365,4 +363,32 @@ mod test {
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_dclogin_ipv4() -> anyhow::Result<()> {
let result = decode_login("dclogin://test@[127.0.0.1]?p=1234&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(address, "test@[127.0.0.1]".to_owned());
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
} else {
unreachable!("wrong type");
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_dclogin_ipv6() -> anyhow::Result<()> {
let result =
decode_login("dclogin://test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]?p=1234&v=1")?;
if let Qr::Login { address, options } = result {
assert_eq!(
address,
"test@[2001:0db8:85a3:0000:0000:8a2e:0370:7334]".to_owned()
);
assert_eq!(options, login_options_just_pw!("1234".to_owned()));
} else {
unreachable!("wrong type");
}
Ok(())
}
}
+12 -2
View File
@@ -747,13 +747,23 @@ Content-Disposition: reaction\n\
alice_reaction_msg.id.get_state(&alice).await?,
MessageState::InSeen
);
// Reactions don't request MDNs.
// Reactions don't request MDNs, but an MDN to self is sent.
assert_eq!(
alice
.sql
.count("SELECT COUNT(*) FROM smtp_mdns", ())
.await?,
0
1
);
assert_eq!(
alice
.sql
.count(
"SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
(ContactId::SELF,)
)
.await?,
1
);
// Alice reacts to own message.
+149 -54
View File
@@ -1,5 +1,6 @@
//! Internet Message Format reception pipeline.
use std::cmp;
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::iter;
use std::sync::LazyLock;
@@ -14,7 +15,7 @@ use mailparse::SingleInfo;
use num_traits::FromPrimitive;
use regex::Regex;
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, save_broadcast_secret};
use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ChatVisibility, save_broadcast_secret};
use crate::config::Config;
use crate::constants::{self, Blocked, Chattype, DC_CHAT_ID_TRASH, EDITED_PREFIX, ShowEmails};
use crate::contact::{self, Contact, ContactId, Origin, mark_contact_id_as_verified};
@@ -195,7 +196,7 @@ async fn insert_tombstone(context: &Context, rfc724_mid: &str) -> Result<MsgId>
async fn get_to_and_past_contact_ids(
context: &Context,
mime_parser: &MimeMessage,
chat_assignment: &ChatAssignment,
chat_assignment: &mut ChatAssignment,
parent_message: &Option<Message>,
incoming_origin: Origin,
) -> Result<(Vec<Option<ContactId>>, Vec<Option<ContactId>>)> {
@@ -384,32 +385,6 @@ async fn get_to_and_past_contact_ids(
// mapped it to a key contact.
// This is an encrypted 1:1 chat.
to_ids = pgp_to_ids
} else if let Some(chat_id) = chat_id {
to_ids = match mime_parser.was_encrypted() {
true => {
lookup_key_contacts_by_address_list(
context,
&mime_parser.recipients,
to_member_fingerprints,
Some(chat_id),
)
.await?
}
false => {
add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
if !mime_parser.incoming {
Origin::OutgoingTo
} else if incoming_origin.is_known() {
Origin::IncomingTo
} else {
Origin::IncomingUnknownTo
},
)
.await?
}
}
} else {
let ids = match mime_parser.was_encrypted() {
true => {
@@ -417,7 +392,7 @@ async fn get_to_and_past_contact_ids(
context,
&mime_parser.recipients,
to_member_fingerprints,
None,
chat_id,
)
.await?
}
@@ -425,13 +400,20 @@ async fn get_to_and_past_contact_ids(
};
if mime_parser.was_encrypted() && !ids.contains(&None)
// Prefer creating PGP chats if there are any key-contacts. At least this prevents
// from replying unencrypted.
// from replying unencrypted. Otherwise downgrade to a non-replyable ad-hoc group.
|| ids
.iter()
.any(|&c| c.is_some() && c != Some(ContactId::SELF))
{
to_ids = ids;
} else {
if mime_parser.was_encrypted() {
warn!(
context,
"No key-contact looked up. Downgrading to AdHocGroup."
);
*chat_assignment = ChatAssignment::AdHocGroup;
}
to_ids = add_or_lookup_contacts_by_address_list(
context,
&mime_parser.recipients,
@@ -633,14 +615,12 @@ pub(crate) async fn receive_imf_inner(
.await?
.filter(|p| Some(p.id) != replace_msg_id);
let chat_assignment =
let mut chat_assignment =
decide_chat_assignment(context, &mime_parser, &parent_message, rfc724_mid, from_id).await?;
info!(context, "Chat assignment is {chat_assignment:?}.");
let (to_ids, past_ids) = get_to_and_past_contact_ids(
context,
&mime_parser,
&chat_assignment,
&mut chat_assignment,
&parent_message,
incoming_origin,
)
@@ -960,6 +940,74 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
// This is a Delta Chat MDN. Mark as read.
markseen_on_imap_table(context, rfc724_mid_orig).await?;
}
if !mime_parser.incoming && !context.get_config_bool(Config::TeamProfile).await? {
let mut updated_chats = BTreeMap::new();
let mut archived_chats_maybe_noticed = false;
for report in &mime_parser.mdn_reports {
for msg_rfc724_mid in report
.original_message_id
.iter()
.chain(&report.additional_message_ids)
{
let Some(msg_id) = rfc724_mid_exists(context, msg_rfc724_mid).await? else {
continue;
};
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
continue;
};
if msg.state < MessageState::InFresh || msg.state >= MessageState::InSeen {
continue;
}
if !mime_parser.was_encrypted() && msg.get_showpadlock() {
warn!(context, "MDN: Not encrypted. Ignoring.");
continue;
}
message::update_msg_state(context, msg_id, MessageState::InSeen).await?;
if let Err(e) = msg_id.start_ephemeral_timer(context).await {
error!(context, "start_ephemeral_timer for {msg_id}: {e:#}.");
}
if !mime_parser.has_chat_version() {
continue;
}
archived_chats_maybe_noticed |= msg.state < MessageState::InNoticed
&& msg.chat_visibility == ChatVisibility::Archived;
updated_chats
.entry(msg.chat_id)
.and_modify(|ts| *ts = cmp::max(*ts, msg.timestamp_sort))
.or_insert(msg.timestamp_sort);
}
}
for (chat_id, timestamp_sort) in updated_chats {
context
.sql
.execute(
"
UPDATE msgs SET state=? WHERE
state=? AND
hidden=0 AND
chat_id=? AND
timestamp<?",
(
MessageState::InNoticed,
MessageState::InFresh,
chat_id,
timestamp_sort,
),
)
.await
.context("UPDATE msgs.state")?;
if chat_id.get_fresh_msg_cnt(context).await? == 0 {
// Removes all notifications for the chat in UIs.
context.emit_event(EventType::MsgsNoticed(chat_id));
} else {
context.emit_msgs_changed_without_msg_id(chat_id);
}
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}
if archived_chats_maybe_noticed {
context.on_archived_chats_maybe_noticed();
}
}
}
if mime_parser.is_call() {
@@ -1244,17 +1292,34 @@ async fn decide_chat_assignment(
if from_id != ContactId::SELF && !has_self_addr {
num_recipients += 1;
}
let can_be_11_chat = num_recipients <= 1
&& (from_id != ContactId::SELF
|| !(mime_parser.recipients.is_empty() || has_self_addr)
|| mime_parser.was_encrypted());
let mut can_be_11_chat_log = String::new();
let mut l = |cond: bool, s: String| {
can_be_11_chat_log += &s;
cond
};
let can_be_11_chat = l(
num_recipients <= 1,
format!("num_recipients={num_recipients}."),
) && (l(from_id != ContactId::SELF, format!(" from_id={from_id}."))
|| !(l(
mime_parser.recipients.is_empty(),
format!(" Raw recipients len={}.", mime_parser.recipients.len()),
) || l(has_self_addr, format!(" has_self_addr={has_self_addr}.")))
|| l(
mime_parser.was_encrypted(),
format!(" was_encrypted={}.", mime_parser.was_encrypted()),
));
let chat_assignment_log;
let chat_assignment = if should_trash {
chat_assignment_log = "".to_string();
ChatAssignment::Trash
} else if mime_parser.get_mailinglist_header().is_some() {
chat_assignment_log = "Mailing list header found.".to_string();
ChatAssignment::MailingListOrBroadcast
} else if let Some(grpid) = mime_parser.get_chat_group_id() {
if mime_parser.was_encrypted() {
chat_assignment_log = "Encrypted group message.".to_string();
ChatAssignment::GroupChat {
grpid: grpid.to_string(),
}
@@ -1263,11 +1328,13 @@ async fn decide_chat_assignment(
lookup_chat_by_reply(context, mime_parser, parent).await?
{
// Try to assign to a chat based on In-Reply-To/References.
chat_assignment_log = "Unencrypted group reply.".to_string();
ChatAssignment::ExistingChat {
chat_id,
chat_id_blocked,
}
} else {
chat_assignment_log = "Unencrypted group reply.".to_string();
ChatAssignment::AdHocGroup
}
} else {
@@ -1278,6 +1345,7 @@ async fn decide_chat_assignment(
// even if it had only two members.
//
// Group ID is ignored, however.
chat_assignment_log = "Unencrypted group message, no parent.".to_string();
ChatAssignment::AdHocGroup
}
} else if let Some(parent) = &parent_message {
@@ -1285,24 +1353,38 @@ async fn decide_chat_assignment(
lookup_chat_by_reply(context, mime_parser, parent).await?
{
// Try to assign to a chat based on In-Reply-To/References.
chat_assignment_log = "Reply w/o grpid.".to_string();
ChatAssignment::ExistingChat {
chat_id,
chat_id_blocked,
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
chat_assignment_log = "Reply with Chat-Group-Name.".to_string();
ChatAssignment::AdHocGroup
} else if can_be_11_chat {
chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
ChatAssignment::OneOneChat
} else {
chat_assignment_log = format!("Non-group reply. {can_be_11_chat_log}");
ChatAssignment::AdHocGroup
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
chat_assignment_log = "Message with Chat-Group-Name, no parent.".to_string();
ChatAssignment::AdHocGroup
} else if can_be_11_chat {
chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
ChatAssignment::OneOneChat
} else {
chat_assignment_log = format!("Non-group message, no parent. {can_be_11_chat_log}");
ChatAssignment::AdHocGroup
};
if !chat_assignment_log.is_empty() {
info!(
context,
"{chat_assignment_log} Chat assignment = {chat_assignment:?}."
);
}
Ok(chat_assignment)
}
@@ -1696,8 +1778,6 @@ async fn add_parts(
}
let is_location_kml = mime_parser.location_kml.is_some();
let is_mdn = !mime_parser.mdn_reports.is_empty();
let mut group_changes = match chat.typ {
_ if chat.id.is_special() => GroupChangesInfo::default(),
Chattype::Single => GroupChangesInfo::default(),
@@ -1733,7 +1813,10 @@ async fn add_parts(
let state = if !mime_parser.incoming {
MessageState::OutDelivered
} else if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
} else if seen
|| !mime_parser.mdn_reports.is_empty()
|| chat_id_blocked == Blocked::Yes
|| group_changes.silent
// No check for `hidden` because only reactions are such and they should be `InFresh`.
{
MessageState::InSeen
@@ -2251,19 +2334,13 @@ RETURNING id
}
}
// Normally outgoing MDNs sent by us never appear in mailboxes, but Gmail saves all
// outgoing messages, including MDNs, to the Sent folder. If we detect such saved MDN,
// delete it.
let needs_delete_job =
!mime_parser.incoming && is_mdn && is_dc_message == MessengerMessage::Yes;
Ok(ReceivedMsg {
chat_id,
state,
hidden,
sort_timestamp,
msg_ids: created_db_entries,
needs_delete_job,
needs_delete_job: false,
})
}
@@ -2602,6 +2679,9 @@ async fn lookup_or_create_adhoc_group(
let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
contact_ids.insert(from_id);
if mime_parser.was_encrypted() {
contact_ids.remove(&ContactId::SELF);
}
let trans_fn = |t: &mut rusqlite::Transaction| {
t.pragma_update(None, "query_only", "0")?;
t.execute(
@@ -2909,6 +2989,8 @@ async fn apply_group_changes(
to_ids: &[Option<ContactId>],
past_ids: &[Option<ContactId>],
) -> Result<GroupChangesInfo> {
let from_is_key_contact = Contact::get_by_id(context, from_id).await?.is_key_contact();
ensure!(from_is_key_contact || chat.grpid.is_empty());
let to_ids_flat: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
ensure!(chat.typ == Chattype::Group);
ensure!(!chat.id.is_special());
@@ -2990,7 +3072,10 @@ async fn apply_group_changes(
)
.await?;
if chat.member_list_is_stale(context).await? {
// Avoid insertion of `from_id` into a group with inappropriate encryption state.
if from_is_key_contact != chat.grpid.is_empty()
&& chat.member_list_is_stale(context).await?
{
info!(context, "Member list is stale.");
let mut new_members: HashSet<ContactId> =
HashSet::from_iter(to_ids_flat.iter().copied());
@@ -2998,7 +3083,9 @@ async fn apply_group_changes(
if !from_id.is_special() {
new_members.insert(from_id);
}
if mime_parser.was_encrypted() && chat.grpid.is_empty() {
new_members.remove(&ContactId::SELF);
}
context
.sql
.transaction(|transaction| {
@@ -3048,7 +3135,7 @@ async fn apply_group_changes(
if self_added {
new_members = HashSet::from_iter(to_ids_flat.iter().copied());
new_members.insert(ContactId::SELF);
if !from_id.is_special() {
if !from_id.is_special() && from_is_key_contact != chat.grpid.is_empty() {
new_members.insert(from_id);
}
} else {
@@ -3072,6 +3159,10 @@ async fn apply_group_changes(
new_members.remove(&removed_id);
}
if mime_parser.was_encrypted() && chat.grpid.is_empty() {
new_members.remove(&ContactId::SELF);
}
if new_members != chat_contacts {
chat::update_chat_contacts_table(
context,
@@ -3713,11 +3804,15 @@ async fn create_adhoc_group(
to_ids: &[ContactId],
grpname: &str,
) -> Result<Option<(ChatId, Blocked)>> {
let mut member_ids: Vec<ContactId> = to_ids.to_vec();
if !member_ids.contains(&(from_id)) {
let mut member_ids: Vec<ContactId> = to_ids
.iter()
.copied()
.filter(|&id| id != ContactId::SELF)
.collect();
if from_id != ContactId::SELF && !member_ids.contains(&from_id) {
member_ids.push(from_id);
}
if !member_ids.contains(&(ContactId::SELF)) {
if !mime_parser.was_encrypted() {
member_ids.push(ContactId::SELF);
}
+85 -8
View File
@@ -4,8 +4,9 @@ use tokio::fs;
use super::*;
use crate::chat::{
ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table, create_group,
get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat, send_text_msg,
CantSendReason, ChatItem, ChatVisibility, add_contact_to_chat, add_to_chat_contacts_table,
create_group, get_chat_contacts, get_chat_msgs, is_contact_in_chat, remove_contact_from_chat,
send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::constants::DC_GCL_FOR_FORWARDING;
@@ -3283,7 +3284,8 @@ async fn test_blocked_contact_creates_group() -> Result<()> {
let sent = bob.send_text(group_id, "Heyho, I'm a spammer!").await;
let rcvd = alice.recv_msg(&sent).await;
// Alice blocked Bob, so she shouldn't get the message
// Alice blocked Bob, so she shouldn't be notified.
assert_eq!(rcvd.state, MessageState::InSeen);
assert_eq!(rcvd.chat_blocked, Blocked::Yes);
// Fiona didn't block Bob, though, so she gets the message
@@ -5106,6 +5108,83 @@ async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recv_outgoing_msg_before_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, a0).await;
let chat_id_a0_bob = a0.create_chat_id(bob).await;
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
bob.recv_msg(&sent_msg).await;
let msg_a1 = a1.recv_msg(&sent_msg).await;
assert!(msg_a1.get_showpadlock());
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
assert_eq!(chat_a1.typ, Chattype::Group);
assert!(!chat_a1.is_encrypted(a1).await?);
assert_eq!(
chat::get_chat_contacts(a1, chat_a1.id).await?,
[a1.add_or_lookup_address_contact_id(bob).await]
);
assert_eq!(
chat_a1.why_cant_send(a1).await?,
Some(CantSendReason::NotAMember)
);
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
bob.recv_msg(&sent_msg).await;
let msg_a1 = a1.recv_msg(&sent_msg).await;
assert!(msg_a1.get_showpadlock());
assert_eq!(msg_a1.chat_id, chat_a1.id);
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
assert_eq!(
chat_a1.why_cant_send(a1).await?,
Some(CantSendReason::NotAMember)
);
let msg_a1 = tcm.send_recv(bob, a1, "Hi back").await;
assert!(msg_a1.get_showpadlock());
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
assert_eq!(chat_a1.typ, Chattype::Single);
assert!(chat_a1.is_encrypted(a1).await?);
// Weird, but fine, anyway the bigger problem is the conversation split into two chats.
assert_eq!(
chat_a1.why_cant_send(a1).await?,
Some(CantSendReason::ContactRequest)
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recv_outgoing_msg_before_having_key_and_after() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
tcm.execute_securejoin(bob, a0).await;
let chat_id_a0_bob = a0.create_chat_id(bob).await;
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi").await;
let msg_a1 = a1.recv_msg(&sent_msg).await;
assert!(msg_a1.get_showpadlock());
let chat_a1 = Chat::load_from_db(a1, msg_a1.chat_id).await?;
assert_eq!(chat_a1.typ, Chattype::Group);
assert!(!chat_a1.is_encrypted(a1).await?);
// Device a1 somehow learns Bob's key and creates the corresponding chat. However, this doesn't
// help currently because we only look up key contacts by address in a particular chat and the
// new chat isn't referenced by the received message. This should be fixed by sending and
// receiving Intended Recipient Fingerprint subpackets.
a1.create_chat_id(bob).await;
let sent_msg = a0.send_text(chat_id_a0_bob, "Hi again").await;
let msg_a1 = a1.recv_msg(&sent_msg).await;
assert!(msg_a1.get_showpadlock());
assert_eq!(msg_a1.chat_id, chat_a1.id);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sanitize_filename_in_received() -> Result<()> {
let alice = &TestContext::new_alice().await;
@@ -5365,16 +5444,14 @@ async fn test_encrypted_adhoc_group_message() -> Result<()> {
assert_eq!(chat.is_encrypted(bob).await?, false);
let contact_ids = get_chat_contacts(bob, chat.id).await?;
assert_eq!(contact_ids.len(), 3);
assert!(chat.is_self_in_chat(bob).await?);
assert_eq!(contact_ids.len(), 2);
assert!(!chat.is_self_in_chat(bob).await?);
// Since the group is unencrypted, all contacts have
// to be address-contacts.
for contact_id in contact_ids {
let contact = Contact::get_by_id(bob, contact_id).await?;
if contact_id != ContactId::SELF {
assert_eq!(contact.is_key_contact(), false);
}
assert_eq!(contact.is_key_contact(), false);
}
// `from_id` of the message corresponds to key-contact of Alice
+59 -10
View File
@@ -13,7 +13,7 @@ use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::events::EventType;
use crate::log::warn;
use crate::log::{LogExt, warn};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
@@ -184,6 +184,9 @@ pub(crate) async fn smtp_send(
smtp: &mut Smtp,
msg_id: Option<MsgId>,
) -> SendResult {
if recipients.is_empty() {
return SendResult::Success;
}
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(context, "SMTP-sending out mime message:\n{message}");
}
@@ -374,15 +377,15 @@ pub(crate) async fn send_msg_to_smtp(
return Ok(());
};
if retries > 6 {
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")
.await?;
}
context
.sql
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
.await
.context("Failed to remove message with exceeded retry limit from smtp table")?;
if let Some(mut msg) = Message::load_from_db_optional(context, msg_id).await? {
message::set_msg_failed(context, &mut msg, "Number of retries exceeded the limit.")
.await?;
}
return Ok(());
}
info!(
@@ -570,17 +573,32 @@ async fn send_mdn_rfc724_mid(
additional_rfc724_mids.clone(),
)
.await?;
let encrypted = mimefactory.will_be_encrypted();
let rendered_msg = mimefactory.render(context).await?;
let body = rendered_msg.message;
let addr = contact.get_addr();
let recipient = async_smtp::EmailAddress::new(addr.to_string())
.map_err(|err| format_err!("invalid recipient: {addr} {err:?}"))?;
let recipients = vec![recipient];
let mut recipients = Vec::new();
if contact_id != ContactId::SELF {
recipients.push(contact.get_addr().to_string());
}
if context.get_config_bool(Config::BccSelf).await? {
add_self_recipients(context, &mut recipients, encrypted).await?;
}
let recipients: Vec<_> = recipients
.into_iter()
.filter_map(|addr| {
async_smtp::EmailAddress::new(addr.clone())
.with_context(|| format!("Invalid recipient: {addr}"))
.log_err(context)
.ok()
})
.collect();
match smtp_send(context, &recipients, &body, smtp, None).await {
SendResult::Success => {
info!(context, "Successfully sent MDN for {rfc724_mid}.");
if !recipients.is_empty() {
info!(context, "Successfully sent MDN for {rfc724_mid}.");
}
context
.sql
.transaction(|transaction| {
@@ -667,3 +685,34 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
}
}
}
/// Adds self-addresses to `recipients` as necessary.
/// This doesn't check `Config::BccSelf`, it should be checked by the caller if needed.
pub(crate) async fn add_self_recipients(
context: &Context,
recipients: &mut Vec<String>,
encrypted: bool,
) -> Result<()> {
// Previous versions of Delta Chat did not send BCC self
// if DeleteServerAfter was set to immediately delete messages
// from the server. This is not the case anymore
// because BCC-self messages are also used to detect
// that message was sent if SMTP server is slow to respond
// and connection is frequently lost
// before receiving status line. NB: This is not a problem for chatmail servers, so `BccSelf`
// disabled by default is fine.
if context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty() {
// Avoid sending unencrypted messages to all transports, chatmail relays won't accept
// them. Normally the user should have a non-chatmail primary transport to send unencrypted
// messages.
if encrypted {
for addr in context.get_secondary_self_addrs().await? {
recipients.push(addr);
}
}
// `from` must be the last addr, see `receive_imf_inner()` why.
let from = context.get_primary_self_addr().await?;
recipients.push(from);
}
Ok(())
}
+1 -9
View File
@@ -63,10 +63,7 @@ pub enum StockMessage {
#[strum(props(fallback = "GIF"))]
Gif = 23,
#[strum(props(fallback = "End-to-end encryption available"))]
E2eAvailable = 25,
#[strum(props(fallback = "No encryption"))]
#[strum(props(fallback = "No encryption."))]
EncrNone = 28,
#[strum(props(fallback = "Fingerprints"))]
@@ -707,11 +704,6 @@ pub(crate) async fn gif(context: &Context) -> String {
translated(context, StockMessage::Gif).await
}
/// Stock string: `End-to-end encryption available.`.
pub(crate) async fn e2e_available(context: &Context) -> String {
translated(context, StockMessage::E2eAvailable).await
}
/// Stock string: `No encryption.`.
pub(crate) async fn encr_none(context: &Context) -> String {
translated(context, StockMessage::EncrNone).await
+10 -1
View File
@@ -44,7 +44,7 @@ use crate::stock_str::StockStrings;
use crate::tools::time;
/// The number of info messages added to new e2ee chats.
/// Currently this is "End-to-end encryption available", string `E2eAvailable`.
/// Currently this is "Messages are end-to-end encrypted.", string `ChatProtectionEnabled`.
pub const E2EE_INFO_MSGS: usize = 1;
#[allow(non_upper_case_globals)]
@@ -1492,6 +1492,15 @@ impl EventTracker {
pub fn clear_events(&self) {
while let Ok(_ev) = self.try_recv() {}
}
/// Takes all items from event queue and returns them.
pub fn take_events(&self) -> Vec<Event> {
let mut events = Vec::new();
while let Ok(event) = self.try_recv() {
events.push(event);
}
events
}
}
/// Gets a specific message from a chat and asserts that the chat has a specific length.