Compare commits

..

112 Commits

Author SHA1 Message Date
link2xt
2ec7c80c24 feat: display TLS certificate checks configuration in connectivity view 2025-10-13 19:51:44 +00:00
link2xt
8dd7c42f69 chore(release): prepare for 2.20.0 2025-10-13 11:34:44 +00:00
B. Petersen
b542eeecc0 fix: accept unknown viewtype in delete-old-messages loop 2025-10-13 13:29:57 +02:00
B. Petersen
bee8295daa fix: accept unknown viewtype in ephemeral loop 2025-10-13 13:29:57 +02:00
link2xt
ab9fd3d5ed chore(release): prepare for 2.19.0 2025-10-12 17:33:43 +00:00
link2xt
cc54a3feda fix: do not try to process calls from partial messages
Any control information from the message
should only be downloaded when the message
is fully downloaded to avoid processing it twice.
Besides, "partial" messages may actually be full messages
with an error that are only processed as partial
to add a message bubble allowing to download the message later.
2025-10-12 17:20:21 +00:00
link2xt
94984f35ec fix: do not fail to receive call accepted/ended messages referring to non-call Message-ID
In-Reply-To may refer to non-call message
as we do not control the sender.
It may also happen that call message
was received by older version and processed
as text, in which case correct In-Reply-To
appears to be referring to the text message.
2025-10-12 17:20:21 +00:00
iequidoo
0e47e89d63 fix: Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that (#7296)
Emitting an `AccountsItemChanged` event is needed for UIs to know when `Contact::get_color()` starts
returning the true color for `SELF`. Before, an address-based color was returned for a new account
which was changing to a fingerprint-based color e.g. on app restart. Now the self-color is our
favorite gray until own keypair is generated or imported e.g. via ASM.
2025-10-12 14:17:24 -03:00
link2xt
2d7dc7a1be fix: do not fail to fully download previously trashed messages 2025-10-12 13:12:00 +00:00
iequidoo
4d76a5b599 refactor: set_chat_profile_image(): Remove !chat.is_mailing_list() check 2025-10-12 01:41:31 -03:00
link2xt
87035ff744 feat: slightly increase saturation of colors
It really depends on the screen,
but on Android phones with higher DPI
than laptop screen the contrast
looks a bit too low.

It should not be increased too high
because of clipping.
2025-10-09 20:28:21 +00:00
iequidoo
e0d123f732 chore(cargo): bump quick-xml from 0.37.5 to 0.38.3 2025-10-09 14:03:56 -03:00
link2xt
8eddcfc9d2 ci: update to Python 3.14
I have not updated Python interpreters for legacy bindings
in scripts/run_all.sh because I have not checked
if manylinux images already exist and work for building wheels.
Legacy Python bindings are deprecated
and we don't publish new releases for them.
2025-10-09 15:55:59 +00:00
Simon Laux
af58b86b60 refactor: Use variables directly in formatted strings (#7284)
made with `cargo clippy --all --fix` then manually reviewed to ensure
this was the only thing that changed.
2025-10-09 15:26:59 +00:00
Simon Laux
00ae7ce33c add nodejs to nix dev env (#7283)
it is required to build jsonrpc client npm package and for the stdio
server npm package
2025-10-09 14:27:12 +00:00
link2xt
0bc9fe841a chore(release): prepare for 2.18.0 2025-10-08 02:01:02 +00:00
iequidoo
e37920ed4e feat: No implicit member changes from old Delta Chat clients (#7220)
Old Delta Chat clients don't provide timestamps for added and removed members in messages, so at
least implicit member changes may be ignored as it's not clear if they are newer than explicit
member changes from modern clients. Lost messages aren't so frequent anyway, and overall
compatibility with old versions may be limited already.
2025-10-07 20:27:14 -03:00
iequidoo
6a7466df93 fix: Only omit group changes messages if SELF is really added (#7220)
If a self-addition message is received, but we're already in the group, there must be no hidden
member changes.
2025-10-07 20:27:14 -03:00
link2xt
1bb966e5a8 ci(nix): switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action
Determinate Systems GitHub action
installs Nix version from Determinate Systems
and Determinate Nix Installer has
dropped support for installing upstream Nix:
https://determinate.systems/blog/installer-dropping-upstream/

This commit switches to upstream Nix
to avoid accidentally depending on any features
of Determinate Nix.
2025-10-07 15:56:02 +00:00
link2xt
34e631395f ci(nix): run the workflow when workflow file changes 2025-10-07 15:56:02 +00:00
link2xt
080ddde68d refactor: assert that Iroh node addresses have home relay URL
With newer Iroh it is possible to obtain
own node address before home relay is selected
and accidentally send own address without relay URL.

It took me some time to debug why Iroh 0.92.0
did not work with iroh-relay 0.92.0
so I'm adding these assertions even
while we still use Iroh 0.35.0.
2025-10-07 15:53:37 +00:00
link2xt
209a8026fb fix: do not fail to load messages with unknown viewtype 2025-10-06 15:29:24 +00:00
link2xt
23bfa4fc43 api!: remove APIs for video chat invitations 2025-10-05 12:19:10 +00:00
link2xt
58d40c118c chore(release): prepare for 2.17.0 2025-10-04 00:41:56 +00:00
link2xt
9d39769445 refactor: return the reason when failing to place calls 2025-10-04 00:23:46 +00:00
link2xt
bfc08abe88 fix: forward calls as text messages 2025-10-04 00:23:46 +00:00
link2xt
6a7b097273 fix: lowercase "call" in "Missed call" and similar strings
This string is used in summaries
and message bubbles and is typically not capitalized,
unlike menu items.
2025-10-03 15:06:29 +00:00
B. Petersen
8f2390ac99 feat: add new strings to ffi 2025-10-03 15:19:02 +02:00
iequidoo
481f5cae22 fix: Prefetch messages in limited batches (#6915)
I have logs from a user where messages are prefetched for long minutes, and while it's not a problem
on its own, we can't rely that the connection overlives such a period, so make
`fetch_new_messages()` prefetch (and then actually download) messages in batches of 500 messages.
2025-10-02 16:33:10 -03:00
iequidoo
b9068b95b8 feat(ffi): Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define 2025-10-02 16:18:57 -03:00
link2xt
df2c35b551 ci: fix CI checking Nix formatting
`nix fmt` by itself tries to read stdin,
not `flake.nix`.
2025-10-02 18:39:16 +00:00
link2xt
3cd4152a3c api!: remove deprecated verified_one_on_one_chats config 2025-10-02 18:35:12 +00:00
WofWca
2534510f0b docs: add docs for JS BaseDeltaChat
Regarding the `@deprecated` addition:
it's not clear to me why the `listAccounts` function exists,
why `getAllAccounts` gets special treatment.
It has been there ever since the introduction of JSON-RPC.
Maybe it's because of the existence of `getContextEvents`,
which has to do with accounts.
But hopefully the new doc on `getContextEvents`
compensates for this.
2025-10-02 16:48:56 +00:00
bjoern
3f8aa4635e docs: clarify CALL events (#7188)
this PR clarifies some events, also updating to recent changes.

see changed code for details

---------

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-10-02 16:48:16 +00:00
B. Petersen
ada59e8205 docs: comment about outdated timespan 2025-10-02 16:45:06 +00:00
Hocuri
9ec0332483 feat: Add strings 'You left the channel.' and 'Scan to join Channel' (#7266)
Close https://github.com/chatmail/core/issues/7233

Part of https://github.com/chatmail/core/issues/6884
2025-10-02 14:57:24 +00:00
link2xt
d509b0cf5c ci: require that Cargo.lock is up to date
Versions in the lockfile should
be compatible with MSRV
and not automatically downgraded in CI.
2025-10-02 12:35:25 +00:00
link2xt
4d624d8c3a Revert "chore(cargo): bump sdp from 0.8.0 to 0.10.0"
This reverts commit eae1ba258a.
2025-10-02 12:35:25 +00:00
link2xt
9f0ba4b9c2 feat: better summary for calls 2025-10-02 11:19:48 +00:00
link2xt
a930ae27be api: stock strings for calls 2025-10-02 11:19:48 +00:00
link2xt
38e4919be1 api!: consistent spelling of "canceled" with a single "l"
This is how it is spelled in iOS UI and Android APIs.
2025-10-02 11:19:48 +00:00
dependabot[bot]
a668047f75 Merge pull request #7259 from chatmail/dependabot/cargo/sdp-0.10.0 2025-10-02 09:18:16 +00:00
link2xt
c2ea2cda4c feat: make text/calendar alternative available as an attachment 2025-10-02 09:15:25 +00:00
iequidoo
f3c3a2c301 Revert "chore(cargo): bump quick-xml from 0.37.5 to 0.38.3"
This reverts commit 58e1fa5c36.
2025-10-02 05:34:05 -03:00
dependabot[bot]
0da7e587a7 Merge pull request #7247 from chatmail/dependabot/cargo/image-0.25.8 2025-10-02 07:32:30 +00:00
dependabot[bot]
e6e686aaf4 Merge pull request #7255 from chatmail/dependabot/cargo/libc-0.2.176 2025-10-02 07:24:24 +00:00
dependabot[bot]
58e1fa5c36 chore(cargo): bump quick-xml from 0.37.5 to 0.38.3
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.37.5 to 0.38.3.
- [Release notes](https://github.com/tafia/quick-xml/releases)
- [Changelog](https://github.com/tafia/quick-xml/blob/master/Changelog.md)
- [Commits](https://github.com/tafia/quick-xml/compare/v0.37.5...v0.38.3)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-version: 0.38.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 04:19:04 -03:00
dependabot[bot]
42549526c7 chore(cargo): bump tempfile from 3.21.0 to 3.23.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.21.0 to 3.23.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.21.0...v3.23.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-version: 3.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 04:10:02 -03:00
dependabot[bot]
9fe1c8fe80 Merge pull request #7254 from chatmail/dependabot/cargo/toml-0.9.7 2025-10-02 07:08:01 +00:00
dependabot[bot]
b8dbcb3dbd chore(cargo): bump hyper-util from 0.1.16 to 0.1.17
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.16 to 0.1.17.
- [Release notes](https://github.com/hyperium/hyper-util/releases)
- [Changelog](https://github.com/hyperium/hyper-util/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper-util/compare/v0.1.16...v0.1.17)

---
updated-dependencies:
- dependency-name: hyper-util
  dependency-version: 0.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:59:30 -03:00
dependabot[bot]
7c5675670a chore(cargo): bump chrono from 0.4.41 to 0.4.42
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.41 to 0.4.42.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.41...v0.4.42)

---
updated-dependencies:
- dependency-name: chrono
  dependency-version: 0.4.42
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:57:23 -03:00
dependabot[bot]
291945a4fd chore(cargo): bump log from 0.4.27 to 0.4.28
Bumps [log](https://github.com/rust-lang/log) from 0.4.27 to 0.4.28.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.27...0.4.28)

---
updated-dependencies:
- dependency-name: log
  dependency-version: 0.4.28
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 03:45:52 -03:00
dependabot[bot]
439e8827bd Merge pull request #7243 from chatmail/dependabot/cargo/quote-1.0.41 2025-10-02 06:45:16 +00:00
link2xt
a745cf78ee test: test reception of multipart/alternative with text/calendar
In messages with text/plain, text/html and text/calendar parts
within a multipart/alternative, text/calendar part is currently ignored,
text/plain is displayed and HTML is available via HTML API.
2025-10-02 02:20:17 +00:00
link2xt
af69756df0 fix: prefer last part in multipart/alternative
This is recommended by RFC 2046
and does not require a separate loop
looking for multipart subpart.
2025-10-02 02:20:17 +00:00
dependabot[bot]
46c42ab6e4 chore(cargo): bump quote from 1.0.40 to 1.0.41
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.41)

---
updated-dependencies:
- dependency-name: quote
  dependency-version: 1.0.41
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-02 01:15:56 +00:00
dependabot[bot]
33a127187b chore(cargo): bump thiserror from 2.0.16 to 2.0.17
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.16 to 2.0.17.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.16...2.0.17)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-version: 2.0.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 22:13:43 -03:00
dependabot[bot]
24ddbdd251 chore(cargo): bump proptest from 1.7.0 to 1.8.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/proptest-rs/proptest/releases)
- [Changelog](https://github.com/proptest-rs/proptest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/proptest-rs/proptest/compare/v1.7.0...v1.8.0)

---
updated-dependencies:
- dependency-name: proptest
  dependency-version: 1.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:58:39 -03:00
dependabot[bot]
0122a98eea chore(cargo): bump anyhow from 1.0.99 to 1.0.100
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.99 to 1.0.100.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.99...1.0.100)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-version: 1.0.100
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:53:53 -03:00
dependabot[bot]
406545c1f1 chore(cargo): bump uuid from 1.18.0 to 1.18.1
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.18.0...v1.18.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:53:23 -03:00
dependabot[bot]
a1b593027b Merge pull request #7241 from chatmail/dependabot/cargo/serde_json-1.0.145 2025-10-02 00:50:51 +00:00
dependabot[bot]
eae1ba258a chore(cargo): bump sdp from 0.8.0 to 0.10.0
Bumps [sdp](https://github.com/webrtc-rs/webrtc) from 0.8.0 to 0.10.0.
- [Release notes](https://github.com/webrtc-rs/webrtc/releases)
- [Commits](https://github.com/webrtc-rs/webrtc/compare/v0.8.0...v0.10.0)

---
updated-dependencies:
- dependency-name: sdp
  dependency-version: 0.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:09:10 +00:00
dependabot[bot]
d2db30eabc chore(cargo): bump libc from 0.2.175 to 0.2.176
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.175 to 0.2.176.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.176/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.175...0.2.176)

---
updated-dependencies:
- dependency-name: libc
  dependency-version: 0.2.176
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:07:13 +00:00
dependabot[bot]
9fb7c52217 chore(cargo): bump toml from 0.9.5 to 0.9.7
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.5 to 0.9.7.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.5...toml-v0.9.7)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:06:53 +00:00
dependabot[bot]
6cab1786d3 chore(cargo): bump image from 0.25.6 to 0.25.8
Bumps [image](https://github.com/image-rs/image) from 0.25.6 to 0.25.8.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.6...v0.25.8)

---
updated-dependencies:
- dependency-name: image
  dependency-version: 0.25.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:04:06 +00:00
dependabot[bot]
362328167c chore(cargo): bump serde_json from 1.0.143 to 1.0.145
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.143 to 1.0.145.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.143...v1.0.145)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-version: 1.0.145
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 21:01:39 +00:00
link2xt
570a9993f7 chore(release): prepare for 2.16.0 2025-10-01 13:34:45 +00:00
Hocuri
5adc68cf0b fix: Don't enable legacy decryption options
Follow-up to https://github.com/chatmail/core/pull/7226/; I believe that
this was enabled by accident? Previously, we had `allow_legacy: false`.
2025-10-01 12:29:26 +00:00
WofWca
1b1757ebf2 api: add chat_id to all call events (#7216) 2025-10-01 10:42:33 +00:00
link2xt
d8950fb7d1 fix: use nine.testrun.org as a default STUN server 2025-09-30 22:45:35 +00:00
iequidoo
ba2e573c23 fix: Allow Exif for stickers, don't recode them because of that (#6447)
Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming from UIs shouldn't
contain sensitive Exif info.
2025-09-30 01:09:14 -03:00
iequidoo
31391fc074 feat: Set dimensions for outgoing Sticker messages
For incoming `Sticker` messages, dimensions are already set, so make the code consistent.
2025-09-30 01:09:14 -03:00
iequidoo
f94b2c3794 feat: Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead (#7196)
Ignoring `receive_imf_inner()` errors, i.e. silently skipping messages on failures, leads to bugs
never fixed. As for temporary I/O errors, ignoring them leads to lost messages, in this case it's
better to bubble up the error and get the IMAP loop stuck. However if there's some logic error, it's
better to show it to the user so that it's more likely reported, and continue receiving messages. To
distinguish these cases, on error, try adding the message as partially downloaded with the error set
to `msgs.error`, this way the user also can retry downloading the message to finally see it if the
problem is fixed.
2025-09-30 00:54:24 -03:00
iequidoo
eb0a5fed8e fix: receive_imf: Report replaced message id in MsgsChanged if chat is the same 2025-09-30 00:54:24 -03:00
link2xt
eaa47d175f feat: get ICE servers from IMAP METADATA 2025-09-28 02:06:43 +00:00
link2xt
e968000a89 api(jsonrpc): add has_video attribute to call info 2025-09-27 18:24:39 +00:00
link2xt
1ba448fe19 api(jsonrpc): add state attribute to call info
Existing boolean attributes removed.
2025-09-26 17:34:54 +00:00
link2xt
a5c82425f4 fix: do not consider the call stale if it is not sent out yet 2025-09-26 17:34:54 +00:00
link2xt
1bd31f6b8e api: add CallState 2025-09-26 17:34:54 +00:00
link2xt
c0ea0e52b3 test: do not match call ID from second alice with first alice event 2025-09-26 17:34:54 +00:00
link2xt
e6a3daacb3 test: update timestamp_sent in pop_sent_msg_opt()
Otherwise "sent" messages in tests have 0 timestamp.
2025-09-26 17:34:54 +00:00
link2xt
09dabda4a3 build: update rPGP from 0.16.0 to 0.17.0 2025-09-26 16:54:49 +00:00
link2xt
f523d912af ci: install rustfmt before checking provider database
Apparently it is not installed by default anymore.
2025-09-26 00:02:47 +00:00
missytake
90b0ca79ea api(deltachat-rpc-client): add Chat.resend_messages() 2025-09-25 08:19:14 +02:00
link2xt
a506e2d5a2 api: add chat ID to SecureJoinInviterProgress 2025-09-23 23:23:21 +00:00
link2xt
4c66518a68 docs: SecurejoinInviterProgress never returns an error 2025-09-23 23:23:21 +00:00
iequidoo
42b4b83f8e fix: Don't add "member removed" messages from nonmembers (#7207)
Such messages are actually ignored. Showing them to users creates confusion.
`ChatGroupMemberAdded` branch is modified for code consistency.
2025-09-23 05:57:57 -03:00
iequidoo
7477ebbdd7 fix: Take the last valid Autocrypt header (#7167)
DKIM-Signatures apply to the last headers, so start from the last header and take a valid one,
i.e. skip headers having unknown critical attributes, etc. Though this means that Autocrypt header
must be "oversigned" to guarantee that a not DKIM-signed header isn't taken, still start from the
last header for consistency with processing other headers. This isn't a security issue anyway.
2025-09-22 15:38:32 -03:00
link2xt
738dc5ce19 api: add call_info() JSON-RPC API 2025-09-20 18:47:47 +00:00
WofWca
3680467e14 fix: don't init Iroh on channel leave (#7210)
Some Delta Chat clients (Desktop, for example)
do `leave_webxdc_realtime`
regardless of whether we've ever joined a realtime channel
in the first place. Such as when closing a WebXDC window.
This might result in unexpected and suspicious firewall warnings.

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-09-20 04:01:09 +00:00
link2xt
c5ada9b203 api: add JSON-RPC API to get ICE servers
Currently with a hardcoded TURN server
so it can be used in the UIs.
2025-09-18 18:35:40 +00:00
link2xt
3d2805bc78 ci: update Rust to 1.90.0 2025-09-18 15:49:59 +00:00
iequidoo
2dde286d68 refactor: Remove unused FolderMeaning::Drafts 2025-09-18 00:59:18 -03:00
iequidoo
2260156c40 feat: Don't fetch messages from unknown folders (#7190)
Actually this leads to fetching messages only from watched folders and Spam. Motivation:
- At least Gmail has virtual folders which aren't correctly detected as such, e.g. "Sent".
- At least Gmail has many virtual folders and scanning all of them takes significant time, 5-6 secs
  in median for me. This slows down receiving new messages and consumes battery.
- Delta Chat shouldn't fetch messages from folders potentially created by other apps for their own
  purposes. NB: All compatible Delta Chat forks should use the "DeltaChat" folder as mvbox.
- Fetching from folders that aren't watched, e.g. from "Sent", may lead to message ordering issues.
2025-09-18 00:59:18 -03:00
link2xt
129e970727 api: add has_video attribute to incoming call events
This allows UI to show if incoming call is a video or audio call
and disable camera by default for audio calls.
2025-09-17 19:34:14 +00:00
link2xt
66271db8c0 test: rename test_udpate_call_text into test_update_call_text 2025-09-17 19:34:14 +00:00
WofWca
09d33e62bd refactor: remove unused prop (TS, BaseDeltaChat)
Apparently it has been unused ever since the introduction
of JSON-RPC, 0887acf1bf
(https://github.com/chatmail/core/pull/3463).
2025-09-17 17:40:15 +00:00
WofWca
bf3dfa4ab6 docs: add more get_next_event docs 2025-09-17 15:47:50 +00:00
link2xt
40b866117e fix: ignore vc-/vg- prefix for SecurejoinInviterProgress
Inviter progress is for group if we added Bob to the group,
not if Bob sent us vg-request-with-auth.
2025-09-16 18:00:15 +00:00
link2xt
cb5f9f3051 api!: get rid of inviter progress other than 0 and 1000
UIs don't display a dialog with a progress bar anyway.
2025-09-16 18:00:15 +00:00
link2xt
80f97cf9bd fix: create 1:1 chat only if auth token is for setup contact
Previously we trusted Bob to send the correct vc- or vg- prefix.
2025-09-16 18:00:15 +00:00
link2xt
6d860f7eae chore(release): prepare for 2.15.0 2025-09-15 15:28:41 +00:00
link2xt
545643b610 build: remove unused quoted_printable dependency 2025-09-15 11:52:31 +00:00
l
7ee6f2c36a api: add JSON-RPC API for calls (#7194) 2025-09-13 02:56:51 +00:00
link2xt
5d9b887624 chore(release): prepare for 2.14.0 2025-09-12 06:06:36 +00:00
link2xt
12c0e298f5 test: test sending SDP offer and answer with newlines 2025-09-12 02:37:57 +00:00
link2xt
f9aec7af0d fix: B-encode SDP offer and answer sent in headers
SDP offer and answer contain newlines.
Without the fix these newlines are not encoded at all
and break the header into multiple headers
or even prevent parsing of the following headers.
2025-09-12 02:37:57 +00:00
link2xt
b181d78dd5 fix(param): split params only on \n
str.lines() splits on both \n and \r\n
We use \n as a field separator,
so \r\n should not separate the fields.
2025-09-12 02:37:57 +00:00
iequidoo
b9ff40c6b5 test: Message is OutFailed if all keys are missing (#6849)
Follow-up to 143ba6d5e7 before which a message remained in
`OutPending` indefinitely in such a case.
2025-09-09 09:48:12 -03:00
iequidoo
0684810d38 refactor: prepare_msg_raw(): don't return MsgId
The function takes `&mut Message` and updates its `id` and `chat_id` before return.
2025-09-09 09:48:12 -03:00
Hocuri
1cc7ce6e27 api: Put the chattype into the SecurejoinInviterProgress event (#7181)
Quoting @adbenitez:

> I have been using the SecurejoinInviterProgress event to show a
welcome message when user scan the QR/link of the bot (== starts a chat
with the bot)

> but this have a big problem: in that event all you know is that a
contact completed the secure-join process, you don't know if it was via
certain 1:1 invite link or a group invitation, then a group-invite bot
would send you a help message in 1:1 every time you join a group with it

Since it's easy enough to add this information to the
SecurejoinInviterProgress event, I wrote a PR to do so.
2025-09-09 08:17:53 +00:00
link2xt
82bc1bf0b1 refactor: use recv_msg_trash() instead of recv_msg_opt() 2025-09-09 06:08:15 +00:00
105 changed files with 2771 additions and 1409 deletions

View File

@@ -20,7 +20,7 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.89.0
RUST_VERSION: 1.90.0
# Minimum Supported Rust Version
MSRV: 1.85.0
@@ -71,6 +71,8 @@ jobs:
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
- name: Check provider database
run: scripts/update-provider-database.sh
@@ -137,12 +139,12 @@ jobs:
- name: Tests
env:
RUST_BACKTRACE: 1
run: cargo nextest run --workspace
run: cargo nextest run --workspace --locked
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --doc
run: cargo test --workspace --locked --doc
- name: Test cargo vendor
run: cargo vendor
@@ -226,9 +228,9 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.13
python: 3.14
- os: macos-latest
python: 3.13
python: 3.14
# PyPy tests
- os: ubuntu-latest
@@ -279,11 +281,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.13
python: 3.14
- os: macos-latest
python: 3.13
python: 3.14
- os: windows-latest
python: 3.13
python: 3.14
# PyPy tests
- os: ubuntu-latest

View File

@@ -34,7 +34,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -58,7 +58,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -109,7 +109,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -136,7 +136,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v5

View File

@@ -5,10 +5,12 @@ on:
paths:
- flake.nix
- flake.lock
- .github/workflows/nix.yml
push:
paths:
- flake.nix
- flake.lock
- .github/workflows/nix.yml
branches:
- main
@@ -23,11 +25,8 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- run: nix fmt
# Check that formatting does not change anything.
- run: git diff --exit-code
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix fmt flake.nix -- --check
build:
name: nix build
@@ -85,7 +84,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -105,5 +104,5 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- run: nix build .#${{ matrix.installable }}

View File

@@ -18,7 +18,7 @@ jobs:
with:
show-progress: false
persist-credentials: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build
run: nix build .#deltachat-repl-win64
- name: Upload binary

View File

@@ -36,7 +36,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
@@ -55,7 +55,7 @@ jobs:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat

View File

@@ -1,5 +1,193 @@
# Changelog
## [2.20.0] - 2025-10-13
This release fixes a bug that resulted in ephemeral loop getting stuck in infinite loop
when trying to delete a message with unknown viewtype.
### Fixes
- Accept unknown viewtype in ephemeral loop.
- Accept unknown viewtype in delete-old-messages loop.
## [2.19.0] - 2025-10-12
### Features / Changes
- Slightly increase saturation of colors.
### Fixes
- Do not fail to receive call accepted/ended messages referring to non-call Message-ID.
- Do not fail to fully download previously trashed messages.
- Emit AccountsItemChanged when own key is generated/imported, use gray self-color until that ([#7296](https://github.com/chatmail/core/pull/7296)).
- Do not try to process calls from partial messages.
### CI
- Update to Python 3.14.
### Refactor
- Use variables directly in formatted strings ([#7284](https://github.com/chatmail/core/pull/7284)).
- Set_chat_profile_image(): Remove !chat.is_mailing_list() check.
### Miscellaneous Tasks
- cargo: Bump quick-xml from 0.37.5 to 0.38.3.
- Add nodejs to nix dev env ([#7283](https://github.com/chatmail/core/pull/7283))
## [2.18.0] - 2025-10-08
### API-Changes
- [**breaking**] Remove APIs for video chat invitations.
### CI
- nix: Run the workflow when workflow file changes.
- nix: Switch from DeterminateSystems/nix-installer-action to cachix/install-nix-action.
### Features / Changes
- No implicit member changes from old Delta Chat clients ([#7220](https://github.com/chatmail/core/pull/7220)).
### Fixes
- Do not fail to load messages with unknown viewtype.
- Only omit group changes messages if SELF is really added ([#7220](https://github.com/chatmail/core/pull/7220)).
### Refactor
- Assert that Iroh node addresses have home relay URL.
## [2.17.0] - 2025-10-04
### API-Changes
- [**breaking**] Remove deprecated verified_one_on_one_chats config.
### CI
- Require that Cargo.lock is up to date.
- Fix CI checking Nix formatting.
### Documentation
- Comment about outdated timespan.
- Clarify CALL events ([#7188](https://github.com/chatmail/core/pull/7188)).
- Add docs for JS `BaseDeltaChat`.
### Features / Changes
- Make `text/calendar` alternative available as an attachment.
- Better summary for calls.
- Add strings 'You left the channel.' and 'Scan to join Channel' ([#7266](https://github.com/chatmail/core/pull/7266)).
- Stock strings for calls.
- ffi: Add DC_STR_CANT_DECRYPT_OUTGOING_MSGS define.
### Fixes
- Prefer last part in `multipart/alternative`.
- Prefetch messages in limited batches ([#6915](https://github.com/chatmail/core/pull/6915)).
- Forward calls as text messages.
- Consistent spelling of "canceled" with a single "l".
- Lowercase "call" in "Missed call" and similar strings.
### Refactor
- Return the reason when failing to place calls.
### Tests
- Test reception of `multipart/alternative` with `text/calendar`.
## [2.16.0] - 2025-10-01
### API-Changes
- [**breaking**] Get rid of inviter progress other than 0 and 1000.
- Add has_video attribute to incoming call events.
- Add JSON-RPC API to get ICE servers.
- Add call_info() JSON-RPC API.
- Add chat ID to SecureJoinInviterProgress.
- deltachat-rpc-client: Add Chat.resend_messages().
- Add `chat_id` to all call events ([#7216](https://github.com/chatmail/core/pull/7216)).
### Build system
- Update rPGP from 0.16.0 to 0.17.0.
### CI
- Update Rust to 1.90.0.
- Install rustfmt before checking provider database.
### Documentation
- Add more `get_next_event` docs.
- SecurejoinInviterProgress never returns an error.
### Features / Changes
- Don't fetch messages from unknown folders ([#7190](https://github.com/chatmail/core/pull/7190)).
- Get ICE servers from IMAP METADATA.
- Don't ignore receive_imf_inner() errors, try adding partially downloaded message instead ([#7196](https://github.com/chatmail/core/pull/7196)).
- Set dimensions for outgoing Sticker messages.
### Fixes
- Create 1:1 chat only if auth token is for setup contact.
- Ignore vc-/vg- prefix for SecurejoinInviterProgress.
- Don't init Iroh on channel leave ([#7210](https://github.com/chatmail/core/pull/7210)).
- Take the last valid Autocrypt header ([#7167](https://github.com/chatmail/core/pull/7167)).
- Don't add "member removed" messages from nonmembers ([#7207](https://github.com/chatmail/core/pull/7207)).
- Do not consider the call stale if it is not sent out yet.
- Receive_imf: Report replaced message id in `MsgsChanged` if chat is the same.
- Allow Exif for stickers, don't recode them because of that ([#6447](https://github.com/chatmail/core/pull/6447)).
### Refactor
- Remove unused prop (TS, `BaseDeltaChat`).
- Remove unused FolderMeaning::Drafts.
### Tests
- Rename test_udpate_call_text into test_update_call_text.
- Update timestamp_sent in pop_sent_msg_opt().
- Do not match call ID from second alice with first alice event.
## [2.15.0] - 2025-09-15
### API-Changes
- Add JSON-RPC API for calls ([#7194](https://github.com/chatmail/core/pull/7194)).
### Build system
- Remove unused `quoted_printable` dependency.
## [2.14.0] - 2025-09-12
### API-Changes
- Put the chattype into the SecurejoinInviterProgress event ([#7181](https://github.com/chatmail/core/pull/7181)).
### Fixes
- param: Split params only on \n.
- B-encode SDP offer and answer sent in headers.
### Refactor
- Use recv_msg_trash() instead of recv_msg_opt().
- Prepare_msg_raw(): don't return MsgId.
### Tests
- Message is OutFailed if all keys are missing ([#6849](https://github.com/chatmail/core/pull/6849)).
- Test sending SDP offer and answer with newlines.
## [2.13.0] - 2025-09-09
### API-Changes
@@ -1678,7 +1866,7 @@ This reverts commit 6f22ce2722b51773d7fbb0d89e4764f963cafd91..
### Fixes
- Reset quota on configured address change ([#5908](https://github.com/chatmail/core/pull/5908)).
- Do not emit progress 1000 when configuration is cancelled.
- Do not emit progress 1000 when configuration is canceled.
- Assume file extensions are 32 chars max and don't contain whitespace ([#5338](https://github.com/chatmail/core/pull/5338)).
- Re-add tokens.foreign_id column ([#6038](https://github.com/chatmail/core/pull/6038)).
@@ -4126,7 +4314,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/chatmail/core/pull/4390)).
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/chatmail/core/pull/4377)).
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/chatmail/core/pull/4391)).
- Delete `smtp` rows when message sending is canceled ([#4391](https://github.com/chatmail/core/pull/4391)).
### Refactor
@@ -4137,7 +4325,7 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
### Fixes
- Fetch at most 100 existing messages even if EXISTS was not received.
- Delete `smtp` rows when message sending is cancelled.
- Delete `smtp` rows when message sending is canceled.
### Changes
@@ -4224,14 +4412,14 @@ Bugfix release attempting to fix the [iOS build error](https://github.com/chatma
## [1.112.3] - 2023-03-30
### Fixes
- `transfer::get_backup` now frees ongoing process when cancelled. #4249
- `transfer::get_backup` now frees ongoing process when canceled. #4249
## [1.112.2] - 2023-03-30
### Changes
- Update iroh, remove `default-net` from `[patch.crates-io]` section.
- transfer backup: Connect to multiple provider addresses concurrently. This should speed up connection time significantly on the getter side. #4240
- Make sure BackupProvider is cancelled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
- Make sure BackupProvider is canceled on drop (or `dc_backup_provider_unref`). The BackupProvider will now always finish with an IMEX event of 1000 or 0, previously it would sometimes finished with 1000 (success) when it really was 0 (failure). #4242
### Fixes
- Do not return media from trashed messages in the "All media" view. #4247
@@ -6725,3 +6913,11 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.10.0]: https://github.com/chatmail/core/compare/v2.9.0..v2.10.0
[2.11.0]: https://github.com/chatmail/core/compare/v2.10.0..v2.11.0
[2.12.0]: https://github.com/chatmail/core/compare/v2.11.0..v2.12.0
[2.13.0]: https://github.com/chatmail/core/compare/v2.12.0..v2.13.0
[2.14.0]: https://github.com/chatmail/core/compare/v2.13.0..v2.14.0
[2.15.0]: https://github.com/chatmail/core/compare/v2.14.0..v2.15.0
[2.16.0]: https://github.com/chatmail/core/compare/v2.15.0..v2.16.0
[2.17.0]: https://github.com/chatmail/core/compare/v2.16.0..v2.17.0
[2.18.0]: https://github.com/chatmail/core/compare/v2.17.0..v2.18.0
[2.19.0]: https://github.com/chatmail/core/compare/v2.18.0..v2.19.0
[2.20.0]: https://github.com/chatmail/core/compare/v2.19.0..v2.20.0

View File

@@ -44,7 +44,7 @@ If you want to contribute a code, follow this guide.
The following prefix types are used:
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"

252
Cargo.lock generated
View File

@@ -104,12 +104,6 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -133,9 +127,9 @@ checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anyhow"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
dependencies = [
"backtrace",
]
@@ -324,7 +318,7 @@ dependencies = [
"log",
"nom 8.0.0",
"pin-project",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
]
@@ -440,23 +434,23 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bitfields"
version = "0.12.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d84268bbf9b487d31fe4b849edbefcd3911422d7a07de855a2da1f70ab3d1c"
checksum = "dcdbce6688e3ab66aff2ab413b762ccde9f37990e27bba0bb38a4b2ad1b5d877"
dependencies = [
"bitfields-impl",
]
[[package]]
name = "bitfields-impl"
version = "0.9.4"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c93edde7bb4416c35c85048e34f78999dcb47d199bde3b1d79286156f3e2fb"
checksum = "57413e4b276d883b77fb368b7b33ae6a5eb97692852d49a5394d4f72ba961827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@@ -823,11 +817,10 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.41"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"serde",
@@ -1296,7 +1289,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "2.13.0"
version = "2.20.0"
dependencies = [
"anyhow",
"async-broadcast",
@@ -1349,7 +1342,6 @@ dependencies = [
"proptest",
"qrcodegen",
"quick-xml",
"quoted_printable",
"rand 0.8.5",
"ratelimit",
"regex",
@@ -1357,6 +1349,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"sanitize-filename",
"sdp",
"serde",
"serde_json",
"serde_urlencoded",
@@ -1370,7 +1363,7 @@ dependencies = [
"tempfile",
"testdir",
"textwrap",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tokio-io-timeout",
"tokio-rustls",
@@ -1406,7 +1399,7 @@ dependencies = [
[[package]]
name = "deltachat-jsonrpc"
version = "2.13.0"
version = "2.20.0"
dependencies = [
"anyhow",
"async-channel 2.5.0",
@@ -1428,7 +1421,7 @@ dependencies = [
[[package]]
name = "deltachat-repl"
version = "2.13.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1444,7 +1437,7 @@ dependencies = [
[[package]]
name = "deltachat-rpc-server"
version = "2.13.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1473,7 +1466,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "2.13.0"
version = "2.20.0"
dependencies = [
"anyhow",
"deltachat",
@@ -1483,7 +1476,7 @@ dependencies = [
"num-traits",
"rand 0.8.5",
"serde_json",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"yerpc",
]
@@ -2430,7 +2423,7 @@ dependencies = [
"once_cell",
"rand 0.9.0",
"ring",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tinyvec",
"tokio",
"tracing",
@@ -2453,7 +2446,7 @@ dependencies = [
"rand 0.9.0",
"resolv-conf",
"smallvec",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
]
@@ -2641,9 +2634,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
dependencies = [
"bytes",
"futures-channel",
@@ -2860,15 +2853,16 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.6"
version = "0.25.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
dependencies = [
"bytemuck",
"byteorder-lite",
"color_quant",
"gif",
"image-webp",
"moxcms",
"num-traits",
"png",
"zune-core",
@@ -2896,9 +2890,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.10.0"
version = "2.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
@@ -2992,7 +2986,7 @@ dependencies = [
"strum 0.26.2",
"stun-rs",
"surge-ping",
"thiserror 2.0.16",
"thiserror 2.0.17",
"time",
"tokio",
"tokio-stream",
@@ -3017,7 +3011,7 @@ dependencies = [
"ed25519-dalek",
"rand_core 0.6.4",
"serde",
"thiserror 2.0.16",
"thiserror 2.0.17",
"url",
]
@@ -3059,7 +3053,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serde-error",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tokio-util",
"tracing",
@@ -3104,7 +3098,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
"web-time",
@@ -3124,7 +3118,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
@@ -3179,7 +3173,7 @@ dependencies = [
"sha1",
"strum 0.26.2",
"stun-rs",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3259,9 +3253,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.176"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
[[package]]
name = "libm"
@@ -3336,9 +3330,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "loom"
@@ -3474,6 +3468,16 @@ dependencies = [
"uuid",
]
[[package]]
name = "moxcms"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd32fa8935aeadb8a8a6b6b351e40225570a37c43de67690383d87ef170cd08"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "mutate_once"
version = "0.1.1"
@@ -3610,7 +3614,7 @@ dependencies = [
"log",
"netlink-packet-core",
"netlink-sys",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@@ -4083,7 +4087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.16",
"thiserror 2.0.17",
"ucd-trie",
]
@@ -4123,9 +4127,9 @@ dependencies = [
[[package]]
name = "pgp"
version = "0.16.0"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91d320242d9b686612b15526fe38711afdf856e112eaa4775ce25b0d9b12b11"
checksum = "7d918d5da2ce943e4c6088d7694f33f47c19374d6f0f2080a0c5e8010afdfd29"
dependencies = [
"aead",
"aes",
@@ -4165,7 +4169,7 @@ dependencies = [
"k256",
"log",
"md-5",
"nom 7.1.3",
"nom 8.0.0",
"num-bigint-dig",
"num-traits",
"num_enum",
@@ -4175,6 +4179,7 @@ dependencies = [
"p521",
"rand 0.8.5",
"regex",
"replace_with",
"ripemd",
"rsa",
"sha1",
@@ -4255,7 +4260,7 @@ dependencies = [
"serde",
"sha1_smol",
"simple-dns",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
"url",
@@ -4361,11 +4366,11 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.16"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.9.1",
"crc32fast",
"fdeflate",
"flate2",
@@ -4581,9 +4586,9 @@ dependencies = [
[[package]]
name = "proptest"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f"
checksum = "2bb0be07becd10686a0bb407298fb425360a5c44a663774406340c59a22de4ce"
dependencies = [
"bitflags 2.9.1",
"lazy_static",
@@ -4595,6 +4600,15 @@ dependencies = [
"unarray",
]
[[package]]
name = "pxfm"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
dependencies = [
"num-traits",
]
[[package]]
name = "qr2term"
version = "0.3.3"
@@ -4625,9 +4639,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
dependencies = [
"memchr",
]
@@ -4645,7 +4659,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tracing",
]
@@ -4664,7 +4678,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tinyvec",
"tracing",
"web-time",
@@ -4686,9 +4700,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
@@ -4881,7 +4895,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
dependencies = [
"getrandom 0.2.16",
"libredox",
"thiserror 2.0.16",
"thiserror 2.0.17",
]
[[package]]
@@ -4919,6 +4933,12 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "replace_with"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51743d3e274e2b18df81c4dc6caf8a5b8e15dbe799e0dca05c7617380094e884"
[[package]]
name = "reqwest"
version = "0.12.15"
@@ -5267,6 +5287,18 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdp"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd277015eada44a0bb810a4b84d3bf6e810573fa62fb442f457edf6a1087a69"
dependencies = [
"rand 0.8.5",
"substring",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "sec1"
version = "0.7.3"
@@ -5338,10 +5370,11 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
@@ -5355,10 +5388,19 @@ dependencies = [
]
[[package]]
name = "serde_derive"
version = "1.0.219"
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -5378,23 +5420,24 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.143"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -5517,7 +5560,7 @@ dependencies = [
"shadowsocks-crypto",
"socket2",
"spin 0.10.0",
"thiserror 2.0.16",
"thiserror 2.0.17",
"tokio",
"tokio-tfo",
"trait-variant",
@@ -5764,6 +5807,15 @@ dependencies = [
"rand 0.9.0",
]
[[package]]
name = "substring"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86"
dependencies = [
"autocfg",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -5883,9 +5935,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.21.0"
version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
dependencies = [
"fastrand",
"getrandom 0.3.3",
@@ -5931,11 +5983,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.16"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl 2.0.16",
"thiserror-impl 2.0.17",
]
[[package]]
@@ -5951,9 +6003,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.16"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
@@ -6169,17 +6221,17 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.5"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0"
dependencies = [
"indexmap",
"serde",
"serde_core",
"serde_spanned",
"toml_datetime 0.7.0",
"toml_datetime 0.7.2",
"toml_parser",
"toml_writer",
"winnow 0.7.11",
"winnow 0.7.13",
]
[[package]]
@@ -6190,11 +6242,11 @@ checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
[[package]]
name = "toml_datetime"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1"
dependencies = [
"serde",
"serde_core",
]
[[package]]
@@ -6217,16 +6269,16 @@ dependencies = [
"indexmap",
"toml_datetime 0.6.11",
"toml_write",
"winnow 0.7.11",
"winnow 0.7.13",
]
[[package]]
name = "toml_parser"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627"
dependencies = [
"winnow 0.7.11",
"winnow 0.7.13",
]
[[package]]
@@ -6237,9 +6289,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "toml_writer"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109"
[[package]]
name = "tower"
@@ -6494,9 +6546,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.0"
version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.3",
"js-sys",
@@ -6840,9 +6892,9 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-registry"
@@ -7115,9 +7167,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.7.11"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
@@ -7151,7 +7203,7 @@ dependencies = [
"futures",
"log",
"serde",
"thiserror 2.0.16",
"thiserror 2.0.17",
"windows 0.59.0",
"windows-core 0.59.0",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "2.13.0"
version = "2.20.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
@@ -79,17 +79,17 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.16.0", default-features = false }
pgp = { version = "0.17.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = "0.37"
quoted_printable = "0.5"
quick-xml = { version = "0.38", features = ["escape-html"] }
rand = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
rustls = { version = "0.23.22", default-features = false }
sanitize-filename = { workspace = true }
sdp = "0.8.0"
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
@@ -177,7 +177,7 @@ harness = false
anyhow = "1"
async-channel = "2.5.0"
base64 = "0.22"
chrono = { version = "0.4.41", default-features = false }
chrono = { version = "0.4.42", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
deltachat = { path = ".", default-features = false }
@@ -194,7 +194,7 @@ rusqlite = "0.36"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.21.0"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.16"

View File

@@ -68,7 +68,7 @@ impl ContactAddress {
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
bail!("invalid address {s:?}");
}
Ok(Self(addr.to_string()))
}
@@ -257,16 +257,16 @@ impl EmailAddress {
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
bail!("empty string is not valid for local part in {:?}", input);
bail!("empty string is not valid for local part in {input:?}");
}
if domain.is_empty() {
bail!("missing domain after '@' in {:?}", input);
bail!("missing domain after '@' in {input:?}");
}
if domain.ends_with('.') {
bail!("Domain {domain:?} should not contain the dot in the end");
@@ -276,7 +276,7 @@ impl EmailAddress {
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
_ => bail!("Email {input:?} must contain '@' character"),
}
}
}

View File

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

View File

@@ -458,12 +458,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* The library uses the `media_quality` setting to use different defaults
* for recoding images sent with type #DC_MSG_IMAGE.
* If needed, recoding other file types is up to the UI.
* - `webrtc_instance` = webrtc instance to use for videochats in the form
* `[basicwebrtc:|jitsi:]https://example.com/subdir#roomname=$ROOM`
* if the URL is prefixed by `basicwebrtc`, the server is assumed to be of the type
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* The type `jitsi:` may be handled by external apps.
* If no type is prefixed, the videochat is handled completely in a browser.
* - `bot` = Set to "1" if this is a bot.
* Prevents adding the "Device messages" and "Saved messages" chats,
* adds Auto-Submitted header to outgoing messages,
@@ -575,11 +569,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
/**
* Set configuration values from a QR code.
* Before this function is called, dc_check_qr() should confirm the type of the
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
* QR code is DC_QR_ACCOUNT or DC_QR_LOGIN.
*
* Internally, the function will call dc_set_config() with the appropriate keys,
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN.
*
* @memberof dc_context_t
* @param context The context object.
@@ -1052,42 +1045,6 @@ void dc_send_edit_request (dc_context_t* context, uint32_t ms
void dc_send_delete_request (dc_context_t* context, const uint32_t* msg_ids, int msg_cnt);
/**
* Send invitation to a videochat.
*
* This function reads the `webrtc_instance` config value,
* may check that the server is working in some way
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
*
* After that, the function sends out a message that contains information to join the room:
*
* - To allow non-delta-clients to join the chat,
* the message contains a text-area with some descriptive text
* and a URL that can be opened in a supported browser to join the videochat.
*
* - delta-clients can get all information needed from
* the message object, using e.g.
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for #DC_MSG_VIDEOCHAT_INVITATION.
*
* dc_send_videochat_invitation() is blocking and may take a while,
* so the UIs will typically call the function from within a thread.
* Moreover, UIs will typically enter the room directly without an additional click on the message,
* for this purpose, the function returns the message id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on success, the message has a delivery state, and so on.
* The recipient will get noticed by the call as usual by #DC_EVENT_INCOMING_MSG or #DC_EVENT_MSGS_CHANGED,
* However, UIs might some things differently, e.g. play a different sound.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to start a videochat for.
* @return The ID of the message sent out
* or 0 for errors.
*/
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* A webxdc instance sends a status update to its other members.
*
@@ -1222,7 +1179,7 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
* Possible actions during ringing:
*
* - caller cancels the call using dc_end_call():
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed Call"
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
*
* - callee accepts using dc_accept_incoming_call():
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
@@ -1230,19 +1187,20 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
*
* - callee declines using dc_end_call():
* caller receives #DC_EVENT_CALL_ENDED and has a "Declinced Call".
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Cancelled Call",
* callee's other devices receive #DC_EVENT_CALL_ENDED and have a "Canceled Call",
*
* - callee is already in a call:
* in this case, UI may decide to show a notification instead of ringing.
* otherwise, this is same as timeout
* what to do depends on the capabilities of UI to handle calls.
* if UI cannot handle multiple calls, an easy approach would be to decline the new call automatically
* and make that visble to the user in the call, e.g. by a notification
*
* - timeout:
* after 1 minute without action,
* caller and callee receive #DC_EVENT_CALL_ENDED
* to prevent endless ringing of callee
* in case caller got offline without being able to send cancellation message.
* for caller, this is a "Cancelled Call";
* for callee, this is a "Missed Call"
* for caller, this is a "Canceled call";
* for callee, this is a "Missed call"
*
* Actions during the call:
*
@@ -1252,6 +1210,13 @@ uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t c
* - callee ends the call using dc_end_call():
* caller receives #DC_EVENT_CALL_ENDED
*
* Contact request handling:
*
* - placing or accepting calls implies accepting contact requests
*
* - ending a call does not accept a contact request;
* instead, the call will timeout on all affected devices.
*
* Note, that the events are for updating the call screen,
* possible status messages are added and updated as usual, including the known events.
* In the UI, the sorted chatlist is used as an overview about calls as well as messages.
@@ -1279,6 +1244,7 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
*
* If the call is already accepted or ended, nothing happens.
* If the chat is a contact request, it is accepted implicitly.
*
* @memberof dc_context_t
* @param context The context object.
@@ -1299,7 +1265,12 @@ uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t ch
* Unaccepted calls ended by the callee are a "decline".
* If the call was accepted, this is a "hangup".
*
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED.
* All participant devices get informed about the ended call via #DC_EVENT_CALL_ENDED unless they are contact requests.
* For contact requests, the call times out on all other affected devices.
*
* If the message ID is wrong or does not exist for whatever reasons, nothing happens.
* Therefore, and for resilience, UI should remove the call UI directly when calling
* this function and not only on the event.
*
* If the call is already ended, nothing happens.
*
@@ -2601,7 +2572,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_BACKUP 251 // deprecated
#define DC_QR_BACKUP2 252
#define DC_QR_BACKUP_TOO_NEW 255
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_PROXY 271 // text1=address (e.g. "127.0.0.1:9050")
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
@@ -2655,10 +2625,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* show a hint to the user that this backup comes from a newer Delta Chat version
* and this device needs an update
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
*
* - DC_QR_PROXY with dc_lot_t::text1=address:
* ask the user if they want to use the given proxy.
* if so, call dc_set_config_from_qr() and restart I/O.
@@ -4730,22 +4696,6 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
/**
* Get URL of a videochat invitation.
*
* Videochat invitations are sent out using dc_send_videochat_invitation()
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return If the message contains a videochat invitation,
* the URL of the invitation is returned.
* If the message is no videochat invitation, NULL is returned.
* Must be released using dc_str_unref() when done.
*/
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
/**
* Gets the error status of the message.
* If there is no error associated with the message, NULL is returned.
@@ -4768,41 +4718,6 @@ char* dc_msg_get_videochat_url (const dc_msg_t* msg);
char* dc_msg_get_error (const dc_msg_t* msg);
/**
* Get type of videochat.
*
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
* in this case, if `basicwebrtc:` as of https://github.com/cracker0dks/basicwebrtc or `jitsi`
* were used to initiate the videochat,
* dc_msg_get_videochat_type() returns the corresponding type.
*
* The videochat URL can be retrieved using dc_msg_get_videochat_url().
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC, DC_VIDEOCHATTYPE_JITSI or DC_VIDEOCHATTYPE_UNKNOWN.
*
* Example:
* ~~~
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
* // videochat invitation that we ship a client for
* } else {
* // use browser for videochat - or add an additional check for DC_VIDEOCHATTYPE_JITSI
* }
* } else {
* // not a videochat invitation
* }
* ~~~
*/
int dc_msg_get_videochat_type (const dc_msg_t* msg);
#define DC_VIDEOCHATTYPE_UNKNOWN 0
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
#define DC_VIDEOCHATTYPE_JITSI 2
/**
* Checks if the message has a full HTML version.
*
@@ -5701,19 +5616,20 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_MSG_FILE 60
/**
* Message indicating an incoming or outgoing videochat.
* The message was created via dc_send_videochat_invitation() on this or a remote device.
*
* Typically, such messages are rendered differently by the UIs,
* e.g. contain a button to join the videochat.
* The URL for joining can be retrieved using dc_msg_get_videochat_url().
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
/**
* Message indicating an incoming or outgoing call.
*
* These messages are created by dc_place_outgoing_call()
* and should be rendered by UI similar to text messages,
* maybe with some "phone icon" at the side.
*
* The message text is updated as needed
* and UI will be informed via #DC_EVENT_MSGS_CHANGED as usual.
*
* Do not start ringing when seeing this message;
* the mesage may belong e.g. to an old missed call.
*
* Instead, ringing should start on the event #DC_EVENT_INCOMING_CALL
*/
#define DC_MSG_CALL 71
@@ -6560,11 +6476,7 @@ void dc_event_unref(dc_event_t* event);
* generated by dc_get_securejoin_qr().
*
* @param data1 (int) The ID of the contact that wants to join.
* @param data2 (int) The progress as:
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
* 600=vg-/vc-request-with-auth received and verified, typically shown as "bob@addr verified".
* 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
* 1000=Protocol finished for this contact.
* @param data2 (int) The progress, always 1000.
*/
#define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060
@@ -6725,7 +6637,8 @@ void dc_event_unref(dc_event_t* event);
*
* Together with this event,
* a message of type #DC_MSG_CALL is added to the corresponding chat;
* this message is announced and updated by the usual even as #DC_EVENT_MSGS_CHANGED.
* this message is announced and updated by the usual event as #DC_EVENT_MSGS_CHANGED,
* there is usually no need to take care of this message from any of the CALL events.
*
* If user takes action, dc_accept_incoming_call() or dc_end_call() should be called.
*
@@ -6734,6 +6647,7 @@ void dc_event_unref(dc_event_t* event);
*
* @param data1 (int) msg_id ID of the message referring to the call.
* @param data2 (char*) place_call_info, text passed to dc_place_outgoing_call()
* @param data2 (int) 1 if incoming call is a video call, 0 otherwise
*/
#define DC_EVENT_INCOMING_CALL 2550
@@ -6741,8 +6655,7 @@ void dc_event_unref(dc_event_t* event);
* The callee accepted an incoming call on this or another device using dc_accept_incoming_call().
* The caller gets the event #DC_EVENT_OUTGOING_CALL_ACCEPTED at the same time.
*
* The event is sent unconditionally when the corresponding message is received.
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
*/
@@ -6751,8 +6664,7 @@ void dc_event_unref(dc_event_t* event);
/**
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
*
* The event is sent unconditionally when the corresponding message is received.
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
* @param data2 (char*) accept_call_info, text passed to dc_accept_incoming_call()
@@ -6760,11 +6672,10 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
/**
* An incoming or outgoing call was ended using dc_end_call().
* An incoming or outgoing call was ended using dc_end_call() on this or another device, by caller or callee.
* Moreover, the event is sent when the call was not accepted within 1 minute timeout.
*
* The event is sent unconditionally when the corresponding message is received.
* UI should only take action in case call UI was opened before, otherwise the event should be ignored.
* UI usually only takes action in case call UI was opened before, otherwise the event should be ignored.
*
* @param data1 (int) msg_id ID of the message referring to the call
*/
@@ -7255,17 +7166,6 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
/// "Video chat invitation"
///
/// Used in summaries.
#define DC_STR_VIDEOCHAT_INVITATION 82
/// "You are invited to a video chat, click %1$s to join."
///
/// Used as message text of outgoing video chat invitations.
/// - %1$s will be replaced by the URL of the video chat
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
/// "Error: %1$s"
///
/// Used in error strings.
@@ -7564,7 +7464,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in status messages.
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
/// "You left."
/// "You left the group."
///
/// Used in status messages.
#define DC_STR_GROUP_LEFT_BY_YOU 132
@@ -7787,6 +7687,12 @@ void dc_event_unref(dc_event_t* event);
/// `%1$s` will be replaced by the provider's domain.
#define DC_STR_INVALID_UNENCRYPTED_MAIL 174
/// "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
///
/// Added to the device chat if could not decrypt a new outgoing message (i.e. not when fetching
/// existing messages). But no more than once a day.
#define DC_STR_CANT_DECRYPT_OUTGOING_MSGS 175
/// "You reacted %1$s to '%2$s'"
///
/// `%1$s` will be replaced by the reaction, usually an emoji
@@ -7823,8 +7729,37 @@ void dc_event_unref(dc_event_t* event);
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200
/// "Outgoing call"
#define DC_STR_OUTGOING_CALL 194
/// "Incoming call"
#define DC_STR_INCOMING_CALL 195
/// "Declined call"
#define DC_STR_DECLINED_CALL 196
/// "Canceled call"
#define DC_STR_CANCELED_CALL 197
/// "Missed call"
#define DC_STR_MISSED_CALL 198
/// "You left the channel."
///
/// Used in status messages.
#define DC_STR_CHANNEL_LEFT_BY_YOU 200
/// "Security"
///
/// Used in connectivity view.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 201
/// "Scan to join channel %1$s"
///
/// Subtitle for channel join qrcode svg image generated by the core.
///
/// `%1$s` will be replaced with the channel name.
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 202
/**
* @}

View File

@@ -679,7 +679,6 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ChatModified(_)
| EventType::ChatDeleted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::IncomingCall { .. }
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
@@ -701,6 +700,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
..
} => status_update_serial.to_u32() as libc::c_int,
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
EventType::IncomingCall { has_video, .. } => *has_video as libc::c_int,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -1097,25 +1098,6 @@ pub unsafe extern "C" fn dc_send_delete_request(
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_videochat_invitation(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
return 0;
}
let ctx = &*context;
block_on(async move {
chat::send_videochat_invitation(ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to send video chat invitation")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_webxdc_status_update(
context: *mut dc_context_t,
@@ -3853,31 +3835,6 @@ pub unsafe extern "C" fn dc_msg_has_html(msg: *mut dc_msg_t) -> libc::c_int {
ffi_msg.message.has_html().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg
.message
.get_videochat_url()
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {

View File

@@ -51,7 +51,6 @@ impl Lot {
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
Qr::Backup2 { .. } => None,
Qr::BackupTooNew { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(Cow::Borrowed(domain)),
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
Qr::Url { url } => Some(Cow::Borrowed(url)),
@@ -105,7 +104,6 @@ impl Lot {
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Proxy { .. } => LotState::QrProxy,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
@@ -132,7 +130,6 @@ impl Lot {
Qr::Account { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::BackupTooNew { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Proxy { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
@@ -185,9 +182,6 @@ pub enum LotState {
QrBackupTooNew = 255,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
/// text1=address, text2=protocol
QrProxy = 271,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "2.13.0"
version = "2.20.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
license = "MPL-2.0"

View File

@@ -8,6 +8,7 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::blob::BlobObject;
use deltachat::calls::ice_servers;
use deltachat::chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
@@ -47,6 +48,7 @@ pub mod types;
use num_traits::FromPrimitive;
use types::account::Account;
use types::calls::JsonrpcCallInfo;
use types::chat::FullChat;
use types::contact::{ContactObject, VcardContact};
use types::events::Event;
@@ -91,7 +93,8 @@ pub struct CommandApi {
/// Receiver side of the event channel.
///
/// Events from it can be received by calling `get_next_event` method.
/// Events from it can be received by calling
/// [`CommandApi::get_next_event`] method.
event_emitter: Arc<EventEmitter>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
@@ -123,7 +126,7 @@ impl CommandApi {
.read()
.await
.get_account(id)
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
Ok(sc)
}
@@ -173,7 +176,15 @@ impl CommandApi {
get_info()
}
/// Get the next event.
/// Get the next event, and remove it from the event queue.
///
/// If no events have happened since the last `get_next_event`
/// (i.e. if the event queue is empty), the response will be returned
/// only when a new event fires.
///
/// Note that if you are using the `BaseDeltaChat` JavaScript class
/// or the `Rpc` Python class, this function will be invoked
/// by those classes internally and should not be used manually.
async fn get_next_event(&self) -> Result<Event> {
self.event_emitter
.recv()
@@ -297,8 +308,7 @@ impl CommandApi {
Ok(Account::from_context(&ctx, account_id).await?)
} else {
Err(anyhow!(
"account with id {} doesn't exist anymore",
account_id
"account with id {account_id} doesn't exist anymore"
))
}
}
@@ -1798,13 +1808,13 @@ impl CommandApi {
/// Offers a backup for remote devices to retrieve.
///
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
/// Can be canceled by stopping the ongoing process. Success or failure can be tracked
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
/// failure.
///
/// This **stops IO** while it is running.
///
/// Returns once a remote device has retrieved the backup, or is cancelled.
/// Returns once a remote device has retrieved the backup, or is canceled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
@@ -1870,7 +1880,7 @@ impl CommandApi {
/// This retrieves the backup from a remote device over the network and imports it into
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
/// Can be canceled by stopping the ongoing process.
///
/// Do not forget to call start_io on the account after a successful import,
/// otherwise it will not connect to the email server.
@@ -1991,6 +2001,11 @@ impl CommandApi {
Ok(())
}
/// Leaves the gossip of the webxdc with the given message id.
///
/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
/// `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id`
/// anymore until the app is open again.
async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await
@@ -2068,6 +2083,53 @@ impl CommandApi {
.map(|msg_id| msg_id.to_u32()))
}
/// Starts an outgoing call.
async fn place_outgoing_call(
&self,
account_id: u32,
chat_id: u32,
place_call_info: String,
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let msg_id = ctx
.place_outgoing_call(ChatId::new(chat_id), place_call_info)
.await?;
Ok(msg_id.to_u32())
}
/// Accepts an incoming call.
async fn accept_incoming_call(
&self,
account_id: u32,
msg_id: u32,
accept_call_info: String,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.accept_incoming_call(MsgId::new(msg_id), accept_call_info)
.await?;
Ok(())
}
/// Ends incoming or outgoing call.
async fn end_call(&self, account_id: u32, msg_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
ctx.end_call(MsgId::new(msg_id)).await?;
Ok(())
}
/// Returns information about the call.
async fn call_info(&self, account_id: u32, msg_id: u32) -> Result<JsonrpcCallInfo> {
let ctx = self.get_context(account_id).await?;
let call_info = JsonrpcCallInfo::from_msg_id(&ctx, MsgId::new(msg_id)).await?;
Ok(call_info)
}
/// Returns JSON with ICE servers, to be used for WebRTC video calls.
async fn ice_servers(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
ice_servers(&ctx).await
}
/// Makes an HTTP GET request and returns a response.
///
/// `url` is the HTTP or HTTPS URL.
@@ -2219,13 +2281,6 @@ impl CommandApi {
}
}
async fn send_videochat_invitation(&self, account_id: u32, chat_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
}
// ---------------------------------------------
// misc prototyping functions
// that might get removed later again
@@ -2256,8 +2311,7 @@ impl CommandApi {
let message = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
ensure!(
message.get_viewtype() == Viewtype::Sticker,
"message {} is not a sticker",
msg_id
"message {msg_id} is not a sticker"
);
let account_folder = ctx
.get_dbfile()
@@ -2477,10 +2531,7 @@ impl CommandApi {
.to_u32();
Ok(msg_id)
} else {
Err(anyhow!(
"chat with id {} doesn't have draft message",
chat_id
))
Err(anyhow!("chat with id {chat_id} doesn't have draft message"))
}
}
}

View File

@@ -0,0 +1,97 @@
use anyhow::{Context as _, Result};
use deltachat::calls::{call_state, sdp_has_video, CallState};
use deltachat::context::Context;
use deltachat::message::MsgId;
use serde::Serialize;
use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "CallInfo", rename_all = "camelCase")]
pub struct JsonrpcCallInfo {
/// SDP offer.
///
/// Can be used to manually answer the call
/// even if incoming call event was missed.
pub sdp_offer: String,
/// True if SDP offer has a video.
pub has_video: bool,
/// Call state.
///
/// For example, if the call is accepted, active, canceled, declined etc.
pub state: JsonrpcCallState,
}
impl JsonrpcCallInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
format!("Attempting to get call state of non-call message {msg_id}")
})?;
let sdp_offer = call_info.place_call_info.clone();
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
Ok(JsonrpcCallInfo {
sdp_offer,
has_video,
state,
})
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename = "CallState", tag = "kind")]
pub enum JsonrpcCallState {
/// Fresh incoming or outgoing call that is still ringing.
///
/// There is no separate state for outgoing call
/// that has been dialled but not ringing on the other side yet
/// as we don't know whether the other side received our call.
Alerting,
/// Active call.
Active,
/// Completed call that was once active
/// and then was terminated for any reason.
Completed {
/// Call duration in seconds.
duration: i64,
},
/// Incoming call that was not picked up within a timeout
/// or was explicitly ended by the caller before we picked up.
Missed,
/// Incoming call that was explicitly ended on our side
/// before picking up or outgoing call
/// that was declined before the timeout.
Declined,
/// Outgoing call that has been canceled on our side
/// before receiving a response.
///
/// Incoming calls cannot be canceled,
/// on the receiver side canceled calls
/// usually result in missed calls.
Canceled,
}
impl JsonrpcCallState {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
let call_state = call_state(context, msg_id).await?;
let jsonrpc_call_state = match call_state {
CallState::Alerting => JsonrpcCallState::Alerting,
CallState::Active => JsonrpcCallState::Active,
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
CallState::Missed => JsonrpcCallState::Missed,
CallState::Declined => JsonrpcCallState::Declined,
CallState::Canceled => JsonrpcCallState::Canceled,
};
Ok(jsonrpc_call_state)
}
}

View File

@@ -1,4 +1,5 @@
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
use num_traits::ToPrimitive;
use serde::Serialize;
use typescript_type_def::TypeDef;
@@ -293,8 +294,8 @@ pub enum EventType {
#[serde(rename_all = "camelCase")]
ImexFileWritten { path: String },
/// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code).
/// Progress event sent when SecureJoin protocol has finished
/// from the view of the inviter (Alice, the person who shows the QR code).
///
/// These events are typically sent after a joiner has scanned the QR code
/// generated by getChatSecurejoinQrCodeSvg().
@@ -303,11 +304,14 @@ pub enum EventType {
/// ID of the contact that wants to join.
contact_id: u32,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received and verified, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// The type of the joined chat.
/// This can take the same values
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
chat_type: u32,
/// ID of the chat in case of success.
chat_id: u32,
/// Progress, always 1000.
progress: usize,
},
@@ -421,8 +425,12 @@ pub enum EventType {
IncomingCall {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
/// User-defined info as passed to place_outgoing_call()
place_call_info: String,
/// True if incoming call is a video call.
has_video: bool,
},
/// Incoming call accepted.
@@ -430,12 +438,16 @@ pub enum EventType {
IncomingCallAccepted {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// Outgoing call accepted.
OutgoingCallAccepted {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
/// User-defined info passed to dc_accept_incoming_call(
accept_call_info: String,
},
@@ -444,6 +456,8 @@ pub enum EventType {
CallEnded {
/// ID of the info message referring to the call.
msg_id: u32,
/// ID of the chat which the message belongs to.
chat_id: u32,
},
}
@@ -551,9 +565,13 @@ impl From<CoreEventType> for EventType {
},
CoreEventType::SecurejoinInviterProgress {
contact_id,
chat_type,
chat_id,
progress,
} => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(),
chat_type: chat_type.to_u32().unwrap_or(0),
chat_id: chat_id.to_u32(),
progress,
},
CoreEventType::SecurejoinJoinerProgress {
@@ -597,23 +615,31 @@ impl From<CoreEventType> for EventType {
CoreEventType::AccountsItemChanged => AccountsItemChanged,
CoreEventType::IncomingCall {
msg_id,
chat_id,
place_call_info,
has_video,
} => IncomingCall {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
place_call_info,
has_video,
},
CoreEventType::IncomingCallAccepted { msg_id } => IncomingCallAccepted {
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::OutgoingCallAccepted {
msg_id,
chat_id,
accept_call_info,
} => OutgoingCallAccepted {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
accept_call_info,
},
CoreEventType::CallEnded { msg_id } => CallEnded {
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
#[allow(unreachable_patterns)]
#[cfg(test)]

View File

@@ -84,9 +84,6 @@ pub struct MessageObject {
dimensions_height: i32,
dimensions_width: i32,
videochat_type: Option<u32>,
videochat_url: Option<String>,
override_sender_name: Option<String>,
sender: ContactObject,
@@ -239,15 +236,6 @@ impl MessageObject {
dimensions_height: message.get_height(),
dimensions_width: message.get_width(),
videochat_type: match message.get_videochat_type() {
Some(vct) => Some(
vct.to_u32()
.context("videochat type conversion to number failed")?,
),
None => None,
},
videochat_url: message.get_videochat_url(),
override_sender_name,
sender,
@@ -321,9 +309,6 @@ pub enum MessageViewtype {
/// Message containing any file, eg. a PDF.
File,
/// Message is an invitation to a videochat.
VideochatInvitation,
/// Message is a call.
Call,
@@ -348,7 +333,6 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::Voice => MessageViewtype::Voice,
Viewtype::Video => MessageViewtype::Video,
Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Call => MessageViewtype::Call,
Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
@@ -368,7 +352,6 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::Voice => Viewtype::Voice,
MessageViewtype::Video => Viewtype::Video,
MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Call => Viewtype::Call,
MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,

View File

@@ -1,4 +1,5 @@
pub mod account;
pub mod calls;
pub mod chat;
pub mod chat_list;
pub mod contact;

View File

@@ -225,13 +225,6 @@ impl From<Qr> for QrObject {
auth_token,
},
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
Qr::WebrtcInstance {
domain,
instance_pattern,
} => QrObject::WebrtcInstance {
domain,
instance_pattern,
},
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
Qr::Addr { contact_id, draft } => {
let contact_id = contact_id.to_u32();

View File

@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "2.13.0"
"version": "2.20.0"
}

View File

@@ -28,7 +28,6 @@ export class BaseDeltaChat<
Transport extends BaseTransport<any>,
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
//@ts-ignore
@@ -36,6 +35,10 @@ export class BaseDeltaChat<
constructor(
public transport: Transport,
/**
* Whether to start calling {@linkcode RawClient.getNextEvent}
* and emitting the respective events on this class.
*/
startEventLoop: boolean,
) {
super();
@@ -45,6 +48,9 @@ export class BaseDeltaChat<
}
}
/**
* @see the constructor's `startEventLoop`
*/
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
@@ -63,10 +69,17 @@ export class BaseDeltaChat<
}
}
/**
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
*/
async listAccounts(): Promise<T.Account[]> {
return await this.rpc.getAllAccounts();
}
/**
* A convenience function to listen on events binned by `account_id`
* (see {@linkcode RawClient.getAllAccounts}).
*/
getContextEvents(account_id: number) {
if (this.contextEmitters[account_id]) {
return this.contextEmitters[account_id];

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "2.13.0"
version = "2.20.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"

View File

@@ -210,13 +210,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else if msg.get_viewtype() == Viewtype::Webxdc {
if msg.get_viewtype() == Viewtype::Webxdc {
match msg.get_webxdc_info(context).await {
Ok(info) => format!(
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
@@ -371,7 +365,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
sendhtml <file for html-part> [<text for plain-part>]\n\
sendsyncmsg\n\
sendupdate <msg-id> <json status update>\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
@@ -425,7 +418,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Ok(setup_code) => {
println!("Setup code for the transferred setup message: {setup_code}",)
}
Err(err) => bail!("Failed to generate setup code: {}", err),
Err(err) => bail!("Failed to generate setup code: {err}"),
},
"get-setupcodebegin" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
@@ -439,7 +432,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
setupcodebegin.unwrap_or_default(),
);
} else {
bail!("{} is no setup message.", msg_id,);
bail!("{msg_id} is no setup message.",);
}
}
"continue-key-transfer" => {
@@ -534,7 +527,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Report written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get connectivity html: {}", err);
bail!("Failed to get connectivity html: {err}");
}
}
}
@@ -962,10 +955,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let msg_id = MsgId::new(arg1.parse()?);
context.send_webxdc_status_update(msg_id, arg2).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
@@ -1298,7 +1287,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
);
}
"" => (),
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
}
Ok(())

View File

@@ -179,7 +179,7 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 39] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
@@ -206,7 +206,6 @@ const CHAT_COMMANDS: [&str; 39] = [
"sendhtml",
"sendsyncmsg",
"sendupdate",
"videochat",
"draft",
"devicemsg",
"listmedia",
@@ -467,7 +466,7 @@ async fn handle_cmd(
println!("QR code svg written to: {file:#?}");
}
Err(err) => {
bail!("Failed to get QR code svg: {}", err);
bail!("Failed to get QR code svg: {err}");
}
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.13.0"
version = "2.20.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
]

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
@@ -470,3 +471,8 @@ class Account:
def initiate_autocrypt_key_transfer(self) -> None:
"""Send Autocrypt Setup Message."""
return self._rpc.initiate_autocrypt_key_transfer(self.id)
def ice_servers(self) -> list:
"""Return ICE servers for WebRTC configuration."""
ice_servers_json = self._rpc.ice_servers(self.id)
return json.loads(ice_servers_json)

View File

@@ -168,6 +168,11 @@ class Chat:
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def resend_messages(self, messages: list[Message]) -> None:
"""Resend a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.resend_messages(self.account.id, msg_ids)
def forward_messages(self, messages: list[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
@@ -289,3 +294,8 @@ class Chat:
f.write(vcard.encode())
f.flush()
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
def place_outgoing_call(self, place_call_info: str) -> Message:
"""Starts an outgoing call."""
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
return Message(self.account, msg_id)

View File

@@ -73,6 +73,10 @@ class EventType(str, Enum):
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
ACCOUNTS_CHANGED = "AccountsChanged"
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
INCOMING_CALL = "IncomingCall"
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
CALL_ENDED = "CallEnded"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
@@ -156,7 +160,6 @@ class ViewType(str, Enum):
VOICE = "Voice"
VIDEO = "Video"
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
@@ -275,11 +278,3 @@ class SocketSecurity(IntEnum):
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type."""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

View File

@@ -102,3 +102,15 @@ class Message:
def send_webxdc_realtime_data(self, data) -> None:
"""Send data to the realtime channel."""
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
def accept_incoming_call(self, accept_call_info):
"""Accepts an incoming call."""
self._rpc.accept_incoming_call(self.account.id, self.id, accept_call_info)
def end_call(self):
"""Ends incoming or outgoing call."""
self._rpc.end_call(self.account.id, self.id)
def get_call_info(self) -> AttrDict:
"""Return information about the call."""
return AttrDict(self._rpc.call_info(self.account.id, self.id))

View File

@@ -28,9 +28,7 @@ class ACFactory:
def get_unconfigured_account(self) -> Account:
"""Create a new unconfigured account."""
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
return self.deltachat.add_account()
def get_unconfigured_bot(self) -> Bot:
"""Create a new unconfigured bot."""

View File

@@ -0,0 +1,86 @@
from deltachat_rpc_client import EventType, Message
def test_calls(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
place_call_info = "offer"
accept_call_info = "answer"
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
outgoing_call_message = alice_chat_bob.place_outgoing_call(place_call_info)
assert outgoing_call_message.get_call_info().state.kind == "Alerting"
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert not incoming_call_event.has_video # Cannot be parsed as SDP, so false by default
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().state.kind == "Alerting"
assert not incoming_call_message.get_call_info().has_video
incoming_call_message.accept_incoming_call(accept_call_info)
assert incoming_call_message.get_call_info().sdp_offer == place_call_info
assert incoming_call_message.get_call_info().state.kind == "Active"
outgoing_call_accepted_event = alice.wait_for_event(EventType.OUTGOING_CALL_ACCEPTED)
assert outgoing_call_accepted_event.accept_call_info == accept_call_info
assert outgoing_call_message.get_call_info().state.kind == "Active"
outgoing_call_message.end_call()
assert outgoing_call_message.get_call_info().state.kind == "Completed"
end_call_event = bob.wait_for_event(EventType.CALL_ENDED)
assert end_call_event.msg_id == outgoing_call_message.id
assert incoming_call_message.get_call_info().state.kind == "Completed"
def test_video_call(acfactory) -> None:
# Example from <https://datatracker.ietf.org/doc/rfc9143/>
# with `s= ` replaced with `s=-`.
#
# `s=` cannot be empty according to RFC 3264,
# so it is more clear as `s=-`.
place_call_info = """v=0\r
o=alice 2890844526 2890844526 IN IP6 2001:db8::3\r
s=-\r
c=IN IP6 2001:db8::3\r
t=0 0\r
a=group:BUNDLE foo bar\r
\r
m=audio 10000 RTP/AVP 0 8 97\r
b=AS:200\r
a=mid:foo\r
a=rtcp-mux\r
a=rtpmap:0 PCMU/8000\r
a=rtpmap:8 PCMA/8000\r
a=rtpmap:97 iLBC/8000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
\r
m=video 10002 RTP/AVP 31 32\r
b=AS:1000\r
a=mid:bar\r
a=rtcp-mux\r
a=rtpmap:31 H261/90000\r
a=rtpmap:32 MPV/90000\r
a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r
"""
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.place_outgoing_call(place_call_info)
incoming_call_event = bob.wait_for_event(EventType.INCOMING_CALL)
assert incoming_call_event.place_call_info == place_call_info
assert incoming_call_event.has_video
incoming_call_message = Message(bob, incoming_call_event.msg_id)
assert incoming_call_message.get_call_info().has_video
def test_ice_servers(acfactory) -> None:
alice = acfactory.get_online_account()
ice_servers = alice.ice_servers()
assert len(ice_servers) == 1

View File

@@ -252,6 +252,7 @@ def test_chat(acfactory) -> None:
bob_chat_alice.get_encryption_info()
group = alice.create_group("test group")
to_resend = group.send_text("will be resent")
group.add_contact(alice_contact_bob)
group.get_qr_code()
@@ -263,6 +264,7 @@ def test_chat(acfactory) -> None:
msg = group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi"
group.resend_messages([to_resend])
group.forward_messages([msg])
group.set_draft(text="test draft")
@@ -329,6 +331,52 @@ def test_message(acfactory) -> None:
assert reactions == snapshot.reactions
def test_receive_imf_failure(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob.set_config("fail_on_receiving_full_msg", "1")
alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event()
chat_id = event.chat_id
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.AVAILABLE
assert snapshot.error is not None
assert snapshot.show_padlock
# The failed message doesn't break the IMAP loop.
bob.set_config("fail_on_receiving_full_msg", "0")
alice_chat_bob.send_text("Hello again!")
event = bob.wait_for_incoming_msg_event()
assert event.chat_id == chat_id
msg_id = event.msg_id
message1 = bob.get_message_by_id(msg_id)
snapshot = message1.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.DONE
assert snapshot.error is None
# The failed message can be re-downloaded later.
bob._rpc.download_full_message(bob.id, message.id)
event = bob.wait_for_event(EventType.MSGS_CHANGED)
message = bob.get_message_by_id(event.msg_id)
snapshot = message.get_snapshot()
assert snapshot.download_state == DownloadState.IN_PROGRESS
event = bob.wait_for_event(EventType.MSGS_CHANGED)
assert event.chat_id == chat_id
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.download_state == DownloadState.DONE
assert snapshot.error is None
assert snapshot.text == "Hello!"
def test_selfavatar_sync(acfactory, data, log) -> None:
alice = acfactory.get_online_account()

View File

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

View File

@@ -15,5 +15,5 @@
},
"type": "module",
"types": "index.d.ts",
"version": "2.13.0"
"version": "2.20.0"
}

View File

@@ -41,22 +41,22 @@ async fn main_impl() -> Result<()> {
if let Some(first_arg) = args.next() {
if first_arg.to_str() == Some("--version") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
eprintln!("{}", &*DC_VERSION_STR);
return Ok(());
} else if first_arg.to_str() == Some("--openrpc") {
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
println!("{}", CommandApi::openrpc_specification()?);
return Ok(());
} else {
return Err(anyhow!("Unrecognized option {:?}", first_arg));
return Err(anyhow!("Unrecognized option {first_arg:?}"));
}
}
if let Some(arg) = args.next() {
return Err(anyhow!("Unrecognized argument {:?}", arg));
return Err(anyhow!("Unrecognized argument {arg:?}"));
}
// Install signal handlers early so that the shutdown is graceful starting from here.

View File

@@ -587,6 +587,7 @@
(python3.withPackages (pypkgs: with pypkgs; [
tox
]))
nodejs
];
};
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.13.0"
version = "2.20.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"

View File

@@ -435,10 +435,6 @@ class Message:
"""return True if it's a video message."""
return self._view_type == const.DC_MSG_VIDEO
def is_videochat_invitation(self):
"""return True if it's a videochat invitation message."""
return self._view_type == const.DC_MSG_VIDEOCHAT_INVITATION
def is_webxdc(self):
"""return True if it's a Webxdc message."""
return self._view_type == const.DC_MSG_WEBXDC
@@ -479,7 +475,6 @@ _view_type_mapping = {
"video": const.DC_MSG_VIDEO,
"file": const.DC_MSG_FILE,
"sticker": const.DC_MSG_STICKER,
"videochat": const.DC_MSG_VIDEOCHAT_INVITATION,
"webxdc": const.DC_MSG_WEBXDC,
}

View File

@@ -1755,12 +1755,12 @@ def test_group_quote(acfactory, lp):
"xyz",
False,
"xyz",
), # Test that emails are recognized in a random folder but not moved
), # Test that emails aren't found in a random folder
(
"xyz",
"Spam",
True,
"DeltaChat",
), # ...emails are found in a random folder and moved to DeltaChat
), # ...emails are moved from the spam folder to "DeltaChat"
(
"Spam",
False,
@@ -1785,7 +1785,7 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
ac1.stop_io()
assert folder in ac1.direct_imap.list_folders()
lp.sec("Send a message to from ac2 to ac1 and manually move it to the mvbox")
lp.sec("Send a message to from ac2 to ac1 and manually move it to `folder`")
ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1:
acfactory.get_accepted_chat(ac2, ac1).send_text("hello")
@@ -1795,10 +1795,17 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
lp.sec("start_io() and see if DeltaChat finds the message (" + variant + ")")
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1.start_io()
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
chat = ac1.create_chat(ac2)
n_msgs = 1 # "Messages are end-to-end encrypted."
if folder == "Spam":
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
n_msgs += 1
else:
ac1._evtracker.wait_idle_inbox_ready()
assert len(chat.get_messages()) == n_msgs
# The message has been downloaded, which means it has reached its destination.
# The message has reached its destination.
ac1.direct_imap.select_folder(expected_destination)
assert len(ac1.direct_imap.get_all_messages()) == 1
if folder != expected_destination:

View File

@@ -1 +1 @@
2025-09-09
2025-10-13

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.89.0
RUST_VERSION=1.90.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -78,7 +78,7 @@ impl Accounts {
ensure!(dir.exists(), "directory does not exist");
let config_file = dir.join(CONFIG_NAME);
ensure!(config_file.exists(), "{:?} does not exist", config_file);
ensure!(config_file.exists(), "{config_file:?} does not exist");
let config = Config::from_file(config_file, writable).await?;
let events = Events::new();
@@ -724,8 +724,7 @@ impl Config {
{
ensure!(
self.inner.accounts.iter().any(|e| e.id == id),
"invalid account id: {}",
id
"invalid account id: {id}"
);
self.inner.selected_account = id;

View File

@@ -35,7 +35,7 @@ impl FromStr for EncryptPreference {
match s {
"mutual" => Ok(EncryptPreference::Mutual),
"nopreference" => Ok(EncryptPreference::NoPreference),
_ => bail!("Cannot parse encryption preference {}", s),
_ => bail!("Cannot parse encryption preference {s}"),
}
}
}

View File

@@ -32,7 +32,7 @@ pub(crate) async fn handle_authres(
let from_domain = match EmailAddress::new(from) {
Ok(email) => email.domain,
Err(e) => {
return Err(anyhow::format_err!("invalid email {}: {:#}", from, e));
return Err(anyhow::format_err!("invalid email {from}: {e:#}"));
}
};

View File

@@ -170,7 +170,7 @@ impl<'a> BlobObject<'a> {
false => name,
};
if !BlobObject::is_acceptible_blob_name(name) {
return Err(format_err!("not an acceptable blob name: {}", name));
return Err(format_err!("not an acceptable blob name: {name}"));
}
Ok(BlobObject {
blobdir: context.get_blobdir(),
@@ -367,11 +367,12 @@ impl<'a> BlobObject<'a> {
|| img.get_pixel(x_max, y_max).0[3] == 0)
{
*vt = Viewtype::Image;
} else {
// Core doesn't auto-assign `Viewtype::Sticker` to messages and stickers coming
// from UIs shouldn't contain sensitive Exif info.
return Ok(name);
}
}
if *vt == Viewtype::Sticker && exif.is_none() {
return Ok(name);
}
img = match orientation {
Some(90) => img.rotate90(),
@@ -457,8 +458,7 @@ impl<'a> BlobObject<'a> {
{
if img_wh < 20 {
return Err(format_err!(
"Failed to scale image to below {}B.",
max_bytes,
"Failed to scale image to below {max_bytes}B.",
));
}

View File

@@ -416,6 +416,28 @@ async fn test_recode_image_balanced_png() {
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sticker_with_exif() {
let bytes = include_bytes!("../../test-data/image/logo.png");
SendImageCheckMediaquality {
viewtype: Viewtype::Sticker,
bytes,
extension: "png",
// TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
// so the test doesn't check all the logic it should.
has_exif: false,
original_width: 135,
original_height: 135,
res_viewtype: Some(Viewtype::Sticker),
compressed_width: 135,
compressed_height: 135,
..Default::default()
}
.test()
.await
.unwrap();
}
/// Tests that RGBA PNG can be recoded into JPEG
/// by dropping alpha channel.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -485,6 +507,7 @@ struct SendImageCheckMediaquality<'a> {
pub(crate) original_width: u32,
pub(crate) original_height: u32,
pub(crate) orientation: i32,
pub(crate) res_viewtype: Option<Viewtype>,
pub(crate) compressed_width: u32,
pub(crate) compressed_height: u32,
pub(crate) set_draft: bool,
@@ -500,6 +523,7 @@ impl SendImageCheckMediaquality<'_> {
let original_width = self.original_width;
let original_height = self.original_height;
let orientation = self.orientation;
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
let compressed_width = self.compressed_width;
let compressed_height = self.compressed_height;
let set_draft = self.set_draft;
@@ -550,7 +574,7 @@ impl SendImageCheckMediaquality<'_> {
}
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file_saved = bob
@@ -564,7 +588,7 @@ impl SendImageCheckMediaquality<'_> {
}
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
assert!(exif.is_none());
assert!(res_viewtype != Viewtype::Image || exif.is_none());
let img = check_image_size(file_saved, compressed_width, compressed_height);

View File

@@ -8,12 +8,17 @@ use crate::contact::ContactId;
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::log::info;
use crate::log::{info, warn};
use crate::message::{self, Message, MsgId, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::net::dns::lookup_host_with_cache;
use crate::param::Param;
use crate::tools::time;
use anyhow::{Result, ensure};
use anyhow::{Context as _, Result, ensure};
use sdp::SessionDescription;
use serde::Serialize;
use std::io::Cursor;
use std::str::FromStr;
use std::time::Duration;
use tokio::task;
use tokio::time::sleep;
@@ -29,10 +34,22 @@ use tokio::time::sleep;
/// as the callee won't start the call afterwards.
const RINGING_SECONDS: i64 = 60;
/// For persisting parameters in the call, we use Param::Arg*
// For persisting parameters in the call, we use Param::Arg*
const CALL_ACCEPTED_TIMESTAMP: Param = Param::Arg;
const CALL_ENDED_TIMESTAMP: Param = Param::Arg4;
const STUN_PORT: u16 = 3478;
/// Set if incoming call was ended explicitly
/// by the other side before we accepted it.
///
/// It is used to distinguish "ended" calls
/// that are rejected by us from the calls
/// canceled by the other side
/// immediately after ringing started.
const CALL_CANCELED_TIMESTAMP: Param = Param::Arg2;
/// Information about the status of a call.
#[derive(Debug, Default)]
pub struct CallInfo {
@@ -48,12 +65,14 @@ pub struct CallInfo {
}
impl CallInfo {
fn is_incoming(&self) -> bool {
/// Returns true if the call is an incoming call.
pub fn is_incoming(&self) -> bool {
self.msg.from_id != ContactId::SELF
}
fn is_stale(&self) -> bool {
self.remaining_ring_seconds() <= 0
/// Returns true if the call should not ring anymore.
pub fn is_stale(&self) -> bool {
(self.is_incoming() || self.msg.timestamp_sent != 0) && self.remaining_ring_seconds() <= 0
}
fn remaining_ring_seconds(&self) -> i64 {
@@ -73,11 +92,11 @@ impl CallInfo {
}
async fn update_text_duration(&self, context: &Context) -> Result<()> {
let minutes = self.get_duration_seconds() / 60;
let minutes = self.duration_seconds() / 60;
let duration = match minutes {
0 => "<1 minute".to_string(),
1 => "1 minute".to_string(),
n => format!("{} minutes", n),
n => format!("{n} minutes"),
};
if self.is_incoming() {
@@ -98,21 +117,50 @@ impl CallInfo {
Ok(())
}
fn is_accepted(&self) -> bool {
/// Returns true if the call is accepted.
pub fn is_accepted(&self) -> bool {
self.msg.param.exists(CALL_ACCEPTED_TIMESTAMP)
}
/// Returns true if the call is missed
/// because the caller canceled it
/// explicitly before ringing stopped.
///
/// For outgoing calls this means
/// the receiver has rejected the call
/// explicitly.
pub fn is_canceled(&self) -> bool {
self.msg.param.exists(CALL_CANCELED_TIMESTAMP)
}
async fn mark_as_ended(&mut self, context: &Context) -> Result<()> {
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, time());
self.msg.update_param(context).await?;
Ok(())
}
fn is_ended(&self) -> bool {
/// Explicitly mark the call as canceled.
///
/// For incoming calls this should be called
/// when "call ended" message is received
/// from the caller before we picked up the call.
/// In this case the call becomes "missed" early
/// before the ringing timeout.
async fn mark_as_canceled(&mut self, context: &Context) -> Result<()> {
let now = time();
self.msg.param.set_i64(CALL_ENDED_TIMESTAMP, now);
self.msg.param.set_i64(CALL_CANCELED_TIMESTAMP, now);
self.msg.update_param(context).await?;
Ok(())
}
/// Returns true if the call is ended.
pub fn is_ended(&self) -> bool {
self.msg.param.exists(CALL_ENDED_TIMESTAMP)
}
fn get_duration_seconds(&self) -> i64 {
/// Returns call duration in seconds.
pub fn duration_seconds(&self) -> i64 {
if let (Some(start), Some(end)) = (
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),
self.msg.param.get_i64(CALL_ENDED_TIMESTAMP),
@@ -135,7 +183,11 @@ impl Context {
place_call_info: String,
) -> Result<MsgId> {
let chat = Chat::load_from_db(self, chat_id).await?;
ensure!(chat.typ == Chattype::Single && !chat.is_self_talk());
ensure!(
chat.typ == Chattype::Single,
"Can only place calls in 1:1 chats"
);
ensure!(!chat.is_self_talk(), "Cannot call self");
let mut call = Message {
viewtype: Viewtype::Call,
@@ -161,7 +213,9 @@ impl Context {
call_id: MsgId,
accept_call_info: String,
) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("accept_incoming_call is called with {call_id} which does not refer to a call")
})?;
ensure!(call.is_incoming());
if call.is_accepted() || call.is_ended() {
info!(self, "Call already accepted/ended");
@@ -188,6 +242,7 @@ impl Context {
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
@@ -195,20 +250,24 @@ impl Context {
/// Cancel, decline or hangup an incoming or outgoing call.
pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let mut call: CallInfo = self.load_call_by_id(call_id).await?;
let mut call: CallInfo = self.load_call_by_id(call_id).await?.with_context(|| {
format!("end_call is called with {call_id} which does not refer to a call")
})?;
if call.is_ended() {
info!(self, "Call already ended");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
} else {
call.update_text(self, "Cancelled call").await?;
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
}
} else {
call.mark_as_ended(self).await?;
call.update_text_duration(self).await?;
}
@@ -224,6 +283,7 @@ impl Context {
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(())
@@ -235,17 +295,25 @@ impl Context {
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let mut call = context.load_call_by_id(call_id).await?;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
);
return Ok(());
};
if !call.is_accepted() && !call.is_ended() {
call.mark_as_ended(&context).await?;
if call.is_incoming() {
call.mark_as_canceled(&context).await?;
call.update_text(&context, "Missed call").await?;
} else {
call.update_text(&context, "Cancelled call").await?;
call.mark_as_ended(&context).await?;
call.update_text(&context, "Canceled call").await?;
}
context.emit_msgs_changed(call.msg.chat_id, call_id);
context.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
Ok(())
@@ -258,7 +326,11 @@ impl Context {
from_id: ContactId,
) -> Result<()> {
if mime_message.is_call() {
let call = self.load_call_by_id(call_id).await?;
let Some(call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_incoming() {
if call.is_stale() {
call.update_text(self, "Missed call").await?;
@@ -266,9 +338,18 @@ impl Context {
} else {
call.update_text(self, "Incoming call").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
let has_video = match sdp_has_video(&call.place_call_info) {
Ok(has_video) => has_video,
Err(err) => {
warn!(self, "Failed to determine if SDP offer has video: {err:#}.");
false
}
};
self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
place_call_info: call.place_call_info.to_string(),
has_video,
});
let wait = call.remaining_ring_seconds();
task::spawn(Context::emit_end_call_if_unaccepted(
@@ -284,7 +365,11 @@ impl Context {
} else {
match mime_message.is_system_message {
SystemMessage::CallAccepted => {
let mut call = self.load_call_by_id(call_id).await?;
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_ended() || call.is_accepted() {
info!(self, "CallAccepted received for accepted/ended call");
return Ok(());
@@ -295,6 +380,7 @@ impl Context {
if call.is_incoming() {
self.emit_event(EventType::IncomingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
} else {
let accept_call_info = mime_message
@@ -302,41 +388,51 @@ impl Context {
.unwrap_or_default();
self.emit_event(EventType::OutgoingCallAccepted {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
accept_call_info: accept_call_info.to_string(),
});
}
}
SystemMessage::CallEnded => {
let mut call = self.load_call_by_id(call_id).await?;
let Some(mut call) = self.load_call_by_id(call_id).await? else {
warn!(self, "{call_id} does not refer to a call message");
return Ok(());
};
if call.is_ended() {
// may happen eg. if a a message is missed
info!(self, "CallEnded received for ended call");
return Ok(());
}
call.mark_as_ended(self).await?;
if !call.is_accepted() {
if call.is_incoming() {
if from_id == ContactId::SELF {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
} else {
call.mark_as_canceled(self).await?;
call.update_text(self, "Missed call").await?;
}
} else {
// outgoing
if from_id == ContactId::SELF {
call.update_text(self, "Cancelled call").await?;
call.mark_as_canceled(self).await?;
call.update_text(self, "Canceled call").await?;
} else {
call.mark_as_ended(self).await?;
call.update_text(self, "Declined call").await?;
}
}
} else {
call.mark_as_ended(self).await?;
call.update_text_duration(self).await?;
}
self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded {
msg_id: call.msg.id,
chat_id: call.msg.chat_id,
});
}
_ => {}
@@ -345,15 +441,27 @@ impl Context {
Ok(())
}
async fn load_call_by_id(&self, call_id: MsgId) -> Result<CallInfo> {
/// Loads information about the call given its ID.
///
/// If the message referred to by ID is
/// not a call message, returns `None`.
pub async fn load_call_by_id(&self, call_id: MsgId) -> Result<Option<CallInfo>> {
let call = Message::load_from_db(self, call_id).await?;
self.load_call_by_message(call)
Ok(self.load_call_by_message(call))
}
fn load_call_by_message(&self, call: Message) -> Result<CallInfo> {
ensure!(call.viewtype == Viewtype::Call);
// Loads information about the call given the `Message`.
//
// If the `Message` is not a call message, returns `None`
fn load_call_by_message(&self, call: Message) -> Option<CallInfo> {
if call.viewtype != Viewtype::Call {
// This can happen e.g. if a "call accepted"
// or "call ended" message is received
// with `In-Reply-To` referring to non-call message.
return None;
}
Ok(CallInfo {
Some(CallInfo {
place_call_info: call
.param
.get(Param::WebrtcRoom)
@@ -369,5 +477,210 @@ impl Context {
}
}
/// Returns true if SDP offer has a video.
pub fn sdp_has_video(sdp: &str) -> Result<bool> {
let mut cursor = Cursor::new(sdp);
let session_description =
SessionDescription::unmarshal(&mut cursor).context("Failed to parse SDP")?;
for media_description in &session_description.media_descriptions {
if media_description.media_name.media == "video" {
return Ok(true);
}
}
Ok(false)
}
/// State of the call for display in the message bubble.
#[derive(Debug, PartialEq, Eq)]
pub enum CallState {
/// Fresh incoming or outgoing call that is still ringing.
///
/// There is no separate state for outgoing call
/// that has been dialled but not ringing on the other side yet
/// as we don't know whether the other side received our call.
Alerting,
/// Active call.
Active,
/// Completed call that was once active
/// and then was terminated for any reason.
Completed {
/// Call duration in seconds.
duration: i64,
},
/// Incoming call that was not picked up within a timeout
/// or was explicitly ended by the caller before we picked up.
Missed,
/// Incoming call that was explicitly ended on our side
/// before picking up or outgoing call
/// that was declined before the timeout.
Declined,
/// Outgoing call that has been canceled on our side
/// before receiving a response.
///
/// Incoming calls cannot be canceled,
/// on the receiver side canceled calls
/// usually result in missed calls.
Canceled,
}
/// Returns call state given the message ID.
///
/// Returns an error if the message is not a call message.
pub async fn call_state(context: &Context, msg_id: MsgId) -> Result<CallState> {
let call = context
.load_call_by_id(msg_id)
.await?
.with_context(|| format!("{msg_id} is not a call message"))?;
let state = if call.is_incoming() {
if call.is_accepted() {
if call.is_ended() {
CallState::Completed {
duration: call.duration_seconds(),
}
} else {
CallState::Active
}
} else if call.is_canceled() {
// Call was explicitly canceled
// by the caller before we picked it up.
CallState::Missed
} else if call.is_ended() {
CallState::Declined
} else if call.is_stale() {
CallState::Missed
} else {
CallState::Alerting
}
} else if call.is_accepted() {
if call.is_ended() {
CallState::Completed {
duration: call.duration_seconds(),
}
} else {
CallState::Active
}
} else if call.is_canceled() {
CallState::Canceled
} else if call.is_ended() || call.is_stale() {
CallState::Declined
} else {
CallState::Alerting
};
Ok(state)
}
/// ICE server for JSON serialization.
#[derive(Serialize, Debug, Clone, PartialEq)]
struct IceServer {
/// STUN or TURN URLs.
pub urls: Vec<String>,
/// Username for TURN server authentication.
pub username: Option<String>,
/// Password for logging into the server.
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.
///
/// IMAP METADATA returns a line such as
/// `example.com:3478:1758650868:8Dqkyyu11MVESBqjbIylmB06rv8=`
///
/// 1758650868 is the username and expiration timestamp
/// at the same time,
/// while `8Dqkyyu11MVESBqjbIylmB06rv8=`
/// is the password.
pub(crate) async fn create_ice_servers_from_metadata(
context: &Context,
metadata: &str,
) -> Result<(i64, String)> {
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?;
Ok((expiration_timestamp, ice_servers))
}
/// Creates JSON with ICE servers when no TURN servers are known.
pub(crate) async fn create_fallback_ice_servers(context: &Context) -> Result<String> {
// Do not use public STUN server from https://stunprotocol.org/.
// It changes the hostname every year
// (e.g. stunserver2025.stunprotocol.org
// which was previously stunserver2024.stunprotocol.org)
// because of bandwidth costs:
// <https://github.com/jselbie/stunserver/issues/50>
// We use nine.testrun.org for a default STUN server.
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 ice_server = IceServer {
urls,
username: None,
credential: None,
};
let json = serde_json::to_string(&[ice_server])?;
Ok(json)
}
/// Returns JSON with ICE servers.
///
/// <https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#iceservers>
///
/// All returned servers are resolved to their IP addresses.
/// The primary point of DNS lookup is that Delta Chat Desktop
/// relies on the servers being specified by IP,
/// because it itself cannot utilize DNS. See
/// <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())
} else {
Ok("[]".to_string())
}
}
#[cfg(test)]
mod calls_tests;

View File

@@ -1,5 +1,8 @@
use super::*;
use crate::chat::forward_msgs;
use crate::config::Config;
use crate::constants::DC_CHAT_ID_TRASH;
use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
use crate::test_utils::{TestContext, TestContextManager};
struct CallSetup {
@@ -18,6 +21,17 @@ async fn assert_text(t: &TestContext, call_id: MsgId, text: &str) -> Result<()>
Ok(())
}
// Offer and answer examples from <https://www.rfc-editor.org/rfc/rfc3264>
const PLACE_INFO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\ns=-\r\nc=IN IP4 host.anywhere.com\r\nt=0 0\r\nm=audio 62986 RTP/AVP 0 4 18\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=rtpmap:18 G729/8000\r\na=inactive\r\n";
const ACCEPT_INFO: &str = "v=0\r\no=bob 2890844730 2890844731 IN IP4 host.example.com\r\ns=\r\nc=IN IP4 host.example.com\r\nt=0 0\r\nm=audio 54344 RTP/AVP 0 4\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:4 G723/8000\r\na=inactive\r\n";
/// Example from <https://datatracker.ietf.org/doc/rfc9143/>
/// with `s= ` replaced with `s=-`.
///
/// `s=` cannot be empty according to RFC 3264,
/// so it is more clear as `s=-`.
const PLACE_INFO_VIDEO: &str = "v=0\r\no=alice 2890844526 2890844526 IN IP6 2001:db8::3\r\ns=-\r\nc=IN IP6 2001:db8::3\r\nt=0 0\r\na=group:BUNDLE foo bar\r\n\r\nm=audio 10000 RTP/AVP 0 8 97\r\nb=AS:200\r\na=mid:foo\r\na=rtcp-mux\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:97 iLBC/8000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n\r\nm=video 10002 RTP/AVP 31 32\r\nb=AS:1000\r\na=mid:bar\r\na=rtcp-mux\r\na=rtpmap:31 H261/90000\r\na=rtpmap:32 MPV/90000\r\na=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid\r\n";
async fn setup_call() -> Result<CallSetup> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
@@ -32,7 +46,7 @@ async fn setup_call() -> Result<CallSetup> {
// Alice's other device sees the same message as an outgoing call.
let alice_chat = alice.create_chat(&bob).await;
let test_msg_id = alice
.place_outgoing_call(alice_chat.id, "place-info-123".to_string())
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string())
.await?;
let sent1 = alice.pop_sent_msg().await;
assert_eq!(sent1.sender_msg_id, test_msg_id);
@@ -41,11 +55,15 @@ async fn setup_call() -> Result<CallSetup> {
for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] {
assert!(!m.is_info());
assert_eq!(m.viewtype, Viewtype::Call);
let info = t.load_call_by_id(m.id).await?;
let info = t
.load_call_by_id(m.id)
.await?
.expect("m should be a call message");
assert!(!info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Outgoing call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
// Bob receives the message referring to the call on two devices;
@@ -58,11 +76,15 @@ async fn setup_call() -> Result<CallSetup> {
t.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCall { .. }))
.await;
let info = t.load_call_by_id(m.id).await?;
let info = t
.load_call_by_id(m.id)
.await?
.expect("IncomingCall event should refer to a call message");
assert!(info.is_incoming());
assert!(!info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_eq!(info.place_call_info, PLACE_INFO);
assert_text(t, m.id, "Incoming call").await?;
assert_eq!(call_state(t, m.id).await?, CallState::Alerting);
}
Ok(CallSetup {
@@ -90,24 +112,32 @@ async fn accept_call() -> Result<CallSetup> {
} = setup_call().await?;
// Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, "accept-info-456".to_string())
bob.accept_incoming_call(bob_call.id, ACCEPT_INFO.to_string())
.await?;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let sent2 = bob.pop_sent_msg().await;
let info = bob.load_call_by_id(bob_call.id).await?;
let info = bob
.load_call_by_id(bob_call.id)
.await?
.expect("bob_call should be a call message");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Active);
bob2.recv_msg_trash(&sent2).await;
assert_text(&bob, bob_call.id, "Incoming call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await;
let info = bob2.load_call_by_id(bob2_call.id).await?;
let info = bob2
.load_call_by_id(bob2_call.id)
.await?
.expect("bob2_call should be a call message");
assert!(info.is_accepted());
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Active);
// Alice receives the acceptance message
alice.recv_msg_trash(&sent2).await;
@@ -119,13 +149,18 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(
ev,
EventType::OutgoingCallAccepted {
msg_id: alice2_call.id,
accept_call_info: "accept-info-456".to_string()
msg_id: alice_call.id,
chat_id: alice_call.chat_id,
accept_call_info: ACCEPT_INFO.to_string()
}
);
let info = alice.load_call_by_id(alice_call.id).await?;
let info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(info.is_accepted());
assert_eq!(info.place_call_info, "place-info-123");
assert_eq!(info.place_call_info, PLACE_INFO);
assert_eq!(call_state(&alice, alice_call.id).await?, CallState::Active);
alice2.recv_msg_trash(&sent2).await;
assert_text(&alice2, alice2_call.id, "Outgoing call").await?;
@@ -133,6 +168,10 @@ async fn accept_call() -> Result<CallSetup> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::OutgoingCallAccepted { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Active
);
Ok(CallSetup {
alice,
@@ -168,12 +207,20 @@ async fn test_accept_call_callee_ends() -> Result<()> {
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
assert!(matches!(
call_state(&bob, bob_call.id).await?,
CallState::Completed { .. }
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob2, bob2_call.id).await?,
CallState::Completed { .. }
));
// Alice receives the ending message
alice.recv_msg_trash(&sent3).await;
@@ -182,6 +229,10 @@ async fn test_accept_call_callee_ends() -> Result<()> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice, alice_call.id).await?,
CallState::Completed { .. }
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
@@ -189,6 +240,10 @@ async fn test_accept_call_callee_ends() -> Result<()> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice2, alice2_call.id).await?,
CallState::Completed { .. }
));
Ok(())
}
@@ -216,6 +271,10 @@ async fn test_accept_call_caller_ends() -> Result<()> {
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
assert!(matches!(
call_state(&alice, alice_call.id).await?,
CallState::Completed { .. }
));
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Outgoing call\n<1 minute").await?;
@@ -223,6 +282,10 @@ async fn test_accept_call_caller_ends() -> Result<()> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&alice2, alice2_call.id).await?,
CallState::Completed { .. }
));
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
@@ -230,12 +293,20 @@ async fn test_accept_call_caller_ends() -> Result<()> {
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob, bob_call.id).await?,
CallState::Completed { .. }
));
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Incoming call\n<1 minute").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert!(matches!(
call_state(&bob2, bob2_call.id).await?,
CallState::Completed { .. }
));
Ok(())
}
@@ -263,12 +334,14 @@ async fn test_callee_rejects_call() -> Result<()> {
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Declined);
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Declined call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Declined);
// Alice receives decline message
alice.recv_msg_trash(&sent3).await;
@@ -277,6 +350,10 @@ async fn test_callee_rejects_call() -> Result<()> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice, alice_call.id).await?,
CallState::Declined
);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Declined call").await?;
@@ -284,6 +361,10 @@ async fn test_callee_rejects_call() -> Result<()> {
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Declined
);
Ok(())
}
@@ -305,19 +386,27 @@ async fn test_caller_cancels_call() -> Result<()> {
// Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?;
assert_text(&alice, alice_call.id, "Cancelled call").await?;
assert_text(&alice, alice_call.id, "Canceled call").await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
assert_eq!(
call_state(&alice, alice_call.id).await?,
CallState::Canceled
);
alice2.recv_msg_trash(&sent3).await;
assert_text(&alice2, alice2_call.id, "Cancelled call").await?;
assert_text(&alice2, alice2_call.id, "Canceled call").await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(
call_state(&alice2, alice2_call.id).await?,
CallState::Canceled
);
// Bob receives the ending message
bob.recv_msg_trash(&sent3).await;
@@ -325,12 +414,19 @@ async fn test_caller_cancels_call() -> Result<()> {
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob, bob_call.id).await?, CallState::Missed);
// Test that message summary says it is a missed call.
let bob_call_msg = Message::load_from_db(&bob, bob_call.id).await?;
let summary = bob_call_msg.get_summary(&bob, None).await?;
assert_eq!(summary.text, "📞 Missed call");
bob2.recv_msg_trash(&sent3).await;
assert_text(&bob2, bob2_call.id, "Missed call").await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
assert_eq!(call_state(&bob2, bob2_call.id).await?, CallState::Missed);
Ok(())
}
@@ -381,14 +477,20 @@ async fn test_mark_calls() -> Result<()> {
alice, alice_call, ..
} = setup_call().await?;
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(!call_info.is_accepted());
assert!(!call_info.is_ended());
call_info.mark_as_accepted(&alice).await?;
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
let mut call_info: CallInfo = alice.load_call_by_id(alice_call.id).await?;
let mut call_info: CallInfo = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
assert!(call_info.is_accepted());
assert!(!call_info.is_ended());
@@ -400,12 +502,15 @@ async fn test_mark_calls() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_udpate_call_text() -> Result<()> {
async fn test_update_call_text() -> Result<()> {
let CallSetup {
alice, alice_call, ..
} = setup_call().await?;
let call_info = alice.load_call_by_id(alice_call.id).await?;
let call_info = alice
.load_call_by_id(alice_call.id)
.await?
.expect("alice_call should be a call message");
call_info.update_text(&alice, "foo bar").await?;
let alice_call = Message::load_from_db(&alice, alice_call.id).await?;
@@ -413,3 +518,151 @@ async fn test_udpate_call_text() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sdp_has_video() {
assert!(sdp_has_video("foobar").is_err());
assert_eq!(sdp_has_video(PLACE_INFO).unwrap(), false);
assert_eq!(sdp_has_video(PLACE_INFO_VIDEO).unwrap(), true);
}
/// Tests that calls are forwarded as text messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forward_call() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let alice_bob_chat = alice.create_chat(bob).await;
let alice_msg_id = alice
.place_outgoing_call(alice_bob_chat.id, PLACE_INFO.to_string())
.await
.context("Failed to place a call")?;
let alice_call = Message::load_from_db(alice, alice_msg_id).await?;
let _alice_sent_call = alice.pop_sent_msg().await;
assert_eq!(alice_call.viewtype, Viewtype::Call);
let alice_charlie_chat = alice.create_chat(charlie).await;
forward_msgs(alice, &[alice_call.id], alice_charlie_chat.id).await?;
let alice_forwarded_call = alice.pop_sent_msg().await;
let alice_forwarded_call_msg = alice_forwarded_call.load_from_db().await;
assert_eq!(alice_forwarded_call_msg.viewtype, Viewtype::Text);
let charlie_forwarded_call = charlie.recv_msg(&alice_forwarded_call).await;
assert_eq!(charlie_forwarded_call.viewtype, Viewtype::Text);
Ok(())
}
/// Tests that "end call" message referring
/// to a text message does not make receive_imf fail.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_end_text_call() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let received1 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Date: Sun, 22 Mar 2020 22:37:57 +0000\n\
Chat-Version: 1.0\n\
\n\
Hello\n",
false,
)
.await?
.unwrap();
assert_eq!(received1.msg_ids.len(), 1);
let msg = Message::load_from_db(alice, received1.msg_ids[0])
.await
.unwrap();
assert_eq!(msg.viewtype, Viewtype::Text);
// Receiving "Call ended" message that refers
// to the text message does not result in an error.
let received2 = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
Date: Sun, 22 Mar 2020 23:37:57 +0000\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n",
false,
)
.await?
.unwrap();
assert_eq!(received2.msg_ids.len(), 1);
assert_eq!(received2.chat_id, DC_CHAT_ID_TRASH);
Ok(())
}
/// Tests that partially downloaded "call ended"
/// messages are not processed.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_partial_calls() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let seen = false;
// The messages in the test
// have no `Date` on purpose,
// so they are treated as new.
let received_call = receive_imf(
alice,
b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call\n\
Chat-Webrtc-Room: YWFhYWFhYWFhCg==\n\
\n\
Hello, this is a call\n",
seen,
)
.await?
.unwrap();
assert_eq!(received_call.msg_ids.len(), 1);
let call_msg = Message::load_from_db(alice, received_call.msg_ids[0])
.await
.unwrap();
assert_eq!(call_msg.viewtype, Viewtype::Call);
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
let imf_raw = b"From: bob@example.net\n\
To: alice@example.org\n\
Message-ID: <second@example.net>\n\
In-Reply-To: <first@example.net>\n\
Chat-Version: 1.0\n\
Chat-Content: call-ended\n\
\n\
Call ended\n";
receive_imf_from_inbox(
alice,
"second@example.net",
imf_raw,
seen,
Some(imf_raw.len().try_into().unwrap()),
)
.await?;
// The call is still not ended.
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Alerting);
// Fully downloading the message ends the call.
receive_imf_from_inbox(alice, "second@example.net", imf_raw, seen, None)
.await
.context("Failed to fully download end call message")?;
assert_eq!(call_state(alice, call_msg.id).await?, CallState::Missed);
Ok(())
}

View File

@@ -373,7 +373,7 @@ impl ChatId {
/// Returns true if the value was modified.
pub(crate) async fn set_blocked(self, context: &Context, new_blocked: Blocked) -> Result<bool> {
if self.is_special() {
bail!("ignoring setting of Block-status for {}", self);
bail!("ignoring setting of Block-status for {self}");
}
let count = context
.sql
@@ -702,8 +702,7 @@ impl ChatId {
) -> Result<()> {
ensure!(
!self.is_special(),
"bad chat_id, can not be special chat: {}",
self
"bad chat_id, can not be special chat: {self}"
);
context
@@ -813,8 +812,7 @@ impl ChatId {
pub(crate) async fn delete_ex(self, context: &Context, sync: sync::Sync) -> Result<()> {
ensure!(
!self.is_special(),
"bad chat_id, can not be a special chat: {}",
self
"bad chat_id, can not be a special chat: {self}"
);
let chat = Chat::load_from_db(context, self).await?;
@@ -1957,7 +1955,7 @@ impl Chat {
}
/// Adds missing values to the msg object,
/// writes the record to the database and returns its msg_id.
/// writes the record to the database.
///
/// If `update_msg_id` is set, that record is reused;
/// if `update_msg_id` is None, a new record is created.
@@ -1966,7 +1964,7 @@ impl Chat {
context: &Context,
msg: &mut Message,
update_msg_id: Option<MsgId>,
) -> Result<MsgId> {
) -> Result<()> {
let mut to_id = 0;
let mut location_id = 0;
@@ -2244,7 +2242,7 @@ impl Chat {
.await?;
}
context.scheduler.interrupt_ephemeral_task().await;
Ok(msg.id)
Ok(())
}
/// Sends a `SyncAction` synchronising chat contacts to other devices.
@@ -2691,10 +2689,7 @@ impl ChatIdBlocked {
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
if msg.viewtype == Viewtype::Text
|| msg.viewtype == Viewtype::VideochatInvitation
|| msg.viewtype == Viewtype::Call
{
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::Call {
// the caller should check if the message text is empty
} else if msg.viewtype.has_file() {
let viewtype_orig = msg.viewtype;
@@ -2972,8 +2967,7 @@ async fn prepare_send_msg(
if !msg.hidden {
chat_id.unarchive_if_not_muted(context, msg.state).await?;
}
msg.id = chat.prepare_msg_raw(context, msg, update_msg_id).await?;
msg.chat_id = chat_id;
chat.prepare_msg_raw(context, msg, update_msg_id).await?;
let row_ids = create_send_msg_jobs(context, msg)
.await
@@ -3149,8 +3143,7 @@ pub async fn send_text_msg(
) -> Result<MsgId> {
ensure!(
!chat_id.is_special(),
"bad chat_id, can not be a special chat: {}",
chat_id
"bad chat_id, can not be a special chat: {chat_id}"
);
let mut msg = Message::new_text(text_to_send);
@@ -3166,10 +3159,6 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin
);
ensure!(!original_msg.is_info(), "Cannot edit info messages");
ensure!(!original_msg.has_html(), "Cannot edit HTML messages");
ensure!(
original_msg.viewtype != Viewtype::VideochatInvitation,
"Cannot edit videochat invitations"
);
ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls");
ensure!(
!original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings
@@ -3218,34 +3207,6 @@ pub(crate) async fn save_text_edit_to_db(
Ok(())
}
/// Sends invitation to a videochat.
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId> {
ensure!(
!chat_id.is_special(),
"video chat invitation cannot be sent to special chat: {}",
chat_id
);
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await? {
if !instance.is_empty() {
instance
} else {
bail!("webrtc_instance is empty");
}
} else {
bail!("webrtc_instance not set");
};
let instance = Message::create_webrtc_instance(&instance, &create_id());
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text =
stock_str::videochat_invite_msg_body(context, &Message::parse_webrtc_instance(&instance).1)
.await;
send_msg(context, chat_id, &mut msg).await
}
async fn donation_request_maybe(context: &Context) -> Result<()> {
let secs_between_checks = 30 * 24 * 60 * 60;
let now = time();
@@ -3950,13 +3911,11 @@ pub(crate) async fn add_contact_to_chat_ex(
let mut chat = Chat::load_from_db(context, chat_id).await?;
ensure!(
chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast,
"{} is not a group/broadcast where one can add members",
chat_id
"{chat_id} is not a group/broadcast where one can add members"
);
ensure!(
Contact::real_exists_by_id(context, contact_id).await? || contact_id == ContactId::SELF,
"invalid contact_id {} for adding to group",
contact_id
"invalid contact_id {contact_id} for adding to group"
);
ensure!(!chat.is_mailing_list(), "Mailing lists can't be changed");
ensure!(
@@ -4169,8 +4128,7 @@ pub async fn remove_contact_from_chat(
) -> Result<()> {
ensure!(
!chat_id.is_special(),
"bad chat_id, can not be special chat: {}",
chat_id
"bad chat_id, can not be special chat: {chat_id}"
);
ensure!(
!contact_id.is_special() || contact_id == ContactId::SELF,
@@ -4184,7 +4142,7 @@ pub async fn remove_contact_from_chat(
"Cannot remove contact {contact_id} from chat {chat_id}: self not in group."
);
context.emit_event(EventType::ErrorSelfNotInGroup(err_msg.clone()));
bail!("{}", err_msg);
bail!("{err_msg}");
} else {
let mut sync = Nosync;
@@ -4208,7 +4166,7 @@ pub async fn remove_contact_from_chat(
if chat.typ == Chattype::Group && chat.is_promoted() {
let addr = contact.get_addr();
let res = send_member_removal_msg(context, chat_id, contact_id, addr).await;
let res = send_member_removal_msg(context, &chat, contact_id, addr).await;
if contact_id == ContactId::SELF {
res?;
@@ -4232,7 +4190,7 @@ pub async fn remove_contact_from_chat(
// For incoming broadcast channels, it's not possible to remove members,
// but it's possible to leave:
let self_addr = context.get_primary_self_addr().await?;
send_member_removal_msg(context, chat_id, contact_id, &self_addr).await?;
send_member_removal_msg(context, &chat, contact_id, &self_addr).await?;
} else {
bail!("Cannot remove members from non-group chats.");
}
@@ -4242,14 +4200,18 @@ pub async fn remove_contact_from_chat(
async fn send_member_removal_msg(
context: &Context,
chat_id: ChatId,
chat: &Chat,
contact_id: ContactId,
addr: &str,
) -> Result<MsgId> {
let mut msg = Message::new(Viewtype::Text);
if contact_id == ContactId::SELF {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
if chat.typ == Chattype::InBroadcast {
msg.text = stock_str::msg_you_left_broadcast(context).await;
} else {
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
}
} else {
msg.text = stock_str::msg_del_member_local(context, contact_id, ContactId::SELF).await;
}
@@ -4259,7 +4221,7 @@ async fn send_member_removal_msg(
msg.param
.set(Param::ContactAddedRemoved, contact_id.to_u32());
send_msg(context, chat_id, &mut msg).await
send_msg(context, chat.id, &mut msg).await
}
async fn set_group_explicitly_left(context: &Context, grpid: &str) -> Result<()> {
@@ -4400,7 +4362,7 @@ pub async fn set_chat_profile_image(
msg.text = stock_str::msg_grp_img_changed(context, ContactId::SELF).await;
}
chat.update_param(context).await?;
if chat.is_promoted() && !chat.is_mailing_list() {
if chat.is_promoted() {
msg.id = send_msg(context, chat_id, &mut msg).await?;
context.emit_msgs_changed(chat_id, msg.id);
}
@@ -4422,7 +4384,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.await?;
let mut chat = Chat::load_from_db(context, chat_id).await?;
if let Some(reason) = chat.why_cant_send(context).await? {
bail!("cannot send to {}: {}", chat_id, reason);
bail!("cannot send to {chat_id}: {reason}");
}
curr_timestamp = create_smeared_timestamps(context, msg_ids.len());
let mut msgs = Vec::with_capacity(msg_ids.len());
@@ -4447,6 +4409,10 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
.set_int(Param::Forwarded, src_msg_id.to_u32() as i32);
}
if msg.get_viewtype() == Viewtype::Call {
msg.viewtype = Viewtype::Text;
}
msg.param.remove(Param::GuaranteeE2ee);
msg.param.remove(Param::ForcePlaintext);
msg.param.remove(Param::Cmd);
@@ -4456,6 +4422,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.param.remove(Param::WebxdcSummary);
msg.param.remove(Param::WebxdcSummaryTimestamp);
msg.param.remove(Param::IsEdited);
msg.param.remove(Param::WebrtcRoom);
msg.param.remove(Param::WebrtcAccepted);
msg.in_reply_to = None;
// do not leak data as group names; a default subject is generated by mimefactory
@@ -4464,13 +4432,13 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
msg.state = MessageState::OutPending;
msg.rfc724_mid = create_outgoing_rfc724_mid();
msg.timestamp_sort = curr_timestamp;
let new_msg_id = chat.prepare_msg_raw(context, &mut msg, None).await?;
chat.prepare_msg_raw(context, &mut msg, None).await?;
curr_timestamp += 1;
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
}
created_msgs.push(new_msg_id);
created_msgs.push(msg.id);
}
for msg_id in created_msgs {
context.emit_msgs_changed(chat_id, msg_id);

View File

@@ -34,7 +34,7 @@ async fn test_chat_info() {
"archived": false,
"param": "",
"is_sending_locations": false,
"color": 29377,
"color": 29381,
"profile_image": {},
"draft": "",
"is_muted": false,
@@ -1933,7 +1933,7 @@ async fn test_chat_get_color() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group_ex(&t, None, "a chat").await?;
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
assert_eq!(color1, 0x613dd7);
assert_eq!(color1, 0x6239dc);
// upper-/lowercase makes a difference for the colors, these are different groups
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
@@ -3026,7 +3026,7 @@ async fn test_leave_broadcast() -> Result<()> {
}
/// Tests that if Bob leaves a broadcast channel with one device,
/// the other device shows a correct info message "You left.".
/// the other device shows a correct info message "You left the channel.".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_broadcast_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3061,10 +3061,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
assert_eq!(rcvd.chat_id, bob1_hello.chat_id);
assert!(rcvd.is_info());
assert_eq!(rcvd.get_info_type(), SystemMessage::MemberRemovedFromGroup);
assert_eq!(
rcvd.text,
stock_str::msg_group_left_local(bob1, ContactId::SELF).await
);
assert_eq!(rcvd.text, stock_str::msg_you_left_broadcast(bob1).await);
Ok(())
}
@@ -3173,6 +3170,30 @@ async fn test_chat_get_encryption_info() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_out_failed_on_all_keys_missing() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = bob
.create_group_with_members(ProtectionStatus::Unprotected, "", &[alice, fiona])
.await;
bob.send_text(bob_chat_id, "Gossiping Fiona's key").await;
alice
.recv_msg(&bob.send_text(bob_chat_id, "No key gossip").await)
.await;
SystemTime::shift(Duration::from_secs(60));
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
let alice_chat_id = alice.recv_msg(&bob.pop_sent_msg().await).await.chat_id;
alice_chat_id.accept(alice).await?;
let mut msg = Message::new_text("Hi".to_string());
send_msg(alice, alice_chat_id, &mut msg).await.ok();
assert_eq!(msg.id.get_state(alice).await?, MessageState::OutFailed);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_chat_media() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -4136,7 +4157,7 @@ async fn test_past_members() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn non_member_cannot_modify_member_list() -> Result<()> {
async fn test_non_member_cannot_modify_member_list() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
@@ -4168,6 +4189,12 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
alice.recv_msg_trash(&bob_sent_add_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
// The same for removal.
let bob_alice_contact_id = bob.add_or_lookup_contact_id(alice).await;
remove_contact_from_chat(bob, bob_chat_id, bob_alice_contact_id).await?;
let bob_sent_add_msg = bob.pop_sent_msg().await;
alice.recv_msg_trash(&bob_sent_add_msg).await;
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 1);
Ok(())
}
@@ -4541,17 +4568,6 @@ async fn test_cannot_send_edit_request() -> Result<()> {
.is_err()
);
// Videochat invitations cannot be edited
alice
.set_config(Config::WebrtcInstance, Some("https://foo.bar"))
.await?;
let msg_id = send_videochat_invitation(alice, chat_id).await?;
assert!(
send_edit_request(alice, msg_id, "bar".to_string())
.await
.is_err()
);
// If not text was given initally, there is nothing to edit
// (this also avoids complexity in UI element changes; focus is typos and rewordings)
let mut msg = Message::new(Viewtype::File);

View File

@@ -28,7 +28,7 @@ fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
/// Lightness is set to half (0.5) to make colors suitable both for light and dark theme.
pub fn str_to_color(s: &str) -> u32 {
let lightness = 0.5;
let chroma = 0.22;
let chroma = 0.23;
let angle = str_to_angle(s);
let oklch = Oklch::new(lightness, chroma, angle);
let rgb = oklch.to_rgb(TransferFunction::Srgb);

View File

@@ -346,9 +346,6 @@ pub enum Config {
/// Unset, when quota falls below minimal warning threshold again.
QuotaExceeding,
/// address to webrtc instance to use for videochats
WebrtcInstance,
/// Timestamp of the last time housekeeping was run
LastHousekeeping,
@@ -413,16 +410,6 @@ pub enum Config {
#[strum(props(default = "172800"))]
GossipPeriod,
/// Deprecated 2025-07. Feature flag for verified 1:1 chats; the UI should set it
/// to 1 if it supports verified 1:1 chats.
/// Regardless of this setting, `chat.is_protected()` returns true while the key is verified,
/// and when the key changes, an info message is posted into the chat.
/// 0=Nothing else happens when the key changes.
/// 1=After the key changed, `can_send()` returns false
/// until `chat_id.accept()` is called.
#[strum(props(default = "0"))]
VerifiedOneOnOneChats,
/// Row ID of the key in the `keypairs` table
/// used for signatures, encryption to self and included in `Autocrypt` header.
KeyId,
@@ -450,6 +437,9 @@ pub enum Config {
/// to avoid encrypting it differently and
/// storing the same token multiple times on the server.
EncryptedDeviceToken,
/// Return an error from `receive_imf_inner()` for a fully downloaded message. For tests.
FailOnReceivingFullMsg,
}
impl Config {

View File

@@ -137,7 +137,7 @@ impl Context {
let res = self
.inner_configure(param)
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
.race(cancel_channel.recv().map(|_| Err(format_err!("Canceled"))))
.await;
self.free_ongoing().await;

View File

@@ -106,7 +106,7 @@ fn parse_server<B: BufRead>(
}
}
Event::Text(ref event) => {
let val = event.unescape().unwrap_or_default().trim().to_owned();
let val = event.xml_content().unwrap_or_default().trim().to_owned();
match tag_config {
MozConfigTag::Hostname => hostname = Some(val),

View File

@@ -79,7 +79,7 @@ fn parse_protocol<B: BufRead>(
}
}
Event::Text(ref e) => {
let val = e.unescape().unwrap_or_default();
let val = e.xml_content().unwrap_or_default();
if let Some(ref tag) = current_tag {
match tag.as_str() {
@@ -123,7 +123,7 @@ fn parse_redirecturl<B: BufRead>(
let mut buf = Vec::new();
match reader.read_event_into(&mut buf)? {
Event::Text(ref e) => {
let val = e.unescape().unwrap_or_default();
let val = e.xml_content().unwrap_or_default();
Ok(val.trim().to_string())
}
_ => Ok("".to_string()),

View File

@@ -60,23 +60,6 @@ pub enum MediaQuality {
Worse = 1,
}
/// Video chat URL type.
#[derive(
Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
)]
#[repr(i8)]
pub enum VideochatType {
/// Unknown type.
#[default]
Unknown = 0,
/// [basicWebRTC](https://github.com/cracker0dks/basicwebrtc) instance.
BasicWebrtc = 1,
/// [Jitsi Meet](https://jitsi.org/jitsi-meet/) instance.
Jitsi = 2,
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -98,6 +81,7 @@ pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14;
// reference is the release date.
// as not all system get speedy updates,
// do not use too small value that will annoy users checking for nonexistent updates.
// "90 days" has proven to be too short at some point (user were informed but there was no update)
pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 183;
/// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again)
@@ -307,16 +291,4 @@ mod tests {
assert_eq!(MediaQuality::Balanced, MediaQuality::from_i32(0).unwrap());
assert_eq!(MediaQuality::Worse, MediaQuality::from_i32(1).unwrap());
}
#[test]
fn test_videochattype_values() {
// values may be written to disk and must not change
assert_eq!(VideochatType::Unknown, VideochatType::default());
assert_eq!(VideochatType::Unknown, VideochatType::from_i32(0).unwrap());
assert_eq!(
VideochatType::BasicWebrtc,
VideochatType::from_i32(1).unwrap()
);
assert_eq!(VideochatType::Jitsi, VideochatType::from_i32(2).unwrap());
}
}

View File

@@ -1582,6 +1582,8 @@ impl Contact {
pub fn get_color(&self) -> u32 {
if let Some(fingerprint) = self.fingerprint() {
str_to_color(&fingerprint.hex())
} else if self.id == ContactId::SELF {
0x808080
} else {
str_to_color(&self.addr.to_lowercase())
}
@@ -1742,8 +1744,7 @@ pub(crate) async fn set_blocked(
) -> Result<()> {
ensure!(
!contact_id.is_special(),
"Can't block special contact {}",
contact_id
"Can't block special contact {contact_id}"
);
let contact = Contact::get_by_id(context, contact_id).await?;

View File

@@ -4,6 +4,7 @@ use super::*;
use crate::chat::{Chat, ProtectionStatus, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
#[test]
@@ -759,7 +760,7 @@ async fn test_contact_get_color() -> Result<()> {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
assert_eq!(color1, 0x4947dc);
assert_eq!(color1, 0x4844e2);
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
@@ -773,6 +774,20 @@ async fn test_contact_get_color() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_self_color_vs_key() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.unconfigured().await;
t.configure_addr("alice@example.org").await;
assert!(t.is_configured().await?);
let color = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
assert_eq!(color, 0x808080);
get_securejoin_qr(t, None).await?;
let color1 = Contact::get_by_id(t, ContactId::SELF).await?.get_color();
assert_ne!(color1, color);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_contact_get_encrinfo() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -972,12 +972,6 @@ impl Context {
res.insert("private_key_count", prv_key_cnt.to_string());
res.insert("public_key_count", pub_key_cnt.to_string());
res.insert("fingerprint", fingerprint_str);
res.insert(
"webrtc_instance",
self.get_config(Config::WebrtcInstance)
.await?
.unwrap_or_else(|| "<unset>".to_string()),
);
res.insert(
"media_quality",
self.get_config_int(Config::MediaQuality).await?.to_string(),
@@ -1054,12 +1048,6 @@ impl Context {
"gossip_period",
self.get_config_int(Config::GossipPeriod).await?.to_string(),
);
res.insert(
"verified_one_on_one_chats", // deprecated 2025-07
self.get_config_bool(Config::VerifiedOneOnOneChats)
.await?
.to_string(),
);
res.insert(
"webxdc_realtime_enabled",
self.get_config_bool(Config::WebxdcRealtimeEnabled)
@@ -1079,6 +1067,13 @@ impl Context {
.await?
.unwrap_or_default(),
);
res.insert(
"fail_on_receiving_full_msg",
self.sql
.get_raw_config("fail_on_receiving_full_msg")
.await?
.unwrap_or_default(),
);
let elapsed = time_elapsed(&self.creation_time);
res.insert("uptime", duration_to_str(elapsed));

View File

@@ -7,6 +7,7 @@ use std::sync::LazyLock;
use quick_xml::{
Reader,
errors::Error as QuickXmlError,
events::{BytesEnd, BytesStart, BytesText},
};
@@ -132,6 +133,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
reader.config_mut().check_end_names = false;
let mut buf = Vec::new();
let mut char_buf = String::with_capacity(4);
loop {
match reader.read_event_into(&mut buf) {
@@ -140,16 +142,9 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
}
Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml),
Ok(quick_xml::events::Event::CData(e)) => match e.escape() {
Ok(e) => dehtml_text_cb(&e, &mut dehtml),
Err(e) => {
eprintln!(
"CDATA escape error at position {}: {:?}",
reader.buffer_position(),
e,
);
}
},
Ok(quick_xml::events::Event::CData(e)) => {
str_cb(&String::from_utf8_lossy(&e as &[_]), &mut dehtml)
}
Ok(quick_xml::events::Event::Empty(ref e)) => {
// Handle empty tags as a start tag immediately followed by end tag.
// For example, `<p/>` is treated as `<p></p>`.
@@ -159,6 +154,33 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
&mut dehtml,
);
}
Ok(quick_xml::events::Event::GeneralRef(ref e)) => {
match e.resolve_char_ref() {
Err(err) => eprintln!(
"resolve_char_ref() error at position {}: {:?}",
reader.buffer_position(),
err,
),
Ok(Some(ch)) => {
char_buf.clear();
char_buf.push(ch);
str_cb(&char_buf, &mut dehtml);
}
Ok(None) => {
let event_str = String::from_utf8_lossy(e);
if let Some(s) = quick_xml::escape::resolve_html5_entity(&event_str) {
str_cb(s, &mut dehtml);
} else {
// Nonstandard entity. Add escaped.
str_cb(&format!("&{event_str};"), &mut dehtml);
}
}
}
}
Err(QuickXmlError::IllFormed(_)) => {
// This is probably not HTML at all and should be left as is.
str_cb(&String::from_utf8_lossy(&buf), &mut dehtml);
}
Err(e) => {
eprintln!(
"Parse html error: Error at position {}: {:?}",
@@ -176,36 +198,36 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
static LINE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
{
let event = event as &[_];
let event_str = std::str::from_utf8(event).unwrap_or_default();
let mut last_added = escaper::decode_html_buf_sloppy(event).unwrap_or_default();
if event_str.starts_with(&last_added) {
last_added = event_str.to_string();
str_cb(event_str, dehtml);
}
}
fn str_cb(event_str: &str, dehtml: &mut Dehtml) {
static LINE_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
let add_text = dehtml.get_add_text();
if add_text == AddText::YesRemoveLineEnds {
// Replace all line ends with spaces.
// E.g. `\r\n\r\n` is replaced with one space.
let event_str = LINE_RE.replace_all(event_str, " ");
// Add a space if `event_str` starts with a space
// and there is no whitespace at the end of the buffer yet.
// Trim the rest of leading whitespace from `event_str`.
let buf = dehtml.get_buf();
if !buf.ends_with(' ') && !buf.ends_with('\n') && event_str.starts_with(' ') {
*buf += " ";
}
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
// Replace all line ends with spaces.
// E.g. `\r\n\r\n` is replaced with one space.
let last_added = LINE_RE.replace_all(&last_added, " ");
// Add a space if `last_added` starts with a space
// and there is no whitespace at the end of the buffer yet.
// Trim the rest of leading whitespace from `last_added`.
let buf = dehtml.get_buf();
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
*buf += " ";
}
*buf += last_added.trim_start();
} else {
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
}
*buf += event_str.trim_start();
} else if add_text == AddText::YesPreserveLineEnds {
*dehtml.get_buf() += LINE_RE.replace_all(event_str, "\n").as_ref();
}
}

View File

@@ -238,14 +238,20 @@ impl MimeMessage {
/// the mime-structure itself is not available.
///
/// The placeholder part currently contains a text with size and availability of the message;
/// `error` is set as the part error;
/// in the future, we may do more advanced things as previews here.
pub(crate) async fn create_stub_from_partial_download(
&mut self,
context: &Context,
org_bytes: u32,
error: Option<String>,
) -> Result<()> {
let prefix = match error {
None => "",
Some(_) => "[❗] ",
};
let mut text = format!(
"[{}]",
"{prefix}[{}]",
stock_str::partial_download_msg_body(context, org_bytes).await
);
if let Some(delete_server_after) = context.get_config_delete_server_after().await? {
@@ -259,9 +265,10 @@ impl MimeMessage {
info!(context, "Partial download: {}", text);
self.parts.push(Part {
self.do_add_single_part(Part {
typ: Viewtype::Text,
msg: text,
error,
..Default::default()
});
@@ -276,8 +283,9 @@ mod tests {
use super::*;
use crate::chat::{get_chat_msgs, send_msg};
use crate::ephemeral::Timer;
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf_from_inbox;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext};
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
#[test]
fn test_downloadstate_values() {
@@ -536,4 +544,43 @@ mod tests {
Ok(())
}
/// Tests that fully downloading the message
/// works even if the Message-ID already exists
/// in the database assigned to the trash chat.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_partial_download_trashed() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let imf_raw = b"From: Bob <bob@example.org>\n\
To: Alice <alice@example.org>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain";
// Download message from Bob partially.
let partial_received_msg =
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, Some(100000))
.await?
.unwrap();
assert_eq!(partial_received_msg.msg_ids.len(), 1);
// Delete the received message.
// Not it is still in the database,
// but in the trash chat.
delete_msgs(alice, &[partial_received_msg.msg_ids[0]]).await?;
// Fully download message after deletion.
let full_received_msg =
receive_imf_from_inbox(alice, "first@example.org", imf_raw, false, None).await?;
// The message does not reappear.
// However, `receive_imf` should not fail.
assert!(full_received_msg.is_none());
Ok(())
}
}

View File

@@ -376,6 +376,10 @@ pub(crate) async fn start_chat_ephemeral_timers(context: &Context, chat_id: Chat
/// `delete_device_after` setting or `ephemeral_timestamp` column.
///
/// For each message a row ID, chat id, viewtype and location ID is returned.
///
/// Unknown viewtypes are returned as `Viewtype::Unknown`
/// and not as errors bubbled up, easily resulting in infinite loop or leaving messages undeleted.
/// (Happens when viewtypes are removed or added on another device which was backup/add-second-device source)
async fn select_expired_messages(
context: &Context,
now: i64,
@@ -395,7 +399,11 @@ WHERE
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row.get("type")?;
let viewtype: Viewtype = row
.get("type")
.context("Using default viewtype for ephemeral handling.")
.log_err(context)
.unwrap_or_default();
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},
@@ -437,7 +445,11 @@ WHERE
|row| {
let id: MsgId = row.get("id")?;
let chat_id: ChatId = row.get("chat_id")?;
let viewtype: Viewtype = row.get("type")?;
let viewtype: Viewtype = row
.get("type")
.context("Using default viewtype for delete-old handling.")
.log_err(context)
.unwrap_or_default();
let location_id: u32 = row.get("location_id")?;
Ok((id, chat_id, viewtype, location_id))
},

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use crate::chat::ChatId;
use crate::config::Config;
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::ephemeral::Timer as EphemeralTimer;
use crate::message::MsgId;
@@ -272,11 +273,13 @@ pub enum EventType {
/// ID of the contact that wants to join.
contact_id: ContactId,
/// Progress as:
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
/// 600=vg-/vc-request-with-auth received and verified, typically shown as "bob@addr verified".
/// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol.
/// 1000=Protocol finished for this contact.
/// ID of the chat in case of success.
chat_id: ChatId,
/// The type of the joined chat.
chat_type: Chattype,
/// Progress, always 1000.
progress: usize,
},
@@ -380,20 +383,28 @@ pub enum EventType {
IncomingCall {
/// ID of the message referring to the call.
msg_id: MsgId,
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// User-defined info as passed to place_outgoing_call()
place_call_info: String,
/// True if incoming call is a video call.
has_video: bool,
},
/// Incoming call accepted.
IncomingCallAccepted {
/// ID of the message referring to the call.
msg_id: MsgId,
/// ID of the chat which the message belongs to.
chat_id: ChatId,
},
/// Outgoing call accepted.
OutgoingCallAccepted {
/// ID of the message referring to the call.
msg_id: MsgId,
/// ID of the chat which the message belongs to.
chat_id: ChatId,
/// User-defined info as passed to accept_incoming_call()
accept_call_info: String,
},
@@ -402,6 +413,8 @@ pub enum EventType {
CallEnded {
/// ID of the message referring to the call.
msg_id: MsgId,
/// ID of the chat which the message belongs to.
chat_id: ChatId,
},
/// Event for using in tests, e.g. as a fence between normally generated events.

View File

@@ -119,6 +119,11 @@ pub enum HeaderDef {
AuthenticationResults,
/// Node address from iroh where direct addresses have been removed.
///
/// The node address sent in this header must have
/// a non-null relay URL as contacting home relay
/// is the only way to reach the node without
/// direct addresses and global discovery.
IrohNodeAddr,
/// Advertised gossip topic for one webxdc.

View File

@@ -24,6 +24,7 @@ use rand::Rng;
use ratelimit::Ratelimit;
use url::Url;
use crate::calls::{create_fallback_ice_servers, create_ice_servers_from_metadata};
use crate::chat::{self, ChatId, ChatIdBlocked};
use crate::chatlist_events;
use crate::config::Config;
@@ -47,7 +48,7 @@ use crate::receive_imf::{
};
use crate::scheduler::connectivity::ConnectivityStore;
use crate::stock_str;
use crate::tools::{self, create_id, duration_to_str};
use crate::tools::{self, create_id, duration_to_str, time};
pub(crate) mod capabilities;
mod client;
@@ -123,6 +124,18 @@ pub(crate) struct ServerMetadata {
pub admin: Option<String>,
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,
/// Timestamp when ICE servers are considered
/// expired and should be updated.
pub ice_servers_expiration_timestamp: i64,
}
impl async_imap::Authenticator for OAuth2 {
@@ -146,7 +159,6 @@ pub enum FolderMeaning {
Mvbox,
Sent,
Trash,
Drafts,
/// Virtual folders.
///
@@ -166,7 +178,6 @@ impl FolderMeaning {
FolderMeaning::Mvbox => Some(Config::ConfiguredMvboxFolder),
FolderMeaning::Sent => Some(Config::ConfiguredSentboxFolder),
FolderMeaning::Trash => Some(Config::ConfiguredTrashFolder),
FolderMeaning::Drafts => None,
FolderMeaning::Virtual => None,
}
}
@@ -555,10 +566,38 @@ impl Imap {
}
session.new_mail = false;
let mut read_cnt = 0;
loop {
let (n, fetch_more) = self
.fetch_new_msg_batch(context, session, folder, folder_meaning)
.await?;
read_cnt += n;
if !fetch_more {
return Ok(read_cnt > 0);
}
}
}
/// Returns number of messages processed and whether the function should be called again.
async fn fetch_new_msg_batch(
&mut self,
context: &Context,
session: &mut Session,
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<(usize, bool)> {
let uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
info!(
context,
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
);
let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
let uids_to_prefetch = 500;
let msgs = session
.prefetch(old_uid_next, uids_to_prefetch)
.await
.context("prefetch")?;
let read_cnt = msgs.len();
let download_limit = context.download_limit().await?;
@@ -718,7 +757,8 @@ impl Imap {
largest_uid_fetched
};
let actually_download_messages_future = async move {
let actually_download_messages_future = async {
let sender = sender;
let mut uids_fetch_in_batch = Vec::with_capacity(max(uids_fetch.len(), 1));
let mut fetch_partially = false;
uids_fetch.push((0, !uids_fetch.last().unwrap_or(&(0, false)).1));
@@ -753,14 +793,17 @@ impl Imap {
// if the message has arrived after selecting mailbox
// and determining its UIDNEXT and before prefetch.
let mut new_uid_next = largest_uid_fetched + 1;
if fetch_res.is_ok() {
let fetch_more = fetch_res.is_ok() && {
let prefetch_uid_next = old_uid_next + uids_to_prefetch;
// If we have successfully fetched all messages we planned during prefetch,
// then we have covered at least the range between old UIDNEXT
// and UIDNEXT of the mailbox at the time of selecting it.
new_uid_next = max(new_uid_next, mailbox_uid_next);
new_uid_next = max(new_uid_next, min(prefetch_uid_next, mailbox_uid_next));
new_uid_next = max(new_uid_next, largest_uid_skipped.unwrap_or(0) + 1);
}
prefetch_uid_next < mailbox_uid_next
};
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;
}
@@ -777,7 +820,7 @@ impl Imap {
// establish a new session if this one is broken.
fetch_res?;
Ok(read_cnt > 0)
Ok((read_cnt, fetch_more))
}
/// Read the recipients from old emails sent by the user and add them as contacts.
@@ -814,7 +857,10 @@ impl Session {
.context("listing folders for resync")?;
for folder in all_folders {
let folder_meaning = get_folder_meaning(&folder);
if folder_meaning != FolderMeaning::Virtual {
if !matches!(
folder_meaning,
FolderMeaning::Virtual | FolderMeaning::Unknown
) {
self.resync_folder_uids(context, folder.name(), folder_meaning)
.await?;
}
@@ -1466,7 +1512,7 @@ impl Session {
context,
"Passing message UID {} to receive_imf().", request_uid
);
match receive_imf_inner(
let res = receive_imf_inner(
context,
folder,
uidvalidity,
@@ -1474,20 +1520,31 @@ impl Session {
rfc724_mid,
body,
is_seen,
partial,
partial.map(|msg_size| (msg_size, None)),
)
.await
{
Ok(received_msg) => {
received_msgs_channel
.send((request_uid, received_msg))
.await?;
}
Err(err) => {
warn!(context, "receive_imf error: {:#}.", err);
received_msgs_channel.send((request_uid, None)).await?;
.await;
let received_msg = if let Err(err) = res {
warn!(context, "receive_imf error: {:#}.", err);
if partial.is_some() {
return Err(err);
}
receive_imf_inner(
context,
folder,
uidvalidity,
request_uid,
rfc724_mid,
body,
is_seen,
Some((body.len().try_into()?, Some(format!("{err:#}")))),
)
.await?
} else {
res?
};
received_msgs_channel
.send((request_uid, received_msg))
.await?;
}
// If we don't process the whole response, IMAP client is left in a broken state where
@@ -1534,7 +1591,43 @@ impl Session {
}
let mut lock = context.metadata.write().await;
if (*lock).is_some() {
if let Some(ref mut old_metadata) = *lock {
let now = time();
// Refresh TURN server credentials if they expire in 12 hours.
if now + 3600 * 12 < old_metadata.ice_servers_expiration_timestamp {
return Ok(());
}
info!(context, "ICE servers expired, requesting new credentials.");
let mailbox = "";
let options = "";
let metadata = self
.get_metadata(mailbox, options, "(/shared/vendor/deltachat/turn)")
.await?;
let mut got_turn_server = false;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn" {
if let Some(value) = m.value {
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
}
}
if !got_turn_server {
// 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?;
}
return Ok(());
}
@@ -1546,6 +1639,8 @@ impl Session {
let mut comment = None;
let mut admin = None;
let mut iroh_relay = None;
let mut ice_servers = None;
let mut ice_servers_expiration_timestamp = 0;
let mailbox = "";
let options = "";
@@ -1553,7 +1648,7 @@ impl Session {
.get_metadata(
mailbox,
options,
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay)",
"(/shared/comment /shared/admin /shared/vendor/deltachat/irohrelay /shared/vendor/deltachat/turn)",
)
.await?;
for m in metadata {
@@ -1576,13 +1671,36 @@ impl Session {
}
}
}
"/shared/vendor/deltachat/turn" => {
if let Some(value) = m.value {
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
ice_servers_expiration_timestamp = parsed_timestamp;
ice_servers = Some(parsed_ice_servers);
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
}
_ => {}
}
}
let ice_servers = if let Some(ice_servers) = ice_servers {
ice_servers
} 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?
};
*lock = Some(ServerMetadata {
comment,
admin,
iroh_relay,
ice_servers,
ice_servers_expiration_timestamp,
});
Ok(())
}
@@ -2120,27 +2238,6 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
"迷惑メール",
"스팸",
];
const DRAFT_NAMES: &[&str] = &[
"Drafts",
"Kladder",
"Entw?rfe",
"Borradores",
"Brouillons",
"Bozze",
"Concepten",
"Wersje robocze",
"Rascunhos",
"Entwürfe",
"Koncepty",
"Kopie robocze",
"Taslaklar",
"Utkast",
"Πρόχειρα",
"Черновики",
"下書き",
"草稿",
"임시보관함",
];
const TRASH_NAMES: &[&str] = &[
"Trash",
"Bin",
@@ -2167,8 +2264,6 @@ fn get_folder_meaning_by_name(folder_name: &str) -> FolderMeaning {
FolderMeaning::Sent
} else if SPAM_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Spam
} else if DRAFT_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Drafts
} else if TRASH_NAMES.iter().any(|s| s.to_lowercase() == lower) {
FolderMeaning::Trash
} else {
@@ -2182,7 +2277,6 @@ fn get_folder_meaning_by_attrs(folder_attrs: &[NameAttribute]) -> FolderMeaning
NameAttribute::Trash => return FolderMeaning::Trash,
NameAttribute::Sent => return FolderMeaning::Sent,
NameAttribute::Junk => return FolderMeaning::Spam,
NameAttribute::Drafts => return FolderMeaning::Drafts,
NameAttribute::All | NameAttribute::Flagged => return FolderMeaning::Virtual,
NameAttribute::Extension(label) => {
match label.as_ref() {

View File

@@ -73,8 +73,8 @@ impl Imap {
// Don't scan folders that are watched anyway
if !watched_folders.contains(&folder.name().to_string())
&& folder_meaning != FolderMeaning::Drafts
&& folder_meaning != FolderMeaning::Trash
&& folder_meaning != FolderMeaning::Unknown
{
self.fetch_move_delete(context, session, folder.name(), folder_meaning)
.await

View File

@@ -110,14 +110,16 @@ impl Session {
Ok(list)
}
/// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results
/// in the order of ascending delivery time to the server (INTERNALDATE).
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
/// order of ascending delivery time to the server (INTERNALDATE).
pub(crate) async fn prefetch(
&mut self,
uid_next: u32,
n_uids: u32,
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
let uid_last = uid_next.saturating_add(n_uids - 1);
// fetch messages with larger UID than the last one seen
let set = format!("{uid_next}:*");
let set = format!("{uid_next}:{uid_last}");
let mut list = self
.uid_fetch(set, PREFETCH_FLAGS)
.await
@@ -126,16 +128,7 @@ impl Session {
let mut msgs = BTreeMap::new();
while let Some(msg) = list.try_next().await? {
if let Some(msg_uid) = msg.uid {
// If the mailbox is not empty, results always include
// at least one UID, even if last_seen_uid+1 is past
// the last UID in the mailbox. It happens because
// uid:* is interpreted the same way as *:uid.
// See <https://tools.ietf.org/html/rfc3501#page-61> for
// standard reference. Therefore, sometimes we receive
// already seen messages and have to filter them out.
if msg_uid >= uid_next {
msgs.insert((msg.internal_date(), msg_uid), msg);
}
msgs.insert((msg.internal_date(), msg_uid), msg);
}
}

View File

@@ -928,75 +928,56 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_backup() -> Result<()> {
for set_verified_oneonone_chats in [true, false] {
let backup_dir = tempfile::tempdir().unwrap();
let backup_dir = tempfile::tempdir().unwrap();
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
if set_verified_oneonone_chats {
context1
.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await?;
}
let context1 = TestContext::new_alice().await;
assert!(context1.is_configured().await?);
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
let context2 = TestContext::new().await;
assert!(!context2.is_configured().await?);
assert!(has_backup(&context2, backup_dir.path()).await.is_err());
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(
imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
// export from context1
assert!(
imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None)
.await
.is_err()
);
.is_ok()
);
let _event = context1
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
// import to context2
let backup = has_backup(&context2, backup_dir.path()).await?;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
assert_eq!(
context2
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
false
);
assert_eq!(
context1
.get_config_bool(Config::VerifiedOneOnOneChats)
.await?,
set_verified_oneonone_chats
);
}
// Import of unencrypted backup with incorrect "foobar" backup passphrase fails.
assert!(
imex(
&context2,
ImexMode::ImportBackup,
backup.as_ref(),
Some("foobar".to_string())
)
.await
.is_err()
);
assert!(
imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None)
.await
.is_ok()
);
let _event = context2
.evtracker
.get_matching(|evt| matches!(evt, EventType::ImexProgress(1000)))
.await;
assert!(context2.is_configured().await?);
assert_eq!(
context2.get_config(Config::Addr).await?,
Some("alice@example.org".to_string())
);
Ok(())
}

View File

@@ -242,7 +242,7 @@ impl BackupProvider {
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).race(
async {
cancel_token.recv().await.ok();
Err(format_err!("Backup transfer cancelled"))
Err(format_err!("Backup transfer canceled"))
}
).race(
async {
@@ -262,12 +262,12 @@ impl BackupProvider {
}
},
_ = cancel_token.recv() => {
info!(context, "Backup transfer cancelled by the user, stopping accept loop.");
info!(context, "Backup transfer canceled by the user, stopping accept loop.");
context.emit_event(EventType::ImexProgress(0));
break;
}
_ = drop_token.cancelled() => {
info!(context, "Backup transfer cancelled by dropping the provider, stopping accept loop.");
info!(context, "Backup transfer canceled by dropping the provider, stopping accept loop.");
context.emit_event(EventType::ImexProgress(0));
break;
}
@@ -364,7 +364,7 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
let res = get_backup2(context, node_addr, auth_token)
.race(async {
cancel_token.recv().await.ok();
Err(format_err!("Backup reception cancelled"))
Err(format_err!("Backup reception canceled"))
})
.await;
if let Err(ref res) = res {

View File

@@ -15,6 +15,7 @@ use rand::thread_rng;
use tokio::runtime::Handle;
use crate::context::Context;
use crate::events::EventType;
use crate::log::{LogExt, info};
use crate::pgp::KeyPair;
use crate::tools::{self, time_elapsed};
@@ -414,15 +415,11 @@ pub(crate) async fn store_self_keypair(context: &Context, keypair: &KeyPair) ->
"INSERT INTO config (keyname, value) VALUES ('key_id', ?)",
(new_key_id,),
)?;
Ok(Some(new_key_id))
Ok(new_key_id)
})
.await?;
if let Some(new_key_id) = new_key_id {
// Update config cache if transaction succeeded and changed current default key.
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
}
context.emit_event(EventType::AccountsItemChanged);
config_cache_lock.insert("key_id".to_string(), Some(new_key_id.to_string()));
Ok(())
}
@@ -500,7 +497,7 @@ impl std::str::FromStr for Fingerprint {
.filter(|&c| c.is_ascii_hexdigit())
.collect();
let v: Vec<u8> = hex::decode(&hex_repr)?;
ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr);
ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}");
let fp = Fingerprint::new(v);
Ok(fp)
}

View File

@@ -140,7 +140,7 @@ impl Kml {
if self.tag == KmlTag::PlacemarkTimestampWhen
|| self.tag == KmlTag::PlacemarkPointCoordinates
{
let val = event.unescape().unwrap_or_default();
let val = event.xml_content().unwrap_or_default();
let val = val.replace(['\n', '\r', '\t', ' '], "");

View File

@@ -888,6 +888,22 @@ impl ConfiguredLoginParam {
| ConfiguredCertificateChecks::AcceptInvalidCertificates2 => false,
}
}
/// Returns true if strict TLS checks are disabled
/// and configuration is not for a known provider
/// with broken TLS setup.
pub fn strict_tls_manually_disabled(&self) -> bool {
match self.certificate_checks {
ConfiguredCertificateChecks::OldAutomatic => {
// Old "Automatic" configuration defaults to no strict TLS.
// User should upgrade configuration.
self.provider.is_none()
}
ConfiguredCertificateChecks::Automatic | ConfiguredCertificateChecks::Strict => false,
ConfiguredCertificateChecks::AcceptInvalidCertificates
| ConfiguredCertificateChecks::AcceptInvalidCertificates2 => true,
}
}
}
#[cfg(test)]

View File

@@ -15,9 +15,7 @@ use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility, send_msg};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL, VideochatType,
};
use crate::constants::{Blocked, Chattype, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL};
use crate::contact::{self, Contact, ContactId};
use crate::context::Context;
use crate::debug_logging::set_debug_logging_xdc;
@@ -492,8 +490,7 @@ impl Message {
pub async fn load_from_db_optional(context: &Context, id: MsgId) -> Result<Option<Message>> {
ensure!(
!id.is_special(),
"Can not load special message ID {} from DB",
id
"Can not load special message ID {id} from DB"
);
let msg = context
.sql
@@ -568,7 +565,7 @@ impl Message {
timestamp_rcvd: row.get("timestamp_rcvd")?,
ephemeral_timer: row.get("ephemeral_timer")?,
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
viewtype: row.get("type")?,
viewtype: row.get("type").unwrap_or_default(),
state: state.with_mdns(mdn_msg_id.is_some()),
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
@@ -651,8 +648,10 @@ impl Message {
if self.viewtype.has_file() {
let file_param = self.param.get_file_path(context)?;
if let Some(path_and_filename) = file_param {
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
&& !self.param.exists(Param::Width)
if matches!(
self.viewtype,
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) && !self.param.exists(Param::Width)
{
let buf = read_file(context, &path_and_filename).await?;
@@ -1015,85 +1014,6 @@ impl Message {
None
}
// add room to a webrtc_instance as defined by the corresponding config-value;
// the result may still be prefixed by the type
pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String {
let (videochat_type, mut url) = Message::parse_webrtc_instance(instance);
// make sure, there is a scheme in the url
if !url.contains(':') {
url = format!("https://{url}");
}
// add/replace room
let url = if url.contains("$ROOM") {
url.replace("$ROOM", room)
} else if url.contains("$NOROOM") {
// there are some usecases where a separate room is not needed to use a service
// eg. if you let in people manually anyway, see discussion at
// <https://support.delta.chat/t/videochat-with-webex/1412/4>.
// hacks as hiding the room behind `#` are not reliable, therefore,
// these services are supported by adding the string `$NOROOM` to the url.
url.replace("$NOROOM", "")
} else {
// if there nothing that would separate the room, add a slash as a separator;
// this way, urls can be given as "https://meet.jit.si" as well as "https://meet.jit.si/"
let maybe_slash = if url.ends_with('/')
|| url.ends_with('?')
|| url.ends_with('#')
|| url.ends_with('=')
{
""
} else {
"/"
};
format!("{url}{maybe_slash}{room}")
};
// re-add and normalize type
match videochat_type {
VideochatType::BasicWebrtc => format!("basicwebrtc:{url}"),
VideochatType::Jitsi => format!("jitsi:{url}"),
VideochatType::Unknown => url,
}
}
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
let instance: String = instance.split_whitespace().collect();
let mut split = instance.splitn(2, ':');
let type_str = split.next().unwrap_or_default().to_lowercase();
let url = split.next();
match type_str.as_str() {
"basicwebrtc" => (
VideochatType::BasicWebrtc,
url.unwrap_or_default().to_string(),
),
"jitsi" => (VideochatType::Jitsi, url.unwrap_or_default().to_string()),
_ => (VideochatType::Unknown, instance.to_string()),
}
}
/// Returns videochat URL if the message is a videochat invitation.
pub fn get_videochat_url(&self) -> Option<String> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).1);
}
}
None
}
/// Returns videochat type if the message is a videochat invitation.
pub fn get_videochat_type(&self) -> Option<VideochatType> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).0);
}
}
None
}
/// Sets or unsets message text.
pub fn set_text(&mut self, text: String) {
self.text = text;
@@ -2275,9 +2195,6 @@ pub enum Viewtype {
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
/// Message is an incoming or outgoing call.
Call = 71,
@@ -2303,7 +2220,6 @@ impl Viewtype {
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
Viewtype::Call => false,
Viewtype::Webxdc => true,
Viewtype::Vcard => true,

View File

@@ -25,82 +25,6 @@ fn test_guess_msgtype_from_suffix() {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "https://foo/bar");
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "url");
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
assert_eq!(webrtc_type, VideochatType::Unknown);
assert_eq!(url, "https://foo/bar?key=val#key=val");
let (webrtc_type, url) = Message::parse_webrtc_instance("jitsi:https://j.si/foo");
assert_eq!(webrtc_type, VideochatType::Jitsi);
assert_eq!(url, "https://j.si/foo");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_webrtc_instance() {
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
let instance = Message::create_webrtc_instance("https://meet.jit.si/", "123");
assert_eq!(instance, "https://meet.jit.si/123");
let instance = Message::create_webrtc_instance("https://meet.jit.si", "456");
assert_eq!(instance, "https://meet.jit.si/456");
let instance = Message::create_webrtc_instance("meet.jit.si", "789");
assert_eq!(instance, "https://meet.jit.si/789");
let instance = Message::create_webrtc_instance("bla.foo?", "123");
assert_eq!(instance, "https://bla.foo?123");
let instance = Message::create_webrtc_instance("jitsi:bla.foo#", "456");
assert_eq!(instance, "jitsi:https://bla.foo#456");
let instance = Message::create_webrtc_instance("bla.foo#room=", "789");
assert_eq!(instance, "https://bla.foo#room=789");
let instance = Message::create_webrtc_instance("https://bla.foo#room", "123");
assert_eq!(instance, "https://bla.foo#room/123");
let instance = Message::create_webrtc_instance("bla.foo#room$ROOM", "123");
assert_eq!(instance, "https://bla.foo#room123");
let instance = Message::create_webrtc_instance("bla.foo#room=$ROOM&after=cont", "234");
assert_eq!(instance, "https://bla.foo#room=234&after=cont");
let instance = Message::create_webrtc_instance(" meet.jit .si ", "789");
assert_eq!(instance, "https://meet.jit.si/789");
let instance = Message::create_webrtc_instance(" basicwebrtc: basic . stuff\n ", "12345ab");
assert_eq!(instance, "basicwebrtc:https://basic.stuff/12345ab");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_webrtc_instance_noroom() {
// webrtc_instance may come from an input field of the ui, be pretty tolerant on input
let instance = Message::create_webrtc_instance("bla.foo$NOROOM", "123");
assert_eq!(instance, "https://bla.foo");
let instance = Message::create_webrtc_instance(" bla . foo $NOROOM ", "456");
assert_eq!(instance, "https://bla.foo");
let instance = Message::create_webrtc_instance(" $NOROOM bla . foo ", "789");
assert_eq!(instance, "https://bla.foo");
let instance = Message::create_webrtc_instance(" bla.foo / $NOROOM ? a = b ", "123");
assert_eq!(instance, "https://bla.foo/?a=b");
// $ROOM has a higher precedence
let instance = Message::create_webrtc_instance("bla.foo/?$NOROOM=$ROOM", "123");
assert_eq!(instance, "https://bla.foo/?$NOROOM=123");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_width_height() {
let t = TestContext::new_alice().await;
@@ -648,10 +572,6 @@ fn test_viewtype_values() {
assert_eq!(Viewtype::Voice, Viewtype::from_i32(41).unwrap());
assert_eq!(Viewtype::Video, Viewtype::from_i32(50).unwrap());
assert_eq!(Viewtype::File, Viewtype::from_i32(60).unwrap());
assert_eq!(
Viewtype::VideochatInvitation,
Viewtype::from_i32(70).unwrap()
);
assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap());
assert_eq!(Viewtype::Vcard, Viewtype::from_i32(90).unwrap());
}
@@ -827,3 +747,21 @@ async fn test_send_empty_file() -> Result<()> {
assert_eq!(bob_received_msg.get_viewtype(), Viewtype::File);
Ok(())
}
/// Tests that viewtype 70
/// which previously corresponded to videochat invitations,
/// is loaded as unknown viewtype without errors.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_load_unknown_viewtype() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg_id = tcm.send_recv(alice, bob, "Hello!").await.id;
bob.sql
.execute("UPDATE msgs SET type=70 WHERE id=?", (msg_id,))
.await?;
let bob_msg = Message::load_from_db(bob, msg_id).await?;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Unknown);
Ok(())
}

View File

@@ -419,10 +419,7 @@ impl MimeFactory {
None
} else {
if keys.is_empty() && !recipients.is_empty() {
bail!(
"No recipient keys are available, cannot encrypt to {:?}.",
recipients
);
bail!("No recipient keys are available, cannot encrypt to {recipients:?}.");
}
// Remove recipients for which the key is missing.
@@ -1522,16 +1519,19 @@ impl MimeFactory {
));
}
SystemMessage::IrohNodeAddr => {
let node_addr = context
.get_or_try_init_peer_channel()
.await?
.get_node_addr()
.await?;
// We should not send `null` as relay URL
// as this is the only way to reach the node.
debug_assert!(node_addr.relay_url().is_some());
headers.push((
HeaderDef::IrohNodeAddr.into(),
mail_builder::headers::text::Text::new(serde_json::to_string(
&context
.get_or_try_init_peer_channel()
.await?
.get_node_addr()
.await?,
)?)
.into(),
mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
.into(),
));
}
SystemMessage::CallAccepted => {
@@ -1565,11 +1565,6 @@ impl MimeFactory {
"Chat-Content",
mail_builder::headers::raw::Raw::new("sticker").into(),
));
} else if msg.viewtype == Viewtype::VideochatInvitation {
headers.push((
"Chat-Content",
mail_builder::headers::raw::Raw::new("videochat-invitation").into(),
));
} else if msg.viewtype == Viewtype::Call {
headers.push((
"Chat-Content",
@@ -1580,27 +1575,15 @@ impl MimeFactory {
);
}
if msg.param.exists(Param::WebrtcRoom) {
if let Some(offer) = msg.param.get(Param::WebrtcRoom) {
headers.push((
"Chat-Webrtc-Room",
mail_builder::headers::raw::Raw::new(
msg.param
.get(Param::WebrtcRoom)
.unwrap_or_default()
.to_string(),
)
.into(),
mail_builder::headers::raw::Raw::new(b_encode(offer)).into(),
));
} else if msg.param.exists(Param::WebrtcAccepted) {
} else if let Some(answer) = msg.param.get(Param::WebrtcAccepted) {
headers.push((
"Chat-Webrtc-Accepted",
mail_builder::headers::raw::Raw::new(
msg.param
.get(Param::WebrtcAccepted)
.unwrap_or_default()
.to_string(),
)
.into(),
mail_builder::headers::raw::Raw::new(b_encode(answer)).into(),
));
}
@@ -1894,5 +1877,17 @@ fn render_rfc724_mid(rfc724_mid: &str) -> String {
}
}
/// Encodes UTF-8 string as a single B-encoded-word.
///
/// We manually encode some headers because as of
/// version 0.4.4 mail-builder crate does not encode
/// newlines correctly if they appear in a text header.
fn b_encode(value: &str) -> String {
format!(
"=?utf-8?B?{}?=",
base64::engine::general_purpose::STANDARD.encode(value)
)
}
#[cfg(test)]
mod mimefactory_tests;

View File

@@ -240,12 +240,12 @@ const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
impl MimeMessage {
/// Parse a mime message.
///
/// If `partial` is set, it contains the full message size in bytes
/// and `body` contains the header only.
/// If `partial` is set, it contains the full message size in bytes and an optional error text
/// for the partially downloaded message, and `body` contains the HEADER only.
pub(crate) async fn from_bytes(
context: &Context,
body: &[u8],
partial: Option<u32>,
partial: Option<(u32, Option<String>)>,
) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
@@ -351,7 +351,7 @@ impl MimeMessage {
let incoming = !context.is_self_addr(&from.addr).await?;
let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
let mail_raw; // Memory location for a possible decrypted message.
let decrypted_msg; // Decrypted signed OpenPGP message.
@@ -378,11 +378,11 @@ impl MimeMessage {
timestamp_rcvd,
);
if let Some(protected_aheader_value) = decrypted_mail
let protected_aheader_values = decrypted_mail
.headers
.get_header_value(HeaderDef::Autocrypt)
{
aheader_value = Some(protected_aheader_value);
.get_all_values(HeaderDef::Autocrypt.into());
if !protected_aheader_values.is_empty() {
aheader_values = protected_aheader_values;
}
(Ok(decrypted_mail), true)
@@ -400,26 +400,27 @@ impl MimeMessage {
}
};
let autocrypt_header = if !incoming {
None
} else if let Some(aheader_value) = aheader_value {
match Aheader::from_str(&aheader_value) {
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
);
None
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
None
}
let mut autocrypt_header = None;
if incoming {
// See `get_all_addresses_from_header()` for why we take the last valid header.
for val in aheader_values.iter().rev() {
autocrypt_header = match Aheader::from_str(val) {
Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
Ok(header) => {
warn!(
context,
"Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
);
continue;
}
Err(err) => {
warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
continue;
}
};
break;
}
} else {
None
};
}
let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
@@ -611,9 +612,9 @@ impl MimeMessage {
};
match partial {
Some(org_bytes) => {
Some((org_bytes, err)) => {
parser
.create_stub_from_partial_download(context, org_bytes)
.create_stub_from_partial_download(context, org_bytes, err)
.await?;
}
None => match mail {
@@ -633,7 +634,7 @@ impl MimeMessage {
error: Some(format!("Decrypting failed: {err:#}")),
..Default::default()
};
parser.parts.push(part);
parser.do_add_single_part(part);
}
},
};
@@ -731,12 +732,10 @@ impl MimeMessage {
.map(|s| s.to_string());
if let Some(part) = self.parts.first_mut() {
if let Some(room) = room {
if content == "videochat-invitation" {
part.typ = Viewtype::VideochatInvitation;
} else if content == "call" {
part.typ = Viewtype::Call
if content == "call" {
part.typ = Viewtype::Call;
part.param.set(Param::WebrtcRoom, room);
}
part.param.set(Param::WebrtcRoom, room);
} else if let Some(accepted) = accepted {
part.param.set(Param::WebrtcAccepted, accepted);
}
@@ -764,10 +763,7 @@ impl MimeMessage {
| Viewtype::Vcard
| Viewtype::File
| Viewtype::Webxdc => true,
Viewtype::Unknown
| Viewtype::Text
| Viewtype::VideochatInvitation
| Viewtype::Call => false,
Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
})
{
let mut parts = std::mem::take(&mut self.parts);
@@ -1071,47 +1067,61 @@ impl MimeMessage {
)?
.0;
match (mimetype.type_(), mimetype.subtype().as_str()) {
/* Most times, multipart/alternative contains true alternatives
as text/plain and text/html. If we find a multipart/mixed
inside multipart/alternative, we use this (happens eg in
apple mail: "plaintext" as an alternative to "html+PDF attachment") */
(mime::MULTIPART, "alternative") => {
for cur_data in &mail.subparts {
let mime_type = get_mime_type(
// multipart/alternative is described in
// <https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.4>.
// Specification says that last part should be preferred,
// so we iterate over parts in reverse order.
// Search for plain text or multipart part.
//
// If we find a multipart inside multipart/alternative
// and it has usable subparts, we only parse multipart.
// This happens e.g. in Apple Mail:
// "plaintext" as an alternative to "html+PDF attachment".
for cur_data in mail.subparts.iter().rev() {
let (mime_type, _viewtype) = get_mime_type(
cur_data,
&get_attachment_filename(context, cur_data)?,
self.has_chat_version(),
)?
.0;
if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
)?;
if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
break;
}
}
if !any_part_added {
/* search for text/plain and add this */
for cur_data in &mail.subparts {
if get_mime_type(
cur_data,
&get_attachment_filename(context, cur_data)?,
self.has_chat_version(),
)?
.0
.type_()
== mime::TEXT
{
any_part_added = self
.parse_mime_recursive(context, cur_data, is_related)
.await?;
break;
}
// Explicitly look for a `text/calendar` part.
// Messages conforming to <https://datatracker.ietf.org/doc/html/rfc6047>
// contain `text/calendar` part as an alternative
// to the text or HTML representation.
//
// While we cannot display `text/calendar` and therefore do not prefer it,
// we still make it available by presenting as an attachment
// with a generic filename.
for cur_data in mail.subparts.iter().rev() {
let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
let filename = get_attachment_filename(context, cur_data)?
.unwrap_or_else(|| "calendar.ics".to_string());
self.do_add_single_file_part(
context,
Viewtype::File,
mimetype,
&mail.ctype.mimetype.to_lowercase(),
&mail.get_body_raw()?,
&filename,
is_related,
)
.await?;
}
}
if !any_part_added {
/* `text/plain` not found - use the first part */
for cur_part in &mail.subparts {
for cur_part in mail.subparts.iter().rev() {
if self
.parse_mime_recursive(context, cur_part, is_related)
.await?
@@ -1542,7 +1552,7 @@ impl MimeMessage {
Ok(true)
}
fn do_add_single_part(&mut self, mut part: Part) {
pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
if self.was_encrypted() {
part.param.set_int(Param::GuaranteeE2ee, 1);
}
@@ -2070,7 +2080,7 @@ pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
if let Some(id) = parse_message_ids(ids).first() {
Ok(id.to_string())
} else {
bail!("could not parse message_id: {}", ids);
bail!("could not parse message_id: {ids}");
}
}

View File

@@ -476,6 +476,10 @@ async fn test_mimeparser_with_avatars() {
assert!(mimeparser.group_avatar.unwrap().is_change());
}
/// Tests that video chat invitations that are not supported anymore
/// are displayed as text messages.
///
/// User can still click on the link manually.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mimeparser_with_videochat() {
let t = TestContext::new_alice().await;
@@ -483,14 +487,8 @@ async fn test_mimeparser_with_videochat() {
let raw = include_bytes!("../../test-data/message/videochat_invitation.eml");
let mimeparser = MimeMessage::from_bytes(&t, &raw[..], None).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
assert_eq!(
mimeparser.parts[0]
.param
.get(Param::WebrtcRoom)
.unwrap_or_default(),
"https://example.org/p2p/?roomname=6HiduoAn4xN"
);
assert_eq!(mimeparser.parts[0].typ, Viewtype::Text);
assert_eq!(mimeparser.parts[0].param.get(Param::WebrtcRoom), None);
assert!(
mimeparser.parts[0]
.msg
@@ -1990,6 +1988,27 @@ async fn test_chat_edit_imf_header() -> Result<()> {
Ok(())
}
/// Tests that the last valid Autocrypt header is taken:
/// - The 3rd header is skipped because of the unknown critical attribute.
/// - The 2nd header is taken despite it has an unknown non-critical attribute.
/// - The 1st header shouldn't be looked at.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_multiple_autocrypt_hdrs() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
let msg_id = receive_imf(
bob,
include_bytes!("../../test-data/message/thunderbird_with_multiple_autocrypts.eml"),
false,
)
.await?
.unwrap()
.msg_ids[0];
let msg = Message::load_from_db(bob, msg_id).await?;
assert!(msg.get_showpadlock());
Ok(())
}
/// Tests that timestamp of signed but not encrypted message is protected.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_protected_date() -> Result<()> {
@@ -2063,3 +2082,48 @@ async fn test_4k_image_stays_image() -> Result<()> {
assert_eq!(msg.param.get_int(Param::Height).unwrap_or_default(), 2160);
Ok(())
}
/// Tests that if multiple alternatives are available in multipart/alternative,
/// the last one is preferred.
///
/// RFC 2046 says the last supported alternative should be preferred:
/// <https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.4>
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn prefer_last_alternative() {
let mut tcm = TestContextManager::new();
let context = &tcm.alice().await;
let raw = br#"From: Bob <bob@example.net>
To: Alice <alice@example.org>
Subject: Alternatives
Date: Tue, 5 May 2020 01:23:45 +0000
MIME-Version: 1.0
Chat-Version: 1.0
Content-Type: multipart/alternative; boundary="boundary"
This is a multipart message in MIME format.
--boundary
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
First alternative.
--boundary
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Second alternative.
--boundary
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Third alternative.
--boundary--
"#;
let message = MimeMessage::from_bytes(context, &raw[..], None)
.await
.unwrap();
assert_eq!(message.parts.len(), 1);
assert_eq!(message.parts[0].typ, Viewtype::Text);
assert_eq!(message.parts[0].msg, "Third alternative.");
}

View File

@@ -265,7 +265,7 @@ impl str::FromStr for Params {
/// or from an upgrade (when a key is dropped but was used in the past)
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut inner = BTreeMap::new();
let mut lines = s.lines().peekable();
let mut lines = s.split('\n').peekable();
while let Some(line) = lines.next() {
if let [key, value] = line.splitn(2, '=').collect::<Vec<_>>()[..] {
@@ -284,7 +284,7 @@ impl str::FromStr for Params {
inner.insert(key, value);
}
} else {
bail!("Not a key-value pair: {:?}", line);
bail!("Not a key-value pair: {line:?}");
}
}
@@ -457,6 +457,7 @@ mod tests {
let mut params = Params::new();
params.set(Param::Height, "foo\nbar=baz\nquux");
params.set(Param::Width, "\n\n\na=\n=");
params.set(Param::WebrtcRoom, "foo\r\nbar\r\n\r\nbaz\r\n");
assert_eq!(params.to_string().parse::<Params>().unwrap(), params);
}

View File

@@ -185,9 +185,14 @@ impl Iroh {
}
/// Get the iroh [NodeAddr] without direct IP addresses.
///
/// The address is guaranteed to have home relay URL set
/// as it is the only way to reach the node
/// without global discovery mechanisms.
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
let mut addr = self.router.endpoint().node_addr().await?;
addr.direct_addresses = BTreeSet::new();
debug_assert!(addr.relay_url().is_some());
Ok(addr)
}
@@ -278,18 +283,24 @@ impl Context {
})
}
/// Returns [`None`] if the peer channels has not been initialized.
pub async fn get_peer_channels(&self) -> Option<tokio::sync::RwLockReadGuard<'_, Iroh>> {
tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
self.iroh.read().await,
|opt_iroh| opt_iroh.as_ref(),
)
.ok()
}
/// Get or initialize the iroh peer channel.
pub async fn get_or_try_init_peer_channel(
&self,
) -> Result<tokio::sync::RwLockReadGuard<'_, Iroh>> {
if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
bail!("Attempt to get Iroh when realtime is disabled");
bail!("Attempt to initialize Iroh when realtime is disabled");
}
if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option<Iroh>>::try_map(
self.iroh.read().await,
|opt_iroh| opt_iroh.as_ref(),
) {
if let Some(lock) = self.get_peer_channels().await {
return Ok(lock);
}
@@ -479,14 +490,17 @@ pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec<u
}
/// Leave the gossip of the webxdc with given [MsgId].
///
/// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that
/// `send_webxdc_realtime_*()` functions aren't called for the given `msg_id` anymore until the app
/// is open again.
pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
let Some(iroh) = ctx.get_peer_channels().await else {
return Ok(());
}
let topic = get_iroh_topic_for_msg(ctx, msg_id)
.await?
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
let iroh = ctx.get_or_try_init_peer_channel().await?;
};
let Some(topic) = get_iroh_topic_for_msg(ctx, msg_id).await? else {
return Ok(());
};
iroh.leave_realtime(topic).await?;
info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
@@ -1110,7 +1124,6 @@ mod tests {
assert!(alice.ctx.iroh.read().await.is_none());
// creates iroh endpoint as side effect
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
@@ -1119,4 +1132,19 @@ mod tests {
// if accidentally called with the setting disabled.
assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_leave_webxdc_realtime_uninitialized() {
let mut tcm = TestContextManager::new();
let alice = &mut tcm.alice().await;
alice
.set_config_bool(Config::WebxdcRealtimeEnabled, true)
.await
.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap();
assert!(alice.ctx.iroh.read().await.is_none());
}
}

View File

@@ -8,9 +8,9 @@ use chrono::SubsecRound;
use deltachat_contact_tools::EmailAddress;
use pgp::armor::BlockType;
use pgp::composed::{
ArmorOptions, Deserializable, KeyType as PgpKeyType, Message, MessageBuilder,
SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey, SignedSecretKey,
StandaloneSignature, SubkeyParamsBuilder, TheRing,
ArmorOptions, DecryptionOptions, Deserializable, DetachedSignature, KeyType as PgpKeyType,
Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
SignedSecretKey, SubkeyParamsBuilder, TheRing,
};
use pgp::crypto::ecc_curve::ECCCurve;
use pgp::crypto::hash::HashAlgorithm;
@@ -226,7 +226,7 @@ pub fn pk_calc_signature(
plain.as_slice(),
)?;
let sig = StandaloneSignature::new(signature);
let sig = DetachedSignature::new(signature);
Ok(sig.to_armored_string(ArmorOptions::default())?)
}
@@ -245,12 +245,13 @@ pub fn pk_decrypt(
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
let empty_pw = Password::empty();
let decrypt_options = DecryptionOptions::new();
let ring = TheRing {
secret_keys: skeys,
key_passwords: vec![&empty_pw],
message_password: vec![],
session_keys: vec![],
allow_legacy: false,
decrypt_options,
};
let (msg, ring_result) = msg.decrypt_the_ring(ring, true)?;
anyhow::ensure!(
@@ -293,10 +294,10 @@ pub fn pk_validate(
) -> Result<HashSet<Fingerprint>> {
let mut ret: HashSet<Fingerprint> = Default::default();
let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0;
let detached_signature = DetachedSignature::from_armor_single(Cursor::new(signature))?.0;
for pkey in public_keys_for_validation {
if standalone_signature.verify(pkey, content).is_ok() {
if detached_signature.verify(pkey, content).is_ok() {
let fp = pkey.dc_fingerprint();
ret.insert(fp);
}

View File

@@ -16,7 +16,6 @@ use crate::contact::{Contact, ContactId, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::net::http::post_empty;
use crate::net::proxy::{DEFAULT_SOCKS_PORT, ProxyConfig};
use crate::token;
@@ -27,7 +26,6 @@ const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
const MAILTO_SCHEME: &str = "mailto:";
const MATMSG_SCHEME: &str = "MATMSG:";
@@ -122,15 +120,6 @@ pub enum Qr {
/// The QR code is a backup, but it is too new. The user has to update its Delta Chat.
BackupTooNew {},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
/// Server domain name.
domain: String,
/// URL pattern for video chat rooms.
instance_pattern: String,
},
/// Ask the user if they want to use the given proxy.
///
/// Note that HTTP(S) URLs without a path
@@ -294,8 +283,6 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_account(qr)?
} else if starts_with_ignore_case(qr, DCLOGIN_SCHEME) {
dclogin_scheme::decode_login(qr)?
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
decode_webrtc_instance(context, qr)?
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
decode_tg_socks_proxy(context, qr)?
} else if qr.starts_with(SHADOWSOCKS_SCHEME) {
@@ -421,7 +408,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => name.to_string(),
Err(err) => bail!("Invalid name: {}", err),
Err(err) => bail!("Invalid name: {err}"),
}
} else {
"".to_string()
@@ -445,7 +432,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result<Qr> {
let encoded_name = encoded_name.replace('+', "%20"); // sometimes spaces are encoded as `+`
match percent_decode_str(&encoded_name).decode_utf8() {
Ok(name) => Some(name.to_string()),
Err(err) => bail!("Invalid group name: {}", err),
Err(err) => bail!("Invalid group name: {err}"),
}
} else {
None
@@ -573,28 +560,6 @@ fn decode_account(qr: &str) -> Result<Qr> {
}
}
/// scheme: `DCWEBRTC:https://meet.jit.si/$ROOM`
fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
let payload = qr
.get(DCWEBRTC_SCHEME.len()..)
.context("Invalid DCWEBRTC payload")?;
let (_type, url) = Message::parse_webrtc_instance(payload);
let url = url::Url::parse(&url).context("Invalid WebRTC instance")?;
if url.scheme() == "http" || url.scheme() == "https" {
Ok(Qr::WebrtcInstance {
domain: url
.host_str()
.context("can't extract WebRTC instance domain")?
.to_string(),
instance_pattern: payload.to_string(),
})
} else {
bail!("Bad URL scheme for WebRTC instance: {:?}", url.scheme());
}
}
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
@@ -616,7 +581,7 @@ fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
}
let Some(host) = host else {
bail!("Bad t.me/socks url: {:?}", url);
bail!("Bad t.me/socks url: {url:?}");
};
let mut url = "socks5://".to_string();
@@ -719,10 +684,7 @@ pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(
context.emit_event(EventType::Error(format!(
"Cannot create account, server response could not be parsed:\n{parse_error:#}\nraw response:\n{response_text}"
)));
bail!(
"Cannot create account, unexpected server response:\n{:?}",
response_text
)
bail!("Cannot create account, unexpected server response:\n{response_text:?}")
}
}
}
@@ -732,14 +694,6 @@ pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<(
pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
match check_qr(context, qr).await? {
Qr::Account { .. } => set_account_from_qr(context, qr).await?,
Qr::WebrtcInstance {
domain: _,
instance_pattern,
} => {
context
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
.await?;
}
Qr::Proxy { url, .. } => {
let old_proxy_url_value = context
.get_config(Config::ProxyUrl)

View File

@@ -122,7 +122,7 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
options,
})
} else {
bail!("Bad scheme for account URL: {:?}.", payload);
bail!("Bad scheme for account URL: {payload:?}.");
}
}
@@ -139,7 +139,7 @@ fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
Some("starttls") => Some(Socket::Starttls),
Some("default") => Some(Socket::Automatic),
Some("plain") => Some(Socket::Plain),
Some(other) => bail!("Unknown security level: {}", other),
Some(other) => bail!("Unknown security level: {other}"),
None => None,
})
}
@@ -152,7 +152,7 @@ fn parse_certificate_checks(
Some("1") => Some(EnteredCertificateChecks::Strict),
Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
Some(other) => bail!("Unknown certificatecheck level: {}", other),
Some(other) => bail!("Unknown certificatecheck level: {other}"),
None => None,
})
}

View File

@@ -712,32 +712,6 @@ async fn test_decode_account() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_webrtc_instance() -> Result<()> {
let ctx = TestContext::new().await;
let qr = check_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://basicurl.com/$ROOM").await?;
assert_eq!(
qr,
Qr::WebrtcInstance {
domain: "basicurl.com".to_string(),
instance_pattern: "basicwebrtc:https://basicurl.com/$ROOM".to_string()
}
);
// Test it again with mixcased "dcWebRTC:" uri scheme
let qr = check_qr(&ctx.ctx, "dcWebRTC:https://example.org/").await?;
assert_eq!(
qr,
Qr::WebrtcInstance {
domain: "example.org".to_string(),
instance_pattern: "https://example.org/".to_string()
}
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_tg_socks_proxy() -> Result<()> {
let t = TestContext::new().await;
@@ -820,34 +794,6 @@ async fn test_decode_account_bad_scheme() {
assert!(res.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
let ctx = TestContext::new().await;
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
let res = set_config_from_qr(&ctx.ctx, "badqr:https://example.org/").await;
assert!(res.is_err());
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
let res = set_config_from_qr(&ctx.ctx, "dcwebrtc:https://example.org/").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
"https://example.org/"
);
let res =
set_config_from_qr(&ctx.ctx, "DCWEBRTC:basicwebrtc:https://foo.bar/?$ROOM&test").await;
assert!(res.is_ok());
assert_eq!(
ctx.ctx.get_config(Config::WebrtcInstance).await?.unwrap(),
"basicwebrtc:https://foo.bar/?$ROOM&test"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_proxy_config_from_qr() -> Result<()> {
let t = TestContext::new().await;

View File

@@ -1,6 +1,6 @@
//! # QR code generation module.
use anyhow::Result;
use anyhow::{Result, bail};
use base64::Engine as _;
use qrcodegen::{QrCode, QrCodeEcc};
@@ -108,8 +108,18 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu
None => None,
};
let qrcode_description = match chat.typ {
crate::constants::Chattype::Group => {
stock_str::secure_join_group_qr_description(context, &chat).await
}
crate::constants::Chattype::OutBroadcast => {
stock_str::secure_join_broadcast_qr_description(context, &chat).await
}
_ => bail!("Unexpected chat type {}", chat.typ),
};
inner_generate_secure_join_qr_code(
&stock_str::secure_join_group_qr_description(context, &chat).await,
&qrcode_description,
&securejoin::get_securejoin_qr(context, Some(chat_id)).await?,
&color_int_to_hex_string(chat.get_color(context).await?),
avatar,

View File

@@ -196,7 +196,7 @@ pub(crate) async fn receive_imf_from_inbox(
rfc724_mid,
imf_raw,
seen,
is_partial_download,
is_partial_download.map(|msg_size| (msg_size, None)),
)
.await
}
@@ -494,9 +494,8 @@ async fn get_to_and_past_contact_ids(
/// If the message is so wrong that we didn't even create a database entry,
/// returns `Ok(None)`.
///
/// If `is_partial_download` is set, it contains the full message size in bytes.
/// Do not confuse that with `replace_msg_id` that will be set when the full message is loaded
/// later.
/// If `partial` is set, it contains the full message size in bytes and an optional error text for
/// the partially downloaded message.
#[expect(clippy::too_many_arguments)]
pub(crate) async fn receive_imf_inner(
context: &Context,
@@ -506,7 +505,7 @@ pub(crate) async fn receive_imf_inner(
rfc724_mid: &str,
imf_raw: &[u8],
seen: bool,
is_partial_download: Option<u32>,
partial: Option<(u32, Option<String>)>,
) -> Result<Option<ReceivedMsg>> {
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
info!(
@@ -515,9 +514,16 @@ pub(crate) async fn receive_imf_inner(
String::from_utf8_lossy(imf_raw),
);
}
if partial.is_none() {
ensure!(
!context
.get_config_bool(Config::FailOnReceivingFullMsg)
.await?
);
}
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, is_partial_download).await
{
let is_partial_download = partial.as_ref().map(|(msg_size, _err)| *msg_size);
let mut mime_parser = match MimeMessage::from_bytes(context, imf_raw, partial).await {
Err(err) => {
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
if rfc724_mid.starts_with(GENERATED_PREFIX) {
@@ -551,24 +557,17 @@ pub(crate) async fn receive_imf_inner(
// make sure, this check is done eg. before securejoin-processing.
let (replace_msg_id, replace_chat_id);
if let Some((old_msg_id, _)) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if is_partial_download.is_some() {
// Should never happen, see imap::prefetch_should_download(), but still.
info!(
context,
"Got a partial download and message is already in DB."
);
return Ok(None);
}
let msg = Message::load_from_db(context, old_msg_id).await?;
replace_msg_id = Some(old_msg_id);
replace_chat_id = if msg.download_state() != DownloadState::Done {
replace_chat_id = if let Some(msg) = Message::load_from_db_optional(context, old_msg_id)
.await?
.filter(|msg| msg.download_state() != DownloadState::Done)
{
// the message was partially downloaded before and is fully downloaded now.
info!(
context,
"Message already partly in DB, replacing by full message."
);
info!(context, "Message already partly in DB, replacing.");
Some(msg.chat_id)
} else {
// The message was already fully downloaded
// or cannot be loaded because it is deleted.
None
};
} else {
@@ -1000,14 +999,17 @@ pub(crate) async fn receive_imf_inner(
}
}
if mime_parser.is_call() {
if is_partial_download.is_none() && mime_parser.is_call() {
context
.handle_call_msg(insert_msg_id, &mime_parser, from_id)
.await?;
} else if received_msg.hidden {
// No need to emit an event about the changed message
} else if let Some(replace_chat_id) = replace_chat_id {
context.emit_msgs_changed_without_msg_id(replace_chat_id);
match replace_chat_id == chat_id {
false => context.emit_msgs_changed_without_msg_id(replace_chat_id),
true => context.emit_msgs_changed(chat_id, replace_msg_id.unwrap_or_default()),
}
} else if !chat_id.is_trash() {
let fresh = received_msg.state == MessageState::InFresh
&& mime_parser.is_system_message != SystemMessage::CallAccepted
@@ -1155,8 +1157,9 @@ async fn decide_chat_assignment(
{
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
true
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
} else if is_partial_download.is_none()
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded)
{
info!(context, "Call state changed (TRASH).");
true
@@ -1984,8 +1987,9 @@ async fn add_parts(
handle_edit_delete(context, mime_parser, from_id).await?;
if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
if is_partial_download.is_none()
&& (mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded)
{
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(call) =
@@ -2866,15 +2870,6 @@ async fn apply_group_changes(
let (mut removed_id, mut added_id) = (None, None);
let mut better_msg = None;
let mut silent = false;
// True if a Delta Chat client has explicitly added our current primary address.
let self_added =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
} else {
false
};
let chat_contacts =
HashSet::<ContactId>::from_iter(chat::get_chat_contacts(context, chat.id).await?);
let is_from_in_chat =
@@ -2907,8 +2902,12 @@ async fn apply_group_changes(
// rather than old display name.
// This could be fixed by looking up the contact with the highest
// `remove_timestamp` after applying Chat-Group-Member-Timestamps.
removed_id = lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?;
if let Some(id) = removed_id {
if !is_from_in_chat {
better_msg = Some(String::new());
} else if let Some(id) =
lookup_key_contact_by_address(context, removed_addr, Some(chat.id)).await?
{
removed_id = Some(id);
better_msg = if id == from_id {
silent = true;
Some(stock_str::msg_group_left_local(context, from_id).await)
@@ -2919,7 +2918,9 @@ async fn apply_group_changes(
warn!(context, "Removed {removed_addr:?} has no contact id.")
}
} else if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
if !is_from_in_chat {
better_msg = Some(String::new());
} else if let Some(key) = mime_parser.gossiped_keys.get(added_addr) {
// TODO: if gossiped keys contain the same address multiple times,
// we may lookup the wrong contact.
// This could be fixed by looking up the contact with
@@ -2998,6 +2999,15 @@ async fn apply_group_changes(
.await?;
} else {
let mut new_members: HashSet<ContactId>;
// True if a Delta Chat client has explicitly and really added our primary address to an
// already existing group.
let self_added =
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
addr_cmp(&context.get_primary_self_addr().await?, added_addr)
&& !chat_contacts.contains(&ContactId::SELF)
} else {
false
};
if self_added {
new_members = HashSet::from_iter(to_ids_flat.iter().copied());
new_members.insert(ContactId::SELF);
@@ -3064,17 +3074,18 @@ async fn apply_group_changes(
.collect();
if let Some(added_id) = added_id {
if !added_ids.remove(&added_id) && !self_added {
// No-op "Member added" message.
//
// Trash it.
if !added_ids.remove(&added_id) && added_id != ContactId::SELF {
// No-op "Member added" message. An exception is self-addition messages because they at
// least must be shown when a chat is created on our side.
better_msg = Some(String::new());
}
}
if let Some(removed_id) = removed_id {
removed_ids.remove(&removed_id);
}
let group_changes_msgs = if self_added {
let group_changes_msgs = if !chat_contacts.contains(&ContactId::SELF)
&& new_chat_contacts.contains(&ContactId::SELF)
{
Vec::new()
} else {
group_changes_msgs(context, &added_ids, &removed_ids, chat.id).await?
@@ -3528,8 +3539,7 @@ async fn apply_in_broadcast_changes(
// The only member added/removed message that is ever sent is "I left.",
// so, this is the only case we need to handle here
if from_id == ContactId::SELF {
better_msg
.get_or_insert(stock_str::msg_group_left_local(context, ContactId::SELF).await);
better_msg.get_or_insert(stock_str::msg_you_left_broadcast(context).await);
}
}

View File

@@ -5557,3 +5557,32 @@ async fn test_lookup_key_contact_by_address_self() -> Result<()> {
);
Ok(())
}
/// Tests reception of multipart/alternative
/// with three parts, one of which is a calendar.
///
/// MS Exchange produces multipart/alternative
/// messages with three parts:
/// `text/plain`, `text/html` and `text/calendar`.
///
/// We display `text/plain` part in this case,
/// but .ics file is available as an attachment.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_calendar_alternative() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let raw = include_bytes!("../../test-data/message/calendar-alternative.eml");
let msg = receive_imf(t, raw, false).await?.unwrap();
assert_eq!(msg.msg_ids.len(), 1);
let calendar_msg = Message::load_from_db(t, msg.msg_ids[0]).await?;
assert_eq!(calendar_msg.text, "Subject was here Hello!");
assert_eq!(calendar_msg.viewtype, Viewtype::File);
assert_eq!(calendar_msg.get_filename().unwrap(), "calendar.ics");
assert!(calendar_msg.has_html());
let html = calendar_msg.get_id().get_html(t).await.unwrap().unwrap();
assert_eq!(html, "<b>Hello!</b>");
Ok(())
}

View File

@@ -932,7 +932,7 @@ impl Scheduler {
// wait for all loops to be started
if let Err(err) = try_join_all(start_recvs).await {
bail!("failed to start scheduler: {}", err);
bail!("failed to start scheduler: {err}");
}
info!(ctx, "scheduler is running");

View File

@@ -8,6 +8,7 @@ use humansize::{BINARY, format_size};
use crate::events::EventType;
use crate::imap::{FolderMeaning, scan_folders::get_watched_folder_configs};
use crate::log::info;
use crate::login_param::ConfiguredLoginParam;
use crate::quota::{QUOTA_ERROR_THRESHOLD_PERCENTAGE, QUOTA_WARN_THRESHOLD_PERCENTAGE};
use crate::stock_str;
use crate::{context::Context, log::LogExt};
@@ -530,6 +531,36 @@ impl Context {
}
ret += "</ul>";
// =============================================================================================
// Add e.g.
// Security
// TLS Certificate Checks: enabled
// =============================================================================================
if let Some(configured_login_param) = ConfiguredLoginParam::load(self).await? {
let security = stock_str::security(self).await;
ret += &format!("<h3>{security}</h3><ul>");
ret += "<li>";
if configured_login_param.strict_tls() {
// GREEN: strict TLS checks are enabled.
ret += &format!(
"<span class=\"green dot\"></span> <b>TLS Certificate Checks:</b> enabled"
);
} else if configured_login_param.strict_tls_manually_disabled() {
// RED: TLS checks are manually disabled.
ret += &format!(
"<span class=\"red dot\"></span> <b>TLS Certificate Checks:</b> disabled"
);
} else {
// YELLOW: TLS checks are automatically disabled.
ret += &format!(
"<span class=\"yellow dot\"></span> <b>TLS Certificate Checks:</b> disabled"
);
}
ret += "</li></ul>";
}
// =============================================================================================
ret += "</body></html>\n";

View File

@@ -16,7 +16,6 @@ use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::key::{DcKey, Fingerprint, load_self_public_key};
use crate::log::{error, info, warn};
use crate::logged_debug_assert;
use crate::message::{Message, Viewtype};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::param::Param;
@@ -32,16 +31,28 @@ use qrinvite::QrInvite;
use crate::token::Namespace;
fn inviter_progress(context: &Context, contact_id: ContactId, progress: usize) {
logged_debug_assert!(
context,
progress <= 1000,
"inviter_progress: contact {contact_id}, progress={progress}, but value in range 0..1000 expected with: 0=error, 1..999=progress, 1000=success."
);
fn inviter_progress(
context: &Context,
contact_id: ContactId,
chat_id: ChatId,
is_group: bool,
) -> Result<()> {
let chat_type = if is_group {
Chattype::Group
} else {
Chattype::Single
};
// No other values are used.
let progress = 1000;
context.emit_event(EventType::SecurejoinInviterProgress {
contact_id,
chat_id,
chat_type,
progress,
});
Ok(())
}
/// Generates a Secure Join QR code.
@@ -266,8 +277,6 @@ pub(crate) async fn handle_securejoin_handshake(
info!(context, "Received secure-join message {step:?}.");
let join_vg = step.starts_with("vg-");
if !matches!(step, "vg-request" | "vc-request") {
let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
@@ -310,8 +319,6 @@ pub(crate) async fn handle_securejoin_handshake(
return Ok(HandshakeMessage::Ignore);
}
inviter_progress(context, contact_id, 300);
let from_addr = ContactAddress::new(&mime_message.from.addr)?;
let autocrypt_fingerprint = mime_message.autocrypt_fingerprint.as_deref().unwrap_or("");
let (autocrypt_contact_id, _) = Contact::add_or_lookup_ex(
@@ -401,11 +408,10 @@ pub(crate) async fn handle_securejoin_handshake(
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
// for setup-contact, make Alice's one-to-one chat with Bob visible
// (secure-join-information are shown in the group chat)
if !join_vg {
if grpid.is_empty() {
ChatId::create_for_contact(context, contact_id).await?;
}
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
inviter_progress(context, contact_id, 600);
if let Some(group_chat_id) = group_chat_id {
// Join group.
secure_connection_established(
@@ -417,17 +423,18 @@ pub(crate) async fn handle_securejoin_handshake(
.await?;
chat::add_contact_to_chat_ex(context, Nosync, group_chat_id, contact_id, true)
.await?;
inviter_progress(context, contact_id, 800);
inviter_progress(context, contact_id, 1000);
let is_group = true;
inviter_progress(context, contact_id, group_chat_id, is_group)?;
// IMAP-delete the message to avoid handling it by another device and adding the
// member twice. Another device will know the member's key from Autocrypt-Gossip.
Ok(HandshakeMessage::Done)
} else {
let chat_id = info_chat_id(context, contact_id).await?;
// Setup verified contact.
secure_connection_established(
context,
contact_id,
info_chat_id(context, contact_id).await?,
chat_id,
mime_message.timestamp_sent,
)
.await?;
@@ -435,7 +442,8 @@ pub(crate) async fn handle_securejoin_handshake(
.await
.context("failed sending vc-contact-confirm message")?;
inviter_progress(context, contact_id, 1000);
let is_group = false;
inviter_progress(context, contact_id, chat_id, is_group)?;
Ok(HandshakeMessage::Ignore) // "Done" would delete the message and break multi-device (the key from Autocrypt-header is needed)
}
}
@@ -554,11 +562,20 @@ pub(crate) async fn observe_securejoin_on_other_device(
ChatId::set_protection_for_contact(context, contact_id, mime_message.timestamp_sent).await?;
if step == "vg-member-added" {
inviter_progress(context, contact_id, 800);
}
if step == "vg-member-added" || step == "vc-contact-confirm" {
inviter_progress(context, contact_id, 1000);
let is_group = mime_message
.get_header(HeaderDef::ChatGroupMemberAdded)
.is_some();
// We don't know the chat ID
// as we may not know about the group yet.
//
// Event is mostly used for bots
// which only have a single device
// and tests which don't care about the chat ID,
// so we pass invalid chat ID here.
let chat_id = ChatId::new(0);
inviter_progress(context, contact_id, chat_id, is_group)?;
}
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {

View File

@@ -72,11 +72,6 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
}
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
};
for t in [&alice, &bob] {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap();
}
assert_eq!(
Chatlist::try_load(&alice, 0, None, None)
@@ -365,6 +360,30 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(alice).await?, true);
// Check Alice signalled success via the SecurejoinInviterProgress event.
let event = alice
.evtracker
.get_matching(|evt| {
matches!(
evt,
EventType::SecurejoinInviterProgress { progress: 1000, .. }
)
})
.await;
match event {
EventType::SecurejoinInviterProgress {
contact_id,
chat_type,
progress,
..
} => {
assert_eq!(contact_id, contact_bob.id);
assert_eq!(chat_type, Chattype::Single);
assert_eq!(progress, 1000);
}
_ => unreachable!(),
}
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
@@ -515,6 +534,31 @@ async fn test_secure_join() -> Result<()> {
alice.recv_msg_trash(&sent).await;
assert_eq!(contact_bob.is_verified(&alice).await?, true);
// Check Alice signalled success via the SecurejoinInviterProgress event.
let event = alice
.evtracker
.get_matching(|evt| {
matches!(
evt,
EventType::SecurejoinInviterProgress { progress: 1000, .. }
)
})
.await;
match event {
EventType::SecurejoinInviterProgress {
contact_id,
chat_type,
chat_id,
progress,
} => {
assert_eq!(contact_id, contact_bob.id);
assert_eq!(chat_type, Chattype::Group);
assert_eq!(chat_id, alice_chatid);
assert_eq!(progress, 1000);
}
_ => unreachable!(),
}
let sent = alice.pop_sent_msg().await;
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
@@ -653,11 +697,6 @@ async fn test_lost_contact_confirm() {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
for t in [&alice, &bob] {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap();
}
let qr = get_securejoin_qr(&alice, None).await.unwrap();
join_securejoin(&bob.ctx, &qr).await.unwrap();

View File

@@ -242,7 +242,7 @@ pub(crate) async fn smtp_send(
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
// should definitely go here, because user has to open the link to
// resume message sending.
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
SendResult::Failure(format_err!("Permanent SMTP error: {err}"))
}
}
async_smtp::error::Error::Transient(ref response) => {
@@ -471,7 +471,7 @@ pub(crate) async fn send_msg_to_smtp(
}
Ok(())
}
SendResult::Failure(err) => Err(format_err!("{}", err)),
SendResult::Failure(err) => Err(format_err!("{err}")),
}
}
@@ -586,7 +586,7 @@ async fn send_mdn_rfc724_mid(
let addr = contact.get_addr();
let recipient = async_smtp::EmailAddress::new(addr.to_string())
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))?;
.map_err(|err| format_err!("invalid recipient: {addr} {err:?}"))?;
let recipients = vec![recipient];
match smtp_send(context, &recipients, &body, smtp, None).await {

View File

@@ -69,7 +69,7 @@ pub(crate) async fn connect_and_auth(
.await
.context("SMTP failed to get OAUTH2 access token")?;
if access_token.is_none() {
bail!("SMTP OAuth 2 error {}", addr);
bail!("SMTP OAuth 2 error {addr}");
}
(
async_smtp::authentication::Credentials::new(

View File

@@ -130,12 +130,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Failed to send message to %1$s."))]
FailedSendingTo = 74,
#[strum(props(fallback = "Video chat invitation"))]
VideochatInvitation = 82,
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
VideochatInviteMsgBody = 83,
#[strum(props(fallback = "Error:\n\n“%1$s”"))]
ConfigurationFailed = 84,
@@ -279,7 +273,7 @@ pub enum StockMessage {
#[strum(props(fallback = "Member %1$s removed by %2$s."))]
MsgDelMemberBy = 131,
#[strum(props(fallback = "You left."))]
#[strum(props(fallback = "You left the group."))]
MsgYouLeftGroup = 132,
#[strum(props(fallback = "Group left by %1$s."))]
@@ -424,6 +418,31 @@ Help keeping us to keep Delta Chat independent and make it more awesome in the f
https://delta.chat/donate"))]
DonationRequest = 193,
#[strum(props(fallback = "Outgoing call"))]
OutgoingCall = 194,
#[strum(props(fallback = "Incoming call"))]
IncomingCall = 195,
#[strum(props(fallback = "Declined call"))]
DeclinedCall = 196,
#[strum(props(fallback = "Canceled call"))]
CanceledCall = 197,
#[strum(props(fallback = "Missed call"))]
MissedCall = 198,
#[strum(props(fallback = "You left the channel."))]
MsgYouLeftBroadcast = 200,
#[strum(props(fallback = "Scan to join channel %1$s"))]
SecureJoinBrodcastQRDescription = 201,
/// "Security" title for connectivity view section.
#[strum(props(fallback = "Security"))]
Security = 202,
}
impl StockMessage {
@@ -696,7 +715,7 @@ pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
translated(context, StockMessage::MsgILeftGroup).await
}
/// Stock string: `You left.` or `Group left by %1$s.`.
/// Stock string: `You left the group.` or `Group left by %1$s.`.
pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
translated(context, StockMessage::MsgYouLeftGroup).await
@@ -707,6 +726,11 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI
}
}
/// Stock string: `You left the channel.`
pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
translated(context, StockMessage::MsgYouLeftBroadcast).await
}
/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
pub(crate) async fn msg_reacted(
context: &Context,
@@ -801,6 +825,31 @@ pub(crate) async fn donation_request(context: &Context) -> String {
translated(context, StockMessage::DonationRequest).await
}
/// Stock string: `Outgoing call`.
pub(crate) async fn outgoing_call(context: &Context) -> String {
translated(context, StockMessage::OutgoingCall).await
}
/// Stock string: `Incoming call`.
pub(crate) async fn incoming_call(context: &Context) -> String {
translated(context, StockMessage::IncomingCall).await
}
/// Stock string: `Declined call`.
pub(crate) async fn declined_call(context: &Context) -> String {
translated(context, StockMessage::DeclinedCall).await
}
/// Stock string: `Canceled call`.
pub(crate) async fn canceled_call(context: &Context) -> String {
translated(context, StockMessage::CanceledCall).await
}
/// Stock string: `Missed call`.
pub(crate) async fn missed_call(context: &Context) -> String {
translated(context, StockMessage::MissedCall).await
}
/// Stock string: `Scan to chat with %1$s`.
pub(crate) async fn setup_contact_qr_description(
context: &Context,
@@ -817,13 +866,20 @@ pub(crate) async fn setup_contact_qr_description(
.replace1(&name)
}
/// Stock string: `Scan to join %1$s`.
/// Stock string: `Scan to join group %1$s`.
pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinGroupQRDescription)
.await
.replace1(chat.get_name())
}
/// Stock string: `Scan to join channel %1$s`.
pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
translated(context, StockMessage::SecureJoinBrodcastQRDescription)
.await
.replace1(chat.get_name())
}
/// Stock string: `%1$s verified.`.
#[allow(dead_code)]
pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
@@ -1001,18 +1057,6 @@ pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: Cont
}
}
/// Stock string: `Video chat invitation`.
pub(crate) async fn videochat_invitation(context: &Context) -> String {
translated(context, StockMessage::VideochatInvitation).await
}
/// Stock string: `You are invited to a video chat, click %1$s to join.`.
pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
translated(context, StockMessage::VideochatInviteMsgBody)
.await
.replace1(url)
}
/// Stock string: `Error:\n\n“%1$s”`.
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
translated(context, StockMessage::ConfigurationFailed)
@@ -1284,6 +1328,11 @@ pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
translated(context, StockMessage::BackupTransferMsgBody).await
}
/// Stock string: `Security`.
pub(crate) async fn security(context: &Context) -> String {
translated(context, StockMessage::Security).await
}
impl Context {
/// Set the stock string for the [StockMessage].
///

View File

@@ -4,6 +4,7 @@ use std::borrow::Cow;
use std::fmt;
use std::str;
use crate::calls::{CallState, call_state};
use crate::chat::Chat;
use crate::constants::Chattype;
use crate::contact::{Contact, ContactId};
@@ -210,12 +211,6 @@ impl Message {
type_file = self.get_filename();
append_text = true
}
Viewtype::VideochatInvitation => {
emoji = None;
type_name = Some(stock_str::videochat_invitation(context).await);
type_file = None;
append_text = false;
}
Viewtype::Webxdc => {
emoji = None;
type_name = None;
@@ -234,11 +229,21 @@ impl Message {
append_text = true;
}
Viewtype::Call => {
let call_state = call_state(context, self.id)
.await
.unwrap_or(CallState::Alerting);
emoji = Some("📞");
type_name = Some(if self.from_id == ContactId::SELF {
"Outgoing call".to_string()
} else {
"Incoming call".to_string()
type_name = Some(match call_state {
CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
if self.from_id == ContactId::SELF {
stock_str::outgoing_call(context).await
} else {
stock_str::incoming_call(context).await
}
}
CallState::Missed => stock_str::missed_call(context).await,
CallState::Declined => stock_str::declined_call(context).await,
CallState::Canceled => stock_str::canceled_call(context).await,
});
type_file = None;
append_text = false
@@ -417,13 +422,6 @@ mod tests {
.unwrap();
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
let file = write_file_to_blobdir(&d).await;
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.set_text(some_text.clone());
msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
.unwrap();
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
let mut msg = Message::new(Viewtype::Vcard);
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();

View File

@@ -35,7 +35,7 @@ use crate::context::Context;
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::key::{self, DcKey, DcSecretKey, self_fingerprint};
use crate::log::warn;
use crate::message::{Message, MessageState, MsgId, Viewtype, update_msg_state};
use crate::message::{Message, MessageState, MsgId, update_msg_state};
use crate::mimeparser::{MimeMessage, SystemMessage};
use crate::pgp::KeyPair;
use crate::receive_imf::receive_imf;
@@ -575,6 +575,13 @@ impl TestContext {
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
self.sql
.execute(
"UPDATE msgs SET timestamp_sent=? WHERE id=?",
(time(), msg_id),
)
.await
.expect("Failed to update timestamp_sent");
}
let payload_headers = payload.split("\r\n\r\n").next().unwrap().lines();
@@ -1433,8 +1440,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
alice0.send_sync_msg().await.unwrap();
let sync_msg = alice0.pop_sent_sync_msg().await;
let no_msg = alice1.recv_msg_opt(&sync_msg).await;
assert!(no_msg.is_none());
alice1.recv_msg_trash(&sync_msg).await;
}
/// Pretty-print an event to stdout
@@ -1529,7 +1535,7 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
let msgtext = msg.get_text();
writeln!(
buf,
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}",
prefix,
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -1557,15 +1563,6 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else {
"".to_string()
},
if msg.is_forwarded() {
"[FORWARDED]"
} else {

View File

@@ -35,7 +35,6 @@ async fn check_verified_oneonone_chat_protection_not_broken(by_classical_email:
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
tcm.execute_securejoin(&alice, &bob).await;
@@ -89,7 +88,6 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await;
tcm.execute_securejoin(&alice, &bob).await;
tcm.execute_securejoin(&bob, &fiona).await;
@@ -151,7 +149,6 @@ async fn test_create_verified_oneonone_chat() -> Result<()> {
drop(fiona);
let fiona_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&fiona_new]).await;
fiona_new.configure_addr("fiona@example.net").await;
e2ee::ensure_secret_key_exists(&fiona_new).await?;
@@ -181,7 +178,6 @@ async fn test_missing_key_reexecute_securejoin() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[alice, bob]).await;
let chat_id = tcm.execute_securejoin(bob, alice).await;
let chat = Chat::load_from_db(bob, chat_id).await?;
assert!(chat.is_protected());
@@ -206,7 +202,6 @@ async fn test_create_unverified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
// A chat with an unknown contact should be created unprotected
let chat = alice.create_chat(&bob).await;
@@ -246,7 +241,6 @@ async fn test_degrade_verified_oneonone_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
@@ -361,7 +355,6 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
bob.set_config_bool(Config::MdnsEnabled, true).await?;
// Alice & Bob verify each other
@@ -386,7 +379,6 @@ async fn test_outgoing_mua_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
@@ -423,7 +415,6 @@ async fn test_outgoing_encrypted_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[alice]).await;
mark_as_verified(alice, bob).await;
let chat_id = alice.create_chat(bob).await.id;
@@ -449,7 +440,6 @@ async fn test_reply() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
if verified {
mark_as_verified(&alice, &bob).await;
@@ -492,7 +482,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
let alice = &tcm.alice().await;
let bob_old = &tcm.unconfigured().await;
enable_verified_oneonone_chats(&[alice, bob_old]).await;
bob_old.configure_addr("bob@example.net").await;
mark_as_verified(bob_old, alice).await;
let chat = bob_old.create_chat(alice).await;
@@ -503,7 +492,6 @@ async fn test_message_from_old_dc_setup() -> Result<()> {
tcm.section("Bob reinstalls DC");
let bob = &tcm.bob().await;
enable_verified_oneonone_chats(&[bob]).await;
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
@@ -535,7 +523,6 @@ async fn test_verify_then_verify_again() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
enable_verified_oneonone_chats(&[&alice, &bob]).await;
mark_as_verified(&alice, &bob).await;
mark_as_verified(&bob, &alice).await;
@@ -546,7 +533,6 @@ async fn test_verify_then_verify_again() -> Result<()> {
tcm.section("Bob reinstalls DC");
drop(bob);
let bob_new = tcm.unconfigured().await;
enable_verified_oneonone_chats(&[&bob_new]).await;
bob_new.configure_addr("bob@example.net").await;
e2ee::ensure_secret_key_exists(&bob_new).await?;
@@ -599,7 +585,6 @@ async fn test_verified_member_added_reordering() -> Result<()> {
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
enable_verified_oneonone_chats(&[alice, bob, fiona]).await;
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
@@ -651,7 +636,6 @@ async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
bob.set_config(Config::Displayname, Some("Bob Smith"))
.await?;
if verified {
enable_verified_oneonone_chats(&[&bob]).await;
mark_as_verified(&bob, &alice).await;
} else {
tcm.send_recv_accept(&alice, &bob, "hi").await;
@@ -799,7 +783,12 @@ async fn test_verified_chat_editor_reordering() -> Result<()> {
tcm.section("Charlie receives member added message");
charlie.recv_msg(&sent_member_added_msg).await;
charlie
.golden_test_chat(
charlie_received_xdc.chat_id,
"verified_chats_editor_reordering",
)
.await;
Ok(())
}
@@ -882,11 +871,3 @@ async fn assert_verified(this: &TestContext, other: &TestContext, protected: Pro
protected == ProtectionStatus::Protected
);
}
async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) {
for t in test_contexts {
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
.await
.unwrap()
}
}

View File

@@ -437,7 +437,8 @@ async fn test_maybe_warn_on_outdated() {
let t = TestContext::new().await;
let timestamp_now: i64 = time();
// in about 3 months, the app should not be outdated
// in about 3 months, the app should not be outdated.
// "90 days" has proven to be too short at some point - user were informed but there was no update
maybe_warn_on_outdated(
&t,
timestamp_now + 90 * 24 * 60 * 60,

View File

@@ -279,7 +279,7 @@ impl Context {
};
if !valid {
bail!("{} is not a valid webxdc file", filename);
bail!("{filename} is not a valid webxdc file");
}
Ok(())
@@ -837,8 +837,8 @@ fn parse_webxdc_manifest(bytes: &[u8]) -> Result<WebxdcManifest> {
}
async fn get_blob(archive: &mut SeekZipFileReader<BufReader<File>>, name: &str) -> Result<Vec<u8>> {
let (i, _) = find_zip_entry(archive.file(), name)
.ok_or_else(|| anyhow!("no entry found for {}", name))?;
let (i, _) =
find_zip_entry(archive.file(), name).ok_or_else(|| anyhow!("no entry found for {name}"))?;
let mut reader = archive.reader_with_entry(i).await?;
let mut buf = Vec::new();
reader.read_to_end_checked(&mut buf).await?;

Some files were not shown because too many files have changed in this diff Show More