Compare commits

..

395 Commits

Author SHA1 Message Date
missytake
18044c2fef fix ruff 2025-10-13 16:12:20 +02:00
missytake
f5dea1d252 feat: allow setting displayname + selfavatar via CLI 2025-10-13 16:12:18 +02: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
link2xt
75bcf8660b chore(release): prepare for 2.13.0 2025-09-09 05:46:13 +00:00
dependabot[bot]
5e1d945198 chore(deps): bump actions/setup-node from 4 to 5
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 02:11:35 +00:00
dependabot[bot]
e047184ede chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-09 02:11:26 +00:00
link2xt
307a2eb6ec feat: withdraw all QR codes when one is withdrawn
This is a preparation for expiring authentication tokens.

If we make authentication token expire,
we need to generate new authentication tokens each time
QR code screen is opened in the UI,
so authentication token is fresh.
We however don't want to completely invalidate
old authentication codes at the same time,
e.g. they should still be valid for joining groups,
just not result in a verification on the inviter side.

Since a group now can have a lot of authentication tokens,
it is easy to lose track of them
without any way to remove them
as they are not displayed anywhere in the UI.
As a solution, we now remove all
tokens corresponding to a group ID
when one token is withdrawn,
or all non-group tokens
when a single non-group token is withdrawn.

"Reset QR code" option already present
in the UI which works by resetting
current QR code will work without any UI changes,
but will now result in invalidation
of all previously created QR codes and invite links.
2025-09-08 17:51:45 +00:00
bjoern
ab8aedf06e refine call states (#7179)
- sync declined calls from callee to caller, as usual in all larger
messengers
- introduce the call states "Missed call", "Declined call" and
"Cancelled all" ("Ended call" is gone)
- allow calling end_call()/accept_call() for already ended/accepted
calls, in practise, handling all cornercases is tricky in UI - and the
state needs anyways to be tracked.
- track and show the call duration

the duration calculation depends on local time, but it is displayed only
coarse and is not needed for any state. this can be improved as needed,
timestamps of the corresponding messages are probably better at some
point. or ending device sends its view of the time around. but for the
first throw, it seems good enough

if we finally want that set of states, it can be exposed to a json-info
in a subsequent call, so that the UI can render it more nicely. fallback
strings as follows will stay for now to make adaption in other UI easy,
and for debugging:

<img width="320" alt="IMG_0154"
src="https://github.com/user-attachments/assets/09a89bfb-66f4-4184-b05c-e8040b96cf44"
/>

successor of https://github.com/chatmail/core/pull/6650
2025-09-08 15:48:35 +02:00
bjoern
b6ab13f1de feat: hide call status change messages (#7175)
this PR uses the initial "call messages" (that has a separate viewtype
since #7174) to show all call status.

this is what most other messengers are doing as well. additional "info
messages" after a call are no longer needed.

on the wire, as we cannot pickpack on visible info messages, we use
hidden messages, similar to eg. webxdc status updates.

in future PR, it is planned to allow getting call state as a json, so
that UI can render nicely. it is then decided if we want to translate
the strings in the core.

<img width="320" alt="IMG_0150"
src="https://github.com/user-attachments/assets/41ee3fa3-8be4-42c3-8dd9-d20f49881650"
/>

successor of https://github.com/chatmail/core/pull/6650
2025-09-05 08:52:15 +02:00
link2xt
53a3e51920 feat: support receiving Autocrypt-Gossip with _verified attribute
This commit is a preparation for
sending Autocrypt-Gossip with `_verified` attribute
instead of `Chat-Verified` header.
2025-09-04 19:46:14 +00:00
link2xt
4033566b4a refactor: remove Aheader::new 2025-09-04 19:46:14 +00:00
bjoern
bed1623dcb feat: use dedicated 'call' viewtype (#7174)
a dedicated viewtype allows the UI to show a more advanced UI, but even
when using the defaults,
it has the advantage that incoming/outgoing and the date are directly
visible.

successor of https://github.com/chatmail/core/pull/6650
2025-09-04 16:51:51 +02:00
link2xt
d4704977bc api!: remove e2ee_enabled preference
The setting is already removed from the UIs,
but users who had it disabled previously have
no way to enable it. After this change
encryption is effectively always preferred.
2025-09-04 13:58:05 +00:00
link2xt
838eed94bc chore: update provider database 2025-09-04 13:58:05 +00:00
link2xt
9870725d1f refactor: remove unused EncryptPreference::Reset 2025-09-04 13:58:05 +00:00
link2xt
ba827283be docs(STYLE.md): prefer BTreeMap and BTreeSet over hash variants 2025-09-04 12:26:50 +00:00
dependabot[bot]
1e37cb8c3c chore(cargo): bump nu-ansi-term from 0.46.0 to 0.50.1
Bumps [nu-ansi-term](https://github.com/nushell/nu-ansi-term) from 0.46.0 to 0.50.1.
- [Release notes](https://github.com/nushell/nu-ansi-term/releases)
- [Changelog](https://github.com/nushell/nu-ansi-term/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nushell/nu-ansi-term/compare/v0.46.0...v0.50.1)

---
updated-dependencies:
- dependency-name: nu-ansi-term
  dependency-version: 0.50.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 13:17:03 -03:00
dependabot[bot]
1991e01641 Merge pull request #7157 from chatmail/dependabot/cargo/tempfile-3.21.0 2025-09-02 23:29:24 +00:00
dependabot[bot]
d7e87b6336 Merge pull request #7152 from chatmail/dependabot/cargo/syn-2.0.106 2025-09-02 23:18:00 +00:00
dependabot[bot]
fde490ba15 chore(cargo): bump tempfile from 3.20.0 to 3.21.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.20.0 to 3.21.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.20.0...v3.21.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:01:42 +00:00
dependabot[bot]
cf5a16d967 chore(cargo): bump syn from 2.0.104 to 2.0.106
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.104 to 2.0.106.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.104...2.0.106)

---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.106
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:01:28 +00:00
dependabot[bot]
e8dde9c63d chore(cargo): bump thiserror from 2.0.12 to 2.0.16
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 2.0.12 to 2.0.16.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/2.0.12...2.0.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 22:00:03 +00:00
dependabot[bot]
667a935665 chore(cargo): bump serde_json from 1.0.142 to 1.0.143
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.142 to 1.0.143.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.142...v1.0.143)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:34:37 +00:00
dependabot[bot]
28cea706fa chore(cargo): bump anyhow from 1.0.98 to 1.0.99
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.98 to 1.0.99.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.98...1.0.99)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:19:06 +00:00
dependabot[bot]
6365a46fac chore(cargo): bump percent-encoding from 2.3.1 to 2.3.2
Bumps [percent-encoding](https://github.com/servo/rust-url) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/commits)

---
updated-dependencies:
- dependency-name: percent-encoding
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 21:18:44 +00:00
dependabot[bot]
a81496e9ab Merge pull request #7150 from chatmail/dependabot/cargo/quick-xml-0.38.3 2025-09-02 18:56:21 +00:00
dependabot[bot]
ca05733b9d Merge pull request #7151 from chatmail/dependabot/cargo/toml-0.9.5 2025-09-02 18:55:42 +00:00
dependabot[bot]
dfb5348a78 Merge pull request #7156 from chatmail/dependabot/cargo/brotli-8.0.2 2025-09-02 18:55:16 +00:00
dependabot[bot]
602e52490c Merge pull request #7158 from chatmail/dependabot/cargo/async_zip-0.0.18 2025-09-02 18:54:53 +00:00
dependabot[bot]
740b24e8a4 Merge pull request #7160 from chatmail/dependabot/cargo/futures-lite-2.6.1 2025-09-02 18:53:52 +00:00
dependabot[bot]
44a09ffd12 Merge pull request #7164 from chatmail/dependabot/cargo/hyper-1.7.0 2025-09-02 18:49:24 +00:00
dependabot[bot]
054c42cbc2 Merge pull request #7159 from chatmail/dependabot/cargo/uuid-1.18.0 2025-09-02 18:48:37 +00:00
dependabot[bot]
34263a70e2 Merge pull request #7162 from chatmail/dependabot/cargo/tokio-util-0.7.16 2025-09-02 18:48:05 +00:00
link2xt
7ea6ca35d7 feat: do not replace messages with an error on verification failure 2025-09-02 18:29:53 +00:00
link2xt
a9aad497fc api!: remove deprecated is_protection_broken 2025-09-02 18:29:53 +00:00
link2xt
7da8489635 api!: remove is_profile_verified APIs
UIs now display green checkmark in a profile
if the contact is verified.
Chats with key-contacts cannot become unprotected,
so there is no need to check 1:1 chat.
2025-09-02 18:29:53 +00:00
link2xt
683561374d test: add TestContext.create_chat_id 2025-09-02 18:29:53 +00:00
link2xt
66c9982822 fix: add "Messages are end-to-end encrypted." to non-protected groups
The messages are end-to-end encrypted
in encrypted group regardless
of whether the group is protected or not.
2025-09-02 18:29:53 +00:00
link2xt
1b6450b210 feat: do not set "unknown sender for this chat" error 2025-09-02 18:29:53 +00:00
dependabot[bot]
aa8a13adb2 chore(cargo): bump hyper from 1.6.0 to 1.7.0
Bumps [hyper](https://github.com/hyperium/hyper) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: hyper
  dependency-version: 1.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:51:13 +00:00
dependabot[bot]
5888541c05 chore(cargo): bump tokio-util from 0.7.14 to 0.7.16
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.14 to 0.7.16.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.14...tokio-util-0.7.16)

---
updated-dependencies:
- dependency-name: tokio-util
  dependency-version: 0.7.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:08:06 +00:00
dependabot[bot]
f893487dc0 chore(cargo): bump futures-lite from 2.6.0 to 2.6.1
Bumps [futures-lite](https://github.com/smol-rs/futures-lite) from 2.6.0 to 2.6.1.
- [Release notes](https://github.com/smol-rs/futures-lite/releases)
- [Changelog](https://github.com/smol-rs/futures-lite/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/futures-lite/compare/v2.6.0...v2.6.1)

---
updated-dependencies:
- dependency-name: futures-lite
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 17:02:54 +00:00
dependabot[bot]
b84beaf974 chore(cargo): bump uuid from 1.17.0 to 1.18.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 16:37:29 +00:00
dependabot[bot]
75a3c55e70 chore(cargo): bump async_zip from 0.0.17 to 0.0.18
Bumps [async_zip](https://github.com/Majored/rs-async-zip) from 0.0.17 to 0.0.18.
- [Commits](https://github.com/Majored/rs-async-zip/compare/v0.0.17...v0.0.18)

---
updated-dependencies:
- dependency-name: async_zip
  dependency-version: 0.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:57:03 +00:00
dependabot[bot]
854a09e12f chore(cargo): bump brotli from 8.0.1 to 8.0.2
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 8.0.1 to 8.0.2.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits/8.0.2)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 15:40:32 +00:00
dependabot[bot]
40412fd4a9 chore(cargo): bump toml from 0.9.4 to 0.9.5
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.4 to 0.9.5.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.4...toml-v0.9.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-02 14:32:01 +00:00
dependabot[bot]
57fc084795 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-09-02 14:25:16 +00:00
Nico de Haen
143ba6d5e7 fix: Mark message as failed if it can't be send (#7143)
resolves #6849
2025-09-02 14:32:25 +02:00
bjoern
6b338a923c feat: warn for outdated versions after 6 months (#7144)
3 months were proven to be too short some years ago, after that issue,
we went far up to 12 months.
however, 12 months were considered too long after recent discussions :)
so, 6 months seems to be a good compromise.

the warning is still repeated every months and the text is unchanged.

advantage is still that this approach does not require network or
opt-in, and catches really all lazy updaters with few effort, cmp
https://github.com/deltachat/deltachat-desktop/issues/5422
2025-09-01 17:44:54 +02:00
iequidoo
e6ab1e3df5 fix: Update verifier_id if it's "unknown" and new verifier has known verifier
Now that the previous commit avoids creating incorrect reverse verification chains, we can do
this. Sure, existing users' dbs aready have verification chains ending with "unknown" roots, but at
least for new users updating `verifier_id` to a known verifier makes sense.
2025-09-01 05:09:19 -03:00
iequidoo
5da6976bf9 fix: Don't verify contacts by others having an unknown verifier
If this happens, mark the contact as verified by an unknown contact instead. This avoids introducing
incorrect reverse chains: if the verifier itself has an unknown verifier, it may be `contact_id`
actually (directly or indirectly) on the other device (which is needed for getting "verified by
unknown contact" in the first place).
2025-09-01 05:09:19 -03:00
iequidoo
bd15d90e77 refactor: Check that verifier is verified in turn 2025-09-01 05:09:19 -03:00
iequidoo
61633cf23b fix: Don't reverify contacts by SELF on receipt of a message from another device
Also verify not yet verified contacts w/o setting a verifier for them (in the db it's stored as
`verifier_id=id` though) because we don't know who verified them for another device.
2025-09-01 05:09:19 -03:00
iequidoo
9f1107c0e7 docs: Fix for SecurejoinInviterProgress with progress == 600 2025-09-01 03:57:51 -03:00
Hocuri
ff0d5ce179 test: Add another TimeShiftFalsePositiveNote (#7142)
test_maybe_warn_on_outdated() can also fail when run with `cargo test`,
rather than `cargo nextest` (just happened to me)
2025-08-31 19:32:30 +00:00
bjoern
0bbd910883 feat: add call ringing API (#6650)
this PR adds a "ringing" api that can be used for calls later.

see deltachat.h for details about the API; jsonrpc is left out until
things are settled for the needs of android/iOS

UI using this PR already successfully are
https://github.com/deltachat/deltachat-ios/pull/2638 and
https://github.com/deltachat/deltachat-android/pull/3785 ; the "payload"
passed forth and back is optimised for
https://github.com/deltachat/calls-webapp

---------

Co-authored-by: l <link2xt@testrun.org>
2025-08-30 23:48:38 +02:00
dependabot[bot]
4258088fb4 chore(cargo): bump tracing-subscriber from 0.3.19 to 0.3.20
Bumps [tracing-subscriber](https://github.com/tokio-rs/tracing) from 0.3.19 to 0.3.20.
- [Release notes](https://github.com/tokio-rs/tracing/releases)
- [Commits](https://github.com/tokio-rs/tracing/compare/tracing-subscriber-0.3.19...tracing-subscriber-0.3.20)

---
updated-dependencies:
- dependency-name: tracing-subscriber
  dependency-version: 0.3.20
  dependency-type: direct:production
...

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-08-30 14:21:08 -03:00
link2xt
6372b677d2 chore(release): prepare for 2.12.0 2025-08-26 21:22:39 +00:00
link2xt
9af00af70f docs: remove the comment about Color Vision Deficiency correction
Color Vision Deficiency correction has been removed from https://xmpp.org/extensions/xep-0392.html
in version 0.8.0, see the reasoning there.
2025-08-26 21:14:08 +00:00
link2xt
4010c60e7b feat: use key fingerprints for color generation
This way contact colors stays the same
even if the address changes later.
2025-08-26 21:14:08 +00:00
link2xt
aaa83a8f52 feat: replace HSLuv colors with OKLCh 2025-08-26 21:14:08 +00:00
link2xt
776408c564 fix: do not create a group if the sender includes self in the To field 2025-08-26 18:06:17 +00:00
iequidoo
d0cb2110e6 feat: Chat::get_color(): Use grpid, if present, instead of name
While testing the previous commit i understood that it's better to try giving different colors to
groups, particularly if their names are equal so that they visually differ, and at the same time
preserve the color if the group is renamed. Using `grpid` solves this. So let groups change colors
once and forever.
2025-08-24 12:10:54 -03:00
iequidoo
11e3480fe8 feat: create_group_ex(): Log and replace invalid chat name with "…"
We can't just fail on an invalid chat name because the user would lose the work already done in the
UI like selecting members. Sometimes happens to me when i put space into name.
2025-08-24 12:10:54 -03:00
Hocuri
2cd54b72b0 refactor: Make ConnectivityStore use a non-async lock (#7129)
Follow-up to https://github.com/chatmail/core/pull/7125: We now have a
mix of non-async (parking_lot) and async (tokio) Mutexes used for the
connectivity. We can just use non-async Mutexes, because we don't
attempt to hold them over an await point. I also tested that we get a
compiler error if we do try to hold one over an await point (rather than
just deadlocking/blocking the executor on runtime).

Not 100% sure about using the parking_lot rather than std Mutex, because
since https://github.com/rust-lang/rust/issues/93740, parking_lot
doesn't have a lot of advantages anymore. But as long as iroh depends on
it, we might as well use it ourselves.
2025-08-23 21:08:17 +02:00
iequidoo
c34ccafb2e fix: Make reaction message hidden only if there are no other parts
RFC 9078 "Reaction: ..." doesn't forbid messages with reactions to have other parts, so be prepared
for this.
2025-08-22 17:32:55 -03:00
iequidoo
6837874d43 fix: get_connectivity(): Get rid of locking SchedulerState::inner (#7124)
`get_connectivity()` is expected to return immediately, not when the scheduler finishes updating its
state in `start_io()/stop_io()/pause_io()`, otherwise it causes app non-responsiveness.

Instead of read-locking `SchedulerState::inner`, store the `ConnectivityStore` collection in
`Context` and fetch it from there in `get_connectivity()`. Update it every time we release a write
lock on `SchedulerState::inner`.
2025-08-22 14:18:30 -03:00
link2xt
3656337d41 refactor: do not return Result from valid_signature_fingerprints()
This function never fails.
2025-08-18 07:47:34 +00:00
link2xt
a89b6321f1 feat: assign messages to key-contacts based on Issuer Fingerprint 2025-08-16 23:22:35 +00:00
link2xt
ac10103b18 api!(python): remove remaining broken API for reactions
CFFI for reactions has been previously deprecated and removed,
so legacy python bindings using it don't work anymore.
2025-08-16 19:50:04 +00:00
iequidoo
b696a242fc feat: wal_checkpoint(): Do wal_checkpoint(PASSIVE) and wal_checkpoint(FULL) before wal_checkpoint(TRUNCATE)
This way the subsequent `wal_checkpoint(TRUNCATE)` is faster. We don't want to block writers and
readers for a long period.
2025-08-15 14:00:41 -03:00
link2xt
7e4822c8ca fix: do not reverify already verified contacts via gossip
If the contact is already introduced by someone,
usually by adding to a verified group,
it should not be reverified because of another
chat message is a verified group.
This usually results is verification loops
and is not meaningful because the verifier
likely got this same contact introduced
in the same group.
2025-08-14 15:19:09 +00:00
link2xt
a955cb5400 docs: remove broken link from documentation comments
There are many servers by now so it is not
just a workaround for one server or location.
Stated reason is clear enough without
pointing to an issue.
2025-08-13 07:03:45 +00:00
link2xt
2e2cfc4cb3 chore(release): prepare for 2.11.0 2025-08-13 00:40:18 +00:00
iequidoo
4157d1986f fix: Add messages that can't be verified as DownloadState::Available (#7059)
We haven't dropped verified groups yet, so we need to do smth with messages that can't be verified
yet which often occurs because of messages reordering, particularly in large groups. Apart from the
reported case #7059, i had other direct reports that sometimes messages can't be verified for
various reasons, but when the reason is already fixed, it should be possible to re-download failed
messages and see them.

Also remove the code replacing the message text with a verification error from
`apply_group_changes()` as `add_parts()` already does this.
2025-08-12 20:52:14 -03:00
iequidoo
d13eb2f580 feat: receive_imf::add_parts(): Get rid of extra Chat::load_from_db() calls 2025-08-12 20:52:14 -03:00
link2xt
5476f69179 fix: don't break long group names with non-ASCII characters
The fix is in mail-builder 0.4.4.
2025-08-12 23:50:58 +00:00
dependabot[bot]
dcdf30da35 Merge pull request #7103 from chatmail/dependabot/github_actions/actions/download-artifact-5 2025-08-12 23:00:40 +00:00
dependabot[bot]
55746c8c19 Merge pull request #7104 from chatmail/dependabot/github_actions/actions/checkout-5 2025-08-12 23:00:03 +00:00
iequidoo
dbdf5f2746 feat: get_securejoin_qr(): Log error if group doesn't have grpid
This doesn't fix anything in UIs currently because they don't call `get_securejoin_qr()` for
unencrypted groups, but it's still better to log an error which will be shown in this case.
2025-08-12 19:59:00 -03:00
iequidoo
b4e28deed3 feat: lookup_key_contact_by_address(): Allow looking up ContactId::SELF without chat id
This doesn't fix anything currently, but let's allow such lookups to avoid future bugs.
2025-08-12 19:59:00 -03:00
iequidoo
f4a604dcfb refactor: Chat::is_encrypted(): Make one query instead of two for 1:1 chats 2025-08-12 19:59:00 -03:00
Hocuri
b3c5787ec8 test: Log the number of the test account if there are multiple alices (#7087)
In order to debug some test failures wrt broadcast channels, I need to
read the log of some tests that have an alice0 and an alice1.

It's currently not possible to tell whether a line was logged by alice0
or alice1, so, I would like to change that with this PR.

Edit: Turns out that there are more tests that call their profiles
"alice1"/"alice2" (or "alice"/"alice2") than "alice0"/"alice1". So, I
changed the logging to count "alice", "alice2", "alice3", ….

Not sure whether I should adapt old tests; it might save someone some
confusion in the future, but I might also make a mistake and make it so
that the test doesn't properly test anymore.
2025-08-12 08:51:23 +00:00
dependabot[bot]
471d0469dd chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 08:21:41 +00:00
dependabot[bot]
113eda575f chore(deps): bump actions/download-artifact from 4 to 5
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-12 05:32:37 +00:00
link2xt
45f1da82fe fix: take Chat-Group-Name into account when matching ad hoc groups 2025-08-12 01:10:49 +00:00
link2xt
5f45ff77e4 chore: typo fix 2025-08-12 01:10:49 +00:00
link2xt
1c0201ee3d fix: assign messages to a group if there is a Chat-Group-Name
Otherwise messages sent to small groups
with only two members are incorrectly assigned
to 1:1 chat.
2025-08-12 01:10:49 +00:00
link2xt
c7340e04ec feat: do not require resent messages to be from the same chat
Chat was only loaded to avoid removing GuaranteeE2ee
for protected chats, but resending a message
in protected chat is guaranteed to be encrypted anyway.
2025-08-11 20:11:43 +00:00
link2xt
0a32476dc5 fix: do not reset GuaranteeE2ee in the database when resending messages
Otherwise if the message is loaded by the UI
after GuaranteeE2ee is reset but before SMTP queue item
is created, the message may appear as unencrypted
even if it was actually resent as encrypted.
2025-08-11 20:11:43 +00:00
link2xt
e02bc6ffb5 ci: update Rust to 1.89.0 2025-08-11 19:40:07 +00:00
link2xt
f41a3970b2 fix: do not add key-contacts to unencrypted groups
Encrypted message may create unencrypted groups
if the message does not have a Chat-Group-ID.
This can happen if v1 client sends an encrypted
message to opportunistically encrypted ad hoc group.
In this case `from_id` corresponds to the key-contact,
but we should add address-contact of the sender
to the member list.
2025-08-11 10:33:27 +00:00
bjoern
6c536f3a9b fix: log and set imex progress error (#7091)
IMEX_PROGRESS(0) event is fired in case of errors, however, the last
error was not set in this case.

this is similar to the fix at #4195
and improves the error shown in the dialog for android and iOS; desktop
does not show an error dialog at all.

<img width="320" alt="IMG_9995"
src="https://github.com/user-attachments/assets/7065fc3d-3f30-4691-b1b2-1950564a25e2"
/>

relates to https://github.com/deltachat/deltachat-android/issues/3533
2025-08-09 01:17:27 +02:00
link2xt
4b24b6a848 refactor: skip loading the contact of 1:1 unencrypted chat to show the avatar
If the chat is unencrypted, it gets unencrypted icon.
There is no need to load the address-contact to get the same icon.
2025-08-08 19:08:23 +00:00
link2xt
5f254a929f refactor: rename icon-address-contact to icon-unencrypted
The icon is mainly used to identify unencrypted chats
in the chatlist where encrypted and unencrypted chats are mixed.
It is used for group chats rather than only for 1:1 chats
with address-contacts.
2025-08-08 19:08:23 +00:00
B. Petersen
8df1a01ace feat: better string when using disappearing messages of one year (365..367 days, so it can be tweaked later) 2025-08-08 18:45:35 +00:00
link2xt
27b5ffb34f fix: do not remove query parameters from URLs
The fix is similar to the one done in
4ca0ce2fb2,
but now extended to requests other than empty POST requests as well.
2025-08-07 14:49:18 +00:00
link2xt
80af012962 fix: set correct sent_timestamp for saved outgoing messages 2025-08-07 07:58:53 +00:00
iequidoo
615c80bef4 feat: Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts
We don't want to prefer returning verified contacts because e.g. if a bot was reinstalled and its
key changed, it may not be verified, and we don't want to bring the user to the old chat if they
click on the bot email address. But trying to return accepted contacts increases security and
doesn't break the described scenario.
2025-08-06 17:37:17 -03:00
iequidoo
f5f4026dbb feat: Contact::lookup_id_by_addr_ex: Prefer returning key-contact
If an address-contact and a key-contact were seen at exactly the same time, that doesn't necessarily
mean that it's a random event, it might occur because some code updates contacts this way in some
scenario. While this is unlikely, prefer to look up the key-contact.
2025-08-06 17:37:17 -03:00
link2xt
b431206ede fix: allow receiving empty files 2025-08-06 13:33:04 +00:00
iequidoo
c4878e9b49 fix: Run wal_checkpoint during housekeeping (#6089)
Work around possible checkpoint starvations (there were cases reported when a WAL file is bigger
than 200M) and also make sure we truncate the WAL periodically. Auto-checkponting does not normally
truncate the WAL (unless the `journal_size_limit` pragma is set), see
https://www.sqlite.org/wal.html.
2025-08-05 14:58:32 -03:00
B. Petersen
aa452971a6 fix: ignore case when trying to detect 'invalid unencrypted mail' and add an info-message 2025-08-05 15:58:43 +02:00
dependabot[bot]
2d798f7cfe Merge pull request #7066 from chatmail/dependabot/cargo/toml-0.9.4 2025-08-04 23:53:05 +00:00
link2xt
08bb0484eb chore(release): prepare for 2.10.0 2025-08-04 22:33:59 +00:00
link2xt
b0b7337f5a chore: upgrade async-imap to 0.11.1 2025-08-04 21:42:28 +00:00
Hocuri
93241a4beb feat: Also lookup key contacts in lookup_id_by_addr() (#7073)
If there is both a key and an address contact, return the most recently
seen one.
2025-08-04 21:32:09 +02:00
iequidoo
4f1bf1f13c chore(deny.toml): add exception for duplicate toml_datetime 0.6.11 dependency 2025-08-02 16:16:43 -03:00
iequidoo
2d0b7b5bd8 chore(cargo): bump human-panic from 2.0.2 to 2.0.3 2025-08-02 16:14:48 -03:00
dependabot[bot]
8fe3ce5cab chore(cargo): bump strum_macros from 0.27.1 to 0.27.2
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

---
updated-dependencies:
- dependency-name: strum_macros
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 14:15:55 -03:00
dependabot[bot]
59a0f1d94f chore(cargo): bump strum from 0.27.1 to 0.27.2
Bumps [strum](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2)

---
updated-dependencies:
- dependency-name: strum
  dependency-version: 0.27.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:55:09 -03:00
dependabot[bot]
5175dc3450 chore(cargo): bump criterion from 0.6.0 to 0.7.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.6.0 to 0.7.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.6.0...0.7.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:39:05 -03:00
dependabot[bot]
9a22ccd058 chore(cargo): bump hyper-util from 0.1.14 to 0.1.16
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.14 to 0.1.16.
- [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.14...v0.1.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:38:26 -03:00
dependabot[bot]
c06ed49a2a chore(cargo): bump async-channel from 2.3.1 to 2.5.0
Bumps [async-channel](https://github.com/smol-rs/async-channel) from 2.3.1 to 2.5.0.
- [Release notes](https://github.com/smol-rs/async-channel/releases)
- [Changelog](https://github.com/smol-rs/async-channel/blob/master/CHANGELOG.md)
- [Commits](https://github.com/smol-rs/async-channel/compare/v2.3.1...v2.5.0)

---
updated-dependencies:
- dependency-name: async-channel
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 13:33:11 -03:00
dependabot[bot]
2e51a5a454 chore(cargo): bump bolero from 0.13.3 to 0.13.4
Bumps [bolero](https://github.com/camshaft/bolero) from 0.13.3 to 0.13.4.
- [Changelog](https://github.com/camshaft/bolero/blob/master/CHANGELOG.md)
- [Commits](https://github.com/camshaft/bolero/commits)

---
updated-dependencies:
- dependency-name: bolero
  dependency-version: 0.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:59:56 -03:00
dependabot[bot]
75cc353528 chore(cargo): bump serde_json from 1.0.140 to 1.0.142
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.140 to 1.0.142.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.140...v1.0.142)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-02 12:51:47 -03:00
dependabot[bot]
3977580426 chore(cargo): bump toml from 0.8.23 to 0.9.4
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 0.9.4.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v0.9.4)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 0.9.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-01 22:00:13 +00:00
link2xt
3a1370e174 chore(release): prepare for 2.9.0 2025-07-31 18:57:58 +00:00
iequidoo
c218c05b96 fix: get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker
We were reporting the UTC midnight timestamp instead. For UTC-N timezones that means reporting
"yesterday".

Fixes https://github.com/deltachat/deltachat-desktop/issues/5215.
2025-07-31 13:55:28 -03:00
iequidoo
db247d9f9a refactor: Don't call add_or_lookup_key_contacts() in advance
Its result isn't needed in all the branches.
2025-07-31 12:07:58 -03:00
iequidoo
78b7715ea6 refactor: Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts()
- It's obvious that addresses are needed to add contacts, so `_by_address_list` looks excessive.
- The function only looks up `SELF` by address.
- The name was confusing because there's also `lookup_key_contacts_by_address_list()` that actually
  looks up key contacts by addresses (not only `SELF`).
2025-07-31 12:07:58 -03:00
link2xt
ba76944d75 fix: display correct timer value for ephemeral timer changes
The timer change should not disappear,
but should display correct timer change.
2025-07-31 15:00:08 +00:00
cliffmccarthy
4a1a2122f0 feat(repl): Add import-vcard and make-vcard commands (#7048)
- Added import-vcard and make-vcard commands to deltachat-repl. These
  wrap the corresponding import_vcard() and make_vcard() operations from
  src/contact.rs. Each command takes a file path argument specifying the
  location of the vCard file to be read or written. The make-vcard command
  takes one or more IDs to write to the file.
- Documented commands in "Using the CLI client" section of README.md.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-30 13:16:29 -03:00
link2xt
d80b749dec chore(release): prepare for 2.8.0 2025-07-28 19:31:43 +00:00
link2xt
039a8b7c36 fix: lookup self by address if there is no fingerprint or gossip 2025-07-28 19:18:07 +00:00
iequidoo
779f58ab16 feat: Remove ProtectionBroken, make such chats Unprotected (#7041)
Chats can't break anymore.
2025-07-28 16:01:14 -03:00
link2xt
b9183fe5eb chore(release): prepare for 2.7.0 2025-07-26 22:49:44 +00:00
Hocuri
9d342671d5 fix: Do not fail to upgrade if the verifier of a contact doesn't exist anymore (#7044)
Fix https://github.com/chatmail/core/issues/7043
2025-07-26 22:41:11 +02:00
link2xt
4e47ebd5fc test: fix flaky test_webxdc_resend 2025-07-24 17:47:19 +00:00
Hocuri
d5c418e909 feat: Put the debug/release build version into the info (#7034)
The info will look like:

```
debug_assertions=On - DO NOT RELEASE THIS BUILD
```
or:
```
debug_assertions=Off
```

I tested that this actually works when compiling on Android.

<details><summary>This is how it looked before the second
commit</summary>
<p>


The deltachat_core_version line in the info will look like:

```
deltachat_core_version=v2.5.0 [debug build]
```
or:
```
deltachat_core_version=v2.5.0 [release build]
```

I tested that this actually works when compiling on Android.



</p>
</details>
2025-07-24 17:24:54 +02:00
Hocuri
85414558c5 test: Add regression test for verification-gossiping crash (#7033)
This adds a test for https://github.com/chatmail/core/pull/7032/.

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created; I managed to create one in
a test, but needed to access some internals of MimeFactory for that.

I then saved the email that is sent by Alice into the test-data
directory, and then made a test that Bob doesn't crash if he receives
this email. And as a sanity-check, also test that Bob marks Fiona as
verified after receiving this email.
2025-07-24 17:24:00 +02:00
iequidoo
d6af8d2526 feat: mimefactory: Order message recipients by time of addition (#6872)
Sort recipients by `add_timestamp DESC` so that if the group is large and there are multiple SMTP
messages, a newly added member receives the member addition message earlier and has gossiped keys of
other members (otherwise the new member may receive messages from other members earlier and fail to
verify them).
2025-07-24 03:11:46 -03:00
Sebastian Klähn
1209e95e34 fix: realtime late join (#6869)
This PR adds a test to reproduce a bug raised by @adbenitez that peer
channels break when the resend feature is used.

---------

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-07-23 12:50:53 +02:00
Hocuri
51f9279e67 chore(release): prepare for 2.6.0 2025-07-23 11:47:05 +02:00
Hocuri
f27d54f7fa fix: Fix crash when receiving a verification-gossiping message which a contact also sends to itself (#7032)
This should fix https://github.com/chatmail/core/issues/7030; @r10s if
you could test whether it fixes your problem

The crash happened if you received a message that has the from contact
also in the "To: " and "Chat-Group-Member-Fpr: " headers. Not sure how
it happened that such a message was created.
2025-07-23 11:09:11 +02:00
link2xt
7f3648f8ae chore(release): prepare for 2.5.0 2025-07-22 14:21:07 +00:00
link2xt
49fc258578 fix: do not ignore errors in add_flag_finalized_with_set 2025-07-22 14:17:30 +00:00
link2xt
0c51b4fe41 docs(STYLE.md): prefer try_next() over next() 2025-07-22 12:33:37 +00:00
bjoern
dbad714539 docs: clarify the meaning of is_verified() vs verifier_id() (#7027)
this PR adapts the documentation UI guidance to recent "green checkmark"
discussions

cmp https://github.com/deltachat/deltachat-pages/pull/1145,
https://github.com/deltachat/deltachat-ios/pull/2781,
https://github.com/deltachat/deltachat-android/pull/3828,
https://github.com/deltachat/deltachat-desktop/pull/5318

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-22 10:40:12 +02:00
Hocuri
edd8008650 fix: Mark all email chats as unprotected in the migration (#7026)
Previously, chat protection was only removed if the chat became an
email-chat because the key-contact has a different email, but not if the
chat became an email-chat for a different reason.

Therefore, it could happen that the migration produced a protected,
unencrypted chat, where you couldn't write any messages.

I tested that applying this fix actually fixes the bug I had.
2025-07-21 20:15:59 +00:00
Hocuri
615a1b3f4e fix: Correctly migrate "verified by me" 2025-07-21 18:28:19 +00:00
bjoern
fe6044e1aa docs: deprecate protection-broken and related stuff (#7018)
came over these parts while targeting the new info message of
https://github.com/chatmail/core/pull/7008 in
https://github.com/deltachat/deltachat-ios/pull/2778 and
https://github.com/deltachat/deltachat-android/pull/3822

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-21 18:40:00 +02:00
link2xt
46b275bfab chore(release): prepare for 2.4.0 2025-07-21 15:08:00 +00:00
link2xt
25f44c517a chore: update async-imap to 0.11.0 2025-07-21 15:03:15 +00:00
link2xt
cac04f8ee4 refactor: use try_next() when processing FETCH responses 2025-07-21 13:55:02 +00:00
link2xt
45d8566ec0 fix: do not ignore errors when draining FETCH responses 2025-07-21 13:55:02 +00:00
link2xt
29a98ba13b fix: update tokio-io-timeout to 1.2.1
tokio-io-timeout 1.2.0 used previously
did not reset the timeout when returning
timeout error. This resulted
in infinite loop spamming
the log with messages that look like this and using 100% CPU:

    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.
    Read error on stream 192.168.1.20:993 after reading 9118 and writing 1036 bytes: timed out.

Normally these messages should be separated by at least 1 minute timeout.

The reason for infinite loop is not figured out yet,
but this change should at least fix 100% CPU usage.

See <https://github.com/sfackler/tokio-io-timeout/issues/13>
for the bugreport and
<https://github.com/sfackler/tokio-io-timeout/pull/14>
for the bugfix.
2025-07-20 10:53:35 +00:00
link2xt
e3973f6448 chore(release): prepare for 2.3.0 2025-07-19 11:58:13 +00:00
link2xt
7b41425fe4 fix: save peer address for LoggingStream early
Socket may lose peer address when it is disconnected from
the server side. In this case debug_assert! failed
for me when running the core in debug mode on desktop.
To avoid the case of peer_addr not being available,
we now store it when LoggingStream is created.
2025-07-19 10:58:35 +00:00
bjoern
2c7d51f98f feat: add "e2ee encrypted" info message to all e2ee chats (#7008)
this PR adds a info message "messages are end-to-end-encrypted" also for
chats created by eg. vcards. by the removal of lock icons, this is a
good place to hint for that in addition; this is also what eg. whatsapp
and others are doing

the wording itself is tweaked at
https://github.com/deltachat/deltachat-android/pull/3817 (and there is
also the rough idea to make the message a little more outstanding, by
some more dedicated colors)

~~did not test in practise, if this leads to double "e2ee info messages"
on secure join, tests look good, however.~~ EDIT: did lots of practise
tests meanwhile :)

most of the changes in this PR are about test ...

ftr, in another PR, after 2.0 reeases, there could probably quite some
code cleanup wrt set-protection, protection-disabled etc.

---------

Co-authored-by: Hocuri <hocuri@gmx.de>
2025-07-18 22:08:33 +02:00
l
a2df29515a feat: log the number of read/written bytes on IMAP stream read error (#6924) 2025-07-17 20:01:16 +00:00
link2xt
6df1d165dd feat: log when background fetch of all accounts finishes successfully 2025-07-17 15:46:46 +00:00
cliffmccarthy
e03e2d9a68 fix: List e-mail contacts in repl listcontacts command
- After the revisions to support key contacts, the 'listcontacts'
  command in the repl only lists key-contacts.  A separate flag now
  has to be passed to Contact::get_all() to list address contacts.
  This makes it difficult to run the example in README.md because we
  need to see the new contact so we can get its ID for the next
  command in the example, that creates a chat by ID.
- Revised 'listcontacts' command to make a second call to
  Contact::get_all() with the DC_GCL_ADDRESS flag, then print the
  e-mail contacts after the key contacts.
- Revised configuration example in top-level README.md to reflect
  current command output.

fixes #7011
2025-07-17 10:27:14 +02:00
iequidoo
8fc6ea19b4 feat: {ensure_and,logged}_debug_assert: Don't evaluate condition twice 2025-07-16 11:41:45 -03:00
iequidoo
c5c947e175 feat(repl): Print errors and debug logs to stderr
Follow-up to 545007aca5.
2025-07-16 11:39:56 -03:00
iequidoo
6d8dff54a7 fix: Ignore protected headers in outer message part (#6357)
Delta Chat always adds protected headers to the inner encrypted or signed message, so if a protected
header is only present in the outer part, it should be ignored because it's probably added by the
server or somebody else. The exceptions are Subject and List-ID because there are known cases when
they are only present in the outer message part.

Also treat any Chat-* headers as protected. This fixes e.g. a case when the server injects a
"Chat-Version" IMF header tricking Delta Chat into thinking that it's a chat message.

Also handle "Auto-Submitted" and "Autocrypt-Setup-Message" as protected headers on the receiver
side, this was apparently forgotten.
2025-07-16 11:39:06 -03:00
link2xt
a0f6bdffeb chore(release): prepare for 2.2.0 2025-07-14 18:43:03 +00:00
link2xt
e6fd52afff fix: always prefer the last header
Headers are normally added at the top of the message,
e.g. when forwarding new `Received` headers are
added at the top.

When headers are protected with DKIM-Signature
and oversigning is not used,
forged headers may be added on top
so headers from the top are generally less trustworthy.

This is tested with `test_take_last_header`,
but so far last header was only preferred
for known headers. This change extends
preference of the last header to all headers.
2025-07-14 14:57:52 +00:00
Nico de Haen
0142515887 api!: In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property (#7003)
- removed ChatListItem.is_broadcast
- mark ChatListItem.is_group as deprecated
2025-07-14 11:16:28 +02:00
link2xt
d45ec7f34d feat: advance next UID even if connection fails while fetching
Connection sometimes fails while processing FETCH
responses. In this case `fetch_new_messages` exits early
and does not advance next expected UID even if
some messages were processed.

This results in prefetching the same messages
after reconnection and log messages
similar to
"Not moving the message ab05c85a-e191-4fd2-a951-9972bc7e167f@localhost that we have seen before.".

With this change we advance next expected UID
even if `fetch_new_messages` returns a network error.
2025-07-13 16:32:26 +00:00
iequidoo
752f45f0f0 test: Unencrypted group creation (#6927) 2025-07-13 12:59:33 -03:00
iequidoo
0299543a86 api(jsonrpc): Add CommandApi::create_group_chat_unencrypted() (#6927) 2025-07-13 12:59:33 -03:00
iequidoo
d3908d6b36 api: Add chat::create_group_ex(), deprecate create_group_chat() (#6927)
`chat::create_group_ex()` gains an `encryption: Option<ProtectionStatus>` parameter to support
unencrypted chats.
2025-07-13 12:59:33 -03:00
iequidoo
2cf979de53 feat: Donation request device message (#6913)
A donation request device message is added if >= 100 messages have been sent and delivered. The
condition is checked every 30 days since the first message is sent. The message is added only once.
2025-07-13 11:53:14 -03:00
Hocuri
f5e8c8083d test: Tune down DELTACHAT_SAVE_TMP_DB hint (#6998)
Follow-up for https://github.com/chatmail/core/pull/6992

Since we're printing the hint to stderr now, rather than stdout (as per
link2xt's suggestion), it was too noisy. Also, it was printed once for
every test account rather than once per test.

Now, it integrates nicely with rust's hint to enable a backtrace:

```
  stderr ───

    thread 'chat::chat_tests::test_broadcasts_name_and_avatar' panicked at src/chat/chat_tests.rs:2757:5:
    assertion failed: `(left == right)`

    Diff < left / right > :
    <true
    >false


    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
    note: If you want to examine the database files, set environment variable DELTACHAT_SAVE_TMP_DB=1

        FAIL [   0.265s] deltachat chat::chat_tests::test_broadcast
```
2025-07-12 12:52:04 +02:00
iequidoo
58b99f59f7 feat: Log failed debug assertions in all configurations
Add `logged_debug_assert` macro logging a warning if a condition is not satisfied, before invoking
`debug_assert!`, and use this macro where `Context` is accessible (i.e. don't change function
signatures for now).
Follow-up to 0359481ba4.
2025-07-12 07:27:55 -03:00
link2xt
402e42f858 chore(release): prepare for 2.1.0 2025-07-11 22:56:57 +00:00
link2xt
fbae0739a6 chore(cargo): update cordyceps from 0.3.2 to 0.3.4 2025-07-11 22:07:27 +00:00
iequidoo
0359481ba4 feat: ensure_and_debug_assert{,_eq,_ne} macros combining debug_assert* and anyhow::ensure (#6907)
We have some debug assertions already, but we also want the corresponding errors in the release
configuration so that it's not less reliable than non-optimized one. This doesn't change any
function signatures, only debug assertions in functions returning `Result` are replaced.

Co-authored-by: l <link2xt@testrun.org>
2025-07-11 14:59:49 -03:00
Hocuri
6406f305b8 feat: Make it possible to leave broadcast channels (#6984)
Part of #6884.
The channel owner will not be notified in any way that you left, they
will only see that there is one member less.

For the translated stock strings, this is what we agreed on in the
group:
- Add a new "Leave Channel" stock string (will need to be done in UIs)
- Reword the existing "Are you sure you want to leave this group?"
string to "Are you sure you want to leave?" (the options are "Cancel"
and "Leave Group" / "Leave Channel", so it's clear what you are leaving)
(will need to be done in the deltachat-android repo, other UIs will pick
it up automatically)
- Reword the existing "You left the group." string to "You left". (done
here, I will adapt the strings in deltachat-android, too)

I adapted DC Android by pushing
6df2740884
to https://github.com/deltachat/deltachat-android/pull/3783.

---------

Co-authored-by: l <link2xt@testrun.org>
2025-07-11 12:34:05 +00:00
Hocuri
e5e0f0cdd7 test: Add option to save database on test failure (#6992)
I had it a few times now that I wanted to examine the database in order
to debug a test failure. Setting this environment variable makes this
easy in the future.
2025-07-11 12:01:04 +00:00
B. Petersen
0bac4acdd8 docs: update showpadlock ffi 2025-07-11 13:11:01 +02:00
Alireza Sadraii
ce5697c5f7 feat: add account ordering functionality (#6993)
New public API `set_accounts_order` allows setting the order of accounts.

The account order is stored as a list of account IDs in `accounts.toml`
under a new `accounts_order: Vec<u32>` field.
2025-07-10 22:59:27 +00:00
ivn
22258f7269 fix!: Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx (#6825)
BREAKING CHANGE: messages with invalid images, images of unknown size,
huge images, will have Viewtype::File

After changing the logic of Viewtype selection, I had to fix 3 old tests
that used invalid Base64 image data.

Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-07-10 18:11:55 -03:00
link2xt
5ab107866a feat: log emitted logging events with tracing 2025-07-10 00:27:24 +00:00
iequidoo
374a5ef687 feat: Don't apply chat name and avatar changes from non-members
Non-members can't modify the member list (incl. adding themselves), modify an ephemeral timer, so
they shouldn't be able to change the group name or avatar, just for consistency. Even if messages
are reordered and a group name change from a new member arrives before its addition, the new group
name will be applied on a receipt of the next message following the addition message because
Chat-Group-Name-Timestamp increases. While Delta Chat groups aimed for chatting with trusted
contacts, accepting group changes from everyone knowing Chat-Group-Id means that if any of the past
members have the key compromised, the group should be recreated which looks impractical.
2025-07-09 17:39:55 -03:00
iequidoo
1a2e355bb8 feat: migrations: Use tools::Time to measure time for logging
There's a comment in `tools` that tells to use `tools::Time` instead of `Instant` because on Android
the latter doesn't advance in the deep sleep mode. The only place except `migrations` where
`Instant` is used is tests, but we don't run CI on Android. It's unlikely that Delta Chat goes to
the deep sleep while executing migrations, but still possible, so let's use `tools::Time` as
everywhere else.
2025-07-09 17:13:07 -03:00
link2xt
192a6a2b9d chore(release): prepare for 2.0.0 2025-07-09 18:31:32 +00:00
Sebastian Klähn
4ca0ce2fb2 fix: Add query to post request for account creation (#6989) 2025-07-09 18:17:17 +00:00
link2xt
ab4cb01065 fix: do not try to lookup key-contacts for unencrypted 1:1 messages 2025-07-09 17:02:31 +00:00
link2xt
661a8864b9 test: add a test reproducing chat assignment bug 2025-07-09 17:02:31 +00:00
link2xt
67f00fbb84 refactor: remove check that is always false
The check for chat_id.is_some() is inside the `else` branch
of a pattern-matching `if` that looks for `Some` pattern.
2025-07-09 17:02:30 +00:00
iequidoo
389649ea8a fix: Save msgs to key-contacts migration state and run migration periodically (#6956)
Save:
- (old contact id) -> (new contact id) mapping.
- The message id starting from which all messages are already migrated.
Run the migration from `housekeeping()` for at least 500 ms and for >= 1000 messages per run.
2025-07-09 09:10:49 -03:00
iequidoo
a87ee030fc fix: migrate_key_contacts(): Remove "id>9" from encrypted messages SELECT
+ Replace LIKE with GLOB, the latter is case-sensitive.
2025-07-09 09:10:49 -03:00
iequidoo
3f66ae91cd feat: Check images passed as File before making them Image
We don't want images having unsupported format or corrupted ones to be sent as `Image` and appear in
the "Images" tab in UIs because they can't be displayed correctly.
2025-07-08 17:43:13 -03:00
iequidoo
75b7bea78f fix: Decide on filename used for sending depending on the original Viewtype
If a user attaches an image as `File`, we should send the original filename. And vice versa, if it's
`Image` originally, we mustn't reveal the filename.

The filename used for sending is now also saved to the db, so all the sender's devices will display
the same filename in the message info.
2025-07-08 17:43:13 -03:00
iequidoo
acba27a328 fix: Treat and send images that can't be decoded as Viewtype::File
Otherwise unsupported and corrupted images are displayed in the "Images" tab in UIs and that looks
as a Delta Chat bug. This should be a rare case though, so log it as error and let the user know
that metadata isn't removed from the image at least.
2025-07-08 17:43:13 -03:00
iequidoo
cba9eb98d6 refactor: build_body_file(): Remove guessing mimetype by file extension
Guessing mimetype is already done in `chat::prepare_msg_blob()`.
2025-07-08 17:43:13 -03:00
iequidoo
da9b24d191 fix: Treat "tgs" as Viewtype::File
`Viewtype::Sticker` has special meaning: the file should be an image having fully transparent
pixels. But "tgs" (Telegram animated sticker) is a compressed JSON and isn't recognized by Core as
image.
2025-07-08 17:43:13 -03:00
Hocuri
c9c5d94666 fix: Prefer encrypted List-Id header (#6983)
If there is an encrypted List-Id header, it should be preferred over an
unencrypted List-Id header.

Part of #6884
2025-07-07 20:45:21 +00:00
Hocuri
aad8f698dd fix: Don't send ChatGroupId for broadcast channels (#6975)
Older versions of Delta Chat ignore the message if it contains a
ChatGroupId header. ("older versions" means all versions without #6901,
i.e.currently released versions)

This means that without this PR, broadcast channel messages sent from
current main don't arrive at a device running latest released DC.

Part of #6884.
2025-07-07 12:06:54 +02:00
Hocuri
35e107e87d api!: Add InBroadcastChannel, OutBroadcastChannel chattypes, add create_broadcast_channel() (#6901)
In https://github.com/chatmail/core/pull/6901, I unfortunately forgot to
document the API change when squash-merging, so, I'm doing this with the
PR here.

The API change is breaking because not adapting to the new channel types
would lead to errors.
2025-07-07 11:42:02 +02:00
link2xt
d9b361f066 docs: remove outdated comment that says MDNs are unencrypted 2025-07-06 22:25:15 +00:00
link2xt
94e75cb3b8 test: add online test for read receipts 2025-07-06 22:25:15 +00:00
link2xt
c7fb64e2f3 fix: send Autocrypt header in MDNs
Otherwise MDNs are attributed to address-contacts
rather than key-contacts.
2025-07-06 22:25:15 +00:00
link2xt
ebddabe958 api(deltachat-rpc-client): add Message.get_read_receipts() 2025-07-06 22:25:15 +00:00
cliffmccarthy
b81f7cfcab fix: Update argument documentation and handling in repl (#6979)
- Updated argument descriptions in help for import-keys,
createbroadcast, and groupimage.
- Revised import-keys to check for the required argument.
2025-07-06 19:06:14 +00:00
cliffmccarthy
9197ef04f7 fix: Update repl help and autocomplete to match implementation (#6978) 2025-07-06 19:05:29 +00:00
iequidoo
7e4d4cf680 api: Contact::get_all(): Support listing address-contacts
Also test-cover `DC_GCL_ADD_SELF`.
2025-07-03 07:10:36 -03:00
Hocuri
0a73c2b7ab feat: Show broadcast channels in their own, proper "Channel" chat (#6901)
Part of #6884 

----

- [x] Add new chat type `InBroadcastChannel` and `OutBroadcastChannel`
for incoming / outgoing channels, where the former is similar to a
`Mailinglist` and the latter is similar to a `Broadcast` (which is
removed)
- Consideration for naming: `InChannel`/`OutChannel` (without
"broadcast") would be shorter, but less greppable because we already
have a lot of occurences of `channel` in the code. Consistently calling
them `BcChannel`/`bc_channel` in the code would be both short and
greppable, but a bit arcane when reading it at first. Opinions are
welcome; if I hear none, I'll keep with `BroadcastChannel`.
- [x] api: Add create_broadcast_channel(), deprecate
create_broadcast_list() (or `create_channel()` / `create_bc_channel()`
if we decide to switch)
  - Adjust code comments to match the new behavior.
- [x] Ask Desktop developers what they use `is_broadcast` field for, and
whether it should be true for both outgoing & incoming channels (or look
it up myself)
- I added `is_out_broadcast_channel`, and deprecated `is_broadcast`, for
now
- [x] When the user changes the broadcast channel name, immediately show
this change on receiving devices
- [x] Allow to change brodacast channel avatar, and immediately apply it
on the receiving device
- [x] Make it possible to block InBroadcastChannel
- [x] Make it possible to set the avatar of an OutgoingChannel, and
apply it on the receiving side
- [x] DECIDE whether we still want to use the broadcast icon as the
default icon or whether we want to use the letter-in-a-circle
- We decided to use the letter-in-a-circle for now, because it's easier
to implement, and I need to stay in the time plan
- [x] chat.rs: Return an error if the user tries to modify a
`InBroadcastChannel`
- [x] Add automated regression tests
- [x] Grep for `broadcast` and see whether there is any other work I
need to do
- [x] Bug: Don't show `~` in front of the sender's same in broadcast
lists

----

Note that I removed the following guard:

```rust
        if !new_chat_contacts.contains(&ContactId::SELF) {
            warn!(
                context,
                "Received group avatar update for group chat {} we are not a member of.", chat.id
            );
        } else if !new_chat_contacts.contains(&from_id) {
            warn!(
                context,
                "Contact {from_id} attempts to modify group chat {} avatar without being a member.",
                chat.id,
            );
        } else [...]
```

i.e. with this change, non-members will be able to modify the avatar.
Things were slightly easier this way, and I think that this is in line
with non-members being able to modify the group name and memberlist
(they need to know the Group-Chat-Id, anyway), but I can also change it
back.
2025-07-02 20:40:30 +00:00
dependabot[bot]
2ee3f58b69 chore(cargo): bump libc from 0.2.172 to 0.2.174
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.172 to 0.2.174.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.174/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.172...0.2.174)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 13:10:57 -03:00
dependabot[bot]
f60af72a5e chore(cargo): bump proptest from 1.6.0 to 1.7.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.6.0 to 1.7.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/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 12:32:16 -03:00
dependabot[bot]
95125d30ef chore(cargo): bump toml from 0.8.19 to 0.8.23
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.19 to 0.8.23.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.19...toml-v0.8.23)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 12:31:10 -03:00
dependabot[bot]
48a9fafe6c chore(cargo): bump hyper-util from 0.1.13 to 0.1.14
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.13 to 0.1.14.
- [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.13...v0.1.14)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 12:28:06 -03:00
dependabot[bot]
c4cc2fe731 chore(cargo): bump syn from 2.0.101 to 2.0.104
---
updated-dependencies:
- dependency-name: syn
  dependency-version: 2.0.104
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 12:26:43 -03:00
dependabot[bot]
3df0bd8890 chore(cargo): bump smallvec from 1.15.0 to 1.15.1
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.15.0 to 1.15.1.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.15.0...v1.15.1)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-version: 1.15.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-02 12:25:39 -03:00
cliffmccarthy
2a5a0717aa fix: Remove listverified from repl
- The implementation of listverified was removed in commit
  37dc1f5ca0, but it still shows up in
  the help and in the auto-complete grammar.
- Removed listverified where it still appears.

closes #6971
2025-07-02 11:56:15 -03:00
link2xt
ee8364913b fix: allow to scan invite links before configuration 2025-06-30 21:27:20 +00:00
iequidoo
3267126a33 feat: Preserve minimum info for trashed messages
+ Make `MsgId::trash()` `pub(crate)`, not public.
+ In `delete_expired_messages()`, prepare SQL statements to be executed in a loop.
2025-06-30 16:50:35 -03:00
link2xt
2ee3675ba2 ci: update Rust to 1.88.0 2025-06-30 18:15:29 +00:00
Hocuri
faf4fd1ca6 api(CFFI): Add dc_contact_is_key_contact() (#6955)
We need this because it's not clear whether Android should switch to
JsonRPC for everything, because of concerns that JsonRPC might be a lot
slower than the CFFI (although we still need to measure that).
2025-06-30 17:22:29 +00:00
link2xt
53ebf2ca27 feat: increase event channel size from 1000 to 10000
SQL migration to key contacts generates a lot of events,
and they are dropped in desktop logs because it does
not read the events fast enough.
This at least reduces the number of dropped messages.
2025-06-30 12:25:57 +00:00
iequidoo
f3eea9937c fix: Key-contacts migration: ignore past members with missing keys (#6941)
Missing key for a past member isn't a reason for conversion of an encrypted group to an ad hoc
group.
2025-06-28 15:56:47 -03:00
link2xt
5c3de759d3 refactor: upgrade to Rust 2024 2025-06-28 17:07:59 +00:00
link2xt
0ffd4d9f87 fix: wait for scheduler tasks shutdown in parallel 2025-06-28 17:05:32 +00:00
link2xt
416131b4a2 feat: key-contacts
This change introduces a new type of contacts
identified by their public key fingerprint
rather than an e-mail address.

Encrypted chats now stay encrypted
and unencrypted chats stay unencrypted.
For example, 1:1 chats with key-contacts
are encrypted and 1:1 chats with address-contacts
are unencrypted.
Groups that have a group ID are encrypted
and can only contain key-contacts
while groups that don't have a group ID ("adhoc groups")
are unencrypted and can only contain address-contacts.

JSON-RPC API `reset_contact_encryption` is removed.
Python API `Contact.reset_encryption` is removed.
"Group tracking plugin" in legacy Python API was removed because it
relied on parsing email addresses from system messages with regexps.

Co-authored-by: Hocuri <hocuri@gmx.de>
Co-authored-by: iequidoo <dgreshilov@gmail.com>
Co-authored-by: B. Petersen <r10s@b44t.com>
2025-06-26 14:07:39 +00:00
link2xt
7ac04d0204 fix: remove display name from get_info()
Display name is rarely needed for debugging,
so there is no need to include it in the logs.
Display name is even already listed in `skip_from_get_info`,
but the test only allowed the values to be skipped
without checking that they are always skipped.
2025-06-25 17:01:51 +00:00
link2xt
a40337f4e0 chore(release): prepare for 1.160.0 2025-06-22 12:26:53 +00:00
link2xt
b45d9aa464 chore: update rusqlite to 0.36.0 2025-06-21 13:46:00 +00:00
link2xt
48b2e2bc1f chore: sort the list in deny.toml 2025-06-21 13:46:00 +00:00
link2xt
545007aca5 api!: make logging macros private 2025-06-21 11:01:25 +00:00
link2xt
07ce319839 api!(jsonrpc): remove webxdc info from MessageObject 2025-06-18 11:48:32 +00:00
link2xt
0d36c85568 chore: disable some Python lints introduced in ruff 0.12 2025-06-18 10:19:48 +00:00
link2xt
139fbfae85 chore: nightly clippy fixes 2025-06-18 10:19:48 +00:00
Hocuri
0568393157 fix: Don't change ConfiguredAddr when adding a transport (#6804)
Before this PR, ConfiguredAddr (which will be used to store the primary
transport) would have been changed when adding a new transport. Doesn't
matter yet because it's not possible yet to have multiple transports.
But I wanted to fix this bug already so that I'm not suprised by it
later.
2025-06-18 11:19:41 +02:00
iequidoo
7ec732977a fix(contact-tools): Escape commas in vCards' FN, KEY, PHOTO, NOTE (#6912)
Citing @link2xt:
> RFC examples sometimes don't escape commas, but there is errata that fixes some of them.

Also this unescapes commas in all fields. This can lead to, say, an email address with commas, but
anyway the caller should check parsed `VcardContact`'s fields for correctness.
2025-06-14 16:54:29 -03:00
link2xt
a8a7cec376 refactor: use CancellationToken instead of a 1-message channel 2025-06-11 14:28:24 +00:00
d2weber
7f6beeeecb feat: put "biography" in the vCard (#6819)
Co-authored-by: iequidoo <117991069+iequidoo@users.noreply.github.com>
2025-06-11 10:08:18 -03:00
link2xt
15092407ea build: enable async-native-tls/vendored feature
OpenSSL is vendored, but because of rusqlite feature
transitively enabling vendoring feature.
This change makes vendoring explicit
even if we disable SQLCipher in the future.
2025-06-09 22:17:52 +00:00
dependabot[bot]
bd70d48cdf chore(cargo): bump criterion from 0.5.1 to 0.6.0
Bumps [criterion](https://github.com/bheisler/criterion.rs) from 0.5.1 to 0.6.0.
- [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bheisler/criterion.rs/compare/0.5.1...0.6.0)

---
updated-dependencies:
- dependency-name: criterion
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-06-07 06:04:05 -03:00
iequidoo
ce04e904e2 fix: Sort multiple saved messages by timestamp (#6862) 2025-06-06 03:59:36 -03:00
link2xt
026ddbf9f1 build: upgrade parking_lot to 0.12.4 2025-06-05 20:15:37 +00:00
link2xt
628b178076 build: update cargo-bolero from 0.8.0 to 0.13.3
New `fuzz` profile is added
because cargo-bolero now requires it and uses
by default, while `--release` option is removed.

Instructions for running AFL are removed from the README
because it requires some system reconfiguration
and I did not test it this time.
2025-06-05 16:15:01 +00:00
WofWca
823a16e8e9 fix: fetch_url: return err on non 2xx reponses
The main reason for this change is the app picker
that Delta Chat clients use, which utilizes
the `fetch_url` function.
Sometimes we get an error from the server,
but we have no way to figure out that it's an error,
other than inspecting the body, which we don't (and shouldn't) do.
This results in us attempting to send webxdc apps
that are not even valid .zip files.

Another, arguably even worse thing is that
we also put the error responses to the cache,
so it's not easy to recover from such an error.

So, let's just return an error if the response code
is not a successful response code.
2025-06-04 23:28:17 +00:00
link2xt
407ec1311e docs: add more code style guide references 2025-06-04 19:03:51 +00:00
link2xt
b9667aae6b feat: better error for quoting a message from another chat 2025-06-04 18:28:35 +00:00
dependabot[bot]
806b437209 chore(cargo): bump tempfile from 3.19.1 to 3.20.0
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.19.1 to 3.20.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.1...v3.20.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:57:04 +00:00
dependabot[bot]
1a5232f863 chore(cargo): bump rustls-pki-types from 1.11.0 to 1.12.0
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.11.0...v/1.12.0)

---
updated-dependencies:
- dependency-name: rustls-pki-types
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:56:30 +00:00
dependabot[bot]
7ad119f126 chore(cargo): bump mail-builder from 0.4.2 to 0.4.3
Bumps [mail-builder](https://github.com/stalwartlabs/mail-builder) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/stalwartlabs/mail-builder/releases)
- [Changelog](https://github.com/stalwartlabs/mail-builder/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stalwartlabs/mail-builder/commits)

---
updated-dependencies:
- dependency-name: mail-builder
  dependency-version: 0.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 16:56:08 +00:00
dependabot[bot]
1682f4b252 chore(cargo): bump hyper-util from 0.1.11 to 0.1.13
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.11 to 0.1.13.
- [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.11...v0.1.13)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:46:22 +00:00
dependabot[bot]
6a320d545b chore(cargo): bump uuid from 1.16.0 to 1.17.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:45:22 +00:00
dependabot[bot]
e7aebd6fbc chore(cargo): bump rustyline from 15.0.0 to 16.0.0
Bumps [rustyline](https://github.com/kkawakam/rustyline) from 15.0.0 to 16.0.0.
- [Release notes](https://github.com/kkawakam/rustyline/releases)
- [Changelog](https://github.com/kkawakam/rustyline/blob/master/History.md)
- [Commits](https://github.com/kkawakam/rustyline/compare/v15.0.0...v16.0.0)

---
updated-dependencies:
- dependency-name: rustyline
  dependency-version: 16.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:44:42 +00:00
dependabot[bot]
8189abd660 chore(cargo): bump num_cpus from 1.16.0 to 1.17.0
Bumps [num_cpus](https://github.com/seanmonstar/num_cpus) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/seanmonstar/num_cpus/releases)
- [Changelog](https://github.com/seanmonstar/num_cpus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/seanmonstar/num_cpus/compare/v1.16.0...v1.17.0)

---
updated-dependencies:
- dependency-name: num_cpus
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:44:23 +00:00
dependabot[bot]
5ded153ae4 chore(cargo): bump tokio from 1.44.2 to 1.45.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.44.2 to 1.45.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.2...tokio-1.45.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:43:58 +00:00
dependabot[bot]
2fd5507c00 chore(cargo): bump brotli from 8.0.0 to 8.0.1
Bumps [brotli](https://github.com/dropbox/rust-brotli) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/dropbox/rust-brotli/releases)
- [Commits](https://github.com/dropbox/rust-brotli/commits)

---
updated-dependencies:
- dependency-name: brotli
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 00:43:48 +00:00
link2xt
becb83faf1 fix: create group chats unprotected on verification error 2025-05-31 12:54:44 +00:00
link2xt
32263b4574 fix: ignore verification error if the chat is not protected yet
If we receive a message from non-verified contact
in a non-protected chat with a Chat-Verified header,
there is no need to upgrade the chat
to verified and display an error.

If it was an attack, an attacker could
just not send the Chat-Verified header.
Most of the time, however, it is just
message reordering.
2025-05-31 12:54:44 +00:00
link2xt
fd3e48dcb2 chore: run npm run prettier:fix 2025-05-29 15:13:42 +00:00
link2xt
69573cd735 chore: update deltachat-jsonrpc JS dependencies 2025-05-29 15:13:42 +00:00
Friedel Ziegelmayer
5c2af42cdd build: update to rPGP 0.16.0 (#6719)
Co-authored-by: Heiko Schaefer <heiko@schaefer.name>
Co-authored-by: link2xt <link2xt@testrun.org>
2025-05-29 13:06:18 +00:00
link2xt
42975b2ff3 chore: expect clippy::large_enum_variant 2025-05-29 11:58:11 +00:00
link2xt
c7063c00f7 ci: use installed toolchain to lint Rust 2025-05-29 11:58:11 +00:00
link2xt
89df9536e9 fix: reduce the scope of the last_full_folder_scan lock in scan_folders
This makes it easier to ensure that holding this lock
does not result in deadlocks.
2025-05-28 15:46:01 +00:00
Sebastian Klähn
0e45c2246f fix: remove faulty test (#6880)
The test was still WIP but got merged together with the fix. I suggest
to keep the fix in main and add the test in a follow-up RP. The test
should suffice becaues I tested it manually.
2025-05-28 17:43:05 +02:00
Sebastian Klähn
81a6afde15 Fix(jsonrpc): Do not error on missign webxdc info (#6866)
When an invalid webxdc is set as draft, json-rpc's `get_draft` fails,
because `get_webxdc_info` which it calls, fails because the zip reader
can not read a non-zip file. With this change, any error occurring in
`get_webxdc_info` is ignored and the None-variant is returned instead. I
also added a test, that setting invalid xdcs is draft is fine core-wise
and checked that the input field stays responsive when a fake.xdc
produced like in #6826 is added to draft

close #6826
2025-05-28 16:29:54 +02:00
link2xt
adcc8a919c build: update Doxygen config and layout file 2025-05-26 18:19:26 +00:00
bjoern
a24e6d4278 feat: sort apps by recently-updated (#6875)
closes #6873 , see there for reasoning.

tested that on iOS already, works like a charm - and was much easier
than expected as @iequidoo already updated `timestamp_rcvd` on status
updates in https://github.com/chatmail/core/pull/5388

~~a test is missing, ordering is not tested at all, will check if that
is doable reasonably easy~~ EDIT: added a test
2025-05-26 18:33:48 +02:00
dependabot[bot]
776b2247dd chore(deps): bump astral-sh/setup-uv from 5 to 6
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 16:31:29 +00:00
link2xt
37dc1f5ca0 api!: deprecate DC_GCL_VERIFIED_ONLY 2025-05-20 16:14:43 +00:00
link2xt
a68ddab703 chore: apply beta clippy fixes 2025-05-20 14:09:07 +00:00
link2xt
877f873910 feat: add more IMAP logging
E.g. log when the folder is selected.
2025-05-19 08:25:05 +00:00
link2xt
53fa0147ae docs: update Imap.prepare() documentation 2025-05-19 08:25:05 +00:00
link2xt
7655c5b150 docs: update Imap.connect() documentation 2025-05-19 08:25:05 +00:00
link2xt
235b625f71 refactor: remove explicit lock drop at the end of scope 2025-05-19 08:25:05 +00:00
dependabot[bot]
014b0024a0 chore(deps): bump dependabot/fetch-metadata from 2.3.0 to 2.4.0
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: 2.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-17 17:49:27 +00:00
dependabot[bot]
b0508e661a Merge pull request #6827 from chatmail/dependabot/cargo/shadowsocks-1.23.1 2025-05-16 04:30:39 +00:00
link2xt
ab3cd6a8f7 chore(deny.toml): add exception for deplicate spin 0.9.8 dependency 2025-05-16 04:14:27 +00:00
dependabot[bot]
85461204c5 chore(cargo): bump shadowsocks from 1.22.0 to 1.23.1
Bumps [shadowsocks](https://github.com/shadowsocks/shadowsocks-rust) from 1.22.0 to 1.23.1.
- [Release notes](https://github.com/shadowsocks/shadowsocks-rust/releases)
- [Commits](https://github.com/shadowsocks/shadowsocks-rust/compare/v1.22.0...v1.23.1)

---
updated-dependencies:
- dependency-name: shadowsocks
  dependency-version: 1.23.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-16 03:07:34 +00:00
link2xt
3abf2b5227 build: increase MSRV to 1.85.0 2025-05-16 03:03:58 +00:00
link2xt
0d5d7032fe build: nix flake update nixpkgs 2025-05-16 03:03:58 +00:00
link2xt
c48b04ab99 ci(nix): test build on macOS without cross-compilation 2025-05-16 03:03:58 +00:00
link2xt
eaa30dbe21 build: nix flake update fenix 2025-05-16 03:03:58 +00:00
link2xt
bb0f812f71 ci: update Rust to 1.87.0 2025-05-16 03:03:58 +00:00
link2xt
4c287075da fix: do not allow chat creation if decryption failed 2025-05-15 18:02:19 +00:00
link2xt
09d18f9097 test: fixup for test_restore_backup_after_60_days 2025-05-15 18:02:19 +00:00
Hocuri
47b9bfc8bf chore(release): prepare for 1.159.5 2025-05-14 16:58:17 +02:00
Hocuri
21d13e8a9c fix: Don't change webxdc self-addr when saving and loading draft (#6854)
Fix https://github.com/chatmail/core/issues/6621; I also tested on
Android that the webxdc self-addr actually stays the same when staging a
draft and then sending.

Follow-up to https://github.com/chatmail/core/pull/6704; #6704 made sure
that the webxdc self-addr doesn't change when creating a message and
then sending it. This PR here makes sure that the rfc724_mid (which is
needed to compute the self-addr) is saved when setting a draft, so that
it's loaded properly after a call to get_draft().

cc @adbenitez @r10s @Septias
2025-05-14 16:14:35 +02:00
link2xt
079260a7cf chore: update async-smtp to 0.10.2 2025-05-13 16:18:25 +00:00
link2xt
fdec78c092 chore: remove duplicate miniz_oxide dependency 2025-05-13 15:25:56 +00:00
link2xt
259ffef0bb chore(release): prepare for 1.159.4 2025-05-13 14:56:09 +00:00
l
6661a0803e chore: update iroh from 0.33.0 to 0.35.0 (#6687) 2025-05-12 20:33:21 +00:00
link2xt
c1471bdbd9 docs: add missing documentation to deltachat-rpc-client 2025-05-12 17:39:50 +00:00
Hocuri
a981573e48 fix: Fix order of operations when handling "vc-request-with-auth" (#6850) 2025-05-12 16:52:10 +02:00
link2xt
8fb3a7514e fix: replace FuturesUnordered from futures with JoinSet from tokio
FuturesUnordered is likely buggy and iroh previously switched
to JoinSet in <https://github.com/n0-computer/iroh/pull/1647>.
We also have reports with logs of background_fetch getting
stuck so apparently task cancellation after timeout does not work
as intended with FuturesUnordered.
2025-05-10 17:26:05 +00:00
235 changed files with 18252 additions and 12154 deletions

View File

@@ -20,22 +20,24 @@ permissions: {}
env:
RUSTFLAGS: -Dwarnings
RUST_VERSION: 1.86.0
RUST_VERSION: 1.90.0
# Minimum Supported Rust Version
MSRV: 1.82.0
MSRV: 1.85.0
jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Install rustfmt and clippy
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
- run: rustup override set $RUST_VERSION
shell: bash
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Run rustfmt
@@ -51,7 +53,7 @@ jobs:
name: cargo deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -65,10 +67,12 @@ jobs:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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
@@ -78,7 +82,7 @@ jobs:
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -113,7 +117,7 @@ jobs:
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -135,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
@@ -152,7 +156,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -177,7 +181,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -199,7 +203,7 @@ jobs:
name: Python lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -224,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
@@ -242,19 +246,19 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
@@ -277,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
@@ -295,13 +299,13 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Install python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python }}
@@ -309,7 +313,7 @@ jobs:
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug

View File

@@ -30,11 +30,11 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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
@@ -54,11 +54,11 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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 }}
@@ -79,7 +79,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -105,11 +105,11 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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
@@ -132,74 +132,74 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -224,7 +224,7 @@ jobs:
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.12
@@ -285,76 +285,76 @@ jobs:
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-python@v5
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
@@ -401,7 +401,7 @@ jobs:
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.3.0
uses: dependabot/fetch-metadata@v2.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Approve a PR

View File

@@ -14,12 +14,12 @@ jobs:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

View File

@@ -16,12 +16,12 @@ jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
- name: Use Node.js 18.x
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 18.x
- name: Add Rust cache

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
@@ -19,15 +21,12 @@ jobs:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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
@@ -81,11 +80,11 @@ jobs:
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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:
@@ -95,14 +94,15 @@ jobs:
fail-fast: false
matrix:
installable:
- deltachat-rpc-server-aarch64-darwin
- deltachat-rpc-server
# Fails to bulid
# - deltachat-rpc-server-aarch64-darwin
# - deltachat-rpc-server-x86_64-darwin
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/

View File

@@ -14,11 +14,11 @@ jobs:
name: Build REPL example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
@@ -31,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
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
@@ -50,12 +50,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
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
@@ -72,13 +72,13 @@ jobs:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '18'
- name: npm install

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
show-progress: false
persist-credentials: false

View File

@@ -14,12 +14,12 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v6
- name: Run zizmor
run: uvx zizmor --format sarif . > results.sarif

1
.gitignore vendored
View File

@@ -53,3 +53,4 @@ result
# direnv
.envrc
.direnv
.aider*

View File

@@ -1,5 +1,635 @@
# 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
- [**breaking**] Remove `is_profile_verified` APIs.
- [**breaking**] Remove deprecated `is_protection_broken`.
- [**breaking**] Remove `e2ee_enabled` preference.
### Features / Changes
- Add call ringing API ([#6650](https://github.com/chatmail/core/pull/6650), [#7174](https://github.com/chatmail/core/pull/7174), [#7175](https://github.com/chatmail/core/pull/7175), [#7179](https://github.com/chatmail/core/pull/7179))
- Warn for outdated versions after 6 months instead of 1 year ([#7144](https://github.com/chatmail/core/pull/7144)).
- Do not set "unknown sender for this chat" error.
- Do not replace messages with an error on verification failure.
- Support receiving Autocrypt-Gossip with `_verified` attribute.
- Withdraw all QR codes when one is withdrawn.
### Fixes
- Don't reverify contacts by SELF on receipt of a message from another device.
- Don't verify contacts by others having an unknown verifier.
- Update verifier_id if it's "unknown" and new verifier has known verifier.
- Mark message as failed if it can't be sent ([#7143](https://github.com/chatmail/core/pull/7143)).
- Add "Messages are end-to-end encrypted." to non-protected groups.
### Documentation
- Fix for SecurejoinInviterProgress with progress == 600.
- STYLE.md: Prefer BTreeMap and BTreeSet over hash variants.
### Miscellaneous Tasks
- Update provider database.
- Update dependencies.
### Refactor
- Check that verifier is verified in turn.
- Remove unused `EncryptPreference::Reset`.
- Remove `Aheader::new`.
### Tests
- Add another TimeShiftFalsePositiveNote ([#7142](https://github.com/chatmail/core/pull/7142)).
- Add TestContext.create_chat_id.
## [2.12.0] - 2025-08-26
### API-Changes
- api!(python): remove remaining broken API for reactions
### Features / Changes
- Use Group ID for chat color generation instead of the name for encrypted groups.
- Use key fingerprints instead of addresses for key-contacts color generation.
- Replace HSLuv colors with OKLCh.
- `wal_checkpoint()`: Do `wal_checkpoint(PASSIVE)` and `wal_checkpoint(FULL)` before `wal_checkpoint(TRUNCATE)`.
- Assign messages to key-contacts based on Issuer Fingerprint.
- Create_group_ex(): Log and replace invalid chat name with "…".
### Fixes
- Do not create a group if the sender includes self in the `To` field.
- Do not reverify already verified contacts via gossip.
- `get_connectivity()`: Get rid of locking SchedulerState::inner ([#7124](https://github.com/chatmail/core/pull/7124)).
- Make reaction message hidden only if there are no other parts.
### Refactor
- Do not return `Result` from `valid_signature_fingerprints()`.
- Make `ConnectivityStore` use a non-async lock ([#7129](https://github.com/chatmail/core/pull/7129)).
### Documentation
- Remove broken link from documentation comments.
- Remove the comment about Color Vision Deficiency correction.
## [2.11.0] - 2025-08-13
### Features / Changes
- Contact::lookup_id_by_addr_ex: Prefer returning key-contact.
- Contact::lookup_id_by_addr_ex: Prefer returning accepted contacts.
- Better string when using disappearing messages of one year (365..367 days, so it can be tweaked later).
- Do not require resent messages to be from the same chat.
- `lookup_key_contact_by_address()`: Allow looking up ContactId::SELF without chat id.
- `get_securejoin_qr()`: Log error if group doesn't have grpid.
- `receive_imf::add_parts()`: Get rid of extra `Chat::load_from_db()` calls.
### Fixes
- Ignore case when trying to detect 'invalid unencrypted mail' and add an info-message.
- Run wal_checkpoint during housekeeping ([#6089](https://github.com/chatmail/core/pull/6089)).
- Allow receiving empty files.
- Set correct sent_timestamp for saved outgoing messages.
- Do not remove query parameters from URLs.
- Log and set imex progress error ([#7091](https://github.com/chatmail/core/pull/7091)).
- Do not add key-contacts to unencrypted groups.
- Do not reset `GuaranteeE2ee` in the database when resending messages.
- Assign messages to a group if there is a `Chat-Group-Name`.
- Take `Chat-Group-Name` into account when matching ad hoc groups.
- Don't break long group names with non-ASCII characters.
- Add messages that can't be verified as `DownloadState::Available` ([#7059](https://github.com/chatmail/core/pull/7059)).
### Tests
- Log the number of the test account if there are multiple alices ([#7087](https://github.com/chatmail/core/pull/7087)).
### CI
- Update Rust to 1.89.0.
### Refactor
- Rename icon-address-contact to icon-unencrypted.
- Skip loading the contact of 1:1 unencrypted chat to show the avatar.
- Chat::is_encrypted(): Make one query instead of two for 1:1 chats.
### Miscellaneous Tasks
- cargo: Bump toml from 0.8.23 to 0.9.4.
- cargo: Bump human-panic from 2.0.2 to 2.0.3.
- deny.toml: Add exception for duplicate toml_datetime 0.6.11 dependency.
- deps: Bump actions/checkout from 4 to 5.
- deps: Bump actions/download-artifact from 4 to 5.
## [2.10.0] - 2025-08-04
### Features / Changes
- Also lookup key contacts in lookup_id_by_addr() ([#7073](https://github.com/chatmail/core/pull/7073)).
### Miscellaneous Tasks
- cargo: Bump serde_json from 1.0.140 to 1.0.142.
- cargo: Bump bolero from 0.13.3 to 0.13.4.
- cargo: Bump async-channel from 2.3.1 to 2.5.0.
- cargo: Bump hyper-util from 0.1.14 to 0.1.16.
- cargo: Bump criterion from 0.6.0 to 0.7.0.
- cargo: Bump strum from 0.27.1 to 0.27.2.
- cargo: Bump strum_macros from 0.27.1 to 0.27.2.
- Upgrade async-imap to 0.11.1.
## [2.9.0] - 2025-07-31
### Features / Changes
- repl: Add import-vcard and make-vcard commands ([#7048](https://github.com/chatmail/core/pull/7048)).
### Fixes
- Display correct timer value for ephemeral timer changes.
- Get_chat_msgs_ex(): Report local midnight in ChatItem::DayMarker.
### Refactor
- Rename add_or_lookup_key_contacts_by_address_list() to add_or_lookup_key_contacts().
- Don't call add_or_lookup_key_contacts() in advance.
## [2.8.0] - 2025-07-28
### Features / Changes
- Remove ProtectionBroken, make such chats Unprotected ([#7041](https://github.com/chatmail/core/pull/7041)).
### Fixes
- Lookup self by address if there is no fingerprint or gossip.
## [2.7.0] - 2025-07-26
### Features / Changes
- Mimefactory: Order message recipients by time of addition ([#6872](https://github.com/chatmail/core/pull/6872)).
- Put the debug/release build version into the info ([#7034](https://github.com/chatmail/core/pull/7034)).
### Fixes
- Realtime late join ([#6869](https://github.com/chatmail/core/pull/6869)).
- Do not fail to upgrade if the verifier of a contact doesn't exist anymore ([#7044](https://github.com/chatmail/core/pull/7044)).
### Tests
- Add regression test for verification-gossiping crash ([#7033](https://github.com/chatmail/core/pull/7033)).
## [2.6.0] - 2025-07-23
### Fixes
- Fix crash when receiving a verification-gossiping message which a contact also sends to itself ([#7032](https://github.com/chatmail/core/pull/7032)).
## [2.5.0] - 2025-07-22
### Fixes
- Correctly migrate "verified by me".
- Mark all email chats as unprotected in the migration ([#7026](https://github.com/chatmail/core/pull/7026)).
- Do not ignore errors in add_flag_finalized_with_set.
### Documentation
- Deprecate protection-broken and related stuff ([#7018](https://github.com/chatmail/core/pull/7018)).
- Clarify the meaning of is_verified() vs verifier_id() ([#7027](https://github.com/chatmail/core/pull/7027)).
- STYLE.md: Prefer `try_next()` over `next()`.
## [2.4.0] - 2025-07-21
### Fixes
- Do not ignore errors when draining FETCH responses. This avoids IMAP loop getting stuck in an infinite loop retrying reading from the connection.
- Update `tokio-io-timeout` to 1.2.1. This release includes a fix to reset timeout after every error, so timeout error is returned at most once a minute if read is attempted after a timeout.
### Miscellaneous Tasks
- Update async-imap to 0.11.0.
### Refactor
- Use `try_next()` when processing FETCH responses.
## [2.3.0] - 2025-07-19
### Features / Changes
- Add "e2ee encrypted" info message to all e2ee chats ([#7008](https://github.com/chatmail/core/pull/7008)).
- repl: Print errors and debug logs to stderr.
- `{ensure_and,logged}_debug_assert`: Don't evaluate condition twice.
- Log when background fetch of all accounts finishes successfully.
- Log the number of read/written bytes on IMAP stream read error ([#6924](https://github.com/chatmail/core/pull/6924)).
### Fixes
- Ignore protected headers in outer message part ([#6357](https://github.com/chatmail/core/pull/6357)).
- List e-mail contacts in repl listcontacts command.
- Save peer address for LoggingStream early.
## [2.2.0] - 2025-07-14
### API-Changes
- Add chat::create_group_ex(), deprecate create_group_chat() ([#6927](https://github.com/chatmail/core/pull/6927)).
- jsonrpc: Add CommandApi::create_group_chat_unencrypted() ([#6927](https://github.com/chatmail/core/pull/6927)).
- [**breaking**] In ChatListItem, replace is_group and is_(out_)broadcast with chat_type property ([#7003](https://github.com/chatmail/core/pull/7003)).
### Features / Changes
- Log failed debug assertions in all configurations.
- Donation request device message ([#6913](https://github.com/chatmail/core/pull/6913)).
- Advance next UID even if connection fails while fetching.
### Fixes
- Always prefer the last header.
### Tests
- Tune down DELTACHAT_SAVE_TMP_DB hint ([#6998](https://github.com/chatmail/core/pull/6998)).
- Unencrypted group creation ([#6927](https://github.com/chatmail/core/pull/6927)).
## [2.1.0] - 2025-07-11
### Features / Changes
- Add account ordering functionality ([#6993](https://github.com/chatmail/core/pull/6993)).
- feat: Make it possible to leave broadcast channels ([#6984](https://github.com/chatmail/core/pull/6984))
- Migrations: Use tools::Time to measure time for logging.
- Log emitted logging events with `tracing`.
- Ensure_and_debug_assert{,_eq,_ne} macros combining `debug_assert*` and anyhow::ensure ([#6907](https://github.com/chatmail/core/pull/6907)).
### Fixes
- Use Viewtype::File for messages with invalid images, images of unknown size, images > 50 Mpx ([#6825](https://github.com/chatmail/core/pull/6825)).
- Don't apply chat name and avatar changes from non-members.
### Documentation
- Update showpadlock ffi.
### Miscellaneous Tasks
- cargo: Update cordyceps from 0.3.2 to 0.3.4.
### Tests
- Add option to save database on test failure ([#6992](https://github.com/chatmail/core/pull/6992)).
## [2.0.0] - 2025-07-09
This release changes the way the core handles contact keys.
Instead of tracking OpenPGP keys corresponding to the
contacts in [Autocrypt](https://autocrypt.org/) peerstate,
the core creates a new "key-contact" for each known public key.
Reception of a message signed with a new unknown key
no longer results in warnings about setup changes,
but creates a new contact and a new 1:1 chat if necessary.
Additionally, there are "address-contacts" corresponding
to the e-mail addresses.
### Features / Changes
- Key-contacts ([#6796](https://github.com/chatmail/core/pull/6796), [#6941](https://github.com/chatmail/core/pull/6941)).
- Increase event channel size from 1000 to 10000.
- Minimize the amount of data preserved for trashed messages.
- Show broadcast channels in their own, proper "Channel" chat ([#6901](https://github.com/chatmail/core/pull/6901), [#6975](https://github.com/chatmail/core/pull/6975)).
- Check images passed as `File` before making them `Image`.
### API-Changes
- CFFI: Add dc_contact_is_key_contact() ([#6955](https://github.com/chatmail/core/pull/6955)).
- Contact::get_all(): Support listing address-contacts.
- [**breaking**] Add InBroadcastChannel, OutBroadcastChannel chattypes, add create_broadcast_channel() ([#6901](https://github.com/chatmail/core/pull/6901)).
- deltachat-rpc-client: Add Message.get_read_receipts().
### Fixes
- Remove display name from get_info(). This information usually goes at the top of the log and we don't want users to include it in bug reports.
- Wait for scheduler tasks shutdown in parallel.
- Update deltachat-repl help and autocomplete to match implementation ([#6978](https://github.com/chatmail/core/pull/6978), ([#6979](https://github.com/chatmail/core/pull/6979)).
- Send Autocrypt header in MDNs. This is needed to assign MDNs to key-contacts.
- Prefer encrypted List-Id header ([#6983](https://github.com/chatmail/core/pull/6983)).
- Treat "tgs" as Viewtype::File.
- Treat and send images that can't be decoded as Viewtype::File.
- Decide on filename used for sending depending on the original Viewtype.
- Migrate_key_contacts(): Remove "id>9" from encrypted messages SELECT.
- Save msgs to key-contacts migration state and run migration periodically ([#6956](https://github.com/chatmail/core/pull/6956)).
- Do not try to lookup key-contacts for unencrypted 1:1 messages.
- Add query to post request for account creation ([#6989](https://github.com/chatmail/core/pull/6989)).
### CI
- Update Rust to 1.88.0.
### Documentation
- Remove outdated comment that says MDNs are unencrypted.
### Refactor
- Upgrade to Rust 2024.
- Build_body_file(): Remove guessing mimetype by file extension.
### Tests
- Add online test for read receipts.
- Add a test reproducing chat assignment bug.
### Miscellaneous Tasks
- cargo: Bump smallvec from 1.15.0 to 1.15.1.
- cargo: Bump syn from 2.0.101 to 2.0.104.
- cargo: Bump hyper-util from 0.1.13 to 0.1.14.
- cargo: Bump toml from 0.8.19 to 0.8.23.
- cargo: Bump proptest from 1.6.0 to 1.7.0.
- cargo: Bump libc from 0.2.172 to 0.2.174.
## [1.160.0] - 2025-06-22
### API-Changes
- [**breaking**] jsonrpc: remove webxdc info from MessageObject.
Users need to call `get_webxdc_info` separately now
and expect that the call may fail e.g. if WebXDC is not a valid ZIP archive.
- [**breaking**] Deprecate `DC_GCL_VERIFIED_ONLY`.
- [**breaking**] Make logging macros private.
### Features / Changes
- Add more IMAP logging.
- Sort apps by recently-updated ([#6875](https://github.com/chatmail/core/pull/6875)).
- Better error for quoting a message from another chat.
- Put "biography" in the vCard ([#6819](https://github.com/chatmail/core/pull/6819)).
### Fixes
- Do not allow chat creation if decryption failed.
- Remove faulty test ([#6880](https://github.com/chatmail/core/pull/6880)).
- Reduce the scope of the last_full_folder_scan lock in scan_folders.
- Ignore verification error if the chat is not protected yet.
- Create group chats unprotected on verification error.
- `fetch_url`: return err on non 2xx reponses.
- Sort multiple saved messages by timestamp ([#6862](https://github.com/chatmail/core/pull/6862)).
- contact-tools: Escape commas in vCards' FN, KEY, PHOTO, NOTE ([#6912](https://github.com/chatmail/core/pull/6912)).
- Don't change ConfiguredAddr when adding a transport ([#6804](https://github.com/chatmail/core/pull/6804)).
### Build system
- Increase MSRV to 1.85.0.
- Update Doxygen config and layout file.
- Update to rPGP 0.16.0 ([#6719](https://github.com/chatmail/core/pull/6719)).
- Enable async-native-tls/vendored feature.
- Update rusqlite to 0.36.0.
### CI
- Update Rust to 1.87.0.
- nix: Test build on macOS without cross-compilation.
- Use installed toolchain to lint Rust.
### Refactor
- Remove explicit lock drop at the end of scope.
- Use CancellationToken instead of a 1-message channel.
### Documentation
- Add more code style guide references.
## [1.159.5] - 2025-05-14
### Fixes
- Don't change webxdc self-addr when saving and loading draft ([#6854](https://github.com/chatmail/core/pull/6854)).
### Miscellaneous Tasks
- Remove duplicate miniz_oxide dependency.
- Update async-smtp to 0.10.2.
## [1.159.4] - 2025-05-13
### Documentation
- Add missing documentation to deltachat-rpc-client.
### Features / Changes
- Better avatar quality ([#6822](https://github.com/chatmail/core/pull/6822)).
- Update iroh from 0.33.0 to 0.35.0 ([#6687](https://github.com/chatmail/core/pull/6687)).
- Other dependency updates.
### Fixes
- Emit progress(0) in case AEAP is tried.
- Replace `FuturesUnordered` from `futures` with `JoinSet` from `tokio`.
- Fix order of operations when handling "vc-request-with-auth" ([#6850](https://github.com/chatmail/core/pull/6850)).
- Generate rfc724_mid when creating Message ([#6704](https://github.com/chatmail/core/pull/6704))
### Tests
- Profile data is attached to group leave messages.
## [1.159.3] - 2025-04-24
### CI
@@ -203,8 +833,8 @@
- Use vCard in TestContext.add_or_lookup_contact().
- Remove test_group_with_removed_message_id.
- Use add_or_lookup_email_contact() in get_chat().
- Use add_or_lookup_email_contact in test_setup_contact_ex.
- Use add_or_lookup_address_contact() in get_chat().
- Use add_or_lookup_address_contact in test_setup_contact_ex.
- Use vCards more in Python tests.
- Use TestContextManager in more tests.
- Use vCards to create contacts in more Rust tests.
@@ -1236,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)).
@@ -3684,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
@@ -3695,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
@@ -3782,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
@@ -6267,3 +6897,27 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1
[1.159.2]: https://github.com/chatmail/core/compare/v1.159.1..v1.159.2
[1.159.3]: https://github.com/chatmail/core/compare/v1.159.2..v1.159.3
[1.159.4]: https://github.com/chatmail/core/compare/v1.159.3..v1.159.4
[1.159.5]: https://github.com/chatmail/core/compare/v1.159.4..v1.159.5
[1.160.0]: https://github.com/chatmail/core/compare/v1.159.5..v1.160.0
[2.0.0]: https://github.com/chatmail/core/compare/v1.160.0..v2.0.0
[2.1.0]: https://github.com/chatmail/core/compare/v2.0.0..v2.1.0
[2.2.0]: https://github.com/chatmail/core/compare/v2.1.0..v2.2.0
[2.3.0]: https://github.com/chatmail/core/compare/v2.2.0..v2.3.0
[2.4.0]: https://github.com/chatmail/core/compare/v2.3.0..v2.4.0
[2.5.0]: https://github.com/chatmail/core/compare/v2.4.0..v2.5.0
[2.6.0]: https://github.com/chatmail/core/compare/v2.5.0..v2.6.0
[2.7.0]: https://github.com/chatmail/core/compare/v2.6.0..v2.7.0
[2.8.0]: https://github.com/chatmail/core/compare/v2.7.0..v2.8.0
[2.9.0]: https://github.com/chatmail/core/compare/v2.8.0..v2.9.0
[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`"

1905
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.159.3"
edition = "2021"
version = "2.20.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.82"
rust-version = "1.85"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -18,6 +18,9 @@ opt-level = 1
debug = 1
opt-level = 0
[profile.fuzz]
inherits = "test"
# Always optimize dependencies.
# This does not apply to crates in the workspace.
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
@@ -41,14 +44,16 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true }
async-broadcast = "0.7.2"
async-channel = { workspace = true }
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
base64 = { workspace = true }
blake3 = "1.8.2"
brotli = { version = "8", default-features=false, features = ["std"] }
bytes = "1"
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
colorutils-rs = { version = "0.7.5", default-features = false }
data-encoding = "2.9.0"
escaper = "0.1"
fast-socks5 = "0.10"
@@ -56,63 +61,62 @@ fd-lock = "4"
futures-lite = { workspace = true }
futures = { workspace = true }
hex = "0.4.0"
hickory-resolver = "=0.25.0-alpha.5"
hickory-resolver = "0.25.2"
http-body-util = "0.1.3"
humansize = "2"
hyper = "1"
hyper-util = "0.1.11"
hyper-util = "0.1.16"
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
iroh = { version = "0.33", default-features = false }
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
iroh = { version = "0.35", default-features = false }
kamadak-exif = "0.6.1"
libc = { workspace = true }
mail-builder = { version = "0.4.2", default-features = false }
mail-builder = { version = "0.4.4", default-features = false }
mailparse = { workspace = true }
mime = "0.3.17"
num_cpus = "1.16"
num_cpus = "1.17"
num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12"
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.15.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"] }
rust-hsluv = "0.1"
rustls-pki-types = "1.11.0"
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"] }
sha-1 = "0.10"
sha2 = "0.10"
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
smallvec = "1.15.0"
smallvec = "1.15.1"
strum = "0.27"
strum_macros = "0.27"
tagger = "4.3.4"
textwrap = "0.16.2"
thiserror = { workspace = true }
tokio-io-timeout = "1.2.0"
tokio-io-timeout = "1.2.1"
tokio-rustls = { version = "0.26.2", default-features = false }
tokio-stream = { version = "0.1.17", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = { workspace = true }
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8"
toml = "0.9"
tracing = "0.1.41"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
webpki-roots = "0.26.8"
blake3 = "1.8.2"
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
criterion = { version = "0.5.1", features = ["async_tokio"] }
criterion = { version = "0.7.0", features = ["async_tokio"] }
futures-lite = { workspace = true }
log = { workspace = true }
nu-ansi-term = { workspace = true }
@@ -171,29 +175,29 @@ harness = false
[workspace.dependencies]
anyhow = "1"
async-channel = "2.3.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 }
futures = "0.3.31"
futures-lite = "2.6.0"
futures-lite = "2.6.1"
libc = "0.2"
log = "0.4"
mailparse = "0.16.1"
nu-ansi-term = "0.46"
nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.8"
regex = "1.10"
rusqlite = "0.32"
rusqlite = "0.36"
sanitize-filename = "0.5"
serde = "1.0"
serde_json = "1"
tempfile = "3.19.1"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.14"
tokio-util = "0.7.16"
tracing-subscriber = "0.3"
yerpc = "0.6.4"
@@ -201,7 +205,8 @@ yerpc = "0.6.4"
default = ["vendored"]
internals = []
vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl"
"rusqlite/bundled-sqlcipher-vendored-openssl",
"async-native-tls/vendored"
]
[lints.rust]

View File

@@ -80,30 +80,41 @@ Connect to your mail server (if already configured):
> connect
```
Create a contact:
Export your public key to a vCard file:
```
> make-vcard my.vcard 1
```
Create contacts by address or vCard file:
```
> addcontact yourfriends@email.org
Command executed successfully.
> import-vcard key-contact.vcard
```
List contacts:
```
> listcontacts
Contact#10: <name unset> <yourfriends@email.org>
Contact#1: Me √ <your@email.org>
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
Contact#Contact#Self: Me √ <your@email.org>
2 key contacts.
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
1 address contacts.
```
Create a chat with your friend and send a message:
```
> createchat 10
Single#10 created successfully.
> chat 10
Single#10: yourfriends@email.org [yourfriends@email.org]
Single#Chat#12 created successfully.
> chat 12
Selecting chat Chat#12
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
0 messages.
81.252µs to create this list, 123.625µs to mark all messages as noticed.
> send hi
Message sent.
```
List messages when inside a chat:
@@ -156,13 +167,13 @@ $ cargo test -- --ignored
Install [`cargo-bolero`](https://github.com/camshaft/bolero) with
```sh
$ cargo install cargo-bolero@0.8.0
$ cargo install cargo-bolero
```
Run fuzzing tests with
```sh
$ cd fuzz
$ cargo bolero test fuzz_mailparse --release=false -s NONE
$ cargo bolero test fuzz_mailparse -s NONE
```
Corpus is created at `fuzz/fuzz_targets/corpus`,
@@ -170,11 +181,6 @@ you can add initial inputs there.
For `fuzz_mailparse` target corpus can be populated with
`../test-data/message/*.eml`.
To run with AFL instead of libFuzzer:
```sh
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
```
## Features
- `vendored`: When using Openssl for TLS, this bundles a vendored version.

View File

@@ -78,12 +78,52 @@ All errors should be handled in one of these ways:
- With `.log_err().ok()`.
- Bubbled up with `?`.
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
```
while let Some(event) = stream.try_next().await? {
todo!();
}
```
instead of
```
while let Some(event_res) = stream.next().await {
todo!();
}
```
as it allows bubbling up the error early with `?`
with no way to accidentally skip error processing
with early `continue` or `break`.
Some streams reading from a connection
return infinite number of `Some(Err(_))`
items when connection breaks and not processing
errors may result in infinite loop.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
`unwrap` and `expect` are not used in the library
because panics are difficult to debug on user devices.
However, in the tests `.expect` may be used.
Follow
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
for `.expect` message style.
## BTreeMap vs HashMap
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
as iterating over these structures returns items in deterministic order.
Non-deterministic code may result in difficult to reproduce bugs,
flaky tests, regression tests that miss bugs
or different behavior on different devices when processing the same messages.
## Logging
For logging, use `info!`, `warn!` and `error!` macros.
@@ -96,3 +136,17 @@ Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
## Documentation comments
All public modules, methods and fields should be documented.
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
Private items do not have to be documented,
but CI uses `cargo doc --document-private-items`
to build the documentation,
so it is preferred that new items
are documented.
Follow Rust guidelines for the documentation comments:
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>

BIN
assets/icon-unencrypted.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="480"
viewBox="0 -960 9600 9600"
width="480"
fill="#ffffff"
version="1.1"
id="svg1"
sodipodi:docname="icon-email.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="0.99091847"
inkscape:cx="263.392"
inkscape:cy="177.613"
inkscape:window-width="1884"
inkscape:window-height="1052"
inkscape:window-x="36"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<rect
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
id="rect1"
width="9951.9541"
height="9767.4756"
x="-71.697792"
y="-1012.83"
ry="0.43547946" />
<path
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
id="path1"
style="stroke-width:5.40098" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,7 @@
BEGIN:VCARD
VERSION:4.0
EMAIL:self_reporting@testrun.org
FN:Statistics bot
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
REV:20250412T195751Z
END:VCARD

View File

@@ -1,9 +1,11 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use deltachat::contact::Contact;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use tempfile::tempdir;
async fn address_book_benchmark(n: u32, read_count: u32) {

View File

@@ -1,7 +1,8 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::accounts::Accounts;
use tempfile::tempdir;

View File

@@ -1,12 +1,13 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
let id = 100;

View File

@@ -1,11 +1,12 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn get_chat_list_benchmark(context: &Context) {
Chatlist::try_load(context, 0, None, None).await.unwrap();

View File

@@ -1,12 +1,13 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
use deltachat::Events;
use deltachat::chat::{self, ChatId};
use deltachat::chatlist::Chatlist;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
use futures_lite::future::block_on;
use tempfile::tempdir;

View File

@@ -1,14 +1,15 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::{
Events,
config::Config,
context::Context,
imex::{imex, ImexMode},
imex::{ImexMode, imex},
receive_imf::receive_imf,
stock_str::StockStrings,
Events,
};
use tempfile::tempdir;

View File

@@ -1,10 +1,11 @@
#![recursion_limit = "256"]
use std::hint::black_box;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::Events;
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::Events;
async fn search_benchmark(dbfile: impl AsRef<Path>) {
let id = 100;

View File

@@ -1,17 +1,17 @@
#![recursion_limit = "256"]
use criterion::{criterion_group, criterion_main, Criterion};
use criterion::{Criterion, criterion_group, criterion_main};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::{info, Event, EventType, Events};
use deltachat::{Event, EventType, Events};
use tempfile::tempdir;
async fn send_events_benchmark(context: &Context) {
let emitter = context.get_event_emitter();
for _i in 0..1_000_000 {
info!(context, "interesting event...");
context.emit_event(EventType::Info("interesting event...".to_string()));
}
info!(context, "DONE");
context.emit_event(EventType::Info("DONE".to_string()));
loop {
match emitter.recv().await.unwrap() {

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()))
}
@@ -76,7 +76,7 @@ impl ContactAddress {
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
@@ -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,13 +276,13 @@ impl EmailAddress {
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
_ => bail!("Email {input:?} must contain '@' character"),
}
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)

View File

@@ -20,6 +20,8 @@ pub struct VcardContact {
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The biography, stored in the vcard property `note`
pub biography: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
@@ -44,10 +46,15 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
fn escape(s: &str) -> String {
s.replace(',', "\\,")
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
// Mustn't contain ',', but it's easier to escape than to error out.
let addr = escape(&c.addr);
let display_name = escape(c.display_name());
res += &format!(
"BEGIN:VCARD\r\n\
VERSION:4.0\r\n\
@@ -55,10 +62,13 @@ pub fn make_vcard(contacts: &[VcardContact]) -> String {
FN:{display_name}\r\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
}
if let Some(biography) = &c.biography {
res += &format!("NOTE:{}\r\n", escape(biography));
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\r\n");
@@ -79,8 +89,8 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
None
}
}
/// Returns (parameters, value) tuple.
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
/// Returns (parameters, raw value) tuple.
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
let remainder = remove_prefix(line, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
@@ -110,23 +120,25 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
}
Some((params, value))
}
/// Returns (parameters, unescaped value) tuple.
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
let (params, value) = vcard_property_raw(line, property)?;
// Some fields can't contain commas, but unescape them everywhere for safety.
Some((params, value.replace("\\,", ",")))
}
fn base64_key(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "key")?;
let (params, value) = vcard_property_raw(line, "key")?;
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
{
return Some(value);
}
None
remove_prefix(value, "data:application/pgp-keys;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
}
fn base64_photo(line: &str) -> Option<&str> {
let (params, value) = vcard_property(line, "photo")?;
let (params, value) = vcard_property_raw(line, "photo")?;
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
@@ -136,13 +148,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
{
return Some(value);
}
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
{
return Some(value);
}
None
remove_prefix(value, "data:image/jpeg;base64\\,")
// Old Delta Chat format.
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
@@ -186,6 +194,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut biography = None;
let mut datetime = None;
for mut line in lines.by_ref() {
@@ -205,18 +214,24 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
key.get_or_insert(k);
} else if let Some(p) = base64_photo(line) {
photo.get_or_insert(p);
} else if let Some((_params, bio)) = vcard_property(line, "note") {
biography.get_or_insert(bio);
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
let (authname, addr) = sanitize_name_and_addr(
&display_name.unwrap_or_default(),
&addr.unwrap_or_default(),
);
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
biography,
timestamp: datetime
.as_deref()
.context("No timestamp in vcard")
.and_then(parse_datetime),
});

View File

@@ -91,6 +91,7 @@ fn test_make_and_parse_vcard() {
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
biography: Some("Hi, I'm Alice".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
@@ -98,6 +99,7 @@ fn test_make_and_parse_vcard() {
authname: "".to_string(),
key: None,
profile_image: None,
biography: None,
timestamp: Ok(0),
},
];
@@ -106,8 +108,9 @@ fn test_make_and_parse_vcard() {
VERSION:4.0\r\n\
EMAIL:alice@example.org\r\n\
FN:Alice Wonderland\r\n\
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
NOTE:Hi\\, I'm Alice\r\n\
REV:20240418T184242Z\r\n\
END:VCARD\r\n",
"BEGIN:VCARD\r\n\
@@ -246,7 +249,8 @@ END:VCARD",
assert_eq!(contacts[0].profile_image, None);
}
/// Proton at some point slightly changed the format of their vcards
/// Proton at some point slightly changed the format of their vcards.
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
#[test]
fn test_protonmail_vcard2() {
let contacts = parse_vcard(

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.20 -->
<?xml version="1.0" encoding="UTF-8"?>
<doxygenlayout version="2.0">
<!-- Generated by doxygen 1.13.2 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
@@ -11,10 +12,16 @@
</tab>
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="modules" visible="yes" title="" intro="">
<tab type="modulelist" visible="yes" title="" intro=""/>
<tab type="modulemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/>
<tab type="namespacemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="concepts" visible="yes" title="">
</tab>
<tab type="interfaces" visible="yes" title="">
<tab type="interfacelist" visible="yes" title="" intro=""/>
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
@@ -35,4 +42,228 @@
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<inheritancegraph visible="yes"/>
<collaborationgraph visible="yes"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicmethods visible="yes" title=""/>
<publicstaticmethods visible="yes" title=""/>
<publicattributes visible="yes" title=""/>
<publicstaticattributes visible="yes" title=""/>
<protectedtypes visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<protectedmethods visible="yes" title=""/>
<protectedstaticmethods visible="yes" title=""/>
<protectedattributes visible="yes" title=""/>
<protectedstaticattributes visible="yes" title=""/>
<packagetypes visible="yes" title=""/>
<packagemethods visible="yes" title=""/>
<packagestaticmethods visible="yes" title=""/>
<packageattributes visible="yes" title=""/>
<packagestaticattributes visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
<privatetypes visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<privatemethods visible="yes" title=""/>
<privatestaticmethods visible="yes" title=""/>
<privateattributes visible="yes" title=""/>
<privatestaticattributes visible="yes" title=""/>
<friends visible="yes" title=""/>
<related visible="yes" title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<constructors visible="yes" title=""/>
<functions visible="yes" title=""/>
<related visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="yes"/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<concepts visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes" visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a concept page -->
<concept>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<definition visible="yes" title=""/>
<detaileddescription visible="yes" title=""/>
<authorsection visible="yes"/>
</concept>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="yes"/>
<includedbygraph visible="yes"/>
<sourcelink visible="yes"/>
<memberdecl>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes" visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="yes"/>
<groupgraph visible="yes"/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<modules visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<pagedocs/>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a C++20 module page -->
<module>
<briefdescription visible="yes"/>
<exportedmodules visible="yes"/>
<memberdecl>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<enums visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<membergroups visible="yes" title=""/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdecl>
<files visible="yes"/>
</memberdecl>
</module>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
</directory>
</doxygenlayout>

View File

@@ -415,7 +415,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* As for `displayname` and `selfstatus`, also the avatar is sent to the recipients.
* To save traffic, however, the avatar is attached only as needed
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts
* default=send and request read receipts, only send but not request if `bot` is set
@@ -459,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,
@@ -503,13 +496,6 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
* seconds. 2 days by default.
* This is not supposed to be changed by UIs and only used for testing.
* - `verified_one_on_one_chats` = 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, `dc_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, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
* until `dc_accept_chat()` is called.
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
* - `is_muted` = Whether a context is muted by the user.
* Muted contexts should not sound, vibrate or show notifications.
@@ -583,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.
@@ -1060,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,6 +1171,117 @@ void dc_set_webxdc_integration (dc_context_t* context, const char* f
uint32_t dc_init_webxdc_integration (dc_context_t* context, uint32_t chat_id);
/**
* Start an outgoing call.
* This sends a message of type #DC_MSG_CALL with all relevant information to the callee,
* who will get informed by an #DC_EVENT_INCOMING_CALL event and rings.
*
* Possible actions during ringing:
*
* - caller cancels the call using dc_end_call():
* callee receives #DC_EVENT_CALL_ENDED and has a "Missed call"
*
* - callee accepts using dc_accept_incoming_call():
* caller receives #DC_EVENT_OUTGOING_CALL_ACCEPTED.
* callee's devices receive #DC_EVENT_INCOMING_CALL_ACCEPTED, call starts
*
* - callee 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 "Canceled Call",
*
* - callee is already in a call:
* 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 "Canceled call";
* for callee, this is a "Missed call"
*
* Actions during the call:
*
* - caller ends the call using dc_end_call():
* callee receives #DC_EVENT_CALL_ENDED
*
* - 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.
* To place a call with a contact that has no chat yet, use dc_create_chat_by_contact_id() first.
*
* UI will usually allow only one call at the same time,
* this has to be tracked by UI across profile, the core does not track this.
*
* @memberof dc_context_t
* @param context The context object.
* @param chat_id The chat to place a call for.
* This needs to be a one-to-one chat.
* @param place_call_info any data that other devices receive
* in #DC_EVENT_INCOMING_CALL.
* @return ID of the system message announcing the call.
*/
uint32_t dc_place_outgoing_call (dc_context_t* context, uint32_t chat_id, const char* place_call_info);
/**
* Accept incoming call.
*
* This implicitly accepts the contact request, if not yet done.
* All affected devices will receive
* either #DC_EVENT_OUTGOING_CALL_ACCEPTED or #DC_EVENT_INCOMING_CALL_ACCEPTED.
*
* If the call is already accepted or ended, nothing happens.
* If the chat is a contact request, it is accepted implicitly.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the call to accept.
* This is the ID reported by #DC_EVENT_INCOMING_CALL
* and equals to the ID of the corresponding info message.
* @param accept_call_info any data that other devices receive
* in #DC_EVENT_OUTGOING_CALL_ACCEPTED.
* @return 1=success, 0=error
*/
int dc_accept_incoming_call (dc_context_t* context, uint32_t msg_id, const char* accept_call_info);
/**
* End incoming or outgoing call.
*
* For unaccepted calls ended by the caller, this is a "cancellation".
* Unaccepted calls ended by the callee are a "decline".
* If the call was accepted, this is a "hangup".
*
* 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.
*
* @memberof dc_context_t
* @param context The context object.
* @param msg_id the ID of the call.
* @return 1=success, 0=error
*/
int dc_end_call (dc_context_t* context, uint32_t msg_id);
/**
* Save a draft for a chat in the database.
*
@@ -1339,12 +1399,14 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
* Optionally, some special markers added to the ID array may help to
* implement virtual lists.
*
* To get the concrete time of the message, use dc_array_get_timestamp().
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID of which the messages IDs should be queried.
* @param flags If set to DC_GCM_ADDDAYMARKER, the marker DC_MSG_ID_DAYMARKER will
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
* To get the concrete time of the marker, use dc_array_get_timestamp().
* The day marker timestamp is the midnight one for the corresponding (following) day in the local timezone.
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
* @param marker1before Deprecated, set this to 0.
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
@@ -2094,9 +2156,19 @@ int dc_may_be_valid_addr (const char* addr);
/**
* Check if an e-mail address belongs to a known and unblocked contact.
* Looks up a known and unblocked contact with a given e-mail address.
* To get a list of all known and unblocked contacts, use dc_get_contacts().
*
* **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
* (e.g. an address-contact and a key-contact),
* this looks up the most recently seen contact,
* i.e. which contact is returned depends on which contact last sent a message.
* If the user just clicked on a mailto: link, then this is the best thing you can do.
* But **DO NOT** internally represent contacts by their email address
* and do not use this function to look them up;
* otherwise this function will sometimes look up the wrong contact.
* Instead, you should internally represent contacts by their ids.
*
* To validate an e-mail address independently of the contact database
* use dc_may_be_valid_addr().
*
@@ -2118,6 +2190,13 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
* To add a number of contacts, see dc_add_address_book() which is much faster for adding
* a bunch of addresses.
*
* This will always create or look up an address-contact,
* i.e. a contact identified by an email address,
* with all messages sent to and from this contact being unencrypted.
* If the user just clicked on an email address,
* you should first check `lookup_contact_id_by_addr`,
* and only if there is no contact yet, call this function here.
*
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
*
* @memberof dc_context_t
@@ -2132,8 +2211,12 @@ uint32_t dc_lookup_contact_id_by_addr (dc_context_t* context, const char*
uint32_t dc_create_contact (dc_context_t* context, const char* name, const char* addr);
#define DC_GCL_VERIFIED_ONLY 0x01
// Deprecated 2025-05-20, setting this flag is a no-op.
#define DC_GCL_DEPRECATED_VERIFIED_ONLY 0x01
#define DC_GCL_ADD_SELF 0x02
#define DC_GCL_ADDRESS 0x04
/**
@@ -2189,13 +2272,13 @@ dc_array_t* dc_import_vcard (dc_context_t* context, const char*
* Returns known and unblocked contacts.
*
* To get information about a single contact, see dc_get_contact().
* By default, key-contacts are listed.
*
* @memberof dc_context_t
* @param context The context object.
* @param flags A combination of flags:
* - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
* - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
* if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
* - DC_GCL_ADD_SELF: SELF is added to the list unless filtered by other parameters
* - DC_GCL_ADDRESS: List address-contacts instead of key-contacts.
* @param query A string to filter the list. Typically used to implement an
* incremental search. NULL for no filtering.
* @return An array containing all contact IDs. Must be dc_array_unref()'d
@@ -2489,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
@@ -2543,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.
@@ -3814,21 +3892,12 @@ int dc_chat_can_send (const dc_chat_t* chat);
/**
* Check if a chat is protected.
*
* End-to-end encryption is guaranteed in protected chats
* and only verified contacts
* Only verified contacts
* as determined by dc_contact_is_verified()
* can be added to protected chats.
*
* Protected chats are created using dc_create_group_chat()
* by setting the 'protect' parameter to 1.
* 1:1 chats become protected or unprotected automatically
* if `verified_one_on_one_chats` setting is enabled.
*
* UI should display a green checkmark
* in the chat title,
* in the chatlist item
* and in the chat profile
* if chat protection is enabled.
*
* @memberof dc_chat_t
* @param chat The chat object.
@@ -3838,23 +3907,18 @@ int dc_chat_is_protected (const dc_chat_t* chat);
/**
* Checks if the chat was protected, and then an incoming message broke this protection.
* Check if the chat is encrypted.
*
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
* otherwise it will return false for all chats.
* 1:1 chats with key-contacts and group chats with key-contacts
* are encrypted.
* 1:1 chats with emails contacts and ad-hoc groups
* created for email threads are not encrypted.
*
* 1:1 chats are automatically set as protected when a contact is verified.
* When a message comes in that is not encrypted / signed correctly,
* the chat is automatically set as unprotected again.
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
*
* The UI should let the user confirm that this is OK with a message like
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
* @memberof dc_chat_t
* @param chat The chat object.
* @return 1=chat protection broken, 0=otherwise.
* @return 1=chat is encrypted, 0=chat is not encrypted.
*/
int dc_chat_is_protection_broken (const dc_chat_t* chat);
int dc_chat_is_encrypted (const dc_chat_t *chat);
/**
@@ -4284,11 +4348,16 @@ int dc_msg_get_duration (const dc_msg_t* msg);
/**
* Check if a padlock should be shown beside the message.
* Check if message was correctly encrypted and signed.
*
* Historically, UIs showed a small padlock on the message then.
* Today, the UIs should instead
* show a small email-icon on the message if the message is not encrypted or signed,
* and nothing otherwise.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return 1=padlock should be shown beside message, 0=do not show a padlock beside the message.
* @return 1=message correctly encrypted and signed, no need to show anything; 0=show email-icon beside the message.
*/
int dc_msg_get_showpadlock (const dc_msg_t* msg);
@@ -4511,12 +4580,12 @@ int dc_msg_is_info (const dc_msg_t* msg);
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is protected"
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
* the UI should change the corresponding string using #DC_STR_INVALID_UNENCRYPTED_MAIL
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4569,9 +4638,10 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_LOCATION_ONLY 9
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
#define DC_INFO_PROTECTION_ENABLED 11
#define DC_INFO_PROTECTION_DISABLED 12
#define DC_INFO_PROTECTION_DISABLED 12 // deprecated 2025-07
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
/**
@@ -4626,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.
@@ -4664,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.
*
@@ -5242,20 +5261,14 @@ int dc_contact_is_blocked (const dc_contact_t* contact);
/**
* Check if the contact
* can be added to verified chats,
* i.e. has a verified key
* and Autocrypt key matches the verified key.
* can be added to protected chats.
*
* If contact is verified
* UI should display green checkmark after the contact name
* in contact list items,
* in chat member list items
* and in profiles if no chat with the contact exist (otherwise, use dc_chat_is_protected()).
* See dc_contact_get_verifier_id() for a guidance how to display these information.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 0: contact is not verified.
* 2: SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown.
* 2: SELF and contact have verified their fingerprints in both directions.
*/
int dc_contact_is_verified (dc_contact_t* contact);
@@ -5269,19 +5282,39 @@ int dc_contact_is_verified (dc_contact_t* contact);
int dc_contact_is_bot (dc_contact_t* contact);
/**
* Returns whether contact is a key-contact,
* i.e. it is identified by the public key
* rather than the email address.
*
* If so, all messages to and from this contact are encrypted.
*
* @memberof dc_contact_t
* @param contact The contact object.
* @return 1 if the contact is a key-contact, 0 if it is an address-contact.
*/
int dc_contact_is_key_contact (dc_contact_t* contact);
/**
* Return the contact ID that verified a contact.
*
* If the function returns non-zero result,
* display green checkmark in the profile and "Introduced by ..." line
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr.
* As verifier may be unknown,
* use dc_contact_is_verified() to check if a contact can be added to a protected chat.
*
* If this function returns a verifier,
* this does not necessarily mean
* you can add the contact to verified chats.
* Use dc_contact_is_verified() to check
* if a contact can be added to a verified chat instead.
* UI should display the information in the contact's profile as follows:
*
* - If dc_contact_get_verifier_id() != 0,
* display text "Introduced by ..."
* with the name and address of the contact
* formatted by dc_contact_get_name_n_addr().
* Prefix the text by a green checkmark.
*
* - If dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() != 0,
* display "Introduced" prefixed by a green checkmark.
*
* - if dc_contact_get_verifier_id() == 0 and dc_contact_is_verified() == 0,
* display nothing
*
* @memberof dc_contact_t
* @param contact The contact object.
@@ -5584,14 +5617,21 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
/**
* Message indicating an incoming or outgoing videochat.
* The message was created via dc_send_videochat_invitation() on this or a remote device.
* Message indicating an incoming or outgoing call.
*
* 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().
* 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_VIDEOCHAT_INVITATION 70
#define DC_MSG_CALL 71
/**
@@ -5714,9 +5754,33 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
#define DC_CHAT_TYPE_MAILINGLIST 140
/**
* A broadcast list. See dc_chat_get_type() for details.
* Outgoing broadcast channel, called "Channel" in the UI.
*
* The user can send into this chat,
* and all recipients will receive messages
* in a `DC_CHAT_TYPE_IN_BROADCAST`.
*
* Called `broadcast` here rather than `channel`,
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
*/
#define DC_CHAT_TYPE_BROADCAST 160
#define DC_CHAT_TYPE_OUT_BROADCAST 160
/**
* Incoming broadcast channel, called "Channel" in the UI.
*
* This chat is read-only,
* and we do not know who the other recipients are.
*
* This is similar to `DC_CHAT_TYPE_MAILINGLIST`,
* with the main difference being that
* broadcasts are encrypted.
*
* Called `broadcast` here rather than `channel`,
* because the word "channel" already appears a lot in the code,
* which would make it hard to grep for it.
*/
#define DC_CHAT_TYPE_IN_BROADCAST 165
/**
* @}
@@ -6323,7 +6387,6 @@ void dc_event_unref(dc_event_t* event);
/**
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
* Or the verify state of a chat has changed.
* See dc_set_chat_name(), dc_set_chat_profile_image(), dc_add_contact_to_chat()
* and dc_remove_contact_from_chat().
*
@@ -6413,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, vg-member-added/vc-contact-confirm sent, 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
@@ -6569,6 +6628,60 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CHANNEL_OVERFLOW 2400
/**
* Incoming call.
* UI will usually start ringing,
* or show a notification if there is already a call in some profile.
*
* 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 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.
*
* Otherwise, ringing should end on #DC_EVENT_CALL_ENDED
* or #DC_EVENT_INCOMING_CALL_ACCEPTED
*
* @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
/**
* 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.
*
* 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
*/
#define DC_EVENT_INCOMING_CALL_ACCEPTED 2560
/**
* A call placed using dc_place_outgoing_call() was accepted by the callee using dc_accept_incoming_call().
*
* 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()
*/
#define DC_EVENT_OUTGOING_CALL_ACCEPTED 2570
/**
* 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.
*
* 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
*/
#define DC_EVENT_CALL_ENDED 2580
/**
* @}
*/
@@ -6836,9 +6949,7 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_GIF 23
/// "Encrypted message"
///
/// Used in subjects of outgoing messages.
/// @deprecated 2025-07, this string is no longer needed.
#define DC_STR_ENCRYPTEDMSG 24
/// "End-to-end encryption available."
@@ -6885,6 +6996,7 @@ void dc_event_unref(dc_event_t* event);
/// "End-to-end encryption preferred."
///
/// Used to build the string returned by dc_get_contact_encrinfo().
/// @deprecated 2025-06-05
#define DC_STR_E2E_PREFERRED 34
/// "%1$s verified"
@@ -6897,12 +7009,14 @@ void dc_event_unref(dc_event_t* event);
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact that cannot be verified
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_NOT_VERIFIED 36
/// "Changed setup for %1$s."
///
/// Used in status messages.
/// - %1$s will be replaced by the name of the contact with the changed setup
/// @deprecated 2025-06-05
#define DC_STR_CONTACT_SETUP_CHANGED 37
/// "Archived chats"
@@ -6988,6 +7102,8 @@ void dc_event_unref(dc_event_t* event);
/// "Unknown sender for this chat. See 'info' for more details."
///
/// Use as message text if assigning the message to a chat is not totally correct.
///
/// @deprecated 2025-08-18
#define DC_STR_UNKNOWN_SENDER_FOR_CHAT 72
/// "Message from %1$s"
@@ -7050,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.
@@ -7292,6 +7397,7 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s changed their address from %2$s to %3$s"
///
/// Used as an info message to chats with contacts that changed their address.
/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
@@ -7529,6 +7635,18 @@ void dc_event_unref(dc_event_t* event);
/// `%2$s` will be replaced by name and address of the contact.
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
/// "You set message deletion timer to 1 year."
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_YOU 158
/// "Message deletion timer is set to 1 year by %1$s."
///
/// `%1$s` will be replaced by name and address of the contact.
///
/// Used in status messages.
#define DC_STR_EPHEMERAL_TIMER_1_YEAR_BY_OTHER 159
/// "Scan to set up second device for %1$s"
///
/// `%1$s` will be replaced by name and address of the account.
@@ -7539,7 +7657,7 @@ void dc_event_unref(dc_event_t* event);
/// Used as a device message after a successful backup transfer.
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
/// "Messages are guaranteed to be end-to-end encrypted from now on."
/// "Messages are end-to-end encrypted."
///
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
@@ -7547,6 +7665,7 @@ void dc_event_unref(dc_event_t* event);
/// "%1$s sent a message from another device."
///
/// Used in info messages.
/// @deprecated 2025-07
#define DC_STR_CHAT_PROTECTION_DISABLED 171
/// "Others will only see this group after you sent a first message."
@@ -7568,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
@@ -7598,10 +7723,38 @@ void dc_event_unref(dc_event_t* event);
/// "The contact must be online to proceed. This process will continue automatically in background."
///
/// Used as info message.
/// @deprecated 2025-06-05
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200
/// "❤️ Seems you're enjoying Delta Chat!"… (donation request device message)
#define DC_STR_DONATION_REQUEST 193
/// "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
/// "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 201
/**
* @}

View File

@@ -235,7 +235,10 @@ pub unsafe extern "C" fn dc_set_config(
.log_err(ctx)
.is_ok() as libc::c_int
} else {
match config::Config::from_str(&key) {
match config::Config::from_str(&key)
.context("Invalid config key")
.log_err(ctx)
{
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
@@ -244,10 +247,7 @@ pub unsafe extern "C" fn dc_set_config(
})
.log_err(ctx)
.is_ok() as libc::c_int,
Err(_) => {
warn!(ctx, "dc_set_config(): invalid key");
0
}
Err(_) => 0,
}
}
})
@@ -276,7 +276,10 @@ pub unsafe extern "C" fn dc_get_config(
.unwrap_or_default()
.strdup()
} else {
match config::Config::from_str(&key) {
match config::Config::from_str(&key)
.with_context(|| format!("Invalid key {:?}", &key))
.log_err(ctx)
{
Ok(key) => ctx
.get_config(key)
.await
@@ -285,10 +288,7 @@ pub unsafe extern "C" fn dc_get_config(
.unwrap_or_default()
.unwrap_or_default()
.strdup(),
Err(_) => {
warn!(ctx, "dc_get_config(): invalid key '{}'", &key);
"".strdup()
}
Err(_) => "".strdup(),
}
}
})
@@ -308,18 +308,17 @@ pub unsafe extern "C" fn dc_set_stock_translation(
let ctx = &*context;
block_on(async move {
match StockMessage::from_u32(stock_id) {
Some(id) => match ctx.set_stock_translation(id, msg).await {
Ok(()) => 1,
Err(err) => {
warn!(ctx, "set_stock_translation failed: {err:#}");
0
}
},
None => {
warn!(ctx, "invalid stock message id {stock_id}");
0
}
match StockMessage::from_u32(stock_id)
.with_context(|| format!("Invalid stock message ID {stock_id}"))
.log_err(ctx)
{
Ok(id) => ctx
.set_stock_translation(id, msg)
.await
.context("set_stock_translation failed")
.log_err(ctx)
.is_ok() as libc::c_int,
Err(_) => 0,
}
})
}
@@ -336,15 +335,10 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
let qr = to_string_lossy(qr);
let ctx = &*context;
block_on(async move {
match qr::set_config_from_qr(ctx, &qr).await {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {err:#}");
0
}
}
})
block_on(qr::set_config_from_qr(ctx, &qr))
.context("Failed to create account from QR code")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -354,15 +348,13 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
return "".strdup();
}
let ctx = &*context;
block_on(async move {
match ctx.get_info().await {
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(err) => {
warn!(ctx, "failed to get info: {err:#}");
"".strdup()
}
}
})
match block_on(ctx.get_info())
.context("Failed to get info")
.log_err(ctx)
{
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(_) => "".strdup(),
}
}
fn render_info(
@@ -383,7 +375,7 @@ pub unsafe extern "C" fn dc_get_connectivity(context: *const dc_context_t) -> li
return 0;
}
let ctx = &*context;
block_on(ctx.get_connectivity()) as u32 as libc::c_int
ctx.get_connectivity() as u32 as libc::c_int
}
#[no_mangle]
@@ -395,15 +387,13 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
return "".strdup();
}
let ctx = &*context;
block_on(async move {
match ctx.get_connectivity_html().await {
Ok(html) => html.strdup(),
Err(err) => {
error!(ctx, "Failed to get connectivity html: {err:#}");
"".strdup()
}
}
})
match block_on(ctx.get_connectivity_html())
.context("Failed to get connectivity html")
.log_err(ctx)
{
Ok(html) => html.strdup(),
Err(_) => "".strdup(),
}
}
#[no_mangle]
@@ -566,6 +556,10 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::AccountsChanged => 2302,
EventType::AccountsItemChanged => 2303,
EventType::EventChannelOverflow { .. } => 2400,
EventType::IncomingCall { .. } => 2550,
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -629,7 +623,11 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
EventType::WebxdcRealtimeData { msg_id, .. }
| EventType::WebxdcStatusUpdate { msg_id, .. }
| EventType::WebxdcRealtimeAdvertisementReceived { msg_id }
| EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
| EventType::WebxdcInstanceDeleted { msg_id, .. }
| EventType::IncomingCall { msg_id, .. }
| EventType::IncomingCallAccepted { msg_id, .. }
| EventType::OutgoingCallAccepted { msg_id, .. }
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
EventType::ChatlistItemChanged { chat_id } => {
chat_id.unwrap_or_default().to_u32() as libc::c_int
}
@@ -681,6 +679,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::ChatModified(_)
| EventType::ChatDeleted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. } => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
@@ -699,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"),
@@ -777,8 +780,21 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
| EventType::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => {
let data2 = place_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::OutgoingCallAccepted {
accept_call_info, ..
} => {
let data2 = accept_call_info.to_c_string().unwrap_or_default();
data2.into_raw()
}
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
EventType::ConfigureProgress { comment, .. } => {
if let Some(comment) = comment {
comment.to_c_string().unwrap_or_default().into_raw()
@@ -1082,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,
@@ -1177,6 +1174,61 @@ pub unsafe extern "C" fn dc_init_webxdc_integration(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_place_outgoing_call(
context: *mut dc_context_t,
chat_id: u32,
place_call_info: *const libc::c_char,
) -> u32 {
if context.is_null() || chat_id == 0 {
eprintln!("ignoring careless call to dc_place_outgoing_call()");
return 0;
}
let ctx = &*context;
let chat_id = ChatId::new(chat_id);
let place_call_info = to_string_lossy(place_call_info);
block_on(ctx.place_outgoing_call(chat_id, place_call_info))
.context("Failed to place call")
.log_err(ctx)
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(ctx, "Failed to place call")
}
#[no_mangle]
pub unsafe extern "C" fn dc_accept_incoming_call(
context: *mut dc_context_t,
msg_id: u32,
accept_call_info: *const libc::c_char,
) -> libc::c_int {
if context.is_null() || msg_id == 0 {
eprintln!("ignoring careless call to dc_accept_incoming_call()");
return 0;
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
let accept_call_info = to_string_lossy(accept_call_info);
block_on(ctx.accept_incoming_call(msg_id, accept_call_info))
.context("Failed to accept call")
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_end_call(context: *mut dc_context_t, msg_id: u32) -> libc::c_int {
if context.is_null() || msg_id == 0 {
eprintln!("ignoring careless call to dc_end_call()");
return 0;
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
block_on(ctx.end_call(msg_id))
.context("Failed to end call")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -1255,22 +1307,19 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
let ctx = &*context;
block_on(async move {
match ChatId::new(chat_id).get_draft(ctx).await {
Ok(Some(draft)) => {
let ffi_msg = MessageWrapper {
context,
message: draft,
};
Box::into_raw(Box::new(ffi_msg))
}
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "Failed to get draft for chat #{chat_id}: {err:#}");
ptr::null_mut()
}
match block_on(ChatId::new(chat_id).get_draft(ctx))
.with_context(|| format!("Failed to get draft for chat #{chat_id}"))
.unwrap_or_default()
{
Some(draft) => {
let ffi_msg = MessageWrapper {
context,
message: draft,
};
Box::into_raw(Box::new(ffi_msg))
}
})
None => ptr::null_mut(),
}
}
#[no_mangle]
@@ -1526,10 +1575,7 @@ pub unsafe extern "C" fn dc_set_chat_visibility(
1 => ChatVisibility::Archived,
2 => ChatVisibility::Pinned,
_ => {
warn!(
ctx,
"ignoring careless call to dc_set_chat_visibility(): unknown archived state",
);
eprintln!("ignoring careless call to dc_set_chat_visibility(): unknown archived state");
return;
}
};
@@ -1683,10 +1729,11 @@ pub unsafe extern "C" fn dc_create_group_chat(
return 0;
}
let ctx = &*context;
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
s
} else {
warn!(ctx, "bad protect-value for dc_create_group_chat()");
let Some(protect) = ProtectionStatus::from_i32(protect)
.context("Bad protect-value for dc_create_group_chat()")
.log_err(ctx)
.ok()
else {
return 0;
};
@@ -1707,8 +1754,8 @@ pub unsafe extern "C" fn dc_create_broadcast_list(context: *mut dc_context_t) ->
return 0;
}
let ctx = &*context;
block_on(chat::create_broadcast_list(ctx))
.context("Failed to create broadcast list")
block_on(chat::create_broadcast(ctx, "Channel".to_string()))
.context("Failed to create broadcast channel")
.log_err(ctx)
.map(|id| id.to_u32())
.unwrap_or(0)
@@ -1832,23 +1879,20 @@ pub unsafe extern "C" fn dc_set_chat_mute_duration(
return 0;
}
let ctx = &*context;
let muteDuration = match duration {
let mute_duration = match duration {
0 => MuteDuration::NotMuted,
-1 => MuteDuration::Forever,
n if n > 0 => SystemTime::now()
.checked_add(Duration::from_secs(duration as u64))
.map_or(MuteDuration::Forever, MuteDuration::Until),
_ => {
warn!(
ctx,
"dc_chat_set_mute_duration(): Can not use negative duration other than -1",
);
eprintln!("dc_chat_set_mute_duration(): Can not use negative duration other than -1");
return 0;
}
};
block_on(async move {
chat::set_muted(ctx, ChatId::new(chat_id), muteDuration)
chat::set_muted(ctx, ChatId::new(chat_id), mute_duration)
.await
.map(|_| 1)
.unwrap_or_log_default(ctx, "Failed to set mute duration")
@@ -1866,16 +1910,10 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
}
let ctx = &*context;
block_on(async move {
ChatId::new(chat_id)
.get_encryption_info(ctx)
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
ptr::null_mut()
})
})
block_on(ChatId::new(chat_id).get_encryption_info(ctx))
.map(|s| s.strdup())
.log_err(ctx)
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
@@ -2032,12 +2070,10 @@ pub unsafe extern "C" fn dc_resend_msgs(
let ctx = &*context;
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
error!(ctx, "Resending failed: {err:#}");
0
} else {
1
}
block_on(chat::resend_msgs(ctx, &msg_ids))
.context("Resending failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2067,26 +2103,22 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
}
let ctx = &*context;
block_on(async move {
let message = match message::Message::load_from_db(ctx, MsgId::new(msg_id)).await {
Ok(msg) => msg,
Err(e) => {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
warn!(
ctx,
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
);
message::Message::new(Viewtype::default())
} else {
warn!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
return ptr::null_mut();
}
let message = match block_on(message::Message::load_from_db(ctx, MsgId::new(msg_id)))
.with_context(|| format!("dc_get_msg could not rectieve msg_id {msg_id}"))
.log_err(ctx)
{
Ok(msg) => msg,
Err(_) => {
if msg_id <= constants::DC_MSG_ID_LAST_SPECIAL {
// C-core API returns empty messages, do the same
message::Message::new(Viewtype::default())
} else {
return ptr::null_mut();
}
};
let ffi_msg = MessageWrapper { context, message };
Box::into_raw(Box::new(ffi_msg))
})
}
};
let ffi_msg = MessageWrapper { context, message };
Box::into_raw(Box::new(ffi_msg))
}
#[no_mangle]
@@ -2316,15 +2348,10 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
}
let ctx = &*context;
block_on(async move {
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
ptr::null_mut()
})
})
block_on(Contact::get_encrinfo(ctx, ContactId::new(contact_id)))
.map(|s| s.strdup())
.log_err(ctx)
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
@@ -2339,15 +2366,10 @@ pub unsafe extern "C" fn dc_delete_contact(
}
let ctx = &*context;
block_on(async move {
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(err) => {
error!(ctx, "cannot delete contact: {err:#}");
0
}
}
})
block_on(Contact::delete(ctx, contact_id))
.context("Cannot delete contact")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2418,17 +2440,13 @@ pub unsafe extern "C" fn dc_imex_has_backup(
}
let ctx = &*context;
block_on(async move {
match imex::has_backup(ctx, to_string_lossy(dir).as_ref()).await {
Ok(res) => res.strdup(),
Err(err) => {
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {err:#}");
ptr::null_mut()
}
}
})
match block_on(imex::has_backup(ctx, to_string_lossy(dir).as_ref()))
.context("dc_imex_has_backup")
.log_err(ctx)
{
Ok(res) => res.strdup(),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
@@ -2439,15 +2457,13 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
}
let ctx = &*context;
block_on(async move {
match imex::initiate_key_transfer(ctx).await {
Ok(res) => res.strdup(),
Err(err) => {
error!(ctx, "dc_initiate_key_transfer(): {err:#}");
ptr::null_mut()
}
}
})
match block_on(imex::initiate_key_transfer(ctx))
.context("dc_initiate_key_transfer()")
.log_err(ctx)
{
Ok(res) => res.strdup(),
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
@@ -2462,17 +2478,14 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
}
let ctx = &*context;
block_on(async move {
match imex::continue_key_transfer(ctx, MsgId::new(msg_id), &to_string_lossy(setup_code))
.await
{
Ok(()) => 1,
Err(err) => {
warn!(ctx, "dc_continue_key_transfer: {err:#}");
0
}
}
})
block_on(imex::continue_key_transfer(
ctx,
MsgId::new(msg_id),
&to_string_lossy(setup_code),
))
.context("dc_continue_key_transfer")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
@@ -2916,12 +2929,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
}
let ffi_list = &*chatlist;
let ctx = &*ffi_list.context;
match ffi_list.list.get_chat_id(index) {
match ffi_list
.list
.get_chat_id(index)
.context("get_chat_id failed")
.log_err(ctx)
{
Ok(chat_id) => chat_id.to_u32(),
Err(err) => {
warn!(ctx, "get_chat_id failed: {err:#}");
0
}
Err(_) => 0,
}
}
@@ -2936,12 +2951,14 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
}
let ffi_list = &*chatlist;
let ctx = &*ffi_list.context;
match ffi_list.list.get_msg_id(index) {
match ffi_list
.list
.get_msg_id(index)
.context("get_msg_id failed")
.log_err(ctx)
{
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
Err(err) => {
warn!(ctx, "get_msg_id failed: {err:#}");
0
}
Err(_) => 0,
}
}
@@ -3095,13 +3112,16 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
let ffi_chat = &*chat;
block_on(async move {
match ffi_chat.chat.get_profile_image(&ffi_chat.context).await {
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ffi_chat.context, "failed to get profile image: {err:#}");
ptr::null_mut()
}
match ffi_chat
.chat
.get_profile_image(&ffi_chat.context)
.await
.context("Failed to get profile image")
.log_err(&ffi_chat.context)
.unwrap_or_default()
{
Some(p) => p.to_string_lossy().strdup(),
None => ptr::null_mut(),
}
})
}
@@ -3196,13 +3216,15 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
}
#[no_mangle]
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
pub unsafe extern "C" fn dc_chat_is_encrypted(chat: *mut dc_chat_t) -> libc::c_int {
if chat.is_null() {
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
eprintln!("ignoring careless call to dc_chat_is_encrypted()");
return 0;
}
let ffi_chat = &*chat;
ffi_chat.chat.is_protection_broken() as libc::c_int
block_on(ffi_chat.chat.is_encrypted(&ffi_chat.context))
.unwrap_or_log_default(&ffi_chat.context, "Failed dc_chat_is_encrypted") as libc::c_int
}
#[no_mangle]
@@ -3258,22 +3280,20 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
let ctx = &*context;
block_on(async move {
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
Ok(chat) => chat,
Err(err) => {
error!(ctx, "dc_get_chat_info_json() failed to load chat: {err:#}");
return "".strdup();
}
let Ok(chat) = chat::Chat::load_from_db(ctx, ChatId::new(chat_id))
.await
.context("dc_get_chat_info_json() failed to load chat")
.log_err(ctx)
else {
return "".strdup();
};
let info = match chat.get_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(
ctx,
"dc_get_chat_info_json() failed to get chat info: {err:#}"
);
return "".strdup();
}
let Ok(info) = chat
.get_info(ctx)
.await
.context("dc_get_chat_info_json() failed to get chat info")
.log_err(ctx)
else {
return "".strdup();
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_get_chat_info_json() failed to serialise to json")
@@ -3533,18 +3553,15 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
block_on(async move {
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {err:#}");
return "".strdup();
}
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
})
let Ok(info) = block_on(ffi_msg.message.get_webxdc_info(ctx))
.context("dc_msg_get_webxdc_info() failed to get info")
.log_err(ctx)
else {
return "".strdup();
};
serde_json::to_string(&info)
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
.strdup()
}
#[no_mangle]
@@ -3818,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() {
@@ -4338,6 +4330,15 @@ pub unsafe extern "C" fn dc_contact_is_bot(contact: *mut dc_contact_t) -> libc::
(*contact).contact.is_bot() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_is_key_contact(contact: *mut dc_contact_t) -> libc::c_int {
if contact.is_null() {
eprintln!("ignoring careless call to dc_contact_is_key_contact()");
return 0;
}
(*contact).contact.is_key_contact() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 {
if contact.is_null() {
@@ -4350,6 +4351,7 @@ pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t)
.context("failed to get verifier")
.log_err(ctx)
.unwrap_or_default()
.unwrap_or_default()
.unwrap_or_default();
verifier_contact_id.to_u32()
@@ -4574,13 +4576,10 @@ trait ResultExt<T, E> {
impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T {
match self {
Ok(t) => t,
Err(err) => {
error!(context, "{message}: {err:#}");
Default::default()
}
}
self.map_err(|err| anyhow::anyhow!("{err:#}"))
.with_context(|| message.to_string())
.log_err(context)
.unwrap_or_default()
}
}

View File

@@ -34,7 +34,7 @@ pub enum Meaning {
}
impl Lot {
pub fn get_text1(&self) -> Option<Cow<str>> {
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
match self {
Self::Summary(summary) => match &summary.prefix {
None => None,
@@ -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)),
@@ -66,7 +65,7 @@ impl Lot {
}
}
pub fn get_text2(&self) -> Option<Cow<str>> {
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
match self {
Self::Summary(summary) => Some(summary.truncated_text(160)),
Self::Qr(_) => None,
@@ -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 = "1.159.3"
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,
@@ -19,6 +20,7 @@ use deltachat::constants::DC_MSG_ID_DAYMARKER;
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
use deltachat::context::get_info;
use deltachat::ephemeral::Timer;
use deltachat::imex;
use deltachat::location;
use deltachat::message::get_msg_read_receipts;
use deltachat::message::{
@@ -35,7 +37,6 @@ use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use deltachat::{imex, info};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
@@ -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()
@@ -224,6 +235,14 @@ impl CommandApi {
self.accounts.read().await.get_selected_account_id()
}
/// Set the order of accounts.
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
async fn set_accounts_order(&self, order: Vec<u32>) -> Result<()> {
self.accounts.write().await.set_accounts_order(order).await
}
/// Get a list of all configured accounts.
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
let mut accounts = Vec::new();
@@ -289,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"
))
}
}
@@ -354,6 +372,20 @@ impl CommandApi {
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
}
/// If there was an error while the account was opened
/// and migrated to the current version,
/// then this function returns it.
///
/// This function is useful because the key-contacts migration could fail due to bugs
/// and then the account will not work properly.
///
/// After opening an account, the UI should call this function
/// and show the error string if one is returned.
async fn get_migration_error(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_migration_error())
}
/// Copy file to blob dir.
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
let ctx = self.get_context(account_id).await?;
@@ -912,7 +944,7 @@ impl CommandApi {
/// explicitly as it may happen that oneself gets removed from a still existing
/// group
///
/// - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
/// - for broadcast channels, all recipients are returned, DC_CONTACT_ID_SELF is not included
///
/// - for mailing lists, the behavior is not documented currently, we will decide on that later.
/// for now, the UI should not show the list for mailing lists.
@@ -931,7 +963,7 @@ impl CommandApi {
Ok(contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>())
}
/// Create a new group chat.
/// Create a new encrypted group chat (with key-contacts).
///
/// After creation,
/// the group has one member with the ID DC_CONTACT_ID_SELF
@@ -949,30 +981,52 @@ impl CommandApi {
///
/// @param protect If set to 1 the function creates group with protection initially enabled.
/// Only verified members are allowed in these groups
/// and end-to-end-encryption is always enabled.
async fn create_group_chat(&self, account_id: u32, name: String, protect: bool) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let protect = match protect {
true => ProtectionStatus::Protected,
false => ProtectionStatus::Unprotected,
};
chat::create_group_chat(&ctx, protect, &name)
chat::create_group_ex(&ctx, Some(protect), &name)
.await
.map(|id| id.to_u32())
}
/// Create a new broadcast list.
/// Create a new unencrypted group chat.
///
/// Broadcast lists are similar to groups on the sending device,
/// however, recipients get the messages in a read-only chat
/// and will see who the other members are.
///
/// For historical reasons, this function does not take a name directly,
/// instead you have to set the name using dc_set_chat_name()
/// after creating the broadcast list.
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
/// Same as [`Self::create_group_chat`], but the chat is unencrypted and can only have
/// address-contacts.
async fn create_group_chat_unencrypted(&self, account_id: u32, name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast_list(&ctx)
chat::create_group_ex(&ctx, None, &name)
.await
.map(|id| id.to_u32())
}
/// Deprecated 2025-07 in favor of create_broadcast().
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
self.create_broadcast(account_id, "Channel".to_string())
.await
}
/// Create a new **broadcast channel**
/// (called "Channel" in the UI).
///
/// Broadcast channels are similar to groups on the sending device,
/// however, recipients get the messages in a read-only chat
/// and will not see who the other members are.
///
/// Called `broadcast` here rather than `channel`,
/// because the word "channel" already appears a lot in the code,
/// which would make it hard to grep for it.
///
/// After creation, the chat contains no recipients and is in _unpromoted_ state;
/// see [`CommandApi::create_group_chat`] for more information on the unpromoted state.
///
/// Returns the created chat's id.
async fn create_broadcast(&self, account_id: u32, chat_name: String) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
chat::create_broadcast(&ctx, chat_name)
.await
.map(|id| id.to_u32())
}
@@ -1183,8 +1237,10 @@ impl CommandApi {
}
/// Returns all messages of a particular chat.
/// If `add_daymarker` is `true`, it will return them as
/// `DC_MSG_ID_DAYMARKER`, e.g. [1234, 1237, 9, 1239].
///
/// * `add_daymarker` - If `true`, add day markers as `DC_MSG_ID_DAYMARKER` to the result,
/// e.g. [1234, 1237, 9, 1239]. The day marker timestamp is the midnight one for the
/// corresponding (following) day in the local timezone.
async fn get_message_ids(
&self,
account_id: u32,
@@ -1427,7 +1483,14 @@ impl CommandApi {
/// Add a single contact as a result of an explicit user action.
///
/// Returns contact id of the created or existing contact
/// This will always create or look up an address-contact,
/// i.e. a contact identified by an email address,
/// with all messages sent to and from this contact being unencrypted.
/// If the user just clicked on an email address,
/// you should first check [`Self::lookup_contact_id_by_addr`]/`lookupContactIdByAddr.`,
/// and only if there is no contact yet, call this function here.
///
/// Returns contact id of the created or existing contact.
async fn create_contact(
&self,
account_id: u32,
@@ -1479,6 +1542,14 @@ impl CommandApi {
Ok(contacts)
}
/// Returns ids of known and unblocked contacts.
///
/// By default, key-contacts are listed.
///
/// * `list_flags` - A combination of flags:
/// - `DC_GCL_ADD_SELF` - Add SELF unless filtered by other parameters.
/// - `DC_GCL_ADDRESS` - List address-contacts instead of key-contacts.
/// * `query` - A string to filter the list.
async fn get_contact_ids(
&self,
account_id: u32,
@@ -1490,8 +1561,10 @@ impl CommandApi {
Ok(contacts.into_iter().map(|c| c.to_u32()).collect())
}
/// Get a list of contacts.
/// (formerly called getContacts2 in desktop)
/// Returns known and unblocked contacts.
///
/// Formerly called `getContacts2` in Desktop.
/// See [`Self::get_contact_ids`] for parameters and more info.
async fn get_contacts(
&self,
account_id: u32,
@@ -1542,15 +1615,6 @@ impl CommandApi {
Ok(())
}
/// Resets contact encryption.
async fn reset_contact_encryption(&self, account_id: u32, contact_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
contact_id.reset_encryption(&ctx).await?;
Ok(())
}
/// Sets display name for existing contact.
async fn change_contact_name(
&self,
@@ -1576,9 +1640,19 @@ impl CommandApi {
Contact::get_encrinfo(&ctx, ContactId::new(contact_id)).await
}
/// Check if an e-mail address belongs to a known and unblocked contact.
/// Looks up a known and unblocked contact with a given e-mail address.
/// To get a list of all known and unblocked contacts, use contacts_get_contacts().
///
/// **POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
/// (e.g. an address-contact and a key-contact),
/// this looks up the most recently seen contact,
/// i.e. which contact is returned depends on which contact last sent a message.
/// If the user just clicked on a mailto: link, then this is the best thing you can do.
/// But **DO NOT** internally represent contacts by their email address
/// and do not use this function to look them up;
/// otherwise this function will sometimes look up the wrong contact.
/// Instead, you should internally represent contacts by their ids.
///
/// To validate an e-mail address independently of the contact database
/// use check_email_validity().
async fn lookup_contact_id_by_addr(
@@ -1734,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?;
@@ -1806,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.
@@ -1844,7 +1918,7 @@ impl CommandApi {
/// If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
async fn get_connectivity(&self, account_id: u32) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
Ok(ctx.get_connectivity().await as u32)
Ok(ctx.get_connectivity() as u32)
}
/// Get an overview of the current connectivity, and possibly more statistics.
@@ -1919,16 +1993,19 @@ impl CommandApi {
instance_msg_id: u32,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let fut = send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?;
if let Some(fut) = fut {
tokio::spawn(async move {
fut.await.ok();
info!(ctx, "send_webxdc_realtime_advertisement done")
});
if let Some(fut) =
send_webxdc_realtime_advertisement(&ctx, MsgId::new(instance_msg_id)).await?
{
tokio::spawn(fut);
}
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
@@ -2006,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.
@@ -2157,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
@@ -2194,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()
@@ -2415,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

@@ -21,15 +21,39 @@ pub struct FullChat {
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
/// Only verified contacts
/// as determined by [`ContactObject::is_verified`] / `Contact.isVerified`
/// can be added to protected chats.
///
/// Protected chats are created using [`create_group_chat`] / `createGroupChat()`
/// by setting the 'protect' parameter to true.
///
/// [`create_group_chat`]: crate::api::CommandApi::create_group_chat
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
@@ -47,7 +71,7 @@ pub struct FullChat {
fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
self_in_group: bool,
is_muted: bool,
@@ -108,6 +132,7 @@ impl FullChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
@@ -120,7 +145,6 @@ impl FullChat {
color,
fresh_message_counter,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(),
@@ -159,6 +183,30 @@ pub struct BasicChat {
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
@@ -167,7 +215,7 @@ pub struct BasicChat {
is_self_talk: bool,
color: String,
is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool,
is_muted: bool,
}
@@ -187,6 +235,7 @@ impl BasicChat {
id: chat_id,
name: chat.name.clone(),
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(context).await?,
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
@@ -195,7 +244,6 @@ impl BasicChat {
is_self_talk: chat.is_self_talk(),
color,
is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(),
})

View File

@@ -23,6 +23,7 @@ pub enum ChatListItemFetchResult {
name: String,
avatar_path: Option<String>,
color: String,
chat_type: u32,
last_updated: Option<i64>,
summary_text1: String,
summary_text2: String,
@@ -30,6 +31,31 @@ pub enum ChatListItemFetchResult {
/// showing preview if last chat message is image
summary_preview_image: Option<String>,
is_protected: bool,
/// True if the chat is encrypted.
/// This means that all messages in the chat are encrypted,
/// and all contacts in the chat are "key-contacts",
/// i.e. identified by the PGP key fingerprint.
///
/// False if the chat is unencrypted.
/// This means that all messages in the chat are unencrypted,
/// and all contacts in the chat are "address-contacts",
/// i.e. identified by the email address.
/// The UI should mark this chat e.g. with a mail-letter icon.
///
/// Unencrypted groups are called "ad-hoc groups"
/// and the user can't add/remove members,
/// create a QR invite code,
/// or set an avatar.
/// These options should therefore be disabled in the UI.
///
/// Note that it can happen that an encrypted chat
/// contains unencrypted messages that were received in core <= v1.159.*
/// and vice versa.
///
/// See also `is_key_contact` on `Contact`.
is_encrypted: bool,
/// deprecated 2025-07, use chat_type instead
is_group: bool,
fresh_message_counter: usize,
is_self_talk: bool,
@@ -40,8 +66,6 @@ pub enum ChatListItemFetchResult {
is_pinned: bool,
is_muted: bool,
is_contact_request: bool,
/// true when chat is a broadcastlist
is_broadcast: bool,
/// contact id if this is a dm chat (for view profile entry in context menu)
dm_chat_contact: Option<u32>,
was_seen_recently: bool,
@@ -131,12 +155,14 @@ pub(crate) async fn get_chat_list_item_by_id(
name: chat.get_name().to_owned(),
avatar_path,
color,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
last_updated,
summary_text1,
summary_text2,
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
summary_preview_image,
is_protected: chat.is_protected(),
is_encrypted: chat.is_encrypted(ctx).await?,
is_group: chat.get_type() == Chattype::Group,
fresh_message_counter,
is_self_talk: chat.is_self_talk(),
@@ -147,7 +173,6 @@ pub(crate) async fn get_chat_list_item_by_id(
is_pinned: visibility == ChatVisibility::Pinned,
is_muted: chat.is_muted(),
is_contact_request: chat.is_contact_request(),
is_broadcast: chat.get_type() == Chattype::Broadcast,
dm_chat_contact,
was_seen_recently,
last_message_type: message_type,

View File

@@ -19,29 +19,48 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
/// Is the contact a key contact.
is_key_contact: bool,
/// Is encryption available for this contact.
///
/// This can only be true for key-contacts.
/// However, it is possible to have a key-contact
/// for which encryption is not available because we don't have a key yet,
/// e.g. if we just scanned the fingerprint from a QR code.
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
/// True if the contact
/// can be added to protected chats
/// because SELF and contact have verified their fingerprints in both directions.
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items,
/// in chat member list items
/// and in profiles if no chat with the contact exist.
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
is_verified: bool,
/// True if the contact profile title should have a green checkmark.
/// The contact ID that verified a contact.
///
/// This indicates whether 1:1 chat has a green checkmark
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The ID of the contact that verified this contact.
/// As verifier may be unknown,
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
///
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
/// UI should display the information in the contact's profile as follows:
///
/// - If `verifierId` != 0,
/// display text "Introduced by ..."
/// with the name and address of the contact
/// formatted by `name_and_addr`/`nameAndAddr`.
/// Prefix the text by a green checkmark.
///
/// - If `verifierId` == 0 and `isVerified` != 0,
/// display "Introduced" prefixed by a green checkmark.
///
/// - if `verifierId` == 0 and `isVerified` == 0,
/// display nothing
///
/// This contains the contact ID of the verifier.
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
/// If it is None/Null, we don't have verifier information or
/// the contact is not verified.
verifier_id: Option<u32>,
/// the contact's last seen timestamp
@@ -62,11 +81,11 @@ impl ContactObject {
None => None,
};
let is_verified = contact.is_verified(context).await?;
let is_profile_verified = contact.is_profile_verified(context).await?;
let verifier_id = contact
.get_verifier_id(context)
.await?
.flatten()
.map(|contact_id| contact_id.to_u32());
Ok(ContactObject {
@@ -80,9 +99,9 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
is_key_contact: contact.is_key_contact(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
verifier_id,
last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(),

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;
@@ -224,7 +225,6 @@ pub enum EventType {
},
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
/// Or the verify state of a chat has changed.
/// See setChatName(), setChatProfileImage(), addContactToChat()
/// and removeContactFromChat().
///
@@ -294,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().
@@ -304,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, vg-member-added/vc-contact-confirm sent, 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,
},
@@ -417,6 +420,45 @@ pub enum EventType {
/// Number of events skipped.
n: u64,
},
/// Incoming call.
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.
/// This is esp. interesting to stop ringing on other devices.
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,
},
/// Call ended.
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,
},
}
impl From<CoreEventType> for EventType {
@@ -523,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 {
@@ -567,6 +613,34 @@ impl From<CoreEventType> for EventType {
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
CoreEventType::AccountsChanged => AccountsChanged,
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, 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, chat_id } => CallEnded {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

@@ -19,10 +19,10 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase", tag = "kind")]
#[expect(clippy::large_enum_variant)]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
@@ -59,6 +59,13 @@ pub struct MessageObject {
// summary - use/create another function if you need it
subject: String,
/// True if the message was correctly encrypted&signed, false otherwise.
/// Historically, UIs showed a small padlock on the message then.
///
/// Today, the UIs should instead show a small email-icon on the message
/// if `show_padlock` is `false`,
/// and nothing if it is `true`.
show_padlock: bool,
is_setupmessage: bool,
is_info: bool,
@@ -77,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,
@@ -90,8 +94,6 @@ pub struct MessageObject {
file_bytes: u64,
file_name: Option<String>,
webxdc_info: Option<WebxdcMessageInfo>,
webxdc_href: Option<String>,
download_state: DownloadState,
@@ -142,12 +144,6 @@ impl MessageObject {
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name();
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
} else {
None
};
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
let download_state = message.download_state().into();
@@ -240,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,
@@ -261,7 +248,6 @@ impl MessageObject {
file_mime: message.get_filemime(),
file_bytes,
file_name: message.get_filename(),
webxdc_info,
// On a WebxdcInfoMessage this might include a hash holding
// information about a specific position or state in a webxdc app
@@ -323,8 +309,8 @@ pub enum MessageViewtype {
/// Message containing any file, eg. a PDF.
File,
/// Message is an invitation to a videochat.
VideochatInvitation,
/// Message is a call.
Call,
/// Message is an webxdc instance.
Webxdc,
@@ -347,7 +333,7 @@ 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,
}
@@ -366,7 +352,7 @@ 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,
}
@@ -418,6 +404,9 @@ pub enum SystemMessageType {
/// Chat ephemeral message timer is changed.
EphemeralTimerChanged,
// Chat is e2ee
ChatE2ee,
// Chat protection state changed
ChatProtectionEnabled,
ChatProtectionDisabled,
@@ -436,6 +425,9 @@ pub enum SystemMessageType {
/// This message contains a users iroh node address.
IrohNodeAddr,
CallAccepted,
CallEnded,
}
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -452,6 +444,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
@@ -461,6 +454,8 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
SystemMessage::CallEnded => SystemMessageType::CallEnded,
}
}
}

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

@@ -2,24 +2,24 @@
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"isomorphic-ws": "^5.0.0",
"yerpc": "^0.6.2"
},
"devDependencies": {
"@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0",
"@types/ws": "^7.2.4",
"c8": "^7.10.0",
"@types/chai": "^4.3.10",
"@types/chai-as-promised": "^7.1.8",
"@types/mocha": "^10.0.4",
"@types/ws": "^8.5.9",
"c8": "^8.0.1",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.17.9",
"esbuild": "^0.25.5",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"mocha": "^10.2.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.6.2",
"typedoc": "^0.23.2",
"typescript": "^4.5.5",
"prettier": "^3.5.3",
"typedoc": "^0.28.5",
"typescript": "^5.8.3",
"ws": "^8.5.0"
},
"exports": {
@@ -54,5 +54,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.159.3"
"version": "2.20.0"
}

View File

@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =
json[Object.keys(json).find((k) => k.includes(generatedFile))];
const fnMap = Object.keys(jsonCoverage.fnMap).map(
(key) => jsonCoverage.fnMap[key]
(key) => jsonCoverage.fnMap[key],
);
const htmlCoverage = readFileSync(
"./coverage/" + generatedFile + ".html",
"utf8"
"utf8",
);
const uncoveredLines = htmlCoverage
.split("\n")
.filter((line) => line.includes(`"function not covered"`));
const uncoveredFunctions = uncoveredLines.map(
(line) => />([\w_]+)\(/.exec(line)[1]
(line) => />([\w_]+)\(/.exec(line)[1],
);
console.log(
"\nUncovered api functions:\n" +
uncoveredFunctions
.map((uF) => fnMap.find(({ name }) => name === uF))
.map(
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
)
.join("\n")
.join("\n"),
);

View File

@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
const constants = data
.filter(
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
)
.sort((lhs, rhs) => {
if (lhs.key < rhs.key) return -1;
@@ -50,5 +50,5 @@ const constants = data
writeFileSync(
resolve(__dirname, "../generated/constants.ts"),
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`,
);

View File

@@ -8,13 +8,13 @@ import { TinyEmitter } from "@deltachat/tiny-emitter";
type Events = { ALL: (accountId: number, event: EventType) => void } & {
[Property in EventType["kind"]]: (
accountId: number,
event: Extract<EventType, { kind: Property }>
event: Extract<EventType, { kind: Property }>,
) => void;
};
type ContextEvents = { ALL: (event: EventType) => void } & {
[Property in EventType["kind"]]: (
event: Extract<EventType, { kind: Property }>
event: Extract<EventType, { kind: Property }>,
) => void;
};
@@ -25,16 +25,22 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
>;
export class BaseDeltaChat<
Transport extends BaseTransport<any>
Transport extends BaseTransport<any>,
> extends TinyEmitter<Events> {
rpc: RawClient;
account?: T.Account;
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
//@ts-ignore
private eventTask: Promise<void>;
constructor(public transport: Transport, startEventLoop: boolean) {
constructor(
public transport: Transport,
/**
* Whether to start calling {@linkcode RawClient.getNextEvent}
* and emitting the respective events on this class.
*/
startEventLoop: boolean,
) {
super();
this.rpc = new RawClient(this.transport);
if (startEventLoop) {
@@ -42,6 +48,9 @@ export class BaseDeltaChat<
}
}
/**
* @see the constructor's `startEventLoop`
*/
async eventLoop(): Promise<void> {
while (true) {
const event = await this.rpc.getNextEvent();
@@ -53,17 +62,24 @@ export class BaseDeltaChat<
this.contextEmitters[event.contextId].emit(
event.event.kind,
//@ts-ignore
event.event as any
event.event as any,
);
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
}
}
}
/**
* @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];
@@ -83,7 +99,10 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
}
export class StdioTransport extends BaseTransport {
constructor(public input: any, public output: any) {
constructor(
public input: any,
public output: any,
) {
super();
var buffer = "";

View File

@@ -1,4 +1,3 @@
import { strictEqual } from "assert";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
@@ -32,14 +31,14 @@ describe("basic tests", () => {
expect(
await Promise.all(
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
),
).to.not.contain(false);
expect(
await Promise.all(
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
)
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
),
).to.not.contain(true);
});
@@ -85,7 +84,7 @@ describe("basic tests", () => {
const contactId = await dc.rpc.createContact(
accountId,
"example@delta.chat",
null
null,
);
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
.false;
@@ -127,7 +126,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});
@@ -139,7 +138,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});
@@ -153,7 +152,7 @@ describe("basic tests", () => {
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
Object.keys(config),
);
expect(retrieved).to.deep.equal(config);
});

View File

@@ -17,12 +17,12 @@ describe("online tests", function () {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error(
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
);
process.exit(1);
}
console.log(
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
);
this.skip();
}
@@ -36,7 +36,7 @@ describe("online tests", function () {
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account1 || !account1.email || !account1.password) {
console.log(
"We didn't got back an account from the api, skip integration tests"
"We didn't got back an account from the api, skip integration tests",
);
this.skip();
}
@@ -44,7 +44,7 @@ describe("online tests", function () {
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
if (!account2 || !account2.email || !account2.password) {
console.log(
"We didn't got back an account2 from the api, skip integration tests"
"We didn't got back an account2 from the api, skip integration tests",
);
this.skip();
}
@@ -92,11 +92,13 @@ describe("online tests", function () {
accountId2,
chatIdOnAccountB,
false,
false
false,
);
expect(messageList).have.length(1);
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
// There are 2 messages in the chat:
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
expect(messageList).have.length(2);
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
expect(message.text).equal("Hello");
expect(message.showPadlock).equal(true);
});
@@ -124,11 +126,11 @@ describe("online tests", function () {
accountId2,
chatIdOnAccountB,
false,
false
false,
);
const message = await dc.rpc.getMessage(
accountId2,
messageList.reverse()[0]
messageList.reverse()[0],
);
expect(message.text).equal("Hello2");
// Send message back from B to A
@@ -150,7 +152,7 @@ describe("online tests", function () {
const info = await dc.rpc.getProviderInfo(acc, "example.com");
expect(info).to.be.not.null;
expect(info?.overviewPage).to.equal(
"https://providers.delta.chat/example-com"
"https://providers.delta.chat/example-com",
);
expect(info?.status).to.equal(3);
});
@@ -167,12 +169,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
dc: DeltaChat,
eventType: T,
accountId: number,
timeout: number = EVENT_TIMEOUT
timeout: number = EVENT_TIMEOUT,
): Promise<Extract<DcEvent, { kind: T }>> {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")),
timeout
timeout,
);
const callback = (contextId: number, event: DcEvent) => {
if (contextId == accountId) {

View File

@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
const pathToServerBinary = resolve(
join(await getTargetDir(), "debug/deltachat-rpc-server")
join(await getTargetDir(), "debug/deltachat-rpc-server"),
);
const server = spawn(pathToServerBinary, {
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
throw new Error(
"Failed to start server executable " +
pathToServerBinary +
", make sure you built it first."
", make sure you built it first.",
);
});
let shouldClose = false;
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
reject(error);
}
}
}
},
);
});
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.159.3"
version = "2.20.0"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/chatmail/core"
@@ -13,7 +13,7 @@ log = { workspace = true }
nu-ansi-term = { workspace = true }
qr2term = "0.3.3"
rusqlite = { workspace = true }
rustyline = "15"
rustyline = "16"
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
tracing-subscriber = { workspace = true, features = ["env-filter"] }

View File

@@ -20,7 +20,6 @@ use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*;
use deltachat::qr::*;
use deltachat::qr_code_generator::create_qr_svg;
use deltachat::reaction::send_reaction;
@@ -35,14 +34,6 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})...");
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", ())
.await
.unwrap();
println!("(2) Peerstates reset.");
}
if 0 != bits & 4 {
context
.sql()
@@ -96,7 +87,7 @@ async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {err:?}");
eprintln!("receive_imf errored: {err:?}");
}
Ok(())
}
@@ -120,7 +111,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} else {
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
if rs.is_none() {
error!(context, "Import: No file or folder given.");
eprintln!("Import: No file or folder given.");
return false;
}
real_spec = rs.unwrap();
@@ -149,7 +140,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
@@ -219,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={}]",
@@ -277,7 +262,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
for contact_id in contacts {
let mut line2 = "".to_string();
let line2 = "".to_string();
let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name();
let addr = contact.get_addr();
@@ -296,15 +281,6 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
verified_str,
if !addr.is_empty() { addr } else { "addr unset" }
);
let peerstate = Peerstate::from_addr(context, addr)
.await
.expect("peerstate error");
if peerstate.is_some() && *contact_id != ContactId::SELF {
line2 = format!(
", prefer-encrypt={}",
peerstate.as_ref().unwrap().prefer_encrypt
);
}
println!("Contact#{}: {}{}", *contact_id, line, line2);
}
@@ -342,7 +318,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
send-backup\n\
receive-backup <qr>\n\
export-keys\n\
import-keys\n\
import-keys <key-file>\n\
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -351,8 +327,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
_ => println!(
"==========================Database commands==\n\
info\n\
open <file to open or create>\n\
close\n\
set <configuration-key> [<value>]\n\
get <configuration-key>\n\
oauth2\n\
@@ -367,28 +341,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
==============================Chat commands==\n\
listchats [<query>]\n\
listarchived\n\
start-realtime <msg-id>\n\
send-realtime <msg-id> <data>\n\
chat [<chat-id>|0]\n\
createchat <contact-id>\n\
creategroup <name>\n\
createbroadcast\n\
createbroadcast <name>\n\
createprotected <name>\n\
addmember <contact-id>\n\
removemember <contact-id>\n\
groupname <name>\n\
groupimage [<file>]\n\
groupimage <image>\n\
chatinfo\n\
sendlocations <seconds>\n\
setlocation <lat> <lng>\n\
dellocations\n\
getlocations [<contact-id>]\n\
send <text>\n\
sendempty\n\
sendimage <file> [<text>]\n\
sendsticker <file> [<text>]\n\
sendfile <file> [<text>]\n\
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\
@@ -400,7 +376,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unmute <chat-id>\n\
delchat <chat-id>\n\
accept <chat-id>\n\
decline <chat-id>\n\
blockchat <chat-id>\n\
===========================Message commands==\n\
listmsgs <query>\n\
msginfo <msg-id>\n\
@@ -414,14 +390,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
react <msg-id> [<reaction>]\n\
===========================Contact commands==\n\
listcontacts [<query>]\n\
listverified [<query>]\n\
addcontact [<name>] <addr>\n\
contactinfo <contact-id>\n\
delcontact <contact-id>\n\
cleanupcontacts\n\
block <contact-id>\n\
unblock <contact-id>\n\
listblocked\n\
import-vcard <file>\n\
make-vcard <file> <contact-id> [contact-id ...]\n\
======================================Misc.==\n\
getqr [<chat-id>]\n\
getqrsvg [<chat-id>]\n\
@@ -442,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.");
@@ -456,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" => {
@@ -493,7 +469,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"send-backup" => {
let provider = BackupProvider::prepare(&context).await?;
let qr = format_backup(&provider.qr())?;
println!("QR code: {}", qr);
println!("QR code: {qr}");
qr2term::print_qr(qr.as_str())?;
provider.await?;
}
@@ -508,13 +484,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Exported to {}.", dir.to_string_lossy());
}
"import-keys" => {
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}
"reset" => {
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
ensure!(
!arg1.is_empty(),
"Argument <bits> missing: 4=private keys, 8=rest but server config"
);
let bits: i32 = arg1.parse()?;
ensure!(bits < 16, "<bits> must be lower than 16.");
reset_tables(&context, bits).await;
@@ -547,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}");
}
}
}
@@ -636,7 +616,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Location streaming enabled.");
}
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"start-realtime" => {
if arg1.is_empty() {
@@ -746,7 +726,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
eprintln!(
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
);
}
@@ -765,7 +745,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("Group#{chat_id} created successfully.");
}
"createbroadcast" => {
let chat_id = chat::create_broadcast_list(&context).await?;
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
println!("Broadcast#{chat_id} created successfully.");
}
@@ -974,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.");
@@ -999,7 +976,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
query,
);
println!("{time_needed:?} to create this list");
eprintln!("{time_needed:?} to create this list");
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1162,19 +1139,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let reaction = arg2;
send_reaction(&context, msg_id, reaction).await?;
}
"listcontacts" | "contacts" | "listverified" => {
let contacts = Contact::get_all(
&context,
if arg0 == "listverified" {
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
} else {
DC_GCL_ADD_SELF
},
Some(arg1),
)
.await?;
"listcontacts" | "contacts" => {
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
log_contactlist(&context, &contacts).await?;
println!("{} contacts.", contacts.len());
println!("{} key contacts.", contacts.len());
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
log_contactlist(&context, &addrcontacts).await?;
println!("{} address contacts.", addrcontacts.len());
}
"addcontact" => {
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
@@ -1238,6 +1209,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
log_contactlist(&context, &contacts).await?;
println!("{} blocked contacts.", contacts.len());
}
"import-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
let contacts = import_vcard(&context, &vcard_content).await?;
println!("vCard contacts imported:");
log_contactlist(&context, &contacts).await?;
}
"make-vcard" => {
ensure!(!arg1.is_empty(), "Argument <file> missing.");
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
let mut contact_ids = vec![];
for x in arg2.split_whitespace() {
contact_ids.push(ContactId::new(x.parse()?))
}
let vcard_content = make_vcard(&context, &contact_ids).await?;
fs::write(&arg1.to_string(), vcard_content).await?;
println!("vCard written to: {arg1}");
}
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;
@@ -1247,7 +1236,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
}
}
"createqrsvg" => {
@@ -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

@@ -5,7 +5,6 @@
//! Usage: cargo run --example repl --release -- <databasefile>
//! All further options can be set using the set-command (type ? for help).
#[macro_use]
extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned};
@@ -41,25 +40,25 @@ fn receive_event(event: EventType) {
match event {
EventType::Info(msg) => {
/* do not show the event as this would fill the screen */
info!("{}", msg);
info!("{msg}");
}
EventType::SmtpConnected(msg) => {
info!("[SMTP_CONNECTED] {}", msg);
info!("[SMTP_CONNECTED] {msg}");
}
EventType::ImapConnected(msg) => {
info!("[IMAP_CONNECTED] {}", msg);
info!("[IMAP_CONNECTED] {msg}");
}
EventType::SmtpMessageSent(msg) => {
info!("[SMTP_MESSAGE_SENT] {}", msg);
info!("[SMTP_MESSAGE_SENT] {msg}");
}
EventType::Warning(msg) => {
warn!("{}", msg);
warn!("{msg}");
}
EventType::Error(msg) => {
error!("{}", msg);
error!("{msg}");
}
EventType::ErrorSelfNotInGroup(msg) => {
error!("[SELF_NOT_IN_GROUP] {}", msg);
error!("[SELF_NOT_IN_GROUP] {msg}");
}
EventType::MsgsChanged { chat_id, msg_id } => {
info!(
@@ -124,7 +123,7 @@ fn receive_event(event: EventType) {
);
}
_ => {
info!("Received {:?}", event);
info!("Received {event:?}");
}
}
}
@@ -180,9 +179,11 @@ const DB_COMMANDS: [&str; 11] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 36] = [
const CHAT_COMMANDS: [&str; 38] = [
"listchats",
"listarchived",
"start-realtime",
"send-realtime",
"chat",
"createchat",
"creategroup",
@@ -198,13 +199,15 @@ const CHAT_COMMANDS: [&str; 36] = [
"dellocations",
"getlocations",
"send",
"sendempty",
"sendimage",
"sendsticker",
"sendfile",
"sendhtml",
"sendsyncmsg",
"sendupdate",
"videochat",
"draft",
"devicemsg",
"listmedia",
"archive",
"unarchive",
@@ -212,47 +215,48 @@ const CHAT_COMMANDS: [&str; 36] = [
"unpin",
"mute",
"unmute",
"protect",
"unprotect",
"delchat",
"accept",
"blockchat",
];
const MESSAGE_COMMANDS: [&str; 9] = [
const MESSAGE_COMMANDS: [&str; 10] = [
"listmsgs",
"msginfo",
"download",
"html",
"listfresh",
"forward",
"resend",
"markseen",
"delmsg",
"download",
"react",
];
const CONTACT_COMMANDS: [&str; 9] = [
"listcontacts",
"listverified",
"addcontact",
"contactinfo",
"delcontact",
"cleanupcontacts",
"block",
"unblock",
"listblocked",
"import-vcard",
"make-vcard",
];
const MISC_COMMANDS: [&str; 12] = [
const MISC_COMMANDS: [&str; 14] = [
"getqr",
"getqrsvg",
"getbadqr",
"checkqr",
"joinqr",
"setqr",
"createqrsvg",
"providerinfo",
"fileinfo",
"estimatedeletion",
"clear",
"exit",
"quit",
"help",
"estimatedeletion",
];
impl Hinter for DcHelper {
@@ -308,7 +312,7 @@ impl Validator for DcHelper {}
async fn start(args: Vec<String>) -> Result<(), Error> {
if args.len() < 2 {
println!("Error: Bad arguments, expected [db-name].");
eprintln!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified");
}
let context = ContextBuilder::new(args[1].clone().into())
@@ -363,7 +367,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
true
}
}
@@ -378,7 +382,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
println!("Error: {err:#}");
eprintln!("Error: {err:#}");
break;
}
}
@@ -462,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 = "1.159.3"
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"
]
@@ -66,6 +67,9 @@ lint.select = [
"RUF006" # asyncio-dangling-task
]
lint.ignore = [
"PLC0415" # `import` should be at the top-level of a file
]
line-length = 120
[tool.isort]

View File

@@ -1,4 +1,4 @@
"""Delta Chat JSON-RPC high-level API"""
"""Delta Chat JSON-RPC high-level API."""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account

View File

@@ -92,6 +92,12 @@ def _run_cli(
)
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
parser.add_argument(
"--displayname", action="store", help="the profile's display name", default=os.getenv("DELTACHAT_DISPLAYNAME"),
)
parser.add_argument(
"--avatar", action="store", help="filename of the profile's avatar", default=os.getenv("DELTACHAT_AVATAR"),
)
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
@@ -108,14 +114,19 @@ def _run_cli(
configure_thread = Thread(
target=client.configure,
daemon=True,
kwargs={"email": args.email, "password": args.password},
kwargs={
"email": args.email,
"password": args.password,
"displayname": args.displayname,
"selfavatar": args.avatar,
},
)
configure_thread.start()
client.run_forever()
def extract_addr(text: str) -> str:
"""extract email address from the given text."""
"""Extract email address from the given text."""
match = re.match(r".*\((.+@.+)\)", text)
if match:
text = match.group(1)
@@ -124,7 +135,7 @@ def extract_addr(text: str) -> str:
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
"""return image changed/deleted info from parsing the given system message text."""
"""Return image changed/deleted info from parsing the given system message text."""
text = text.lower()
match = re.match(r"group image (changed|deleted) by (.+).", text)
if match:
@@ -143,7 +154,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
"""return add/remove info from parsing the given system message text.
"""Return add/remove info from parsing the given system message text.
returns a (action, affected, actor) tuple.
"""

View File

@@ -1,5 +1,8 @@
"""Account module."""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from warnings import warn
@@ -34,7 +37,10 @@ class Account:
return next_event
def clear_all_events(self):
"""Removes all queued-up events for a given account. Useful for tests."""
"""Remove all queued-up events for a given account.
Useful for tests.
"""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
@@ -43,7 +49,9 @@ class Account:
def clone(self) -> "Account":
"""Clone given account.
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
"""
future = self._rpc.provide_backup.future(self.id)
qr = self._rpc.get_backup_qr(self.id)
new_account = self.manager.add_account()
@@ -80,7 +88,7 @@ class Account:
return self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None:
"""update config values."""
"""Update config values."""
for key, value in kwargs.items():
self.set_config(key, value)
@@ -99,10 +107,12 @@ class Account:
"""Parse QR code contents.
This function takes the raw text scanned
and checks what can be done with it."""
and checks what can be done with it.
"""
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
"""Set configuration values from a QR code."""
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
@@ -117,7 +127,7 @@ class Account:
@futuremethod
def list_transports(self):
"""Returns the list of all email accounts that are used as a transport in the current profile."""
"""Return the list of all email accounts that are used as a transport in the current profile."""
transports = yield self._rpc.list_transports.future(self.id)
return transports
@@ -158,7 +168,8 @@ class Account:
def import_vcard(self, vcard: str) -> list[Contact]:
"""Import vCard.
Return created or modified contacts in the order they appear in vCard."""
Return created or modified contacts in the order they appear in vCard.
"""
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
return [Contact(self, contact_id) for contact_id in contact_ids]
@@ -175,7 +186,21 @@ class Account:
return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
@@ -201,8 +226,8 @@ class Account:
def get_contacts(
self,
query: Optional[str] = None,
*,
with_self: bool = False,
verified_only: bool = False,
snapshot: bool = False,
) -> Union[list[Contact], list[AttrDict]]:
"""Get a filtered list of contacts.
@@ -210,12 +235,9 @@ class Account:
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param with_self: if True the self-contact is also included if it matches the query.
:param only_verified: if True only return verified contacts.
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
"""
flags = 0
if verified_only:
flags |= ContactFlag.VERIFIED_ONLY
if with_self:
flags |= ContactFlag.ADD_SELF
@@ -227,12 +249,12 @@ class Account:
@property
def self_contact(self) -> Contact:
"""This account's identity as a Contact."""
"""Account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
@property
def device_contact(self) -> Chat:
"""This account's device contact."""
"""Account's device contact."""
return Contact(self, SpecialContactId.DEVICE)
def get_chatlist(
@@ -281,17 +303,52 @@ class Account:
def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state.
After creation,
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
and is in _unpromoted_ state.
This means, you can add or remove members, change the name,
the group image and so on without messages being sent to all group members.
This changes as soon as the first message is sent to the group members
and the group becomes _promoted_.
After that, all changes are synced with all group members
by sending status message.
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
(see `get_full_snapshot()` / `get_basic_snapshot()`).
This may be useful if you want to show some help for just created groups.
:param protect: If set to 1 the function creates group with protection initially enabled.
Only verified members are allowed in these groups
and end-to-end-encryption is always enabled.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
def create_broadcast(self, name: str) -> Chat:
"""Create a new **broadcast channel**
(called "Channel" in the UI).
Broadcast channels are similar to groups on the sending device,
however, recipients get the messages in a read-only chat
and will not see who the other members are.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
After creation, the chat contains no recipients and is in _unpromoted_ state;
see `create_group()` for more information on the unpromoted state.
Returns the created chat.
"""
return Chat(self, self._rpc.create_broadcast(self.id, name))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
The function returns immediately and the handshake runs in background, sending
and receiving several messages.
@@ -361,22 +418,26 @@ class Account:
def wait_for_incoming_msg(self):
"""Wait for incoming message and return it.
Consumes all events before the next incoming message event."""
Consumes all events before the next incoming message event.
"""
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
def wait_for_securejoin_inviter_success(self):
"""Wait until SecureJoin process finishes successfully on the inviter side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
"""Wait until SecureJoin process finishes successfully on the joiner side."""
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def wait_for_reactions_changed(self):
"""Wait for reaction change event."""
return self.wait_for_event(EventType.REACTIONS_CHANGED)
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
@@ -410,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

@@ -1,3 +1,5 @@
"""Chat module."""
from __future__ import annotations
import calendar
@@ -89,7 +91,8 @@ class Chat:
def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds.
0 means the timer is disabled, use 1 for immediate deletion."""
0 means the timer is disabled, use 1 for immediate deletion.
"""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
@@ -165,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]
@@ -199,12 +207,12 @@ class Chat:
return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
"""get the list of messages in this chat."""
"""Get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
"""Get number of fresh messages in this chat."""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None:
@@ -286,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

@@ -48,6 +48,7 @@ class Client:
self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
"""Register multiple hooks."""
for hook, event in hooks:
self.add_hook(hook, event)
@@ -77,9 +78,11 @@ class Client:
self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool:
"""Return True if the client is configured."""
return self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the client."""
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
for key, value in kwargs.items():
@@ -198,5 +201,6 @@ class Bot(Client):
"""Simple bot implementation that listens to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
"""Configure the bot."""
kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs)

View File

@@ -1,14 +1,20 @@
"""Constants module."""
from enum import Enum, IntEnum
COMMAND_PREFIX = "/"
class ContactFlag(IntEnum):
VERIFIED_ONLY = 0x01
"""Bit flags for get_contacts() method."""
ADD_SELF = 0x02
ADDRESS = 0x04
class ChatlistFlag(IntEnum):
"""Bit flags for get_chatlist() method."""
ARCHIVED_ONLY = 0x01
NO_SPECIALS = 0x02
ADD_ALLDONE_HINT = 0x04
@@ -16,6 +22,8 @@ class ChatlistFlag(IntEnum):
class SpecialContactId(IntEnum):
"""Special contact IDs."""
SELF = 1
INFO = 2 # centered messages as "member added", used in all chats
DEVICE = 5 # messages "update info" in the device-chat
@@ -23,7 +31,7 @@ class SpecialContactId(IntEnum):
class EventType(str, Enum):
"""Core event types"""
"""Core event types."""
INFO = "Info"
SMTP_CONNECTED = "SmtpConnected"
@@ -65,13 +73,17 @@ 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"
class ChatId(IntEnum):
"""Special chat ids"""
"""Special chat IDs."""
TRASH = 3
ARCHIVED_LINK = 6
@@ -80,17 +92,47 @@ class ChatId(IntEnum):
class ChatType(IntEnum):
"""Chat types"""
"""Chat type."""
UNDEFINED = 0
SINGLE = 100
"""1:1 chat, i.e. a direct chat with a single contact"""
GROUP = 120
MAILINGLIST = 140
BROADCAST = 160
OUT_BROADCAST = 160
"""Outgoing broadcast channel, called "Channel" in the UI.
The user can send into this channel,
and all recipients will receive messages
in an `IN_BROADCAST`.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
"""
IN_BROADCAST = 165
"""Incoming broadcast channel, called "Channel" in the UI.
This channel is read-only,
and we do not know who the other recipients are.
This is similar to a `MAILINGLIST`,
with the main difference being that
`IN_BROADCAST`s are encrypted.
Called `broadcast` here rather than `channel`,
because the word "channel" already appears a lot in the code,
which would make it hard to grep for it.
"""
class ChatVisibility(str, Enum):
"""Chat visibility types"""
"""Chat visibility types."""
NORMAL = "Normal"
ARCHIVED = "Archived"
@@ -98,7 +140,7 @@ class ChatVisibility(str, Enum):
class DownloadState(str, Enum):
"""Message download state"""
"""Message download state."""
DONE = "Done"
AVAILABLE = "Available"
@@ -118,7 +160,6 @@ class ViewType(str, Enum):
VOICE = "Voice"
VIDEO = "Video"
FILE = "File"
VIDEOCHAT_INVITATION = "VideochatInvitation"
WEBXDC = "Webxdc"
VCARD = "Vcard"
@@ -159,14 +200,14 @@ class MessageState(IntEnum):
class MessageId(IntEnum):
"""Special message ids"""
"""Special message IDs."""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
"""Certificate checks mode."""
AUTOMATIC = 0
STRICT = 1
@@ -174,7 +215,7 @@ class CertificateChecks(IntEnum):
class Connectivity(IntEnum):
"""Connectivity states"""
"""Connectivity states."""
NOT_CONNECTED = 1000
CONNECTING = 2000
@@ -183,7 +224,7 @@ class Connectivity(IntEnum):
class KeyGenType(IntEnum):
"""Type of the key to generate"""
"""Type of the key to generate."""
DEFAULT = 0
RSA2048 = 1
@@ -193,21 +234,21 @@ class KeyGenType(IntEnum):
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
"""Authorization flags."""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
"""Media quality setting."""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
"""Provider status according to manual testing."""
OK = 1
PREPARATION = 2
@@ -215,7 +256,7 @@ class ProviderStatus(IntEnum):
class PushNotifyState(IntEnum):
"""Push notifications state"""
"""Push notifications state."""
NOT_CONNECTED = 0
HEARTBEAT = 1
@@ -223,7 +264,7 @@ class PushNotifyState(IntEnum):
class ShowEmails(IntEnum):
"""Show emails mode"""
"""Show emails mode."""
OFF = 0
ACCEPTED_CONTACTS = 1
@@ -231,17 +272,9 @@ class ShowEmails(IntEnum):
class SocketSecurity(IntEnum):
"""Socket security"""
"""Socket security."""
AUTOMATIC = 0
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

View File

@@ -1,3 +1,5 @@
"""Contact module."""
from dataclasses import dataclass
from typing import TYPE_CHECKING
@@ -11,8 +13,7 @@ if TYPE_CHECKING:
@dataclass
class Contact:
"""
Contact API.
"""Contact API.
Essentially a wrapper for RPC, account ID and a contact ID.
"""
@@ -36,17 +37,14 @@ class Contact:
"""Delete contact."""
self._rpc.delete_contact(self.account.id, self.id)
def reset_encryption(self) -> None:
"""Reset contact encryption."""
self._rpc.reset_contact_encryption(self.account.id, self.id)
def set_name(self, name: str) -> None:
"""Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact.
"""Get a multi-line encryption info.
Encryption info contains your fingerprint and the fingerprint of the contact.
"""
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
@@ -66,4 +64,5 @@ class Contact:
)
def make_vcard(self) -> str:
"""Make a vCard for the contact."""
return self.account.make_vcard([self])

View File

@@ -1,3 +1,5 @@
"""Account manager module."""
from __future__ import annotations
from typing import TYPE_CHECKING
@@ -10,12 +12,13 @@ if TYPE_CHECKING:
class DeltaChat:
"""
Delta Chat accounts manager.
"""Delta Chat accounts manager.
This is the root of the object oriented API.
"""
def __init__(self, rpc: "Rpc") -> None:
"""Initialize account manager."""
self.rpc = rpc
def add_account(self) -> Account:
@@ -37,9 +40,7 @@ class DeltaChat:
self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network
conditions might have changed.
"""
"""Indicate that the network conditions might have changed."""
self.rpc.maybe_network()
def get_system_info(self) -> AttrDict:

View File

@@ -36,7 +36,7 @@ class EventFilter(ABC):
@abstractmethod
def __hash__(self) -> int:
"""Object's unique hash"""
"""Object's unique hash."""
@abstractmethod
def __eq__(self, other) -> bool:
@@ -52,9 +52,7 @@ class EventFilter(ABC):
@abstractmethod
def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
"""Return True-like value if the event passed the filter."""
class RawEvent(EventFilter):
@@ -82,31 +80,17 @@ class RawEvent(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Filter an event.
Return true if the event should be processed.
"""
if self.types and event.kind not in self.types:
return False
return self._call_func(event)
class NewMessage(EventFilter):
"""Matches whenever a new message arrives.
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param pattern: if set, this Pattern will be used to filter the message by its text
content.
:param command: If set, only match messages with the given command (ex. /help).
Setting this property implies `is_info==False`.
:param is_bot: If set to True only match messages sent by bots, if set to None
match messages from bots and users. If omitted or set to False
only messages from users will be matched.
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
"""Matches whenever a new message arrives."""
def __init__(
self,
@@ -121,6 +105,25 @@ class NewMessage(EventFilter):
is_info: Optional[bool] = None,
func: Optional[Callable[["AttrDict"], bool]] = None,
) -> None:
"""Initialize a new message filter.
Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots.
:param pattern: if set, this Pattern will be used to filter the message by its text
content.
:param command: If set, only match messages with the given command (ex. /help).
Setting this property implies `is_info==False`.
:param is_bot: If set to True only match messages sent by bots, if set to None
match messages from bots and users. If omitted or set to False
only messages from users will be matched.
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
super().__init__(func=func)
self.is_bot = is_bot
self.is_info = is_info
@@ -159,6 +162,7 @@ class NewMessage(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return true if if the event is a new message event."""
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -199,6 +203,7 @@ class MemberListChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return true if if the event is a member addition event."""
if self.added is not None and self.added != event.member_added:
return False
return self._call_func(event)
@@ -231,6 +236,7 @@ class GroupImageChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return True if event is matched."""
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return self._call_func(event)
@@ -256,13 +262,12 @@ class GroupNameChanged(EventFilter):
return False
def filter(self, event: "AttrDict") -> bool:
"""Return True if event is matched."""
return self._call_func(event)
class HookCollection:
"""
Helper class to collect event hooks that can later be added to a Delta Chat client.
"""
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
def __init__(self) -> None:
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()

View File

@@ -1,6 +1,8 @@
"""Message module."""
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, List, Optional, Union
from ._utils import AttrDict, futuremethod
from .const import EventType
@@ -37,6 +39,11 @@ class Message:
snapshot["message"] = self
return snapshot
def get_read_receipts(self) -> List[AttrDict]:
"""Get message read receipts."""
read_receipts = self._rpc.get_message_read_receipts(self.account.id, self.id)
return [AttrDict(read_receipt) for read_receipt in read_receipts]
def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
@@ -45,6 +52,7 @@ class Message:
return None
def get_sender_contact(self) -> Contact:
"""Return sender contact."""
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
@@ -53,6 +61,11 @@ class Message:
self._rpc.markseen_msgs(self.account.id, [self.id])
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
"""Continue the Autocrypt Setup Message key transfer.
This function can be called on received Autocrypt Setup Message
to import the key encrypted with the provided setup code.
"""
self._rpc.continue_autocrypt_key_transfer(self.account.id, self.id, setup_code)
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
@@ -62,6 +75,7 @@ class Message:
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
"""Return a list of Webxdc status updates for Webxdc instance message."""
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_info(self) -> str:
@@ -69,6 +83,7 @@ class Message:
return self._rpc.get_message_info(self.account.id, self.id)
def get_webxdc_info(self) -> dict:
"""Get info from a Webxdc message in JSON format."""
return self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
@@ -80,8 +95,22 @@ class Message:
@futuremethod
def send_webxdc_realtime_advertisement(self):
"""Send an advertisement to join the realtime channel."""
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
@futuremethod
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

@@ -1,3 +1,5 @@
"""Pytest plugin module."""
from __future__ import annotations
import os
@@ -11,26 +13,36 @@ from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Messag
from ._utils import futuremethod
from .rpc import Rpc
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
class ACFactory:
"""Test account factory."""
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
account = self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
"""Create a new unconfigured account."""
return self.deltachat.add_account()
def get_unconfigured_bot(self) -> Bot:
"""Create a new unconfigured bot."""
return Bot(self.get_unconfigured_account())
def get_credentials(self) -> (str, str):
"""Generate new credentials for chatmail account."""
domain = os.getenv("CHATMAIL_DOMAIN")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
addr, password = self.get_credentials()
account = self.get_unconfigured_account()
params = {"addr": addr, "password": password}
@@ -40,6 +52,7 @@ class ACFactory:
return account
def new_configured_bot(self) -> Bot:
"""Create a new configured bot."""
addr, password = self.get_credentials()
bot = self.get_unconfigured_bot()
bot.configure(addr, password)
@@ -47,11 +60,13 @@ class ACFactory:
@futuremethod
def get_online_account(self):
"""Create a new account and start I/O."""
account = yield self.new_configured_account.future()
account.bring_online()
return account
def get_online_accounts(self, num: int) -> list[Account]:
"""Create multiple online accounts."""
futures = [self.get_online_account.future() for _ in range(num)]
return [f() for f in futures]
@@ -66,6 +81,10 @@ class ACFactory:
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
"""Create a new 1:1 chat between ac1 and ac2 accepted on both sides.
Returned chat is a chat with ac2 from ac1 point of view.
"""
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
@@ -77,6 +96,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> Message:
"""Send a message."""
if not from_account:
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account)
@@ -95,6 +115,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
"""Send a message and wait until recipient processes it."""
self.send_message(
to_account=to_client.account,
from_account=from_account,
@@ -108,6 +129,7 @@ class ACFactory:
@pytest.fixture
def rpc(tmp_path) -> AsyncGenerator:
"""RPC client fixture."""
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@@ -115,6 +137,7 @@ def rpc(tmp_path) -> AsyncGenerator:
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
"""Return account factory fixture."""
return ACFactory(DeltaChat(rpc))
@@ -132,7 +155,7 @@ def data():
raise Exception("Data path cannot be found")
def get_path(self, bn):
"""return path of file or None if it doesn't exist."""
"""Return path of file or None if it doesn't exist."""
fn = os.path.join(self.path, *bn.split("/"))
assert os.path.exists(fn)
return fn

View File

@@ -1,3 +1,5 @@
"""JSON-RPC client module."""
from __future__ import annotations
import itertools
@@ -12,16 +14,19 @@ from typing import Any, Iterator, Optional
class JsonRpcError(Exception):
pass
"""JSON-RPC error."""
class RpcFuture:
"""RPC future waiting for RPC call result."""
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
"""Wait for the future to return the result."""
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
@@ -32,17 +37,19 @@ class RpcFuture:
class RpcMethod:
"""RPC method."""
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Synchronously calls JSON-RPC method."""
"""Call JSON-RPC method synchronously."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Asynchronously calls JSON-RPC method."""
"""Call JSON-RPC method asynchronously."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
@@ -58,8 +65,13 @@ class RpcMethod:
class Rpc:
"""RPC client."""
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
"""Initialize RPC client.
The given arguments will be passed to subprocess.Popen().
"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -81,6 +93,7 @@ class Rpc:
self.events_thread: Thread
def start(self) -> None:
"""Start RPC server subprocess."""
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
@@ -130,6 +143,7 @@ class Rpc:
self.close()
def reader_loop(self) -> None:
"""Process JSON-RPC responses from the RPC server process output."""
try:
while line := self.process.stdout.readline():
response = json.loads(line)
@@ -157,12 +171,13 @@ class Rpc:
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
"""Get event queue corresponding to the given account ID."""
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
"""Request new events and distributes them between queues."""
try:
while True:
if self.closing:
@@ -178,12 +193,12 @@ class Rpc:
logging.exception("Exception in the event loop")
def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
"""Wait for the next event from the given account and returns it."""
queue = self.get_queue(account_id)
return queue.get()
def clear_all_events(self, account_id: int):
"""Removes all queued-up events for a given account. Useful for tests."""
"""Remove all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:

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

@@ -36,6 +36,9 @@ def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
assert ac1.get_config("bcc_self") == "1"
# Second client receives only second message, but not the first.
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == "Messages are end-to-end encrypted."
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text

View File

@@ -1,5 +1,4 @@
import logging
import time
import pytest
@@ -16,14 +15,14 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -84,7 +83,7 @@ def test_qr_securejoin(acfactory, protect):
bob.wait_for_securejoin_joiner_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob = alice.create_contact(bob)
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
@@ -93,7 +92,7 @@ def test_qr_securejoin(acfactory, protect):
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice = bob.create_contact(alice)
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
@@ -101,7 +100,7 @@ def test_qr_securejoin(acfactory, protect):
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
alice2_contact_bob = alice2.create_contact(bob)
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
@@ -213,72 +212,8 @@ def test_setup_contact_resetup(acfactory) -> None:
bob.wait_for_securejoin_joiner_success()
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hi!"
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Hi!"
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert ac1_contact.get_snapshot().is_verified
# ac2 can write messages to the group.
snapshot.chat.send_text("Works again!")
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
"""Tests verified group recovery by reverifying then removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
@@ -291,7 +226,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2 = ac1.create_contact(ac2)
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
@@ -299,6 +234,8 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
ac3_contact_ac2_old = ac3.create_contact(ac2)
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
@@ -311,21 +248,10 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
ac3_contact_ac2 = ac3.create_contact(ac2)
ac3_chat.remove_contact(ac3_contact_ac2_old)
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
@@ -354,19 +280,16 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2 = ac1.create_contact(ac2)
ac1_contact_ac3 = ac1.create_contact(ac3)
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
assert ac1_contact_ac2_snapshot.verifier_id == ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
issue <https://github.com/chatmail/core/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
@@ -400,12 +323,12 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
assert ac3.create_contact(ac2).get_snapshot().is_verified
assert ac2.create_contact(ac3).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
vg.add_contact(ac3.create_contact(ac2))
# ensure ac2 receives message in VG
vg.send_text("hello")
@@ -443,7 +366,7 @@ def test_qr_new_group_unblocked(acfactory):
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
ac1_new_chat.add_contact(ac1.create_contact(ac2))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
@@ -577,30 +500,8 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
# Loop sending message from ac1 to ac2
# until ac2 accepts new ac1 key.
#
# This may not happen immediately because resetup of ac1
# rewinds "smeared timestamp" so Date: header for messages
# sent by new ac1 are in the past compared to the last Date:
# header sent by old ac1.
while True:
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2, "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
logging.info("ac2 received Hello!")
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
logging.info("ac2 addr={}, ac1 addr={}".format(ac2.get_config("addr"), ac1.get_config("addr")))
if not ac2_contact_ac1.get_snapshot().is_verified:
break
time.sleep(1)
ac2_contact_ac1 = ac2.create_contact(ac1, "")
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()

View File

@@ -11,7 +11,8 @@ from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.const import ChatType, DownloadState, MessageState
from deltachat_rpc_client.pytestplugin import E2EE_INFO_MSGS
from deltachat_rpc_client.rpc import JsonRpcError
@@ -170,7 +171,10 @@ def test_account(acfactory) -> None:
assert alice.get_size()
assert alice.is_configured()
assert not alice.get_avatar()
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
# get_contact_by_addr() can lookup a key contact by address:
bob_contact = alice.get_contact_by_addr(bob_addr).get_snapshot()
assert bob_contact.display_name == "Bob"
assert bob_contact.is_key_contact
assert alice.get_contacts()
assert alice.get_contacts(snapshot=True)
assert alice.self_contact
@@ -248,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()
@@ -259,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")
@@ -287,7 +293,6 @@ def test_contact(acfactory) -> None:
assert repr(alice_contact_bob)
alice_contact_bob.block()
alice_contact_bob.unblock()
alice_contact_bob.reset_encryption()
alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot()
@@ -326,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()
@@ -458,8 +509,12 @@ def test_wait_next_messages(acfactory) -> None:
alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
if len(next_messages) == E2EE_INFO_MSGS:
next_messages += bot.wait_next_messages()
assert len(next_messages) == 1 + E2EE_INFO_MSGS
snapshot = next_messages[0 + E2EE_INFO_MSGS].get_snapshot()
assert snapshot.text == "Hello!"
@@ -727,6 +782,26 @@ def test_markseen_contact_request(acfactory):
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_read_receipt(acfactory):
"""
Test sending a read receipt and ensure it is attributed to the correct contact.
"""
alice, bob = acfactory.get_online_accounts(2)
alice_chat_bob = alice.create_chat(bob)
alice_contact_bob = alice.create_contact(bob)
bob.create_chat(alice) # Accept the chat
alice_chat_bob.send_text("Hello Bob!")
msg = bob.wait_for_incoming_msg()
msg.mark_seen()
read_msg = alice.get_message_by_id(alice.wait_for_event(EventType.MSG_READ).msg_id)
read_receipts = read_msg.get_read_receipts()
assert len(read_receipts) == 1
assert read_receipts[0].contact_id == alice_contact_bob.id
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
@@ -847,3 +922,36 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
assert msg.text == "hello"
assert "DeltaChat" in ac1_direct_imap.list_folders()
def test_broadcast(acfactory):
alice, bob = acfactory.get_online_accounts(2)
alice_chat = alice.create_broadcast("My great channel")
snapshot = alice_chat.get_basic_snapshot()
assert snapshot.name == "My great channel"
assert snapshot.is_unpromoted
assert snapshot.is_encrypted
assert snapshot.chat_type == ChatType.OUT_BROADCAST
alice_contact_bob = alice.create_contact(bob, "Bob")
alice_chat.add_contact(alice_contact_bob)
alice_msg = alice_chat.send_message(text="hello").get_snapshot()
assert alice_msg.text == "hello"
assert alice_msg.show_padlock
bob_msg = bob.wait_for_incoming_msg().get_snapshot()
assert bob_msg.text == "hello"
assert bob_msg.show_padlock
assert bob_msg.error is None
bob_chat = bob.get_chat_by_id(bob_msg.chat_id)
bob_chat_snapshot = bob_chat.get_basic_snapshot()
assert bob_chat_snapshot.name == "My great channel"
assert not bob_chat_snapshot.is_unpromoted
assert bob_chat_snapshot.is_encrypted
assert bob_chat_snapshot.chat_type == ChatType.IN_BROADCAST
assert bob_chat_snapshot.is_contact_request
assert not bob_chat.can_send()

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.159.3"
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": "1.159.3"
"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.
@@ -73,7 +73,7 @@ async fn main_impl() -> Result<()> {
.init();
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
log::info!("Starting with accounts directory `{}`.", path);
log::info!("Starting with accounts directory `{path}`.");
let writable = true;
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
@@ -97,7 +97,7 @@ async fn main_impl() -> Result<()> {
Some(message) => serde_json::to_string(&message)?,
}
};
log::trace!("RPC send {}", message);
log::trace!("RPC send {message}");
println!("{message}");
}
Ok(())
@@ -141,7 +141,7 @@ async fn main_impl() -> Result<()> {
Some(message) => message,
}
};
log::trace!("RPC recv {}", message);
log::trace!("RPC recv {message}");
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;

View File

@@ -10,9 +10,6 @@ ignore = [
# Unmaintained instant
"RUSTSEC-2024-0384",
# Unmaintained backoff
"RUSTSEC-2025-0012",
# Unmaintained paste
"RUSTSEC-2024-0436",
]
@@ -25,31 +22,31 @@ ignore = [
skip = [
{ name = "async-channel", version = "1.9.0" },
{ name = "bitflags", version = "1.3.2" },
{ name = "core-foundation", version = "0.9.4" },
{ name = "derive_more-impl", version = "1.0.0" },
{ name = "derive_more", version = "1.0.0" },
{ name = "event-listener", version = "2.5.3" },
{ name = "generator", version = "0.7.5" },
{ name = "getrandom", version = "0.2.12" },
{ name = "hashbrown", version = "0.14.5" },
{ name = "heck", version = "0.4.1" },
{ name = "http", version = "0.2.12" },
{ name = "linux-raw-sys", version = "0.4.14" },
{ name = "loom", version = "0.5.6" },
{ name = "lru", version = "0.12.3" },
{ name = "netlink-packet-route", version = "0.17.1" },
{ name = "nix", version = "0.26.4" },
{ name = "nix", version = "0.27.1" },
{ name = "nom", version = "7.1.3" },
{ name = "rand_chacha", version = "0.3.1" },
{ name = "rand_core", version = "0.6.4" },
{ name = "rand", version = "0.8.5" },
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rtnetlink", version = "0.13.1" },
{ name = "redox_syscall", version = "0.4.1" },
{ name = "rustix", version = "0.38.44" },
{ name = "security-framework", version = "2.11.1" },
{ name = "serdect", version = "0.2.0" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
{ name = "syn", version = "1.0.109" },
{ name = "thiserror-impl", version = "1.0.69" },
{ name = "thiserror", version = "1.0.69" },
{ name = "toml_datetime", version = "0.6.11" },
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
{ name = "windows" },
{ name = "windows_aarch64_gnullvm" },
@@ -84,6 +81,7 @@ allow = [
"MPL-2.0",
"Unicode-3.0",
"Unicode-DFS-2016",
"Unlicense",
"Zlib",
]

24
flake.lock generated
View File

@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1737527504,
"narHash": "sha256-Z8S5gLPdIYeKwBXDaSxlJ72ZmiilYhu3418h3RSQZA0=",
"lastModified": 1747291057,
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"owner": "nix-community",
"repo": "fenix",
"rev": "aa13f23e3e91b95377a693ac655bbc6545ebec0d",
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1737469691,
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -175,11 +175,11 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1731139594,
"narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=",
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1737453499,
"narHash": "sha256-fa5AJI9mjFU2oVXqdCq2oA2pripAXbHzkUkewJRQpxA=",
"lastModified": 1746889290,
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "0b68402d781955d526b80e5d479e9e47addb4075",
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"type": "github"
},
"original": {

View File

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

View File

@@ -6,7 +6,7 @@ edition = "2021"
license = "MPL-2.0"
[dev-dependencies]
bolero = "0.8"
bolero = "0.13.4"
[dependencies]
mailparse = { workspace = true }

View File

@@ -1,49 +0,0 @@
# content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline
class GroupTrackingPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
print("process_incoming message", message)
if message.text.strip() == "/quit":
message.account.shutdown()
else:
# unconditionally accept the chat
message.create_chat()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text(f"echoing from {addr}:\n{text}")
@account_hookimpl
def ac_outgoing_message(self, message):
print("ac_outgoing_message:", message)
@account_hookimpl
def ac_configure_completed(self, success):
print("ac_configure_completed:", success)
@account_hookimpl
def ac_chat_modified(self, chat):
print("ac_chat_modified:", chat.id, chat.get_name())
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
def main(argv=None):
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
if __name__ == "__main__":
main()

View File

@@ -1,10 +1,7 @@
import echo_and_quit
import group_tracking
import py
import pytest
from deltachat.events import FFIEventLogger
@pytest.fixture(scope="session")
def datadir():
@@ -36,55 +33,3 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("send quit sequence")
bot_chat.send_text("/quit")
botproc.wait()
def test_group_tracking_plugin(acfactory, lp):
lp.sec("creating one group-tracking bot and two temp accounts")
botproc = acfactory.run_bot_process(group_tracking)
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
lp.sec("creating bot test group with bot")
bot_chat = ac1.qr_setup_contact(botproc.qr)
ac1._evtracker.wait_securejoin_joiner_progress(1000)
bot_contact = bot_chat.get_contacts()[0]
ch = ac1.create_group_chat("bot test group")
ch.add_contact(bot_contact)
ch.send_text("hello")
botproc.fnmatch_lines(
"""
*ac_chat_modified*bot test group*
""",
)
lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2)
ch.add_contact(contact3)
reply = ac1._evtracker.wait_next_incoming_message()
assert "hello" in reply.text
lp.sec("now looking at what the bot received")
botproc.fnmatch_lines(
"""
*ac_member_added {}*from*{}*
""".format(
contact3.addr,
ac1.get_config("addr"),
),
)
lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3)
botproc.fnmatch_lines(
"""
*ac_member_removed {}*from*{}*
""".format(
contact3.addr,
ac1.get_config("addr"),
),
)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "1.159.3"
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"
@@ -47,6 +47,10 @@ line-length = 120
[tool.ruff]
lint.select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
lint.ignore = [
"PLC0415", # `import` should be at the top-level of a file
"PLW1641" # Object does not implement `__hash__` method
]
line-length = 120
[tool.isort]

View File

@@ -293,6 +293,8 @@ class Account:
return Contact(self, contact_id)
def get_contact(self, obj) -> Optional[Contact]:
if isinstance(obj, Account):
return self.create_contact(obj)
if isinstance(obj, Contact):
return obj
(_, addr) = self.get_contact_addr_and_name(obj)
@@ -328,7 +330,21 @@ class Account:
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
"""get a contact for the email address or None if it's blocked or doesn't exist."""
"""Looks up a known and unblocked contact with a given e-mail address.
To get a list of all known and unblocked contacts, use contacts_get_contacts().
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
(e.g. an address-contact and a key-contact),
this looks up the most recently seen contact,
i.e. which contact is returned depends on which contact last sent a message.
If the user just clicked on a mailto: link, then this is the best thing you can do.
But **DO NOT** internally represent contacts by their email address
and do not use this function to look them up;
otherwise this function will sometimes look up the wrong contact.
Instead, you should internally represent contacts by their ids.
To validate an e-mail address independently of the contact database
use check_email_validity()."""
_, addr = parseaddr(email)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
@@ -355,20 +371,16 @@ class Account:
self,
query: Optional[str] = None,
with_self: bool = False,
only_verified: bool = False,
) -> List[Contact]:
"""get a (filtered) list of contacts.
:param query: if a string is specified, only return contacts
whose name or e-mail matches query.
:param only_verified: if true only return verified contacts.
:param with_self: if true the self-contact is also returned.
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
flags = 0
query_c = as_dc_charpointer(query)
if only_verified:
flags |= const.DC_GCL_VERIFIED_ONLY
if with_self:
flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)

View File

@@ -417,7 +417,13 @@ class Chat:
:raises ValueError: if contact could not be added
:returns: None
"""
contact = self.account.create_contact(obj)
from .contact import Contact
if isinstance(obj, Contact):
contact = obj
else:
contact = self.account.create_contact(obj)
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError(f"could not add contact {contact!r} to chat")

View File

@@ -13,7 +13,6 @@ from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
@@ -304,21 +303,15 @@ class EventThread(threading.Thread):
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
yield ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
yield map_system_message(msg) or (
"ac_incoming_message",
{"message": msg},
)
yield "ac_incoming_message", {"message": msg}
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)

View File

@@ -2,14 +2,12 @@
import json
import os
import re
from datetime import datetime, timezone
from typing import Optional, Union
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
from .reactions import Reactions
class Message:
@@ -165,17 +163,6 @@ class Message:
),
)
def send_reaction(self, reaction: str):
"""Send a reaction to message and return the resulting Message instance."""
msg_id = lib.dc_send_reaction(self.account._dc_context, self.id, as_dc_charpointer(reaction))
if msg_id == 0:
raise ValueError("reaction could not be send")
return Message.from_db(self.account, msg_id)
def get_reactions(self) -> Reactions:
"""Get :class:`deltachat.reactions.Reactions` to the message."""
return Reactions.from_msg(self)
def is_system_message(self):
"""return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg))
@@ -448,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
@@ -492,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,
}
@@ -504,56 +486,3 @@ def get_viewtype_code_from_name(view_type_name):
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
)
#
# some helper code for turning system messages into hook events
#
def map_system_message(msg):
if msg.is_system_message():
res = parse_system_add_remove(msg.text)
if not res:
return None
action, affected, actor = res
affected = msg.account.get_contact_by_addr(affected)
actor = None if actor == "me" else msg.account.get_contact_by_addr(actor)
d = {"chat": msg.chat, "contact": affected, "actor": actor, "message": msg}
return "ac_member_" + res[0], d
def extract_addr(text):
m = re.match(r".*\((.+@.+)\)", text)
if m:
text = m.group(1)
text = text.rstrip(".")
return text.strip()
def parse_system_add_remove(text):
"""return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple
"""
# You removed member a@b.
# You added member a@b.
# Member Me (x@y) removed by a@b.
# Member x@y added by a@b
# Member With space (tmp1@x.org) removed by tmp2@x.org.
# Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
# Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org.
text = text.lower()
m = re.match(r"member (.+) (removed|added) by (.+)", text)
if m:
affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor)
m = re.match(r"you (removed|added) member (.+)", text)
if m:
action, affected = m.groups()
return action, extract_addr(affected), "me"
if text.startswith("group left by "):
addr = extract_addr(text[13:])
if addr:
return "removed", addr, addr

View File

@@ -1,43 +0,0 @@
"""The Reactions object."""
from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions:
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions) -> None:
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self) -> str:
return f"<Reactions dc_reactions={self._dc_reactions}>"
@classmethod
def from_msg(cls, msg):
assert msg.id > 0
return cls(
msg.account,
ffi.gc(lib.dc_get_msg_reactions(msg.account._dc_context, msg.id), lib.dc_reactions_unref),
)
def get_contacts(self) -> list:
"""Get list of contacts reacted to the message.
:returns: list of :class:`deltachat.contact.Contact` objects for this reaction.
"""
from .contact import Contact
dc_array = ffi.gc(lib.dc_reactions_get_contacts(self._dc_reactions), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self.account, x)))
def get_by_contact(self, contact) -> str:
"""Get a string containing space-separated reactions of a single :class:`deltachat.contact.Contact`."""
return from_dc_charpointer(lib.dc_reactions_get_by_contact_id(self._dc_reactions, contact.id))

View File

@@ -20,6 +20,12 @@ import deltachat
from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker
E2EE_INFO_MSGS = 1
"""
The number of info messages added to new e2ee chats.
Currently this is "End-to-end encryption available".
"""
def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options")
@@ -606,7 +612,7 @@ class ACFactory:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg is not None
assert msg.text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert msg.text == "Messages are end-to-end encrypted."
msg = ac2._evtracker.wait_next_incoming_message()
assert msg is not None
assert "Member Me " in msg.text and " added by " in msg.text

View File

@@ -133,8 +133,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert "added" in msg.text.lower()
assert any(
m.is_system_message() and m.text == "Messages are guaranteed to be end-to-end encrypted from now on."
for m in msg.chat.get_messages()
m.is_system_message() and m.text == "Messages are end-to-end encrypted." for m in msg.chat.get_messages()
)
lp.sec("ac1: send message")
msg_out = chat1.send_text("hello")
@@ -187,83 +186,6 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
assert msg.is_encrypted()
def test_undecipherable_group(acfactory, lp):
"""Test how group messages that cannot be decrypted are
handled.
Group name is encrypted and plaintext subject is set to "..." in
this case, so we should assign the messages to existing chat
instead of creating a new one. Since there is no existing group
chat, the messages should be assigned to 1-1 chat with the sender
of the message.
"""
lp.sec("creating and configuring three accounts")
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
acfactory.introduce_each_other([ac1, ac2, ac3])
lp.sec("ac3 reinstalls DC and generates a new key")
ac3.stop_io()
acfactory.remove_preconfigured_keys()
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
acfactory.wait_configured(ac4)
# Create contacts to make sure incoming messages are not treated as contact requests
chat41 = ac4.create_chat(ac1)
chat42 = ac4.create_chat(ac2)
ac4.start_io()
ac4._evtracker.wait_idle_inbox_ready()
lp.sec("ac1: creating group chat with 2 other members")
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
lp.sec("ac1: send message to new group chat")
msg = chat.send_text("hello")
lp.sec("ac2: checking that the chat arrived correctly")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert msg.is_encrypted(), "Message is not encrypted"
# ac4 cannot decrypt the message.
# Error message should be assigned to the chat with ac1.
lp.sec("ac4: checking that message is assigned to the sender chat")
error_msg = ac4._evtracker.wait_next_incoming_message()
assert error_msg.error # There is an error decrypting the message
assert error_msg.chat == chat41
lp.sec("ac2: sending a reply to the chat")
msg.chat.send_text("reply")
reply = ac1._evtracker.wait_next_incoming_message()
assert reply.text == "reply"
assert reply.is_encrypted(), "Reply is not encrypted"
lp.sec("ac4: checking that reply is assigned to ac2 chat")
error_reply = ac4._evtracker.wait_next_incoming_message()
assert error_reply.error # There is an error decrypting the message
assert error_reply.chat == chat42
# Test that ac4 replies to error messages don't appear in the
# group chat on ac1 and ac2.
lp.sec("ac4: replying to ac1 and ac2")
# Otherwise reply becomes a contact request.
chat41.send_text("I can't decrypt your message, ac1!")
chat42.send_text("I can't decrypt your message, ac2!")
msg = ac1._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac1!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac1.create_chat(ac3)
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.error is None
assert msg.text == "I can't decrypt your message, ac2!"
assert msg.is_encrypted(), "Message is not encrypted"
assert msg.chat == ac2.create_chat(ac4)
def test_ephemeral_timer(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -415,7 +337,7 @@ def test_use_new_verified_group_after_going_online(acfactory, data, tmp_path, lp
assert contact.addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
assert chat2.get_messages()[0].text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert chat2.get_messages()[0].text == "Messages are end-to-end encrypted."
assert open(contact.get_profile_image(), "rb").read() == open(avatar_path, "rb").read()
lp.sec("ac2_offl: sending message")
@@ -434,9 +356,10 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
- First device of the user downloads "member added" from the group.
- First device removes "member added" from the server.
- Some new messages are sent to the group.
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
- Second device comes online, receives these new messages.
The result is an unverified group with unverified members.
- First device re-gossips Autocrypt keys to the group.
- Now the seconds device has all members verified.
- Now the second device has all members and group verified.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.remove_preconfigured_keys()
@@ -474,12 +397,12 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ac2_offl.start_io()
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert not msg_in.is_system_message()
assert msg_in.text.startswith("[The message was sent with non-verified encryption")
assert msg_in.text == "hi"
ac2_offl_ac1_contact = msg_in.get_sender_contact()
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
assert not ac2_offl_ac1_contact.is_verified()
chat2_offl = msg_in.chat
assert chat2_offl.is_protected()
assert not chat2_offl.is_protected()
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
chat2.send_text("hi2")
@@ -488,7 +411,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2_offl.get_message_by_id(ev.data2)
assert msg_in.is_system_message()
assert msg_in.text == "Messages are guaranteed to be end-to-end encrypted from now on."
assert msg_in.text == "Messages are end-to-end encrypted."
# We need to consume one event that has data2=0
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
@@ -500,6 +423,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
assert msg_in.text == "hi2"
assert msg_in.chat == chat2_offl
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.chat.is_protected()
assert ac2_offl_ac1_contact.is_verified()

View File

@@ -10,6 +10,7 @@ from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat.tracker import ImexTracker
from deltachat.testplugin import E2EE_INFO_MSGS
def test_basic_imap_api(acfactory, tmp_path):
@@ -159,32 +160,6 @@ def test_html_message(acfactory, lp):
assert html_text in msg2.html
def test_videochat_invitation_message(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
text = "You are invited to a video chat, click https://meet.jit.si/WxEGad0gGzX to join."
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.is_videochat_invitation()
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
assert not msg2.is_videochat_invitation()
lp.sec("ac1: prepare and send videochat invitation to ac2")
msg1 = Message.new_empty(ac1, "videochat")
msg1.set_text(text)
msg1 = chat.send_msg(msg1)
assert msg1.is_videochat_invitation()
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == text
assert msg2.is_videochat_invitation()
def test_webxdc_message(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -408,6 +383,10 @@ def test_forward_messages(acfactory, lp):
msg_out = chat.send_text("message2")
lp.sec("ac2: wait for receive")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -427,7 +406,7 @@ def test_forward_messages(acfactory, lp):
lp.sec("ac2: check new chat has a forwarded message")
assert chat3.is_promoted()
messages = chat3.get_messages()
assert len(messages) == 2
assert len(messages) == 3
msg = messages[-1]
assert msg.is_forwarded()
ac2.delete_messages(messages)
@@ -622,6 +601,11 @@ def test_moved_markseen(acfactory):
with ac2.direct_imap.idle() as idle2:
ac2.start_io()
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
assert msg.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
msg = ac2.get_message_by_id(ev.data2)
@@ -738,7 +722,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("sending text message from ac1 to ac2")
msg_out = chat.send_text("message1")
assert len(chat.get_messages()) == 1
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
lp.sec("disable ac1 MDNs")
ac1.set_config("mdns_enabled", "0")
@@ -746,7 +730,7 @@ def test_mdn_asymmetric(acfactory, lp):
lp.sec("wait for ac2 to receive message")
msg = ac2._evtracker.wait_next_incoming_message()
assert len(msg.chat.get_messages()) == 1
assert len(msg.chat.get_messages()) == 1 + E2EE_INFO_MSGS
lp.sec("ac2: mark incoming message as seen")
ac2.mark_seen_messages([msg])
@@ -755,7 +739,7 @@ def test_mdn_asymmetric(acfactory, lp):
# MDN should be moved even though MDNs are already disabled
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
assert len(chat.get_messages()) == 1
assert len(chat.get_messages()) == 1 + E2EE_INFO_MSGS
# Wait for the message to be marked as seen on IMAP.
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
@@ -767,7 +751,7 @@ def test_mdn_asymmetric(acfactory, lp):
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
def test_send_receive_encrypt(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.get_device_chat().mark_noticed()
@@ -798,12 +782,11 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
msg3.mark_seen()
assert not list(ac1.get_fresh_messages())
lp.sec("create group chat with two members, one of which has no encrypt state")
lp.sec("create group chat with two members")
chat = ac1.create_group_chat("encryption test")
chat.add_contact(ac2)
chat.add_contact(ac1.create_contact("notexisting@testrun.org"))
msg = chat.send_text("test not encrypt")
assert not msg.is_encrypted()
assert msg.is_encrypted()
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
@@ -1124,6 +1107,11 @@ def test_send_and_receive_image(acfactory, lp, data):
assert m == msg_out
lp.sec("wait for ac2 to receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
msg_in = ac2.get_message_by_id(ev.data2)
assert msg_in.text == "Messages are end-to-end encrypted."
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED|DC_EVENT_INCOMING_MSG")
assert ev.data2 == msg_out.id
msg_in = ac2.get_message_by_id(msg_out.id)
@@ -1139,9 +1127,9 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
lp.sec("create some chat content")
some1_addr = some1.get_config("addr")
chat1 = ac1.create_contact(some1_addr, name="some1").create_chat()
chat1 = ac1.create_contact(some1).create_chat()
chat1.send_text("msg1")
assert len(ac1.get_contacts(query="some1")) == 1
assert len(ac1.get_contacts()) == 1
original_image_path = data.get_path("d.png")
chat1.send_image(original_image_path)
@@ -1153,16 +1141,16 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
chat1.send_file(str(path))
def assert_account_is_proper(ac):
contacts = ac.get_contacts(query="some1")
contacts = ac.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == some1_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 3
assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
assert len(messages) == 3 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert messages[1 + E2EE_INFO_MSGS].filemime == "image/png"
assert os.stat(messages[1 + E2EE_INFO_MSGS].filename).st_size == os.stat(original_image_path).st_size
ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname"
@@ -1288,79 +1276,6 @@ def test_set_get_contact_avatar(acfactory, data, lp):
assert msg6.get_sender_contact().get_profile_image() is None
def test_add_remove_member_remote_events(acfactory, lp):
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
ac1_addr = ac1.get_config("addr")
ac3_addr = ac3.get_config("addr")
# activate local plugin for ac2
in_list = queue.Queue()
class EventHolder:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
class InPlugin:
@account_hookimpl
def ac_incoming_message(self, message):
# we immediately accept the sender because
# otherwise we won't see member_added contacts
message.create_chat()
@account_hookimpl
def ac_chat_modified(self, chat):
in_list.put(EventHolder(action="chat-modified", chat=chat))
@account_hookimpl
def ac_member_added(self, chat, contact, message):
in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message))
@account_hookimpl
def ac_member_removed(self, chat, contact, message):
in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message))
ac2.add_account_plugin(InPlugin())
lp.sec("ac1: create group chat with ac2")
chat = ac1.create_group_chat("hello", contacts=[ac2])
lp.sec("ac1: send a message to group chat to promote the group")
chat.send_text("afterwards promoted")
ev = in_list.get()
assert ev.action == "chat-modified"
assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
lp.sec("ac1: add address2")
# note that if the above create_chat() would not
# happen we would not receive a proper member_added event
contact2 = chat.add_contact(ac3)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "added"
assert ev.message.get_sender_contact().addr == ac1_addr
assert ev.contact.addr == ac3_addr
lp.sec("ac1: remove address2")
chat.remove_contact(contact2)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "removed"
assert ev.contact.addr == contact2.addr
assert ev.message.get_sender_contact().addr == ac1_addr
lp.sec("ac1: remove ac2 contact from chat")
chat.remove_contact(ac2)
ev = in_list.get()
assert ev.action == "chat-modified"
ev = in_list.get()
assert ev.action == "removed"
assert ev.message.get_sender_contact().addr == ac1_addr
def test_system_group_msg_from_blocked_user(acfactory, lp):
"""
Tests that a blocked user removes you from a group.
@@ -1488,8 +1403,8 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 1
assert msgs[0].text == "Hi"
assert len(msgs) == 1 + E2EE_INFO_MSGS
assert msgs[0 + E2EE_INFO_MSGS].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched")
@@ -1499,8 +1414,8 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
assert msgs[1].text == "Hi 2"
assert len(msgs) == 2 + E2EE_INFO_MSGS
assert msgs[1 + E2EE_INFO_MSGS].text == "Hi 2"
def test_fetch_deleted_msg(acfactory, lp):
@@ -1617,7 +1532,7 @@ def test_immediate_autodelete(acfactory, lp):
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
lp.sec("ac2: check that message was autodeleted on server")
assert len(ac2.direct_imap.get_all_messages()) == 0
@@ -1653,7 +1568,7 @@ def test_delete_multiple_messages(acfactory, lp):
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
ac2._evtracker.get_info_contains("Close/expunge succeeded.")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0
@@ -1760,44 +1675,6 @@ def test_configure_error_msgs_invalid_server(acfactory):
assert "configuration" not in ev.data2.lower()
def test_name_changes(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("displayname", "Account 1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
contact = None
def update_name():
"""Send a message from ac1 to ac2 to update the name"""
nonlocal contact
chat12.send_text("Hello")
msg = ac2._evtracker.wait_next_incoming_message()
contact = msg.get_sender_contact()
return contact.name
assert update_name() == "Account 1"
ac1.set_config("displayname", "Account 1 revision 2")
assert update_name() == "Account 1 revision 2"
# Explicitly rename contact on ac2 to "Renamed"
ac2.create_contact(contact, name="Renamed")
assert contact.name == "Renamed"
ev = ac2._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
assert ev.data1 == contact.id
# ac1 also renames itself into "Renamed"
assert update_name() == "Renamed"
ac1.set_config("displayname", "Renamed")
assert update_name() == "Renamed"
# Contact name was set to "Renamed" explicitly before,
# so it should not be changed.
ac1.set_config("displayname", "Renamed again")
updated_name = update_name()
assert updated_name == "Renamed"
def test_status(acfactory):
"""Test that status is transferred over the network."""
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -1878,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,
@@ -1908,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")
@@ -1918,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,51 +1,12 @@
import os
import time
from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat.tracker import ImexFailed
from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
("msgtext", "res"),
[
(
"Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by me",
("removed", "tmp1@x.org", "me"),
),
(
"Group left by some one (tmp1@x.org).",
("removed", "tmp1@x.org", "tmp1@x.org"),
),
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
(
"Member tmp1@x.org added by tmp2@x.org.",
("added", "tmp1@x.org", "tmp2@x.org"),
),
("Member nothing bla bla", None),
("Another unknown system message", None),
],
)
def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove
out = parse_system_add_remove(msgtext)
assert out == res
from deltachat import Account, Message
from deltachat.testplugin import E2EE_INFO_MSGS
class TestOfflineAccountBasic:
@@ -177,15 +138,15 @@ class TestOfflineContact:
def test_get_contacts_and_delete(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
ac2 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac2)
contacts = ac1.get_contacts()
assert len(contacts) == 1
assert contact1 in contacts
assert not ac1.get_contacts(query="some2")
assert ac1.get_contacts(query="some1")
assert not ac1.get_contacts(only_verified=True)
assert not ac1.get_contacts(query="some1")
assert len(ac1.get_contacts(with_self=True)) == 2
assert contact1 in ac1.get_contacts()
assert ac1.delete_contact(contact1)
assert contact1 not in ac1.get_contacts()
@@ -200,9 +161,9 @@ class TestOfflineContact:
def test_create_chat_flexibility(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat1 = ac1.create_chat(ac2)
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
assert chat1 == chat2
chat1 = ac1.create_chat(ac2) # This creates a key-contact chat
chat2 = ac1.create_chat(ac2.get_self_contact().addr) # This creates address-contact chat
assert chat1 != chat2
ac3 = acfactory.get_unconfigured_account()
with pytest.raises(ValueError):
ac1.create_chat(ac3)
@@ -260,17 +221,18 @@ class TestOfflineChat:
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_group_chat(name="title1")
with pytest.raises(ValueError):
chat.add_contact(ac2.get_self_contact())
contact = chat.add_contact(ac2)
assert contact.addr == ac2.get_config("addr")
assert contact.name == ac2.get_config("displayname")
assert contact.account == ac1
chat.remove_contact(ac2)
def test_group_chat_creation(self, ac1):
contact1 = ac1.create_contact("some1@example.org", name="some1")
contact2 = ac1.create_contact("some2@example.org", name="some2")
def test_group_chat_creation(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
ac3 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact(ac2)
contact2 = ac1.create_contact(ac3)
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
assert chat.get_name() == "title1"
assert contact1 in chat.get_contacts()
@@ -317,13 +279,14 @@ class TestOfflineChat:
qr = chat.get_join_qr()
assert ac2.check_qr(qr).is_ask_verifygroup
def test_removing_blocked_user_from_group(self, ac1, lp):
def test_removing_blocked_user_from_group(self, ac1, acfactory, lp):
"""
Test that blocked contact is not unblocked when removed from a group.
See https://github.com/deltachat/deltachat-core-rust/issues/2030
"""
lp.sec("Create a group chat with a contact")
contact = ac1.create_contact("some1@example.org")
ac2 = acfactory.get_pseudo_configured_account()
contact = ac1.create_contact(ac2)
group = ac1.create_group_chat("title", contacts=[contact])
group.send_text("First group message")
@@ -335,10 +298,6 @@ class TestOfflineChat:
group.remove_contact(contact)
assert contact.is_blocked()
lp.sec("ac1 adding blocked contact unblocks it")
group.add_contact(contact)
assert not contact.is_blocked()
def test_get_set_profile_image_simple(self, ac1, data):
chat = ac1.create_group_chat(name="title1")
p = data.get_path("d.png")
@@ -481,7 +440,8 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -496,15 +456,15 @@ class TestOfflineChat:
assert os.path.exists(path)
ac2 = acfactory.get_unconfigured_account()
ac2.import_all(path)
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
passphrase1 = "passphrase1"
@@ -512,8 +472,9 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
ac2 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
chat = ac1.create_contact(ac2).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -534,15 +495,15 @@ class TestOfflineChat:
ac2.import_all(path)
# check data integrity
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
contact2_addr = contact2.addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
ac2.shutdown()
@@ -551,15 +512,15 @@ class TestOfflineChat:
ac2.open(passphrase2)
# check data integrity
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == contact2_addr
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_export_with_passphrase(self, acfactory, tmp_path):
passphrase = "test_passphrase"
@@ -567,8 +528,9 @@ class TestOfflineChat:
backupdir = tmp_path / "backup"
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -590,15 +552,15 @@ class TestOfflineChat:
ac2.import_all(path, passphrase)
# check data integrity
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
"""
@@ -612,7 +574,8 @@ class TestOfflineChat:
backupdir.mkdir()
ac1 = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
ac_contact = acfactory.get_pseudo_configured_account()
chat = ac1.create_contact(ac_contact).create_chat()
# send a text message
msg = chat.send_text("msg1")
# send a binary file
@@ -635,15 +598,15 @@ class TestOfflineChat:
ac2.import_all(path, bak_passphrase)
# check data integrity
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
ac2.shutdown()
@@ -652,15 +615,15 @@ class TestOfflineChat:
ac2.open(acct_passphrase)
# check data integrity
contacts = ac2.get_contacts(query="some1")
contacts = ac2.get_contacts()
assert len(contacts) == 1
contact2 = contacts[0]
assert contact2.addr == "some1@example.org"
assert contact2.addr == ac_contact.get_config("addr")
chat2 = contact2.create_chat()
messages = chat2.get_messages()
assert len(messages) == 2
assert messages[0].text == "msg1"
assert os.path.exists(messages[1].filename)
assert len(messages) == 2 + E2EE_INFO_MSGS
assert messages[0 + E2EE_INFO_MSGS].text == "msg1"
assert os.path.exists(messages[1 + E2EE_INFO_MSGS].filename)
def test_set_get_draft(self, chat1):
msg1 = Message.new_empty(chat1.account, "text")
@@ -682,78 +645,10 @@ class TestOfflineChat:
assert not res.is_ask_verifygroup()
assert res.contact_id == 10
def test_group_chat_many_members_add_remove(self, ac1, lp):
lp.sec("ac1: creating group chat with 10 other members")
chat = ac1.create_group_chat(name="title1")
# promote chat
chat.send_text("hello")
assert chat.is_promoted()
def test_audit_log_view_without_daymarker(self, acfactory, lp):
ac1 = acfactory.get_pseudo_configured_account()
ac2 = acfactory.get_pseudo_configured_account()
# activate local plugin
in_list = []
class InPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor):
in_list.append(("added", chat, contact, actor))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor):
in_list.append(("removed", chat, contact, actor))
ac1.add_account_plugin(InPlugin())
# perform add contact many times
contacts = []
for i in range(10):
lp.sec("create contact")
contact = ac1.create_contact(f"some{i}@example.org")
contacts.append(contact)
lp.sec("add contact")
chat.add_contact(contact)
assert chat.num_contacts() == 11
# let's make sure the events perform plugin hooks
def wait_events(cond):
now = time.time()
while time.time() < now + 5:
if cond():
break
time.sleep(0.1)
else:
pytest.fail("failed to get events")
wait_events(lambda: len(in_list) == 10)
assert len(in_list) == 10
chat_contacts = chat.get_contacts()
for in_cmd, in_chat, in_contact, in_actor in in_list:
assert in_cmd == "added"
assert in_chat == chat
assert in_contact in chat_contacts
assert in_actor is None
chat_contacts.remove(in_contact)
assert chat_contacts[0].id == 1 # self contact
in_list[:] = []
lp.sec("ac1: removing two contacts and checking things are right")
chat.remove_contact(contacts[9])
chat.remove_contact(contacts[3])
assert chat.num_contacts() == 9
wait_events(lambda: len(in_list) == 2)
assert len(in_list) == 2
assert in_list[0][0] == "removed"
assert in_list[0][1] == chat
assert in_list[0][2] == contacts[9]
assert in_list[1][0] == "removed"
assert in_list[1][1] == chat
assert in_list[1][2] == contacts[3]
def test_audit_log_view_without_daymarker(self, ac1, lp):
lp.sec("ac1: test audit log (show only system messages)")
chat = ac1.create_group_chat(name="audit log sample data")
@@ -762,10 +657,10 @@ class TestOfflineChat:
assert chat.is_promoted()
lp.sec("create test data")
chat.add_contact(ac1.create_contact("some-1@example.org"))
chat.add_contact(ac2)
chat.set_name("audit log test group")
chat.send_text("a message in between")
lp.sec("check message count of only system messages (without daymarkers)")
sysmessages = [x for x in chat.get_messages() if x.is_system_message()]
assert len(sysmessages) == 3
assert len(sysmessages) == 4

View File

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

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=77cbf92a8565fdf1bcaba10fa93c1455c750a1e9
REV=1cce91c1f1065b47e4f307d6fe2f4cca68c74d2e
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"
@@ -15,7 +15,7 @@ cd "$TMP"
git checkout "$REV"
DATE=$(git show -s --format=%cs)
"$CORE_ROOT"/scripts/create-provider-data-rs.py "$TMP/_providers" "$DATE" >"$CORE_ROOT/src/provider/data.rs"
rustfmt "$CORE_ROOT/src/provider/data.rs"
rustfmt --edition 2024 "$CORE_ROOT/src/provider/data.rs"
rm -fr "$TMP"
cd "$CORE_ROOT"

View File

@@ -1,25 +1,24 @@
//! # Account manager module.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::future::Future;
use std::path::{Path, PathBuf};
use anyhow::{bail, ensure, Context as _, Result};
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use anyhow::{Context as _, Result, bail, ensure};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::task::JoinHandle;
use tokio::task::{JoinHandle, JoinSet};
use uuid::Uuid;
#[cfg(not(target_os = "ios"))]
use tokio::sync::oneshot;
#[cfg(not(target_os = "ios"))]
use tokio::time::{sleep, Duration};
use tokio::time::{Duration, sleep};
use crate::context::{Context, ContextBuilder};
use crate::events::{Event, EventEmitter, EventType, Events};
use crate::log::{info, warn};
use crate::push::PushSubscriber;
use crate::stock_str::StockStrings;
@@ -54,6 +53,14 @@ impl Accounts {
Accounts::open(dir, writable).await
}
/// Get the ID used to log events.
///
/// Account manager logs events with ID 0
/// which is not used by any accounts.
fn get_id(&self) -> u32 {
0
}
/// Creates a new default structure.
async fn create(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)
@@ -71,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();
@@ -263,9 +270,51 @@ impl Accounts {
}
}
/// Get a list of all account ids.
/// Gets a list of all account ids in the user-configured order.
pub fn get_all(&self) -> Vec<u32> {
self.accounts.keys().copied().collect()
let mut ordered_ids = Vec::new();
let mut all_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// First, add accounts in the configured order
for &id in &self.config.inner.accounts_order {
if all_ids.remove(&id) {
ordered_ids.push(id);
}
}
// Then add any accounts not in the order list (newly added accounts)
for id in all_ids {
ordered_ids.push(id);
}
ordered_ids
}
/// Sets the order of accounts.
///
/// The provided list should contain all account IDs in the desired order.
/// If an account ID is missing from the list, it will be appended at the end.
/// If the list contains non-existent account IDs, they will be ignored.
pub async fn set_accounts_order(&mut self, order: Vec<u32>) -> Result<()> {
let existing_ids: BTreeSet<u32> = self.accounts.keys().copied().collect();
// Filter out non-existent account IDs
let mut filtered_order: Vec<u32> = order
.into_iter()
.filter(|id| existing_ids.contains(id))
.collect();
// Add any missing account IDs at the end
for &id in &existing_ids {
if !filtered_order.contains(&id) {
filtered_order.push(id);
}
}
self.config.inner.accounts_order = filtered_order;
self.config.sync().await?;
self.emit_event(EventType::AccountsChanged);
Ok(())
}
/// Starts background tasks such as IMAP and SMTP loops for all accounts.
@@ -304,24 +353,28 @@ impl Accounts {
/// This is an auxiliary function and not part of public API.
/// Use [Accounts::background_fetch] instead.
async fn background_fetch_no_timeout(accounts: Vec<Context>, events: Events) {
async fn background_fetch_and_log_error(account: Context) {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
}
let n_accounts = accounts.len();
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Starting background fetch for {} accounts.",
accounts.len()
"Starting background fetch for {n_accounts} accounts."
)),
});
let mut set = JoinSet::new();
for account in accounts {
set.spawn(async move {
if let Err(error) = account.background_fetch().await {
warn!(account, "{error:#}");
}
});
}
set.join_all().await;
events.emit(Event {
id: 0,
typ: EventType::Info(format!(
"Finished background fetch for {n_accounts} accounts."
)),
});
let mut futures_unordered: FuturesUnordered<_> = accounts
.into_iter()
.map(background_fetch_and_log_error)
.collect();
while futures_unordered.next().await.is_some() {}
}
/// Auxiliary function for [Accounts::background_fetch].
@@ -355,7 +408,10 @@ impl Accounts {
///
/// Returns a future that resolves when background fetch is done,
/// but does not capture `&self`.
pub fn background_fetch(&self, timeout: std::time::Duration) -> impl Future<Output = ()> {
pub fn background_fetch(
&self,
timeout: std::time::Duration,
) -> impl Future<Output = ()> + use<> {
let accounts: Vec<Context> = self.accounts.values().cloned().collect();
let events = self.events.clone();
Self::background_fetch_with_timeout(accounts, events, timeout)
@@ -407,6 +463,10 @@ struct InnerConfig {
pub selected_account: u32,
pub next_id: u32,
pub accounts: Vec<AccountConfig>,
/// Ordered list of account IDs, representing the user's preferred order.
/// If an account ID is not in this list, it will be appended at the end.
#[serde(default)]
pub accounts_order: Vec<u32>,
}
impl Drop for Config {
@@ -459,7 +519,9 @@ impl Config {
Ok(())
});
if locked_rx.await.is_err() {
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
bail!(
"Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)"
);
};
Ok(Some(lock_task))
}
@@ -471,6 +533,7 @@ impl Config {
accounts: Vec::new(),
selected_account: 0,
next_id: 1,
accounts_order: Vec::new(),
};
if !lock {
let cfg = Self {
@@ -503,11 +566,13 @@ impl Config {
/// protects from parallel calls resulting to a wrong file contents.
async fn sync(&mut self) -> Result<()> {
#[cfg(not(target_os = "ios"))]
ensure!(!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished());
ensure!(
!self
.lock_task
.as_ref()
.context("Config is read-only")?
.is_finished()
);
let tmp_path = self.file.with_extension("toml.tmp");
let mut file = fs::File::create(&tmp_path)
@@ -601,6 +666,10 @@ impl Config {
uuid,
});
self.inner.next_id += 1;
// Add new account to the end of the order list
self.inner.accounts_order.push(id);
id
};
@@ -622,6 +691,10 @@ impl Config {
// remove account from the configs
self.inner.accounts.remove(idx);
}
// Remove from order list as well
self.inner.accounts_order.retain(|&x| x != id);
if self.inner.selected_account == id {
// reset selected account
self.inner.selected_account = self
@@ -651,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;
@@ -965,9 +1037,11 @@ mod tests {
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
assert!(tokio::time::timeout(duration, event_emitter.recv())
.await
.is_err());
assert!(
tokio::time::timeout(duration, event_emitter.recv())
.await
.is_err()
);
// When account manager is dropped, event emitter is exhausted.
drop(accounts);

View File

@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::str::FromStr;
use anyhow::{bail, Context as _, Error, Result};
use anyhow::{Context as _, Error, Result, bail};
use crate::key::{DcKey, SignedPublicKey};
@@ -17,7 +17,6 @@ pub enum EncryptPreference {
#[default]
NoPreference = 0,
Mutual = 1,
Reset = 20,
}
impl fmt::Display for EncryptPreference {
@@ -25,7 +24,6 @@ impl fmt::Display for EncryptPreference {
match *self {
EncryptPreference::Mutual => write!(fmt, "mutual"),
EncryptPreference::NoPreference => write!(fmt, "nopreference"),
EncryptPreference::Reset => write!(fmt, "reset"),
}
}
}
@@ -37,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}"),
}
}
}
@@ -48,21 +46,13 @@ pub struct Aheader {
pub addr: String,
pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference,
}
impl Aheader {
/// Creates new autocrypt header
pub fn new(
addr: String,
public_key: SignedPublicKey,
prefer_encrypt: EncryptPreference,
) -> Self {
Aheader {
addr,
public_key,
prefer_encrypt,
}
}
// Whether `_verified` attribute is present.
//
// `_verified` attribute is an extension to `Autocrypt-Gossip`
// header that is used to tell that the sender
// marked this key as verified.
pub verified: bool,
}
impl fmt::Display for Aheader {
@@ -71,6 +61,9 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?;
}
if self.verified {
write!(fmt, " _verified=1;")?;
}
// adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322
@@ -125,6 +118,8 @@ impl FromStr for Aheader {
.and_then(|raw| raw.parse().ok())
.unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -135,6 +130,7 @@ impl FromStr for Aheader {
addr,
public_key,
prefer_encrypt,
verified,
})
}
}
@@ -152,10 +148,11 @@ mod tests {
assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(())
}
// EncryptPreference::Reset is an internal value, parser should never return it
// Non-standard values of prefer-encrypt such as `reset` are treated as no preference.
#[test]
fn test_from_str_reset() -> Result<()> {
let raw = format!("addr=reset@example.com; prefer-encrypt=reset; keydata={RAWKEY}");
@@ -217,7 +214,9 @@ mod tests {
let rendered = ah.to_string();
assert_eq!(rendered, fixed_header);
let ah = Aheader::from_str(&format!(" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"))?;
let ah = Aheader::from_str(&format!(
" _foo; __FOO=BAR ;;; addr = a@b.example.org ;\r\n prefer-encrypt = mutual ; keydata = {RAWKEY}"
))?;
assert_eq!(ah.addr, "a@b.example.org");
assert_eq!(ah.prefer_encrypt, EncryptPreference::Mutual);
@@ -240,38 +239,60 @@ mod tests {
#[test]
fn test_display_aheader() {
assert!(format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
assert!(
format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
)
.contains("prefer-encrypt=mutual;"));
.contains("prefer-encrypt=mutual;")
);
// According to Autocrypt Level 1 specification,
// only "prefer-encrypt=mutual;" can be used.
// If the setting is nopreference, the whole attribute is omitted.
assert!(!format!(
"{}",
Aheader::new(
"test@example.com".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::NoPreference
assert!(
!format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: false
}
)
)
.contains("prefer-encrypt"));
.contains("prefer-encrypt")
);
// Always lowercase the address in the header.
assert!(format!(
"{}",
Aheader::new(
"TeSt@eXaMpLe.cOm".to_string(),
SignedPublicKey::from_base64(RAWKEY).unwrap(),
EncryptPreference::Mutual
assert!(
format!(
"{}",
Aheader {
addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual,
verified: false
}
)
)
.contains("test@example.com"));
.contains("test@example.com")
);
assert!(
format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: true
}
)
.contains("_verified")
);
}
}

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:#}"));
}
};
@@ -266,7 +266,6 @@ mod tests {
use super::*;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::test_utils::TestContext;
use crate::test_utils::TestContextManager;
use crate::tools;
@@ -520,41 +519,6 @@ Authentication-Results: dkim=";
handle_authres(&t, &mail, "invalid@rom.com").await.unwrap();
}
// Test that Autocrypt works with mailing list.
//
// Previous versions of Delta Chat ignored Autocrypt based on the List-Post header.
// This is not needed: comparing of the From address to Autocrypt header address is enough.
// If the mailing list is not rewriting the From header, Autocrypt should be applied.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_autocrypt_in_mailinglist_not_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let alice_bob_chat = alice.create_chat(&bob).await;
let bob_alice_chat = bob.create_chat(&alice).await;
let mut sent = alice.send_text(alice_bob_chat.id, "hellooo").await;
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
bob.recv_msg(&sent).await;
let peerstate = Peerstate::from_addr(&bob, "alice@example.org").await?;
assert!(peerstate.is_some());
// Bob can now write encrypted to Alice:
let mut sent = bob
.send_text(bob_alice_chat.id, "hellooo in the mailinglist again")
.await;
assert!(sent.load_from_db().await.get_showpadlock());
sent.payload
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_authres_in_mailinglist_ignored() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -584,12 +548,13 @@ Authentication-Results: dkim=";
let rcvd = bob.recv_msg(&sent).await;
// The message info should contain a warning:
assert!(rcvd
.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false"));
assert!(
rcvd.id
.get_info(&bob)
.await
.unwrap()
.contains("DKIM Results: Passed=false")
);
Ok(())
}

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