Compare commits

...

91 Commits

Author SHA1 Message Date
link2xt
6db2cf6144 chore(release): prepare for 2.30.0 2025-12-04 17:01:28 +00:00
link2xt
47c1e54219 ci: use "trusted publishing" for NPM packages
See the documentation at <https://docs.npmjs.com/trusted-publishers>.
I have removed the token that was used since <https://github.com/chatmail/core/pull/5575>,
created two new GitHub deployment environments and configured
trusted publishing for two packages (see the environment URLs) on https://www.npmjs.com/
2025-12-04 15:40:28 +00:00
d2weber
b41c309e21 fix: shutdown shortly after call 2025-12-04 15:37:33 +00:00
link2xt
f7ae2abe52 feat: synchronize transports via sync messages 2025-12-04 14:40:36 +00:00
link2xt
3a7f82c66e api: add TransportsModified event 2025-12-04 14:40:36 +00:00
holger krekel
d75a78d446 feat: introduce cross-core testing along with improvements to test frameworking 2025-12-04 14:29:16 +00:00
iequidoo
676132457f feat: Disable SNI for STARTTLS (#7499)
Many clients don't send it currently, so it is unlikely that servers depend on it:
https://mastodon.social/@cks/114690055923939576.

For "implicit TLS", do not turn it off yet, it will serve as a fallback in case of rare server that
needs it. If the server only supports STARTTLS and requires SNI then it is really weird, likely
should not happen.
2025-12-04 05:11:50 -03:00
Simon Laux
8bce137e06 chore: remove two outdated todo comments (#7550)
remove two small outdated to do comments that are not relevant anymore
(already done or solved differently)
2025-12-02 19:08:24 +00:00
dependabot[bot]
f359a9c451 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-12-02 13:51:14 -03:00
dependabot[bot]
0d97a5b511 chore(deps): bump astral-sh/setup-uv from 7.1.3 to 7.1.4
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.3 to 7.1.4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](5a7eac68fb...1e862dfacb)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 10:51:20 +00:00
dependabot[bot]
7ccc021aea chore(cargo): bump syn from 2.0.110 to 2.0.111
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.110 to 2.0.111.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.110...2.0.111)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 10:45:01 +00:00
dependabot[bot]
08e9cdc487 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [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/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-02 10:40:16 +00:00
link2xt
12cee23924 chore(release): prepare for 2.29.0 2025-12-01 02:07:21 +00:00
link2xt
5fb118e5a3 refactor: make signing key non-optional for pk_encrypt 2025-12-01 01:16:01 +00:00
link2xt
1ec3f45dc1 refactor: use SYMMETRIC_KEY_ALGORITHM constant in symm_encrypt_message() 2025-12-01 01:16:01 +00:00
link2xt
e4e19b57b3 ci: build Python wheels in separate jobs 2025-11-30 02:59:17 +00:00
link2xt
2efb128fec ci: do not build fake RPC server source packages
The source packages were needed for Android
to repack compatible Linux wheels,
but now Android is officially supported.
2025-11-30 02:59:17 +00:00
link2xt
4a5d5bdeb1 ci: do not install Python manually to bulid RPC server wheels
Python preinstalled in runners should be up to date by now.
2025-11-30 02:59:17 +00:00
link2xt
cde4a61be7 ci: fix a typo in deltachat-rpc-server publishing workflow 2025-11-30 02:59:17 +00:00
dependabot[bot]
ca2b4d7a6f chore(cargo): bump tokio from 1.45.1 to 1.48.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.45.1 to 1.48.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.1...tokio-1.48.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...
2025-11-29 21:20:08 -03:00
iequidoo
ef61c0c408 test: test_remove_member_bcc: Test unencrypted group as it was initially 2025-11-29 17:44:52 -03:00
link2xt
dc5f939ac6 feat!: increase backup version from 3 to 4
Migration 140 merged in version 2.28.0
introduced `NOT NULL` transport_id
columns, so old versions of core not aware of it
fail e.g. when they expect conflict on `(folder)`
column rather than `(transport_id, folder)`.
2025-11-29 17:24:08 +00:00
iequidoo
2c0092738f feat: Don't update self-{avatar,status} from received messages (#7002)
The normal way of synchronizing self-avatar and -status nowadays is sync messages.
2025-11-29 02:19:09 -03:00
dependabot[bot]
b1e6cf2052 chore(cargo): bump rand from 0.9.0 to 0.9.2 (#7501) 2025-11-29 00:18:03 +00:00
link2xt
343dca87f7 fix: do not use deprecated ConfiguredProvider in get_configured_provider 2025-11-29 00:17:26 +00:00
link2xt
4d06f5a8ae fix: upload sync messages only with the primary transport
Currently there is a race between transports
to upload sync messages and delete them from `imap_send` table.
Sometimes mulitple transports upload the same message
and sometimes only some of them "win".
With this change only the primary transport
will upload the sync message.
2025-11-29 00:17:26 +00:00
link2xt
a4bec7dc70 refactor: remove update_icons and disable_server_delete migrations 2025-11-29 00:17:26 +00:00
link2xt
1f32c5ab40 refactor: use ConfiguredProvider config directly when loading legacy settings 2025-11-29 00:17:26 +00:00
link2xt
43e8d5cc6c ci: unpin mypy 2025-11-29 00:16:58 +00:00
link2xt
7e4547582e fix: do not configure folders during transport configuration
We do not have transport ID assigned until configuration finishes,
so we cannot save UID validitiy for folders and write
into any tables that have transport_id column yet.
2025-11-29 00:05:29 +00:00
link2xt
721b9cebef build: pin mypy to 1.18.2
mypy depends on librt since 1.19.0
and it fails to build with PyPy 3.10.
2025-11-28 22:14:38 +00:00
link2xt
0d50d8703f build: use SPDX license expression in Python package metadata
License classifiers are replaced with `license`.
This is supported since `setuptools` v77.0.0:
<https://setuptools.pypa.io/en/stable/history.html#v77-0-0>

Without this change we get
`SetuptoolsDeprecationWarning: License classifiers are deprecated.`
with a reference to
<https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license>
2025-11-28 22:14:38 +00:00
link2xt
9aba299c75 build: increase minimum supported Python version to 3.10
Python 3.9 is not supported since 2025-10-31:
https://devguide.python.org/versions/

mypy just dropped support for Python 3.9:
<1a6ff59049>
2025-11-28 22:14:38 +00:00
iequidoo
2854f87a9d fix: CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid) (#7490)
This effectively readds the old `imap_rfc724_mid` built only on `rfc724_mid`, otherwise
`sql::prune_tombstones()` which is called from `housekeeping()` becomes very slow because of no
suitable index.
2025-11-27 22:23:03 -03:00
iequidoo
145a5813e8 feat: Don't send Chat-Group-Avatar header in unencrypted groups
`chat::set_chat_profile_image()` already checks that the group has grpid, still it makes sense to
check that a message is encrypted when sending, in case if the chat has a profile image in the db
for some reason.
2025-11-27 22:02:11 -03:00
iequidoo
4cb129a67e fix: Don't send self-avatar in unencrypted messages (#7136)
We don't display avatars for address-contacts, so sending avatars w/o encryption is not useful and
causes e.g. Outlook to reject a message with a big header, see
https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
2025-11-27 22:02:11 -03:00
link2xt
7bf7ec3d32 api(deltachat-rpc-client): add Message.exists() 2025-11-28 00:30:36 +00:00
link2xt
8a7498a9a8 fix: handle the case when account does not exist in get_existing_msg_ids()
If account is removed, this means the messages are removed as well.
We do not reuse account IDs, so the account will not reappear.
2025-11-28 00:30:36 +00:00
dependabot[bot]
c41a69ea1e chore(cargo): bump image from 0.25.8 to 0.25.9
Bumps [image](https://github.com/image-rs/image) from 0.25.8 to 0.25.9.
- [Changelog](https://github.com/image-rs/image/blob/main/CHANGES.md)
- [Commits](https://github.com/image-rs/image/compare/v0.25.8...v0.25.9)

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

Co-authored-by: iequidoo <dgreshilov@gmail.com>
2025-11-27 18:39:17 +01:00
link2xt
38a547dfda build: make scripts for remote testing usable
This updates `scripts/remote_tests_{rust,python}.sh`.
The scripts were previously used to run tests
from CI on remote faster machine,
but they are still usable to run tests remotely
e.g. from a laptop that is on battery.
2025-11-26 17:12:25 +00:00
link2xt
7c998af973 ci: set timeout-minutes for all jobs in ci.yaml workflow
Default is 360 minutes, that is 6 hours.
If the job is running for more than 1 hour,
it is surely stuck, no need to keep running it.
2025-11-26 17:06:20 +00:00
link2xt
6b6ec2a4b7 fix: use the same webxdc ratelimit for all email servers
This removes another distinction between chatmail and non-chatmail
and fixes flaky tests/test_webxdc.py::test_webxdc

Closes <https://github.com/chatmail/core/issues/7522>
2025-11-26 00:47:43 +00:00
dependabot[bot]
b1fa1055d7 chore(cargo): bump bytes from 1.10.1 to 1.11.0
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.10.1...v1.11.0)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 16:05:26 -03:00
dependabot[bot]
15ce05b0c7 chore(cargo): bump libc from 0.2.176 to 0.2.177
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.176 to 0.2.177.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Changelog](https://github.com/rust-lang/libc/blob/0.2.177/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.176...0.2.177)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 14:56:34 +00:00
dependabot[bot]
8112183429 chore(cargo): bump quote from 1.0.41 to 1.0.42
Bumps [quote](https://github.com/dtolnay/quote) from 1.0.41 to 1.0.42.
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.41...1.0.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 14:56:11 +00:00
holger krekel
b9ae74fab2 feat: deltachat_rpc_client.Rpc accepts rpc_server_path for using a particular deltachat-rpc-server (#7493)
also simplifies and make more readable popen-call to rpc-server 

addresses #7428
2025-11-25 11:02:42 +01:00
dependabot[bot]
fdff90eba4 chore(cargo): bump sanitize-filename from 0.5.0 to 0.6.0
Bumps [sanitize-filename](https://github.com/kardeiz/sanitize-filename) from 0.5.0 to 0.6.0.
- [Commits](https://github.com/kardeiz/sanitize-filename/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:30:50 +00:00
dependabot[bot]
a4a54d3648 chore(cargo): bump nu-ansi-term from 0.50.1 to 0.50.3
Bumps [nu-ansi-term](https://github.com/nushell/nu-ansi-term) from 0.50.1 to 0.50.3.
- [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.50.1...v0.50.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:12:49 +00:00
dependabot[bot]
98fb760f49 chore(cargo): bump rustls-pki-types from 1.12.0 to 1.13.0
Bumps [rustls-pki-types](https://github.com/rustls/pki-types) from 1.12.0 to 1.13.0.
- [Release notes](https://github.com/rustls/pki-types/releases)
- [Commits](https://github.com/rustls/pki-types/compare/v/1.12.0...v/1.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:07:20 +00:00
dependabot[bot]
f553c094eb chore(cargo): bump quick-xml from 0.38.3 to 0.38.4
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.38.3 to 0.38.4.
- [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.38.3...v0.38.4)

---
updated-dependencies:
- dependency-name: quick-xml
  dependency-version: 0.38.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:06:55 +00:00
dependabot[bot]
bbb4bed996 chore(cargo): bump syn from 2.0.106 to 2.0.110
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.106 to 2.0.110.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.106...2.0.110)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:06:38 +00:00
dependabot[bot]
25088f2dcb chore(cargo): bump parking_lot from 0.12.4 to 0.12.5
Bumps [parking_lot](https://github.com/Amanieu/parking_lot) from 0.12.4 to 0.12.5.
- [Release notes](https://github.com/Amanieu/parking_lot/releases)
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/parking_lot-v0.12.4...parking_lot-v0.12.5)

---
updated-dependencies:
- dependency-name: parking_lot
  dependency-version: 0.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:05:37 +00:00
dependabot[bot]
552e9f4052 chore(cargo): bump proptest from 1.8.0 to 1.9.0
Bumps [proptest](https://github.com/proptest-rs/proptest) from 1.8.0 to 1.9.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.8.0...v1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:04:44 +00:00
dependabot[bot]
b3616a013f chore(cargo): bump toml from 0.9.7 to 0.9.8
Bumps [toml](https://github.com/toml-rs/toml) from 0.9.7 to 0.9.8.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.9.7...toml-v0.9.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 03:04:29 +00:00
dependabot[bot]
c378b1218d chore(cargo): bump tokio-util from 0.7.16 to 0.7.17
Bumps [tokio-util](https://github.com/tokio-rs/tokio) from 0.7.16 to 0.7.17.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-util-0.7.16...tokio-util-0.7.17)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 02:24:48 +00:00
dependabot[bot]
21f6e7c676 chore(cargo): bump rusqlite from 0.36.0 to 0.37.0
Bumps [rusqlite](https://github.com/rusqlite/rusqlite) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/rusqlite/rusqlite/releases)
- [Changelog](https://github.com/rusqlite/rusqlite/blob/master/Changelog.md)
- [Commits](https://github.com/rusqlite/rusqlite/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: rusqlite
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 02:08:51 +00:00
dependabot[bot]
ac543ad251 chore(cargo): bump hyper-util from 0.1.17 to 0.1.18
Bumps [hyper-util](https://github.com/hyperium/hyper-util) from 0.1.17 to 0.1.18.
- [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.17...v0.1.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 02:07:16 +00:00
dependabot[bot]
183898b137 chore(cargo): bump human-panic from 2.0.3 to 2.0.4
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 2.0.3 to 2.0.4.
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: human-panic
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 02:06:07 +00:00
dependabot[bot]
56204ae701 chore(cargo): bump hyper from 1.7.0 to 1.8.1
Bumps [hyper](https://github.com/hyperium/hyper) from 1.7.0 to 1.8.1.
- [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.7.0...v1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 02:04:59 +00:00
dependabot[bot]
7906405400 chore(deps): bump cachix/install-nix-action from 31.8.1 to 31.8.4
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.8.1 to 31.8.4.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](fd24c48048...0b0e072294)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 23:25:32 +00:00
Hocuri
531e0bc914 refactor!: Remove some unneeded stock strings (#7496)
There are quite some unneeded stock strings; this PR removes some of
them. None of these stock strings were actually set by the UI, or even
have translations in Transifex.
- We don't have AEAP anymore.
- The "I added/removed member" and "I left the group" strings are
anyways not meant to be shown to the user. Also, starting to translate
them now would leak the device language.

BREAKING CHANGE: This can theoretically be a breaking change because a
UI could reference one of the removed stock strings, so I marked it as
breaking just in case.
2025-11-24 19:55:12 +01:00
link2xt
3637fe67a7 feat: Hide To header in encrypted messages 2025-11-24 02:33:56 -03:00
holger krekel
8eef79f95d refactor: strike events in rpc-client request handling, get result from queue 2025-11-23 21:23:53 +01:00
link2xt
6077499f07 chore(release): prepare for 2.28.0 2025-11-23 17:08:42 +00:00
Simon Laux
94d2d8cfd7 feat: add api to get information about diskspace usage of database. (jsonrpc method: get_storage_usage_report_string) (#7486)
new jsonrpc api: `get_storage_usage_report_string(accountId)`
new rust API: `get_storage_usage(&context)`
2025-11-23 15:18:00 +00:00
iequidoo
ba3cad6ad6 docs: Mark db encryption support as deprecated (#7403)
- Db encryption does nothing with blobs, so fs/disk encryption is recommended.
- Isolation from other apps is needed anyway.
- Experimental database encryption was removed on iOS and Android.
- Delta Touch is using CFFI API with a manually entered password because Ubuntu Touch does not offer
  filesystem or disk encryption, but we don't want new users of these APIs, such as bot developers.
2025-11-22 18:36:40 -03:00
link2xt
c9c362d5ff api: get_existing_msg_ids()
This API allows to check if the message with
given ID exists and distinguish between
message not existing and database error.
It might also be faster than
checking messages one by one
if multiple messages need to be checked
because of using a single SQL transaction.
2025-11-22 18:19:44 +00:00
iequidoo
6514b4ca7f fix: Look up or create ad-hoc group if there are duplicate addresses in "To"
Fix `test_unencrypted_doesnt_goto_self_chat` as well, it was only testing the first message because
of using the same Message-ID for all messages.
2025-11-22 02:48:27 -03:00
link2xt
e7e31d7914 ci: do not use --encoding option for rst-lint
It was removed in rst-lint 2.0:
7b43036b4d
2025-11-22 05:26:03 +00:00
B. Petersen
51d6855e0d fix: add missing stock strings 2025-11-21 14:42:50 +01:00
Hocuri
2f90b55309 feat: Stock string for joining a channel (#7480)
Add a stock string `%1$s invited you to join this channel.\n\nWaiting
for the device of %2$s to reply…`, which is shown when a user starts to
join a channel.

I did _not_ add an equivalent to `%1$s replied, waiting for being added
to the group…`, which is shown when vg-auth-required was received. I
don't think that this would add any information that's interesting to
the user, other than 'something is happening, hang on'. And the more
text on the screen, the less likely that anyone reads it. But if others
think differently, we can also add it.

With this PR, joining a channel looks like this:

```
Msg#2003: info (Contact#Contact#Info): Messages are end-to-end encrypted. [NOTICED][INFO]
Msg#2004: info (Contact#Contact#Info): Alice invited you to join this channel.

Waiting for the device of Alice to reply… [NOTICED][INFO]
Msg#2007🔒:  (Contact#Contact#2001): You joined the channel. [FRESH][INFO]
```
2025-11-20 21:13:04 +00:00
link2xt
be3e202470 feat: allow adding second transport 2025-11-20 15:51:19 +00:00
link2xt
57aadfbbf6 chore: update preloaded DNS cache 2025-11-19 19:55:53 +00:00
link2xt
849cde9757 refactor: remove some easy to remove unwrap() calls 2025-11-19 17:38:58 +00:00
link2xt
b4cd99fc56 docs: replace some references to Delta Chat with chatmail 2025-11-19 04:11:12 +00:00
iequidoo
9305a0676c fix: Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF (#7409)
Before, outgoing self-sent unencrypted messages were assigned to the self-chat. Now we assign them
to ad-hoc groups with only SELF instead of 1:1 chats with address contacts corresponding to our own
addresses because we don't want to create such address contacts; we still use SELF for `from_id` of
such messages. Not assigning such messages to the encrypted chat should be safe enough and such
messages can actually be sent by the user from another MUA.
2025-11-18 20:34:56 -03:00
B. Petersen
39c9ba19ef docs: add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC 2025-11-18 23:16:10 +01:00
link2xt
af574279fd docs: remove unsupported RFC 3503 ($MDNSent flag) from the list of standards 2025-11-18 21:44:41 +00:00
Hocuri
713c929e03 refactor: Rename add_encrypted_msg -> add_e2ee_notice 2025-11-18 18:58:26 +01:00
Hocuri
c83c131a37 feat: Rephrase "Establishing end-to-end encryption" -> "Establishing connection" 2025-11-18 18:58:26 +01:00
Hocuri
0d0602a4a5 fix: Sort system messages to the bottom of the chat
Fix #7435

For most messages, `calc_sort_timestamp()` makes sure that they are at the correct place; esp. that they are not above system messages or other noticed/seen messages.

Most callers of `add_info_msg()`, however, didn't call `calc_sort_timestamp()`, and just used `time()` or `smeared_time()` to get the sort timestamp. Because of this, system messages could sometimes wrongly be sorted above other messages.

This PR fixes this by making the sort timestamp optional in `add_info_msg*()`. If the sort timestamp isn't passed, then the message is sorted to the bottom of the chat. `sent_rcvd_timestamp` is not optional anymore, because we need _some_ timestamp that can be shown to the user (most callers just pass `time()` there).
2025-11-18 18:58:26 +01:00
link2xt
abfb556377 fix: set SQLite busy timeout to 1 minute on iOS
Closes <https://github.com/chatmail/core/issues/7464>
2025-11-18 17:07:27 +00:00
link2xt
72788daca0 refactor: use HashMap::extract_if() stabilized in Rust 1.88.0 2025-11-18 13:16:44 +00:00
iequidoo
16bd87c78f test: Contact shalln't be verified by another having unknown verifier
It must be verified by "unknown verifier" instead. But if the verifier has known verifier in turn,
it must reverify contacts having unknown verifier. Add a check for this also.
2025-11-18 05:42:46 -03:00
iequidoo
d44e2420bc fix: ContactId::set_name_ex(): Emit ContactsChanged when transaction is completed
This fixes flaky JSON-RPC's `test_rename_synchronization()`.
2025-11-18 02:17:31 -03:00
dependabot[bot]
88d213fcdb chore(deps): bump astral-sh/setup-uv from 7.1.2 to 7.1.3
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](85856786d1...5a7eac68fb)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 7.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-18 02:12:53 +00:00
link2xt
fb14acb0fb fix: limit the range of Date to up to 6 days in the past
Previous value (1000000 seconds) was slightly more than 11.5 days.
2025-11-17 23:17:55 +00:00
link2xt
a5c470fbae build(nix): update fenix and use it for all Rust builds
`fenix` input provides updated Rust packages.
Updating it is needed since current version is 1.86.0
and MSRV has been increased to 1.88.0.
2025-11-17 15:23:58 +00:00
link2xt
6bdba33d32 build: update rPGP from 0.17.0 to 0.18.0 2025-11-16 15:18:55 +00:00
link2xt
c6ace749e3 build: increase MSRV to 1.88.0
It is required by rPGP 0.18.0.

All the changes in `.rs` files are made automatically with `clippy --fix`.
2025-11-16 14:48:50 +00:00
link2xt
22ebd6436f feat: default bcc_self to 0 for new accounts 2025-11-16 10:00:00 +00:00
121 changed files with 3715 additions and 2475 deletions

View File

@@ -23,14 +23,15 @@ env:
RUST_VERSION: 1.91.0
# Minimum Supported Rust Version
MSRV: 1.85.0
MSRV: 1.88.0
jobs:
lint_rust:
name: Lint Rust
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -52,8 +53,9 @@ jobs:
cargo_deny:
name: cargo deny
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -66,8 +68,9 @@ jobs:
provider_database:
name: Check provider database
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -79,10 +82,11 @@ jobs:
docs:
name: Rust doc comments
runs-on: ubuntu-latest
timeout-minutes: 60
env:
RUSTDOCFLAGS: -Dwarnings
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -107,6 +111,7 @@ jobs:
- os: ubuntu-latest
rust: minimum
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- run:
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
@@ -117,7 +122,7 @@ jobs:
shell: bash
if: matrix.rust == 'latest'
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -155,8 +160,9 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -180,8 +186,9 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -202,8 +209,9 @@ jobs:
python_lint:
name: Python lint
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -219,6 +227,38 @@ jobs:
working-directory: deltachat-rpc-client
run: tox -e lint
# mypy does not work with PyPy since mypy 1.19
# as it introduced native `librt` dependency
# that uses CPython internals.
# We only run mypy with CPython because of this.
cffi_python_mypy:
name: CFFI Python mypy
needs: ["c_library", "python_lint"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v6
with:
name: ubuntu-latest-libdeltachat.a
path: target/debug
- name: Install tox
run: pip install tox
- name: Run mypy
env:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e mypy
cffi_python_tests:
name: CFFI Python tests
needs: ["c_library", "python_lint"]
@@ -238,15 +278,16 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.8
# Minimum Supported Python Version = 3.10
# This is the minimum version for which manylinux Python wheels are
# built. Test it with minimum supported Rust version.
- os: ubuntu-latest
python: 3.8
python: "3.10"
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -271,7 +312,7 @@ jobs:
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e mypy,doc,py
run: tox -e doc,py
rpc_python_tests:
name: JSON-RPC Python tests
@@ -293,13 +334,14 @@ jobs:
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.8
# Minimum Supported Python Version = 3.10
- os: ubuntu-latest
python: 3.8
python: "3.10"
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false

View File

@@ -30,11 +30,11 @@ jobs:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
@@ -46,6 +46,30 @@ jobs:
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_linux_wheel:
name: Linux wheel
strategy:
fail-fast: false
matrix:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
path: result/*.whl
if-no-files-found: error
build_windows:
name: Windows
strategy:
@@ -54,11 +78,11 @@ jobs:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
@@ -70,6 +94,30 @@ jobs:
path: result/bin/deltachat-rpc-server.exe
if-no-files-found: error
build_windows_wheel:
name: Windows wheel
strategy:
fail-fast: false
matrix:
arch: [win32, win64]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
path: result/*.whl
if-no-files-found: error
build_macos:
name: macOS
strategy:
@@ -79,7 +127,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -105,11 +153,11 @@ jobs:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
@@ -121,9 +169,33 @@ jobs:
path: result/bin/deltachat-rpc-server
if-no-files-found: error
build_android_wheel:
name: Android wheel
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build deltachat-rpc-server wheels
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
- name: Upload wheel
uses: actions/upload-artifact@v5
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
path: result/*.whl
if-no-files-found: error
publish:
name: Build wheels and upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"]
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
@@ -132,11 +204,11 @@ jobs:
contents: write
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v6
@@ -144,42 +216,84 @@ jobs:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux aarch64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-aarch64-linux-wheel
path: deltachat-rpc-server-aarch64-linux-wheel.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv7l wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv7l-linux-wheel
path: deltachat-rpc-server-armv7l-linux-wheel.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux armv6l wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armv6l-linux-wheel
path: deltachat-rpc-server-armv6l-linux-wheel.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux i686 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-i686-linux-wheel
path: deltachat-rpc-server-i686-linux-wheel.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Linux x86_64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-x86_64-linux-wheel
path: deltachat-rpc-server-x86_64-linux-wheel.d
- name: Download Win32 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win32 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win32-wheel
path: deltachat-rpc-server-win32-wheel.d
- name: Download Win64 binary
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download Win64 wheel
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-win64-wheel
path: deltachat-rpc-server-win64-wheel.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v6
with:
@@ -198,12 +312,24 @@ jobs:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android wheel for arm64-v8a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-arm64-v8a-android-wheel
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Download Android wheel for armeabi-v7a
uses: actions/download-artifact@v6
with:
name: deltachat-rpc-server-armeabi-v7a-android-wheel
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
- name: Create bin/ directory
run: |
mkdir -p bin
@@ -222,38 +348,21 @@ jobs:
- name: List binaries
run: ls -l bin/
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v6
with:
python-version: 3.12
- name: Install wheel
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
- name: Build deltachat-rpc-server Python wheels
run: |
mkdir -p dist
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-arm64-v8a-android-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armeabi-v7a-android-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
@@ -271,7 +380,7 @@ jobs:
--repo ${{ github.repository }} \
bin/* dist/*
- name: Publish deltachat-rpc-client to PyPI
- name: Publish deltachat-rpc-server to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
@@ -279,13 +388,16 @@ jobs:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
environment:
name: npm-stdio-rpc-server
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
permissions:
id-token: write
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -412,5 +524,3 @@ jobs:
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,11 +10,14 @@ jobs:
pack-module:
name: "Publish @deltachat/jsonrpc-client"
runs-on: ubuntu-latest
environment:
name: npm-jsonrpc-client
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -37,5 +40,3 @@ jobs:
- name: Publish
working-directory: deltachat-jsonrpc/typescript
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

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

View File

@@ -21,11 +21,11 @@ jobs:
name: check flake formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix fmt flake.nix -- --check
build:
@@ -80,11 +80,11 @@ jobs:
#- deltachat-rpc-server-x86_64-android
#- deltachat-rpc-server-x86-android
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix build .#${{ matrix.installable }}
build-macos:
@@ -101,9 +101,9 @@ jobs:
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
# - deltachat-rpc-server-aarch64-darwin
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- run: nix build .#${{ matrix.installable }}

View File

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

View File

@@ -14,11 +14,11 @@ jobs:
name: Build REPL example
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # 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@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
@@ -31,12 +31,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # 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@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
@@ -72,7 +72,7 @@ jobs:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
show-progress: false
persist-credentials: false

View File

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

View File

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

View File

@@ -1,5 +1,156 @@
# Changelog
## [2.30.0] - 2025-12-04
### Features / Changes
- Disable SNI for STARTTLS ([#7499](https://github.com/chatmail/core/pull/7499)).
- Introduce cross-core testing along with improvements to test frameworking.
- Synchronize transports via sync messages.
### Fixes
- Fix shutdown shortly after call.
### API-Changes
- Add `TransportsModified` event (for tests).
### CI
- Use "trusted publishing" for NPM packages.
### Miscellaneous Tasks
- deps: Bump actions/checkout from 5 to 6.
- cargo: Bump syn from 2.0.110 to 2.0.111.
- deps: Bump astral-sh/setup-uv from 7.1.3 to 7.1.4.
- cargo: Bump sdp from 0.8.0 to 0.10.0.
- Remove two outdated todo comments ([#7550](https://github.com/chatmail/core/pull/7550)).
## [2.29.0] - 2025-12-01
### API-Changes
- deltachat-rpc-client: Add Message.exists().
### Features / Changes
- [**breaking**] Increase backup version from 3 to 4.
- Hide `To` header in encrypted messages.
- `deltachat_rpc_client.Rpc` accepts `rpc_server_path` for using a particular deltachat-rpc-server ([#7493](https://github.com/chatmail/core/pull/7493)).
- Don't send `Chat-Group-Avatar` header in unencrypted groups.
- Don't update `self-{avatar,status}` from received messages ([#7002](https://github.com/chatmail/core/pull/7002)).
### Fixes
- `CREATE INDEX imap_only_rfc724_mid ON imap(rfc724_mid)` ([#7490](https://github.com/chatmail/core/pull/7490)).
- Use the same webxdc ratelimit for all email servers.
- Handle the case when account does not exist in `get_existing_msg_ids()`.
- Don't send self-avatar in unencrypted messages ([#7136](https://github.com/chatmail/core/pull/7136)).
- Do not configure folders during transport configuration.
- Upload sync messages only with the primary transport.
- Do not use deprecated ConfiguredProvider in get_configured_provider.
### Build system
- Make scripts for remote testing usable.
- Increase minimum supported Python version to 3.10.
- Use SPDX license expression in Python package metadata.
### CI
- Set timeout-minutes for all jobs in ci.yaml workflow.
- Do not install Python manually to bulid RPC server wheels.
- Do not build fake RPC server source packages.
- Build Python wheels in separate jobs.
### Refactor
- [**breaking**] Remove some unneeded stock strings ([#7496](https://github.com/chatmail/core/pull/7496)).
- Strike events in rpc-client request handling, get result from queue.
- Use ConfiguredProvider config directly when loading legacy settings.
- Remove update_icons and disable_server_delete migrations.
- Use `SYMMETRIC_KEY_ALGORITHM` constant in `symm_encrypt_message()`.
- Make signing key non-optional for `pk_encrypt`.
### Tests
- `test_remove_member_bcc`: Test unencrypted group as it was initially.
### Miscellaneous Tasks
- deps: Bump cachix/install-nix-action from 31.8.1 to 31.8.4.
- cargo: Bump hyper from 1.7.0 to 1.8.1.
- cargo: Bump human-panic from 2.0.3 to 2.0.4.
- cargo: Bump hyper-util from 0.1.17 to 0.1.18.
- cargo: Bump rusqlite from 0.36.0 to 0.37.0.
- cargo: Bump tokio-util from 0.7.16 to 0.7.17.
- cargo: Bump toml from 0.9.7 to 0.9.8.
- cargo: Bump proptest from 1.8.0 to 1.9.0.
- cargo: Bump parking_lot from 0.12.4 to 0.12.5.
- cargo: Bump syn from 2.0.106 to 2.0.110.
- cargo: Bump quick-xml from 0.38.3 to 0.38.4.
- cargo: Bump rustls-pki-types from 1.12.0 to 1.13.0.
- cargo: Bump nu-ansi-term from 0.50.1 to 0.50.3.
- cargo: Bump sanitize-filename from 0.5.0 to 0.6.0.
- cargo: Bump quote from 1.0.41 to 1.0.42.
- cargo: Bump libc from 0.2.176 to 0.2.177.
- cargo: Bump bytes from 1.10.1 to 1.11.0.
- cargo: Bump image from 0.25.8 to 0.25.9.
- cargo: Bump rand from 0.9.0 to 0.9.2 ([#7501](https://github.com/chatmail/core/pull/7501)).
- cargo: Bump tokio from 1.45.1 to 1.48.0.
## [2.28.0] - 2025-11-23
### API-Changes
- New API `get_existing_msg_ids()` to check if the messages with given IDs exist.
- Add API to get storage usage information. (JSON-RPC method: `get_storage_usage_report_string`) ([#7486](https://github.com/chatmail/core/pull/7486)).
### Features / Changes
- Experimentaly allow adding second transport.
There is no synchronization yet, so UIs should not allow the user to change the address manually and only expose the ability to add transports if `bcc_self` is disabled.
- Default `bcc_self` to 0 for all new accounts.
- Rephrase "Establishing end-to-end encryption" -> "Establishing connection".
- Stock string for joining a channel ([#7480](https://github.com/chatmail/core/pull/7480)).
### Fixes
- Limit the range of `Date` to up to 6 days in the past.
- `ContactId::set_name_ex()`: Emit ContactsChanged when transaction is completed.
- Set SQLite busy timeout to 1 minute on iOS.
- Sort system messages to the bottom of the chat.
- Assign outgoing self-sent unencrypted messages to ad-hoc groups with only SELF ([#7409](https://github.com/chatmail/core/pull/7409)).
- Add missing stock strings.
- Look up or create ad-hoc group if there are duplicate addresses in "To".
### Documentation
- Add missing RFC 9788, link 'Header Protection for Cryptographically Protected Email' as other RFC.
- Remove unsupported RFC 3503 (`$MDNSent` flag) from the list of standards.
- Mark database encryption support as deprecated ([#7403](https://github.com/chatmail/core/pull/7403)).
### Build system
- Increase Minimum Supported Rust Version to 1.88.0.
- Update rPGP from 0.17.0 to 0.18.0.
- nix: Update `fenix` and use it for all Rust builds.
### CI
- Do not use --encoding option for rst-lint.
### Refactor
- Use `HashMap::extract_if()` stabilized in Rust 1.88.0.
- Remove some easy to remove unwrap() calls.
### Tests
- Contact shalln't be verified by another having unknown verifier.
## [2.27.0] - 2025-11-16
### API-Changes
@@ -7185,3 +7336,6 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
[2.25.0]: https://github.com/chatmail/core/compare/v2.24.0..v2.25.0
[2.26.0]: https://github.com/chatmail/core/compare/v2.25.0..v2.26.0
[2.27.0]: https://github.com/chatmail/core/compare/v2.26.0..v2.27.0
[2.28.0]: https://github.com/chatmail/core/compare/v2.27.0..v2.28.0
[2.29.0]: https://github.com/chatmail/core/compare/v2.28.0..v2.29.0
[2.30.0]: https://github.com/chatmail/core/compare/v2.29.0..v2.30.0

View File

@@ -1,4 +1,4 @@
# Contributing to Delta Chat
# Contributing to chatmail core
## Bug reports

376
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "2.27.0"
version = "2.30.0"
edition = "2024"
license = "MPL-2.0"
rust-version = "1.85"
rust-version = "1.88"
repository = "https://github.com/chatmail/core"
[profile.dev]
@@ -78,7 +78,7 @@ num-derive = "0.4"
num-traits = { workspace = true }
parking_lot = "0.12.4"
percent-encoding = "2.3"
pgp = { version = "0.17.0", default-features = false }
pgp = { version = "0.18.0", default-features = false }
pin-project = "1"
qrcodegen = "1.7.0"
quick-xml = { version = "0.38", features = ["escape-html"] }
@@ -88,7 +88,7 @@ regex = { workspace = true }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rustls-pki-types = "1.12.0"
sanitize-filename = { workspace = true }
sdp = "0.8.0"
sdp = "0.10.0"
serde_json = { workspace = true }
serde_urlencoded = "0.7.1"
serde = { workspace = true, features = ["derive"] }
@@ -194,14 +194,14 @@ nu-ansi-term = "0.50"
num-traits = "0.2"
rand = "0.9"
regex = "1.10"
rusqlite = "0.36"
sanitize-filename = "0.5"
rusqlite = "0.37"
sanitize-filename = "0.6"
serde = "1.0"
serde_json = "1"
tempfile = "3.23.0"
thiserror = "2"
tokio = "1"
tokio-util = "0.7.16"
tokio-util = "0.7.17"
tracing-subscriber = "0.3"
yerpc = "0.6.4"

View File

@@ -108,7 +108,7 @@ fn criterion_benchmark(c: &mut Criterion) {
pk_encrypt(
plain.clone(),
vec![black_box(key_pair.public.clone())],
Some(key_pair.secret.clone()),
key_pair.secret.clone(),
true,
true,
)

View File

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

View File

@@ -247,7 +247,7 @@ typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
// create/open/config/information
/**
* Create a new context object and try to open it without passphrase. If
* Create a new context object and try to open it. If
* database is encrypted, the result is the same as using
* dc_context_new_closed() and the database should be opened with
* dc_context_open() before using.
@@ -283,8 +283,13 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
/**
* Opens the database with the given passphrase. This can only be used on
* closed context, such as created by dc_context_new_closed(). If the database
* Opens the database with the given passphrase.
* NB: Nonempty passphrase (db encryption) is deprecated 2025-11:
* - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
* - Isolation from other apps is needed anyway.
*
* This can only be used on closed context, such as
* created by dc_context_new_closed(). If the database
* is new, this operation sets the database passphrase. For existing databases
* the passphrase should be the one used to encrypt the database the first
* time.
@@ -301,6 +306,8 @@ int dc_context_open (dc_context_t *context, const char*
/**
* Changes the passphrase on the open database.
* Deprecated 2025-11, see `dc_context_open()` for reasoning.
*
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
@@ -7208,11 +7215,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as device message text.
#define DC_STR_SELF_DELETED_MSG_BODY 91
/// "'Delete messages from server' turned off as now all folders are affected."
///
/// Used as device message text.
#define DC_STR_SERVER_TURNED_OFF 92
/// "Message deletion timer is set to %1$s minutes."
///
/// Used in status messages.
@@ -7409,19 +7411,6 @@ void dc_event_unref(dc_event_t* event);
/// @deprecated 2025-06-05
#define DC_STR_AEAP_ADDR_CHANGED 122
/// "You changed your email address from %1$s to %2$s.
/// If you now send a message to a group, contacts there will automatically
/// replace the old with your new address.\n\n It's highly advised to set up
/// your old email provider to forward all emails to your new email address.
/// Otherwise you might miss messages of contacts who did not get your new
/// address yet." + the link to the AEAP blog post
///
/// As soon as there is a post about AEAP, the UIs should add it:
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
///
/// Used in a device message that explains AEAP.
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
/// "You changed group name from \"%1$s\" to \"%2$s\"."
///
/// `%1$s` will be replaced by the old group name.
@@ -7670,12 +7659,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in info messages.
#define DC_STR_CHAT_PROTECTION_ENABLED 170
/// "%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."
///
/// Used as the first info messages in newly created groups.
@@ -7712,7 +7695,12 @@ void dc_event_unref(dc_event_t* event);
/// Used in summaries.
#define DC_STR_REACTED_BY 177
/// "Establishing guaranteed end-to-end encryption, please wait…"
/// "Member %1$s removed."
///
/// `%1$s` will be replaced by name of the removed contact.
#define DC_STR_REMOVE_MEMBER 178
/// "Establishing connection, please wait…"
///
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT 190
@@ -7756,7 +7744,22 @@ void dc_event_unref(dc_event_t* event);
/// 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
#define DC_STR_SECURE_JOIN_CHANNEL_QR_DESC 201
/// "You joined the channel."
#define DC_STR_MSG_YOU_JOINED_CHANNEL 202
/// "%1$s invited you to join this channel. Waiting for the device of %2$s to reply…"
///
/// Added as an info-message directly after scanning a QR code for joining a broadcast channel.
///
/// `%1$s` and `%2$s` will both be replaced by the name of the inviter.
#define DC_STR_SECURE_JOIN_CHANNEL_STARTED 203
/// "The attachment contains anonymous usage statistics, which help us improve Delta Chat. Thank you!"
///
/// Used as the message body for statistics sent out.
#define DC_STR_STATS_MSG_BODY 210
/// "Proxy Enabled"
///

View File

@@ -559,6 +559,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
EventType::IncomingCallAccepted { .. } => 2560,
EventType::OutgoingCallAccepted { .. } => 2570,
EventType::CallEnded { .. } => 2580,
EventType::TransportsModified => 2600,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
@@ -593,7 +594,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
| EventType::AccountsBackgroundFetchDone
| EventType::ChatlistChanged
| EventType::AccountsChanged
| EventType::AccountsItemChanged => 0,
| EventType::AccountsItemChanged
| EventType::TransportsModified => 0,
EventType::IncomingReaction { contact_id, .. }
| EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int,
EventType::MsgsChanged { chat_id, .. }
@@ -681,7 +683,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
| EventType::IncomingCallAccepted { .. }
| EventType::OutgoingCallAccepted { .. }
| EventType::CallEnded { .. }
| EventType::EventChannelOverflow { .. } => 0,
| EventType::EventChannelOverflow { .. }
| EventType::TransportsModified => 0,
EventType::MsgsChanged { msg_id, .. }
| EventType::ReactionsChanged { msg_id, .. }
| EventType::IncomingReaction { msg_id, .. }
@@ -780,7 +783,8 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
| EventType::AccountsChanged
| EventType::AccountsItemChanged
| EventType::IncomingCallAccepted { .. }
| EventType::WebxdcRealtimeAdvertisementReceived { .. } => ptr::null_mut(),
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
| EventType::TransportsModified => ptr::null_mut(),
EventType::IncomingCall {
place_call_info, ..
} => {

View File

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

View File

@@ -21,9 +21,9 @@ 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::{
self, delete_msgs_ex, markseen_msgs, Message, MessageState, MsgId, Viewtype,
self, delete_msgs_ex, get_existing_msg_ids, get_msg_read_receipts, markseen_msgs, Message,
MessageState, MsgId, Viewtype,
};
use deltachat::peer_channels::{
leave_webxdc_realtime, send_webxdc_realtime_advertisement, send_webxdc_realtime_data,
@@ -34,6 +34,7 @@ use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::reaction::{get_msg_reactions, send_reaction};
use deltachat::securejoin;
use deltachat::stock_str::StockMessage;
use deltachat::storage_usage::get_storage_usage;
use deltachat::webxdc::StatusUpdateSerial;
use deltachat::EventEmitter;
use sanitize_filename::is_sanitized;
@@ -120,14 +121,14 @@ impl CommandApi {
}
}
async fn get_context_opt(&self, id: u32) -> Option<deltachat::context::Context> {
self.accounts.read().await.get_account(id)
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
let sc = self
.accounts
.read()
self.get_context_opt(id)
.await
.get_account(id)
.ok_or_else(|| anyhow!("account with id {id} not found"))?;
Ok(sc)
.ok_or_else(|| anyhow!("account with id {id} not found"))
}
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
@@ -366,6 +367,13 @@ impl CommandApi {
ctx.get_info().await
}
/// Get storage usage report as formatted string
async fn get_storage_usage_report_string(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let storage_usage = get_storage_usage(&ctx).await?;
Ok(storage_usage.to_string())
}
/// Get the blob dir.
async fn get_blob_dir(&self, account_id: u32) -> Result<Option<String>> {
let ctx = self.get_context(account_id).await?;
@@ -1295,6 +1303,24 @@ impl CommandApi {
.collect())
}
/// Checks if the messages with given IDs exist.
///
/// Returns IDs of existing messages.
async fn get_existing_msg_ids(&self, account_id: u32, msg_ids: Vec<u32>) -> Result<Vec<u32>> {
if let Some(context) = self.get_context_opt(account_id).await {
let msg_ids: Vec<MsgId> = msg_ids.into_iter().map(MsgId::new).collect();
let existing_msg_ids = get_existing_msg_ids(&context, &msg_ids).await?;
Ok(existing_msg_ids
.into_iter()
.map(|msg_id| msg_id.to_u32())
.collect())
} else {
// Account does not exist, so messages do not exist either,
// but this is not an error.
Ok(Vec::new())
}
}
async fn get_message_list_items(
&self,
account_id: u32,

View File

@@ -15,7 +15,7 @@ pub enum Account {
display_name: Option<String>,
addr: Option<String>,
// size: u32,
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
profile_image: Option<String>,
color: String,
/// Optional tag as "Work", "Family".
/// Meant to help profile owner to differ between profiles with similar names.

View File

@@ -69,7 +69,7 @@ pub struct FullChat {
// but that would be an extra DB query.
self_in_group: bool,
is_muted: bool,
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
ephemeral_timer: u32,
can_send: bool,
was_seen_recently: bool,
mailing_list_address: Option<String>,

View File

@@ -460,6 +460,15 @@ pub enum EventType {
/// ID of the chat which the message belongs to.
chat_id: u32,
},
/// One or more transports has changed.
///
/// This event is used for tests to detect when transport
/// synchronization messages arrives.
/// UIs don't need to use it, it is unlikely
/// that user modifies transports on multiple
/// devices simultaneously.
TransportsModified,
}
impl From<CoreEventType> for EventType {
@@ -642,6 +651,8 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(),
chat_id: chat_id.to_u32(),
},
CoreEventType::TransportsModified => TransportsModified,
#[allow(unreachable_patterns)]
#[cfg(test)]
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),

View File

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

View File

@@ -64,6 +64,7 @@ describe("online tests", function () {
await dc.rpc.setConfig(accountId1, "addr", account1.email);
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
await dc.rpc.configure(accountId1);
await waitForEvent(dc, "ImapInboxIdle", accountId1);
accountId2 = await dc.rpc.addAccount();
await dc.rpc.batchSetConfig(accountId2, {
@@ -71,6 +72,7 @@ describe("online tests", function () {
mail_pw: account2.password,
});
await dc.rpc.configure(accountId2);
await waitForEvent(dc, "ImapInboxIdle", accountId2);
accountsConfigured = true;
});

View File

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

View File

@@ -30,6 +30,15 @@ $ pip install .
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
## Activating current checkout of deltachat-rpc-client and -server for development
Go to root repository directory and run:
```
$ scripts/make-rpc-testenv.sh
$ source venv/bin/activate
```
## Using in REPL
Setup a development environment:

View File

@@ -1,20 +1,18 @@
[build-system]
requires = ["setuptools>=45"]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "2.27.0"
version = "2.30.0"
license = "MPL-2.0"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -24,7 +22,7 @@ classifiers = [
"Topic :: Communications :: Email"
]
readme = "README.md"
requires-python = ">=3.8"
requires-python = ">=3.10"
[tool.setuptools.package-data]
deltachat_rpc_client = [

View File

@@ -130,6 +130,10 @@ class Account:
"""Add a new transport using a QR code."""
yield self._rpc.add_transport_from_qr.future(self.id, qr)
def delete_transport(self, addr: str):
"""Delete a transport."""
self._rpc.delete_transport(self.id, addr)
@futuremethod
def list_transports(self):
"""Return the list of all email accounts that are used as a transport in the current profile."""

View File

@@ -80,6 +80,7 @@ class EventType(str, Enum):
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
TRANSPORTS_MODIFIED = "TransportsModified"
class ChatId(IntEnum):

View File

@@ -60,6 +60,10 @@ class Message:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
def exists(self) -> bool:
"""Return True if the message exists."""
return bool(self._rpc.get_existing_msg_ids(self.account.id, [self.id]))
def continue_autocrypt_key_transfer(self, setup_code: str) -> None:
"""Continue the Autocrypt Setup Message key transfer.

View File

@@ -3,9 +3,14 @@
from __future__ import annotations
import os
import pathlib
import platform
import random
import subprocess
import sys
from typing import AsyncGenerator, Optional
import execnet
import py
import pytest
@@ -20,6 +25,18 @@ Currently this is "End-to-end encryption available".
"""
def pytest_report_header():
for base in os.get_exec_path():
fn = pathlib.Path(base).joinpath(base, "deltachat-rpc-server")
if fn.exists():
proc = subprocess.Popen([str(fn), "--version"], stderr=subprocess.PIPE)
proc.wait()
version = proc.stderr.read().decode().strip()
return f"deltachat-rpc-server: {fn} [{version}]"
return None
class ACFactory:
"""Test account factory."""
@@ -40,12 +57,17 @@ class ACFactory:
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
return f"{username}@{domain}", f"{username}${username}"
def get_account_qr(self):
"""Return "dcaccount:" QR code for testing chatmail relay."""
domain = os.getenv("CHATMAIL_DOMAIN")
return f"dcaccount:{domain}"
@futuremethod
def new_configured_account(self):
"""Create a new configured account."""
account = self.get_unconfigured_account()
domain = os.getenv("CHATMAIL_DOMAIN")
yield account.add_transport_from_qr.future(f"dcaccount:{domain}")
qr = self.get_account_qr()
yield account.add_transport_from_qr.future(qr)
assert account.is_configured()
return account
@@ -77,6 +99,7 @@ class ACFactory:
ac_clone = self.get_unconfigured_account()
for transport in transports:
ac_clone.add_or_update_transport(transport)
ac_clone.bring_online()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
@@ -191,3 +214,134 @@ def log():
print(" " + msg)
return Printer()
#
# support for testing against different deltachat-rpc-server/clients
# installed into a temporary virtualenv and connected via 'execnet' channels
#
def find_path(venv, name):
is_windows = platform.system() == "Windows"
bin = venv / ("bin" if not is_windows else "Scripts")
tryadd = [""]
if is_windows:
tryadd += os.environ["PATHEXT"].split(os.pathsep)
for ext in tryadd:
p = bin.joinpath(name + ext)
if p.exists():
return str(p)
return None
@pytest.fixture(scope="session")
def get_core_python_env(tmp_path_factory):
"""Return a factory to create virtualenv environments with rpc server/client packages
installed.
The factory takes a version and returns a (python_path, rpc_server_path) tuple
of the respective binaries in the virtualenv.
"""
envs = {}
def get_versioned_venv(core_version):
venv = envs.get(core_version)
if not venv:
venv = tmp_path_factory.mktemp(f"temp-{core_version}")
subprocess.check_call([sys.executable, "-m", "venv", venv])
python = find_path(venv, "python")
pkgs = [f"deltachat-rpc-server=={core_version}", f"deltachat-rpc-client=={core_version}", "pytest"]
subprocess.check_call([python, "-m", "pip", "install"] + pkgs)
envs[core_version] = venv
python = find_path(venv, "python")
rpc_server_path = find_path(venv, "deltachat-rpc-server")
print(f"python={python}\nrpc_server={rpc_server_path}")
return python, rpc_server_path
return get_versioned_venv
@pytest.fixture
def alice_and_remote_bob(tmp_path, acfactory, get_core_python_env):
"""return local Alice account, a contact to bob, and a remote 'eval' function for bob.
The 'eval' function allows to remote-execute arbitrary expressions
that can use the `bob` online account, and the `bob_contact_alice`.
"""
def factory(core_version):
python, rpc_server_path = get_core_python_env(core_version)
gw = execnet.makegateway(f"popen//python={python}")
accounts_dir = str(tmp_path.joinpath("account1_venv1"))
channel = gw.remote_exec(remote_bob_loop)
cm = os.environ.get("CHATMAIL_DOMAIN")
# trigger getting an online account on bob's side
channel.send((accounts_dir, str(rpc_server_path), cm))
# meanwhile get a local alice account
alice = acfactory.get_online_account()
channel.send(alice.self_contact.make_vcard())
# wait for bob to have started
sysinfo = channel.receive()
assert sysinfo == f"v{core_version}"
bob_vcard = channel.receive()
[alice_contact_bob] = alice.import_vcard(bob_vcard)
def eval(eval_str):
channel.send(eval_str)
return channel.receive()
return alice, alice_contact_bob, eval
return factory
def remote_bob_loop(channel):
# This function executes with versioned
# deltachat-rpc-client/server packages
# installed into the virtualenv.
#
# The "channel" argument is a send/receive pipe
# to the process that runs the corresponding remote_exec(remote_bob_loop)
import os
from deltachat_rpc_client import DeltaChat, Rpc
from deltachat_rpc_client.pytestplugin import ACFactory
accounts_dir, rpc_server_path, chatmail_domain = channel.receive()
os.environ["CHATMAIL_DOMAIN"] = chatmail_domain
# older core versions don't support specifying rpc_server_path
# so we can't just pass `rpc_server_path` argument to Rpc constructor
basepath = os.path.dirname(rpc_server_path)
os.environ["PATH"] = os.pathsep.join([basepath, os.environ["PATH"]])
rpc = Rpc(accounts_dir=accounts_dir)
with rpc:
dc = DeltaChat(rpc)
channel.send(dc.rpc.get_system_info()["deltachat_core_version"])
acfactory = ACFactory(dc)
bob = acfactory.get_online_account()
alice_vcard = channel.receive()
[alice_contact] = bob.import_vcard(alice_vcard)
ns = {"bob": bob, "bob_contact_alice": alice_contact}
channel.send(bob.self_contact.make_vcard())
while 1:
eval_str = channel.receive()
res = eval(eval_str, ns)
try:
channel.send(res)
except Exception:
# some unserializable result
channel.send(None)

View File

@@ -9,7 +9,7 @@ import os
import subprocess
import sys
from queue import Empty, Queue
from threading import Event, Thread
from threading import Thread
from typing import Any, Iterator, Optional
@@ -17,25 +17,6 @@ class JsonRpcError(Exception):
"""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:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
class RpcMethod:
"""RPC method."""
@@ -57,20 +38,26 @@ class RpcMethod:
"params": args,
"id": request_id,
}
event = Event()
self.rpc.request_events[request_id] = event
self.rpc.request_results[request_id] = queue = Queue()
self.rpc.request_queue.put(request)
return RpcFuture(self.rpc, request_id, event)
def rpc_future():
"""Wait for the request to receive a result."""
response = queue.get()
if "error" in response:
raise JsonRpcError(response["error"])
return response.get("result", None)
return rpc_future
class Rpc:
"""RPC client."""
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
def __init__(self, accounts_dir: Optional[str] = None, rpc_server_path="deltachat-rpc-server", **kwargs):
"""Initialize RPC client.
The given arguments will be passed to subprocess.Popen().
The 'kwargs' arguments will be passed to subprocess.Popen().
"""
if accounts_dir:
kwargs["env"] = {
@@ -79,13 +66,12 @@ class Rpc:
}
self._kwargs = kwargs
self.rpc_server_path = rpc_server_path
self.process: subprocess.Popen
self.id_iterator: Iterator[int]
self.event_queues: dict[int, Queue]
# Map from request ID to `threading.Event`.
self.request_events: dict[int, Event]
# Map from request ID to the result.
self.request_results: dict[int, Any]
# Map from request ID to a Queue which provides a single result
self.request_results: dict[int, Queue]
self.request_queue: Queue[Any]
self.closing: bool
self.reader_thread: Thread
@@ -94,27 +80,18 @@ class Rpc:
def start(self) -> None:
"""Start RPC server subprocess."""
popen_kwargs = {"stdin": subprocess.PIPE, "stdout": subprocess.PIPE}
if sys.version_info >= (3, 11):
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# Prevent subprocess from capturing SIGINT.
process_group=0,
**self._kwargs,
)
# Prevent subprocess from capturing SIGINT.
popen_kwargs["process_group"] = 0
else:
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# `process_group` is not supported before Python 3.11.
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
# `process_group` is not supported before Python 3.11.
popen_kwargs["preexec_fn"] = os.setpgrp # noqa: PLW1509
popen_kwargs.update(self._kwargs)
self.process = subprocess.Popen(self.rpc_server_path, **popen_kwargs)
self.id_iterator = itertools.count(start=1)
self.event_queues = {}
self.request_events = {}
self.request_results = {}
self.request_queue = Queue()
self.closing = False
@@ -149,9 +126,7 @@ class Rpc:
response = json.loads(line)
if "id" in response:
response_id = response["id"]
event = self.request_events.pop(response_id)
self.request_results[response_id] = response
event.set()
self.request_results.pop(response_id).put(response)
else:
logging.warning("Got a response without ID: %s", response)
except Exception:

View File

@@ -0,0 +1,44 @@
import subprocess
import pytest
from deltachat_rpc_client import DeltaChat, Rpc
def test_install_venv_and_use_other_core(tmp_path, get_core_python_env):
python, rpc_server_path = get_core_python_env("2.24.0")
subprocess.check_call([python, "-m", "pip", "install", "deltachat-rpc-server==2.24.0"])
rpc = Rpc(accounts_dir=tmp_path.joinpath("accounts"), rpc_server_path=rpc_server_path)
with rpc:
dc = DeltaChat(rpc)
assert dc.rpc.get_system_info()["deltachat_core_version"] == "v2.24.0"
@pytest.mark.parametrize("version", ["2.24.0"])
def test_qr_setup_contact(alice_and_remote_bob, version) -> None:
"""Test other-core Bob profile can do securejoin with Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob(version)
qr_code = alice.get_qr_code()
remote_eval(f"bob.secure_join({qr_code!r})")
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
remote_eval("bob.wait_for_securejoin_joiner_success()")
# Test that Bob verified Alice's profile.
assert remote_eval("bob_contact_alice.get_snapshot().is_verified")
def test_send_and_receive_message(alice_and_remote_bob) -> None:
"""Test other-core Bob profile can send a message to Alice on current core."""
alice, alice_contact_bob, remote_eval = alice_and_remote_bob("2.20.0")
remote_eval("bob_contact_alice.create_chat().send_text('hello')")
msg = alice.wait_for_incoming_msg()
assert msg.get_snapshot().text == "hello"

View File

@@ -143,7 +143,7 @@ def test_delete_deltachat_folder(acfactory, direct_imap):
# Wait until new folder is created and UIDVALIDITY is updated.
while True:
event = ac1.wait_for_event()
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
if event.kind == EventType.INFO and "transport 1: UID validity for folder DeltaChat changed from " in event.msg:
break
ac2 = acfactory.get_online_account()

View File

@@ -0,0 +1,203 @@
import pytest
from deltachat_rpc_client import EventType
from deltachat_rpc_client.rpc import JsonRpcError
def test_add_second_address(acfactory) -> None:
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
# When the first transport is created,
# mvbox_move and only_fetch_mvbox should be disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 2
account.add_transport_from_qr(qr)
assert len(account.list_transports()) == 3
first_addr = account.list_transports()[0]["addr"]
second_addr = account.list_transports()[1]["addr"]
# Cannot delete the first address.
with pytest.raises(JsonRpcError):
account.delete_transport(first_addr)
account.delete_transport(second_addr)
assert len(account.list_transports()) == 2
# Enabling mvbox_move or only_fetch_mvbox
# is not allowed when multi-transport is enabled.
for option in ["mvbox_move", "only_fetch_mvbox"]:
with pytest.raises(JsonRpcError):
account.set_config(option, "1")
with pytest.raises(JsonRpcError):
account.set_config("show_emails", "0")
@pytest.mark.parametrize("key", ["mvbox_move", "only_fetch_mvbox"])
def test_no_second_transport_with_mvbox(acfactory, key) -> None:
"""Test that second transport cannot be configured if mvbox is used."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("mvbox_move") == "0"
assert account.get_config("only_fetch_mvbox") == "0"
qr = acfactory.get_account_qr()
account.set_config(key, "1")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_no_second_transport_without_classic_emails(acfactory) -> None:
"""Test that second transport cannot be configured if classic emails are not fetched."""
account = acfactory.new_configured_account()
assert len(account.list_transports()) == 1
assert account.get_config("show_emails") == "2"
qr = acfactory.get_account_qr()
account.set_config("show_emails", "0")
with pytest.raises(JsonRpcError):
account.add_transport_from_qr(qr)
def test_change_address(acfactory) -> None:
"""Test Alice configuring a second transport and setting it as a primary one."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("configured_addr")
bob.create_chat(alice)
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello!")
msg1 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr1 = msg1.sender.get_snapshot().address
alice.stop_io()
old_alice_addr = alice.get_config("configured_addr")
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr in alice_vcard
qr = acfactory.get_account_qr()
alice.add_transport_from_qr(qr)
new_alice_addr = alice.list_transports()[1]["addr"]
with pytest.raises(JsonRpcError):
# Cannot use the address that is not
# configured for any transport.
alice.set_config("configured_addr", bob_addr)
# Load old address so it is cached.
assert alice.get_config("configured_addr") == old_alice_addr
alice.set_config("configured_addr", new_alice_addr)
# Make sure that setting `configured_addr` invalidated the cache.
assert alice.get_config("configured_addr") == new_alice_addr
alice_vcard = alice.self_contact.make_vcard()
assert old_alice_addr not in alice_vcard
assert new_alice_addr in alice_vcard
with pytest.raises(JsonRpcError):
alice.delete_transport(new_alice_addr)
alice.start_io()
alice_chat_bob.send_text("Hello again!")
msg2 = bob.wait_for_incoming_msg().get_snapshot()
sender_addr2 = msg2.sender.get_snapshot().address
assert msg1.sender == msg2.sender
assert sender_addr1 != sender_addr2
assert sender_addr1 == old_alice_addr
assert sender_addr2 == new_alice_addr
@pytest.mark.parametrize("is_chatmail", ["0", "1"])
def test_mvbox_move_first_transport(acfactory, is_chatmail) -> None:
"""Test that mvbox_move is disabled by default even for non-chatmail accounts.
Disabling mvbox_move is required to be able to setup a second transport.
"""
account = acfactory.get_unconfigured_account()
account.set_config("fix_is_chatmail", "1")
account.set_config("is_chatmail", is_chatmail)
# The default value when the setting is unset is "1".
# This is not changed for compatibility with old databases
# imported from backups.
assert account.get_config("mvbox_move") == "1"
qr = acfactory.get_account_qr()
account.add_transport_from_qr(qr)
# Once the first transport is set up,
# mvbox_move is disabled.
assert account.get_config("mvbox_move") == "0"
assert account.get_config("is_chatmail") == is_chatmail
def test_reconfigure_transport(acfactory) -> None:
"""Test that reconfiguring the transport works
even if settings not supported for multi-transport
like mvbox_move are enabled."""
account = acfactory.get_online_account()
account.set_config("mvbox_move", "1")
[transport] = account.list_transports()
account.add_or_update_transport(transport)
# Reconfiguring the transport should not reset
# the settings as if when configuring the first transport.
assert account.get_config("mvbox_move") == "1"
def test_transport_synchronization(acfactory, log) -> None:
"""Test synchronization of transports between devices."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_clone = ac1.clone()
ac1_clone.bring_online()
qr = acfactory.get_account_qr()
ac1.add_transport_from_qr(qr)
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 2
assert len(ac1_clone.list_transports()) == 2
ac1_clone.add_transport_from_qr(qr)
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
assert len(ac1.list_transports()) == 3
assert len(ac1_clone.list_transports()) == 3
log.section("ac1 clone removes second transport")
[transport1, transport2, transport3] = ac1_clone.list_transports()
addr3 = transport3["addr"]
ac1_clone.delete_transport(transport2["addr"])
ac1.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport1, transport3] = ac1.list_transports()
log.section("ac1 changes the primary transport")
ac1.set_config("configured_addr", transport3["addr"])
log.section("ac1 removes the first transport")
ac1.delete_transport(transport1["addr"])
ac1_clone.wait_for_event(EventType.TRANSPORTS_MODIFIED)
[transport3] = ac1_clone.list_transports()
assert transport3["addr"] == addr3
assert ac1_clone.get_config("configured_addr") == addr3
ac2_chat = ac2.create_chat(ac1)
ac2_chat.send_text("Hello!")
assert ac1.wait_for_incoming_msg().get_snapshot().text == "Hello!"
assert ac1_clone.wait_for_incoming_msg().get_snapshot().text == "Hello!"

View File

@@ -158,29 +158,29 @@ def test_qr_securejoin_broadcast(acfactory, all_devices_online):
chat = get_broadcast(ac)
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs[0].get_snapshot()
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
member_added_msg = chat_msgs[1].get_snapshot()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert "invited you to join this channel" in first_msg.text
assert first_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
else:
assert member_added_msg.text == "You joined the channel."
assert member_added_msg.is_info
hello_msg = chat_msgs[2].get_snapshot()
hello_msg = chat_msgs.pop(0).get_snapshot()
assert hello_msg.text == "Hello everyone!"
assert not hello_msg.is_info
assert hello_msg.show_padlock
assert hello_msg.error is None
assert len(chat_msgs) == 3
assert len(chat_msgs) == 0
chat_snapshot = chat.get_full_snapshot()
assert chat_snapshot.is_encrypted

View File

@@ -467,7 +467,7 @@ def test_bot(acfactory) -> None:
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
alice = acfactory.get_online_account()
# Create a bot account so it does not receive device messages in the beginning.
addr, password = acfactory.get_credentials()
@@ -475,6 +475,7 @@ def test_wait_next_messages(acfactory) -> None:
bot.set_config("bot", "1")
bot.add_or_update_transport({"addr": addr, "password": password})
assert bot.is_configured()
bot.bring_online()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
@@ -660,8 +661,6 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
contact = alice.create_contact(account)
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
@@ -677,15 +676,7 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
alice_group.send_file(str(path))
snapshot = bob.wait_for_incoming_msg().get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
assert snapshot.chat == bob_group
def test_markseen_contact_request(acfactory):
@@ -867,15 +858,15 @@ def test_leave_broadcast(acfactory, all_devices_online):
contact_snapshot = contact.get_snapshot()
chat_msgs = chat.get_messages()
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert first_msg.text == "Establishing guaranteed end-to-end encryption, please wait…"
assert first_msg.is_info
encrypted_msg = chat_msgs.pop(0).get_snapshot()
assert encrypted_msg.text == "Messages are end-to-end encrypted."
assert encrypted_msg.is_info
if please_wait_info_msg:
first_msg = chat_msgs.pop(0).get_snapshot()
assert "invited you to join this channel" in first_msg.text
assert first_msg.is_info
member_added_msg = chat_msgs.pop(0).get_snapshot()
if inviter_side:
assert member_added_msg.text == f"Member {contact_snapshot.display_name} added."
@@ -1002,3 +993,22 @@ def test_background_fetch(acfactory, dc):
snapshot = messages[-1].get_snapshot()
if snapshot.text == "Hello again!":
break
def test_message_exists(acfactory):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
message1 = chat.send_text("Hello!")
message2 = chat.send_text("Hello again!")
assert message1.exists()
assert message2.exists()
ac1.delete_messages([message1])
assert not message1.exists()
assert message2.exists()
# There is no error when checking if
# the message exists for deleted account.
ac1.remove()
assert not message1.exists()
assert not message2.exists()

View File

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

View File

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

View File

@@ -38,6 +38,7 @@ skip = [
{ name = "rand", version = "0.8.5" },
{ name = "rustix", version = "0.38.44" },
{ name = "serdect", version = "0.2.0" },
{ name = "socket2", version = "0.5.9" },
{ name = "spin", version = "0.9.8" },
{ name = "strum_macros", version = "0.26.2" },
{ name = "strum", version = "0.26.2" },
@@ -62,7 +63,6 @@ skip = [
{ name = "windows_x86_64_gnu" },
{ name = "windows_x86_64_gnullvm" },
{ name = "windows_x86_64_msvc" },
{ name = "zerocopy", version = "0.7.32" },
]

18
flake.lock generated
View File

@@ -47,11 +47,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1747291057,
"narHash": "sha256-9Wir6aLJAeJKqdoQUiwfKdBn7SyNXTJGRSscRyVOo2Y=",
"lastModified": 1763361733,
"narHash": "sha256-ka7dpwH3HIXCyD2wl5F7cPLeRbqZoY2ullALsvxdPt8=",
"owner": "nix-community",
"repo": "fenix",
"rev": "76ffc1b7b3ec8078fe01794628b6abff35cbda8f",
"rev": "6c8d48e3b0ae371b19ac1485744687b788e80193",
"type": "github"
},
"original": {
@@ -147,11 +147,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"lastModified": 1762977756,
"narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55",
"type": "github"
},
"original": {
@@ -202,11 +202,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1746889290,
"narHash": "sha256-h3LQYZgyv2l3U7r+mcsrEOGRldaK0zJFwAAva4hV/6g=",
"lastModified": 1762860488,
"narHash": "sha256-rMfWMCOo/pPefM2We0iMBLi2kLBAnYoB9thi4qS7uk4=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "2bafe9d96c6734aacfd49e115f6cf61e7adc68bc",
"rev": "2efc80078029894eec0699f62ec8d5c1a56af763",
"type": "github"
},
"original": {

View File

@@ -1,5 +1,5 @@
{
description = "Delta Chat core";
description = "Chatmail core";
inputs = {
fenix.url = "github:nix-community/fenix";
flake-utils.url = "github:numtide/flake-utils";
@@ -14,7 +14,15 @@
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs.stdenv) isDarwin;
fenixPkgs = fenix.packages.${system};
naersk' = pkgs.callPackage naersk { };
fenixToolchain = fenixPkgs.combine [
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
fenixPkgs.stable.rust-std
];
naersk' = pkgs.callPackage naersk {
cargo = fenixToolchain;
rustc = fenixToolchain;
};
manifest = (pkgs.lib.importTOML ./Cargo.toml).package;
androidSdk = android.sdk.${system} (sdkPkgs:
builtins.attrValues {
@@ -470,6 +478,12 @@
};
libdeltachat =
let
rustPlatform = (pkgs.makeRustPlatform {
cargo = fenixToolchain;
rustc = fenixToolchain;
});
in
pkgs.stdenv.mkDerivation {
pname = "libdeltachat";
version = manifest.version;
@@ -479,8 +493,9 @@
nativeBuildInputs = [
pkgs.perl # Needed to build vendored OpenSSL.
pkgs.cmake
pkgs.rustPlatform.cargoSetupHook
pkgs.cargo
rustPlatform.cargoSetupHook
fenixPkgs.stable.rustc
fenixPkgs.stable.cargo
];
postInstall = ''

View File

@@ -14,6 +14,7 @@ def datadir():
return None
@pytest.mark.skip("The test is flaky in CI and crashes the interpreter as of 2025-11-12")
def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating one echo_and_quit bot")
botproc = acfactory.run_bot_process(echo_and_quit)

View File

@@ -1,20 +1,20 @@
[build-system]
requires = ["setuptools>=45", "wheel", "cffi>=1.0.0", "pkgconfig"]
requires = ["setuptools>=77", "wheel", "cffi>=1.0.0", "pkgconfig"]
build-backend = "setuptools.build_meta"
[project]
name = "deltachat"
version = "2.27.0"
version = "2.30.0"
license = "MPL-2.0"
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
readme = "README.rst"
requires-python = ">=3.8"
requires-python = ">=3.10"
authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
@@ -23,7 +23,6 @@ classifiers = [
dependencies = [
"cffi>=1.0.0",
"imap-tools",
"importlib_metadata;python_version<'3.8'",
"pluggy",
"requests",
]

View File

@@ -35,7 +35,7 @@ class TestOfflineAccountBasic:
d = ac1.get_info()
assert d["arch"]
assert d["number_of_chats"] == "0"
assert d["bcc_self"] == "1"
assert d["bcc_self"] == "0"
def test_is_not_configured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -69,7 +69,7 @@ class TestOfflineAccountBasic:
def test_has_bccself(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
assert ac1.get_config("bcc_self") == "1"
assert ac1.get_config("bcc_self") == "0"
def test_selfcontact_if_unconfigured(self, acfactory):
ac1 = acfactory.get_unconfigured_account()

View File

@@ -46,7 +46,7 @@ deps =
commands =
ruff format --diff setup.py src/deltachat examples/ tests/
ruff check src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst
rst-lint README.rst
[testenv:mypy]
deps =

View File

@@ -1 +1 @@
2025-11-16
2025-12-04

View File

@@ -26,10 +26,10 @@ and an own build machine.
i.e. `deltachat-rpc-client` and `deltachat-rpc-server`.
- `remote_tests_python.sh` rsyncs to a build machine and runs
`run-python-test.sh` remotely on the build machine.
JSON-RPC Python tests remotely on the build machine.
- `remote_tests_rust.sh` rsyncs to the build machine and runs
`run-rust-test.sh` remotely on the build machine.
Rust tests remotely on the build machine.
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/

View File

@@ -1,45 +1,32 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
BUILD_ID=${1:?specify build ID}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
set -x
if ! test -v SSHTARGET; then
echo >&2 SSHTARGET is not set
exit 1
fi
BUILDDIR=ci_builds/chatmailcore
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
set -xe
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
# we seem to need .git for setuptools_scm versioning
find .git >>.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
set +x
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running Python tests remotely"
ssh $SSHTARGET <<_HERE
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
export RUSTC_WRAPPER=\`command -v sccache\`
cd $BUILDDIR
export TARGET=release
export CHATMAIL_DOMAIN=$CHATMAIL_DOMAIN
#we rely on tox/virtualenv being available in the host
#rm -rf virtualenv venv
#virtualenv -q -p python3.7 venv
#source venv/bin/activate
#pip install -q tox virtualenv
scripts/make-rpc-testenv.sh
. venv/bin/activate
set -x
which python
source \$HOME/venv/bin/activate
which python
bash scripts/run-python-test.sh
cd deltachat-rpc-client
pytest -n6 $@
_HERE

View File

@@ -1,29 +1,25 @@
#!/bin/bash
#!/usr/bin/env bash
set -euo pipefail
BUILD_ID=${1:?specify build ID}
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
BUILDDIR=ci_builds/$BUILD_ID
set -e
if ! test -v SSHTARGET; then
echo >&2 SSHTARGET is not set
exit 1
fi
BUILDDIR=ci_builds/chatmailcore
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
git ls-files >.rsynclist
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
rsync -az --delete --mkpath --files-from=<(git ls-files) ./ "$SSHTARGET:$BUILDDIR"
echo "--- Running Rust tests remotely"
ssh $SSHTARGET <<_HERE
ssh -oBatchMode=yes -- "$SSHTARGET" <<_HERE
set +x -e
# make sure all processes exit when ssh dies
shopt -s huponexit
export RUSTC_WRAPPER=\`which sccache\`
export RUSTC_WRAPPER=\`command -v sccache\`
cd $BUILDDIR
export TARGET=x86_64-unknown-linux-gnu
export RUSTC_WRAPPER=sccache
bash scripts/run-rust-test.sh
cargo nextest run
_HERE

View File

@@ -31,6 +31,6 @@ unset CHATMAIL_DOMAIN
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
tox --workdir "$TOXWORKDIR" -e py38,py39,py310,py311,py312,py313,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py310,py311,py312,py313,pypy310 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

View File

@@ -678,13 +678,12 @@ impl Config {
// Convert them to relative paths.
let mut modified = false;
for account in &mut config.inner.accounts {
if account.dir.is_absolute() {
if let Some(old_path_parent) = account.dir.parent() {
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
account.dir = new_path.to_path_buf();
modified = true;
}
}
if account.dir.is_absolute()
&& let Some(old_path_parent) = account.dir.parent()
&& let Ok(new_path) = account.dir.strip_prefix(old_path_parent)
{
account.dir = new_path.to_path_buf();
modified = true;
}
}
if modified && writable {

View File

@@ -173,11 +173,8 @@ async fn test_selfavatar_outside_blobdir() {
.unwrap();
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
let avatar_path = Path::new(&avatar_blob);
assert!(
avatar_blob.ends_with("7dde69e06b5ae6c27520a436bbfd65b.jpg"),
"The avatar filename should be its hash, put instead it's {avatar_blob}"
);
let scaled_avatar_size = file_size(avatar_path).await;
info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
check_image_size(avatar_src, 1000, 1000);
@@ -187,6 +184,11 @@ async fn test_selfavatar_outside_blobdir() {
constants::BALANCED_AVATAR_SIZE,
);
assert!(
avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
"The avatar filename should be its hash, but instead it's {avatar_blob}"
);
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
let viewtype = &mut Viewtype::Image;
let strict_limits = true;

View File

@@ -6,7 +6,7 @@ use crate::chat::ChatIdBlocked;
use crate::chat::{Chat, ChatId, send_msg};
use crate::constants::{Blocked, Chattype};
use crate::contact::ContactId;
use crate::context::Context;
use crate::context::{Context, WeakContext};
use crate::events::EventType;
use crate::headerdef::HeaderDef;
use crate::log::warn;
@@ -199,8 +199,9 @@ impl Context {
call.id = send_msg(self, chat_id, &mut call).await?;
let wait = RINGING_SECONDS;
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
context,
wait.try_into()?,
call.id,
));
@@ -291,11 +292,12 @@ impl Context {
}
async fn emit_end_call_if_unaccepted(
context: Context,
context: WeakContext,
wait: u64,
call_id: MsgId,
) -> Result<()> {
sleep(Duration::from_secs(wait)).await;
let context = context.upgrade()?;
let Some(mut call) = context.load_call_by_id(call_id).await? else {
warn!(
context,
@@ -368,8 +370,9 @@ impl Context {
}
}
let wait = call.remaining_ring_seconds();
let context = self.get_weak_context();
task::spawn(Context::emit_end_call_if_unaccepted(
self.clone(),
context,
wait.try_into()?,
call.msg.id,
));

View File

@@ -301,7 +301,7 @@ impl ChatId {
let chat = Chat::load_from_db(context, chat_id).await?;
if chat.is_encrypted(context).await? {
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
}
info!(
@@ -462,19 +462,15 @@ impl ChatId {
}
/// Adds message "Messages are end-to-end encrypted".
pub(crate) async fn add_encrypted_msg(
self,
context: &Context,
timestamp_sort: i64,
) -> Result<()> {
pub(crate) async fn add_e2ee_notice(self, context: &Context, timestamp: i64) -> Result<()> {
let text = stock_str::messages_e2e_encrypted(context).await;
add_info_msg_with_cmd(
context,
self,
&text,
SystemMessage::ChatE2ee,
timestamp_sort,
None,
Some(timestamp),
timestamp,
None,
None,
None,
@@ -740,16 +736,15 @@ impl ChatId {
}
}
_ => {
if msg.viewtype == Viewtype::File {
if let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
if msg.viewtype == Viewtype::File
&& let Some((better_type, _)) = message::guess_msgtype_from_suffix(msg)
// We do not do an automatic conversion to other viewtypes here so that
// users can send images as "files" to preserve the original quality
// (usually we compress images). The remaining conversions are done by
// `prepare_msg_blob()` later.
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
{
msg.viewtype = better_type;
}
{
msg.viewtype = better_type;
}
if msg.viewtype == Viewtype::Vcard {
let blob = msg
@@ -767,13 +762,13 @@ impl ChatId {
msg.chat_id = self;
// if possible, replace existing draft and keep id
if !msg.id.is_special() {
if let Some(old_draft) = self.get_draft(context).await? {
if old_draft.id == msg.id
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
if !msg.id.is_special()
&& let Some(old_draft) = self.get_draft(context).await?
&& old_draft.id == msg.id
&& old_draft.chat_id == self
&& old_draft.state == MessageState::OutDraft
{
let affected_rows = context
.sql.execute(
"UPDATE msgs
SET timestamp=?1,type=?2,txt=?3,txt_normalized=?4,param=?5,mime_in_reply_to=?6
@@ -793,9 +788,7 @@ impl ChatId {
msg.id,
),
).await?;
return Ok(affected_rows > 0);
}
}
return Ok(affected_rows > 0);
}
let row_id = context
@@ -993,11 +986,11 @@ impl ChatId {
let mut res = Vec::new();
let now = time();
for (chat_id, metric) in chats_with_metrics {
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await? {
if now > chat_timestamp + 42 * 24 * 3600 {
// Chat was inactive for 42 days, skip.
continue;
}
if let Some(chat_timestamp) = chat_id.get_timestamp(context).await?
&& now > chat_timestamp + 42 * 24 * 3600
{
// Chat was inactive for 42 days, skip.
continue;
}
if metric < 0.1 {
@@ -1252,10 +1245,10 @@ impl ChatId {
None
};
if let Some(last_msg_time) = last_msg_time {
if last_msg_time > sort_timestamp {
sort_timestamp = last_msg_time;
}
if let Some(last_msg_time) = last_msg_time
&& last_msg_time > sort_timestamp
{
sort_timestamp = last_msg_time;
}
Ok(sort_timestamp)
@@ -1376,10 +1369,10 @@ impl Chat {
let mut chat_name = "Err [Name not found]".to_owned();
match get_chat_contacts(context, chat.id).await {
Ok(contacts) => {
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
contact.get_display_name().clone_into(&mut chat_name);
}
if let Some(contact_id) = contacts.first()
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
{
contact.get_display_name().clone_into(&mut chat_name);
}
}
Err(err) => {
@@ -1576,10 +1569,10 @@ impl Chat {
if self.typ == Chattype::Single {
let contacts = get_chat_contacts(context, self.id).await?;
if let Some(contact_id) = contacts.first() {
if let Ok(contact) = Contact::get_by_id(context, *contact_id).await {
color = contact.get_color();
}
if let Some(contact_id) = contacts.first()
&& let Ok(contact) = Contact::get_by_id(context, *contact_id).await
{
color = contact.get_color();
}
} else if !self.grpid.is_empty() {
color = str_to_color(&self.grpid);
@@ -1841,8 +1834,8 @@ impl Chat {
}
// add independent location to database
if msg.param.exists(Param::SetLatitude) {
if let Ok(row_id) = context
if msg.param.exists(Param::SetLatitude)
&& let Ok(row_id) = context
.sql
.insert(
"INSERT INTO locations \
@@ -1857,9 +1850,8 @@ impl Chat {
),
)
.await
{
location_id = row_id;
}
{
location_id = row_id;
}
let ephemeral_timer = if msg.param.get_cmd() == SystemMessage::EphemeralTimerChanged {
@@ -2429,7 +2421,7 @@ impl ChatIdBlocked {
&& !chat.param.exists(Param::Devicetalk)
&& !chat.param.exists(Param::Selftalk)
{
chat_id.add_encrypted_msg(context, smeared_time).await?;
chat_id.add_e2ee_notice(context, smeared_time).await?;
}
Ok(Self {
@@ -2497,18 +2489,18 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
msg.param.set(Param::File, blob.as_name());
}
if !msg.param.exists(Param::MimeType) {
if let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg) {
// If we unexpectedly didn't recognize the file as image, don't send it as such,
// either the format is unsupported or the image is corrupted.
let mime = match viewtype != Viewtype::Image
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
{
true => mime,
false => "application/octet-stream",
};
msg.param.set(Param::MimeType, mime);
}
if !msg.param.exists(Param::MimeType)
&& let Some((viewtype, mime)) = message::guess_msgtype_from_suffix(msg)
{
// If we unexpectedly didn't recognize the file as image, don't send it as such,
// either the format is unsupported or the image is corrupted.
let mime = match viewtype != Viewtype::Image
|| matches!(msg.viewtype, Viewtype::Image | Viewtype::Sticker)
{
true => mime,
false => "application/octet-stream",
};
msg.param.set(Param::MimeType, mime);
}
msg.try_calc_and_set_dimensions(context).await?;
@@ -2692,15 +2684,15 @@ async fn prepare_send_msg(
// This is meant as a last line of defence, the UI should check that before as well.
// (We allow Chattype::Single in general for "Reply Privately";
// checking for exact contact_id will produce false positives when ppl just left the group)
if chat.typ != Chattype::Single && !context.get_config_bool(Config::Bot).await? {
if let Some(quoted_message) = msg.quoted_message(context).await? {
if quoted_message.chat_id != chat_id {
bail!(
"Quote of message from {} cannot be sent to {chat_id}",
quoted_message.chat_id
);
}
}
if chat.typ != Chattype::Single
&& !context.get_config_bool(Config::Bot).await?
&& let Some(quoted_message) = msg.quoted_message(context).await?
&& quoted_message.chat_id != chat_id
{
bail!(
"Quote of message from {} cannot be sent to {chat_id}",
quoted_message.chat_id
);
}
// check current MessageState for drafts (to keep msg_id) ...
@@ -2830,16 +2822,15 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
let now = smeared_time(context);
if rendered_msg.last_added_location_id.is_some() {
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if rendered_msg.last_added_location_id.is_some()
&& let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await
{
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
}
if attach_selfavatar {
if let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await {
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
if attach_selfavatar && let Err(err) = msg.chat_id.set_selfavatar_timestamp(context, now).await
{
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
}
if rendered_msg.is_encrypted {
@@ -3457,7 +3448,7 @@ pub(crate) async fn create_group_ex(
if !grpid.is_empty() {
// Add "Messages are end-to-end encrypted." message.
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
}
if !context.get_config_bool(Config::Bot).await?
@@ -3470,7 +3461,7 @@ pub(crate) async fn create_group_ex(
// Add "Messages in this chat use classic email and are not encrypted." message.
stock_str::chat_unencrypted_explanation(context).await
};
add_info_msg(context, chat_id, &text, create_smeared_timestamp(context)).await?;
add_info_msg(context, chat_id, &text).await?;
}
if let (true, true) = (sync.into(), !grpid.is_empty()) {
let id = SyncId::Grpid(grpid);
@@ -3538,7 +3529,7 @@ pub(crate) async fn create_out_broadcast_ex(
Ok(chat_id)
};
let chat_id = context.sql.transaction(trans_fn).await?;
chat_id.add_encrypted_msg(context, timestamp).await?;
chat_id.add_e2ee_notice(context, timestamp).await?;
context.emit_msgs_changed_without_ids();
chatlist_events::emit_chatlist_changed(context);
@@ -3739,10 +3730,16 @@ pub(crate) async fn add_contact_to_chat_ex(
chat.typ != Chattype::OutBroadcast || contact_id != ContactId::SELF,
"Cannot add SELF to broadcast channel."
);
ensure!(
chat.is_encrypted(context).await? == contact.is_key_contact(),
"Only key-contacts can be added to encrypted chats"
);
match chat.is_encrypted(context).await? {
true => ensure!(
contact.is_key_contact(),
"Only key-contacts can be added to encrypted chats"
),
false => ensure!(
!contact.is_key_contact(),
"Only address-contacts can be added to unencrypted chats"
),
}
if !chat.is_self_in_chat(context).await? {
context.emit_event(EventType::ErrorSelfNotInGroup(
@@ -4483,11 +4480,11 @@ pub async fn add_device_msg_with_importance(
let mut chat_id = ChatId::new(0);
let mut msg_id = MsgId::new_unset();
if let Some(label) = label {
if was_device_msg_ever_added(context, label).await? {
info!(context, "Device-message {label} already added.");
return Ok(msg_id);
}
if let Some(label) = label
&& was_device_msg_ever_added(context, label).await?
{
info!(context, "Device-message {label} already added.");
return Ok(msg_id);
}
if let Some(msg) = msg {
@@ -4499,10 +4496,10 @@ pub async fn add_device_msg_with_importance(
// makes sure, the added message is the last one,
// even if the date is wrong (useful esp. when warning about bad dates)
msg.timestamp_sort = timestamp_sent;
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
if msg.timestamp_sort <= last_msg_time {
msg.timestamp_sort = last_msg_time + 1;
}
if let Some(last_msg_time) = chat_id.get_timestamp(context).await?
&& msg.timestamp_sort <= last_msg_time
{
msg.timestamp_sort = last_msg_time + 1;
}
prepare_msg_blob(context, msg).await?;
let state = MessageState::InFresh;
@@ -4621,9 +4618,11 @@ pub(crate) async fn add_info_msg_with_cmd(
chat_id: ChatId,
text: &str,
cmd: SystemMessage,
timestamp_sort: i64,
// Timestamp to show to the user (if this is None, `timestamp_sort` will be shown to the user)
timestamp_sent_rcvd: Option<i64>,
// Timestamp where in the chat the message will be sorted.
// If this is None, the message will be sorted to the bottom.
timestamp_sort: Option<i64>,
// Timestamp to show to the user
timestamp_sent_rcvd: i64,
parent: Option<&Message>,
from_id: Option<ContactId>,
added_removed_id: Option<ContactId>,
@@ -4639,6 +4638,22 @@ pub(crate) async fn add_info_msg_with_cmd(
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
}
let timestamp_sort = if let Some(ts) = timestamp_sort {
ts
} else {
let sort_to_bottom = true;
let (received, incoming) = (false, false);
chat_id
.calc_sort_timestamp(
context,
smeared_time(context),
sort_to_bottom,
received,
incoming,
)
.await?
};
let row_id =
context.sql.insert(
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
@@ -4648,8 +4663,8 @@ pub(crate) async fn add_info_msg_with_cmd(
from_id.unwrap_or(ContactId::INFO),
ContactId::INFO,
timestamp_sort,
timestamp_sent_rcvd.unwrap_or(0),
timestamp_sent_rcvd.unwrap_or(0),
timestamp_sent_rcvd,
timestamp_sent_rcvd,
Viewtype::Text,
MessageState::InNoticed,
text,
@@ -4669,19 +4684,14 @@ pub(crate) async fn add_info_msg_with_cmd(
}
/// Adds info message with a given text and `timestamp` to the chat.
pub(crate) async fn add_info_msg(
context: &Context,
chat_id: ChatId,
text: &str,
timestamp: i64,
) -> Result<MsgId> {
pub(crate) async fn add_info_msg(context: &Context, chat_id: ChatId, text: &str) -> Result<MsgId> {
add_info_msg_with_cmd(
context,
chat_id,
text,
SystemMessage::Unknown,
timestamp,
None,
time(),
None,
None,
None,

View File

@@ -815,15 +815,6 @@ async fn test_self_talk() -> Result<()> {
assert!(msg.get_showpadlock());
let sent_msg = t.pop_sent_msg().await;
let payload = sent_msg.payload();
// Make sure the `To` field contains the address and not
// "undisclosed recipients".
// Otherwise Delta Chat core <1.153.0 assigns the message
// to the trash chat.
assert_eq!(
payload.match_indices("To: <alice@example.org>\r\n").count(),
1
);
let t2 = TestContext::new_alice().await;
t2.recv_msg(&sent_msg).await;
@@ -1238,7 +1229,7 @@ async fn test_unarchive_if_muted() -> Result<()> {
chat_id.set_visibility(&t, ChatVisibility::Archived).await?;
set_muted(&t, chat_id, MuteDuration::Forever).await?;
send_text_msg(&t, chat_id, "out".to_string()).await?;
add_info_msg(&t, chat_id, "info", time()).await?;
add_info_msg(&t, chat_id, "info").await?;
assert_eq!(get_archived_cnt(&t).await?, 1);
// finally, unarchive on sending to not muted chat
@@ -1637,7 +1628,7 @@ async fn test_set_mute_duration() {
async fn test_add_info_msg() -> Result<()> {
let t = TestContext::new().await;
let chat_id = create_group(&t, "foo").await?;
add_info_msg(&t, chat_id, "foo info", time()).await?;
add_info_msg(&t, chat_id, "foo info").await?;
let msg = t.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_chat_id(), chat_id);
@@ -1659,8 +1650,8 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
chat_id,
"foo bar info",
SystemMessage::EphemeralTimerChanged,
time(),
None,
time(),
None,
None,
None,
@@ -3321,10 +3312,7 @@ async fn test_leave_broadcast_multidevice() -> Result<()> {
let leave_msg = bob0.pop_sent_msg().await;
let parsed = MimeMessage::from_bytes(bob1, leave_msg.payload().as_bytes(), None).await?;
assert_eq!(
parsed.parts[0].msg,
stock_str::msg_group_left_remote(bob0).await
);
assert_eq!(parsed.parts[0].msg, "I left the group.");
let rcvd = bob1.recv_msg(&leave_msg).await;
@@ -4507,7 +4495,7 @@ async fn test_info_not_referenced() -> Result<()> {
let bob_received_message = tcm.send_recv_accept(alice, bob, "Hi!").await;
let bob_chat_id = bob_received_message.chat_id;
add_info_msg(bob, bob_chat_id, "Some info", create_smeared_timestamp(bob)).await?;
add_info_msg(bob, bob_chat_id, "Some info").await?;
// Bob sends a message.
// This message should reference Alice's "Hi!" message and not the info message.

View File

@@ -18,7 +18,7 @@ use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
use crate::provider::{Provider, get_provider_by_id};
use crate::provider::Provider;
use crate::sync::{self, Sync::*, SyncData};
use crate::tools::get_abs_path;
use crate::transport::ConfiguredLoginParam;
@@ -144,11 +144,11 @@ pub enum Config {
/// Send BCC copy to self.
///
/// Should be enabled for multidevice setups.
/// Default is 0 for chatmail accounts, 1 otherwise.
/// Should be enabled for multi-device setups.
///
/// This is automatically enabled when importing/exporting a backup,
/// setting up a second device, or receiving a sync message.
#[strum(props(default = "0"))]
BccSelf,
/// True if Message Delivery Notifications (read receipts) should
@@ -477,7 +477,10 @@ impl Config {
/// Whether the config option needs an IO scheduler restart to take effect.
pub(crate) fn needs_io_restart(&self) -> bool {
matches!(self, Config::MvboxMove | Config::OnlyFetchMvbox)
matches!(
self,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ConfiguredAddr
)
}
}
@@ -523,10 +526,6 @@ impl Context {
// Default values
let val = match key {
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
false => Some("1".to_string()),
true => Some("0".to_string()),
},
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
Config::DeleteServerAfter => {
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
@@ -647,15 +646,14 @@ impl Context {
Ok(val)
}
/// Gets the configured provider, as saved in the `configured_provider` value.
/// Gets the configured provider.
///
/// The provider is determined by `get_provider_info()` during configuration and then saved
/// to the db in `param.save_to_database()`, together with all the other `configured_*` values.
/// The provider is determined by the current primary transport.
pub async fn get_configured_provider(&self) -> Result<Option<&'static Provider>> {
if let Some(cfg) = self.get_config(Config::ConfiguredProvider).await? {
return Ok(get_provider_by_id(&cfg));
}
Ok(None)
let provider = ConfiguredLoginParam::load(self)
.await?
.and_then(|(_transport_id, param)| param.provider);
Ok(provider)
}
/// Gets configured "delete_device_after" value.
@@ -717,6 +715,16 @@ impl Context {
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
Self::check_config(key, value)?;
let n_transports = self.count_transports().await?;
if n_transports > 1
&& matches!(
key,
Config::MvboxMove | Config::OnlyFetchMvbox | Config::ShowEmails
)
{
bail!("Cannot reconfigure {key} when multiple transports are configured");
}
let _pause = match key.needs_io_restart() {
true => self.scheduler.pause(self).await?,
_ => Default::default(),
@@ -802,20 +810,59 @@ impl Context {
.await?;
}
Config::ConfiguredAddr => {
if self.is_configured().await? {
bail!("Cannot change ConfiguredAddr");
}
if let Some(addr) = value {
let Some(addr) = value else {
bail!("Cannot unset configured_addr");
};
if !self.is_configured().await? {
info!(
self,
"Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!"
);
ConfiguredLoginParam::from_json(&format!(
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
))?
.save_to_transports_table(self, &EnteredLoginParam::default())
.await?;
self.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
(
addr,
serde_json::to_string(&EnteredLoginParam::default())?,
format!(r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#)
),
)
.await?;
self.sql
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(addr))
.await?;
}
self.sql
.transaction(|transaction| {
if transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(addr,),
|row| {
let res: i64 = row.get(0)?;
Ok(res)
},
)? == 0
{
bail!("Address does not belong to any transport.");
}
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(addr,),
)?;
// Clean up SMTP and IMAP APPEND queue.
//
// The messages in the queue have a different
// From address so we cannot send them over
// the new SMTP transport.
transaction.execute("DELETE FROM smtp", ())?;
transaction.execute("DELETE FROM imap_send", ())?;
Ok(())
})
.await?;
self.sql.uncache_raw_config("configured_addr").await;
}
_ => {
self.sql.set_raw_config(key.as_ref(), value).await?;

View File

@@ -278,7 +278,6 @@ async fn test_sync() -> Result<()> {
Ok(())
}
/// Sync message mustn't be sent if self-{status,avatar} is changed by a self-sent message.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_sync_on_self_sent_msg() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -288,7 +287,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
let status = "Synced via usual message";
let status = "Sent via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.send_sync_msg().await?;
alice0.pop_sent_sync_msg().await;
@@ -297,7 +296,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
tcm.send_recv(alice0, alice1, "hi Alice!").await;
assert_eq!(
alice1.get_config(Config::Selfstatus).await?,
Some(status.to_string())
Some(status1.to_string())
);
sync(alice1, alice0).await;
assert_eq!(
@@ -328,7 +327,7 @@ async fn test_no_sync_on_self_sent_msg() -> Result<()> {
alice1
.get_config(Config::Selfavatar)
.await?
.filter(|path| path.ends_with(".png"))
.filter(|path| path.ends_with(".jpg"))
.is_some()
);
sync(alice1, alice0).await;

View File

@@ -27,7 +27,7 @@ use crate::config::{self, Config};
use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
use crate::context::Context;
use crate::imap::Imap;
use crate::log::{LogExt, warn};
use crate::log::warn;
use crate::login_param::EnteredCertificateChecks;
pub use crate::login_param::EnteredLoginParam;
use crate::message::Message;
@@ -40,11 +40,10 @@ use crate::sync::Sync::*;
use crate::tools::time;
use crate::transport::{
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
ConnectionCandidate,
ConnectionCandidate, send_sync_transports,
};
use crate::{EventType, stock_str};
use crate::{chat, provider};
use deltachat_contact_tools::addr_cmp;
macro_rules! progress {
($context:tt, $progress:expr, $comment:expr) => {
@@ -130,12 +129,6 @@ impl Context {
"cannot configure, database not opened."
);
param.addr = addr_normalize(&param.addr);
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), &param.addr) {
let error_msg = "Changing your email address is not supported right now. Check back in a few months!";
progress!(self, 0, Some(error_msg.to_string()));
bail!(error_msg);
}
let cancel_channel = self.alloc_ongoing().await?;
let res = self
@@ -204,23 +197,92 @@ impl Context {
Ok(transports)
}
/// Returns the number of configured transports.
pub async fn count_transports(&self) -> Result<usize> {
self.sql.count("SELECT COUNT(*) FROM transports", ()).await
}
/// Removes the transport with the specified email address
/// (i.e. [EnteredLoginParam::addr]).
#[expect(clippy::unused_async)]
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
bail!(
"Adding and removing additional transports is not supported yet. Check back in a few months!"
)
pub async fn delete_transport(&self, addr: &str) -> Result<()> {
let now = time();
self.sql
.transaction(|transaction| {
let primary_addr = transaction.query_row(
"SELECT value FROM config WHERE keyname='configured_addr'",
(),
|row| {
let addr: String = row.get(0)?;
Ok(addr)
},
)?;
if primary_addr == addr {
bail!("Cannot delete primary transport");
}
let (transport_id, add_timestamp) = transaction.query_row(
"DELETE FROM transports WHERE addr=? RETURNING id, add_timestamp",
(addr,),
|row| {
let id: u32 = row.get(0)?;
let add_timestamp: i64 = row.get(1)?;
Ok((id, add_timestamp))
},
)?;
transaction.execute("DELETE FROM imap WHERE transport_id=?", (transport_id,))?;
transaction.execute(
"DELETE FROM imap_sync WHERE transport_id=?",
(transport_id,),
)?;
// Removal timestamp should not be lower than addition timestamp
// to be accepted by other devices when synced.
let remove_timestamp = std::cmp::max(now, add_timestamp);
transaction.execute(
"INSERT INTO removed_transports (addr, remove_timestamp)
VALUES (?, ?)
ON CONFLICT (addr)
DO UPDATE SET remove_timestamp = excluded.remove_timestamp",
(addr, remove_timestamp),
)?;
Ok(())
})
.await?;
send_sync_transports(self).await?;
Ok(())
}
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
info!(self, "Configure ...");
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
if old_addr.is_some()
&& !self
.sql
.exists(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(&param.addr,),
)
.await?
{
if self.get_config(Config::MvboxMove).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with mvbox_move enabled.");
}
if self.get_config(Config::OnlyFetchMvbox).await?.as_deref() != Some("0") {
bail!("Cannot use multi-transport with only_fetch_mvbox enabled.");
}
if self.get_config(Config::ShowEmails).await?.as_deref() != Some("2") {
bail!("Cannot use multi-transport with disabled fetching of classic emails.");
}
}
let provider = configure(self, param).await?;
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
.await?;
on_configure_completed(self, provider, old_addr).await?;
on_configure_completed(self, provider).await?;
Ok(())
}
}
@@ -228,7 +290,6 @@ impl Context {
async fn on_configure_completed(
context: &Context,
provider: Option<&'static Provider>,
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = provider {
if let Some(config_defaults) = provider.config_defaults {
@@ -258,21 +319,6 @@ async fn on_configure_completed(
}
}
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
if let Some(old_addr) = old_addr {
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new_text(
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await,
);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
.log_err(context)
.ok();
}
}
}
Ok(())
}
@@ -504,71 +550,40 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
// Configure IMAP
let transport_id = 0;
let (_s, r) = async_channel::bounded(1);
let mut imap = Imap::new(
configured_param.imap.clone(),
configured_param.imap_password.clone(),
proxy_config,
&configured_param.addr,
strict_tls,
configured_param.oauth2,
r,
);
let mut imap = Imap::new(ctx, transport_id, configured_param.clone(), r).await?;
let configuring = true;
let mut imap_session = match imap.connect(ctx, configuring).await {
Ok(session) => session,
Err(err) => bail!(
if let Err(err) = imap.connect(ctx, configuring).await {
bail!(
"{}",
nicer_configuration_error(ctx, format!("{err:#}")).await
),
);
};
progress!(ctx, 850);
// Wait for SMTP configuration
smtp_config_task.await.unwrap()?;
smtp_config_task.await??;
progress!(ctx, 900);
let is_chatmail = match ctx.get_config_bool(Config::FixIsChatmail).await? {
false => {
let is_chatmail = imap_session.is_chatmail();
ctx.set_config(
Config::IsChatmail,
Some(match is_chatmail {
false => "0",
true => "1",
}),
)
.await?;
is_chatmail
}
true => ctx.get_config_bool(Config::IsChatmail).await?,
};
if is_chatmail {
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
ctx.set_config(Config::ShowEmails, None).await?;
let is_configured = ctx.is_configured().await?;
if !is_configured {
ctx.sql.set_raw_config("mvbox_move", Some("0")).await?;
ctx.sql.set_raw_config("only_fetch_mvbox", None).await?;
}
let create_mvbox = !is_chatmail;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
let create = true;
imap_session
.select_with_uidvalidity(ctx, "INBOX", create)
.await
.context("could not read INBOX status")?;
drop(imap);
progress!(ctx, 910);
let provider = configured_param.provider;
configured_param
.save_to_transports_table(ctx, param)
.clone()
.save_to_transports_table(ctx, param, time())
.await?;
send_sync_transports(ctx).await?;
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
.await?;

View File

@@ -154,10 +154,10 @@ fn parse_xml_reader<B: BufRead>(
if let Some(incoming_server) = parse_server(reader, event)? {
incoming_servers.push(incoming_server);
}
} else if tag == "outgoingserver" {
if let Some(outgoing_server) = parse_server(reader, event)? {
outgoing_servers.push(outgoing_server);
}
} else if tag == "outgoingserver"
&& let Some(outgoing_server) = parse_server(reader, event)?
{
outgoing_servers.push(outgoing_server);
}
}
Event::Eof => break,

View File

@@ -130,35 +130,37 @@ impl ContactId {
Ok((addr, fingerprint))
},
)?;
context.emit_event(EventType::ContactsChanged(Some(self)));
Ok(Some((addr, fingerprint)))
} else {
Ok(None)
}
})
.await?;
if row.is_some() {
context.emit_event(EventType::ContactsChanged(Some(self)));
}
if sync.into() {
if let Some((addr, fingerprint)) = row {
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
if sync.into()
&& let Some((addr, fingerprint)) = row
{
if fingerprint.is_empty() {
chat::sync(
context,
chat::SyncId::ContactAddr(addr),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
} else {
chat::sync(
context,
chat::SyncId::ContactFingerprint(fingerprint),
chat::SyncAction::Rename(name.to_string()),
)
.await
.log_err(context)
.ok();
}
}
Ok(())
@@ -381,25 +383,21 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
},
None => None,
};
if let Some(path) = path {
// Currently this value doesn't matter as we don't import the contact of self.
let was_encrypted = false;
if let Err(e) =
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
}
if let Some(path) = path
&& let Err(e) = set_profile_image(context, id, &AvatarAction::Change(path)).await
{
warn!(
context,
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
);
}
if let Some(biography) = &contact.biography {
if let Err(e) = set_status(context, id, biography.to_owned(), false, false).await {
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
if let Some(biography) = &contact.biography
&& let Err(e) = set_status(context, id, biography.to_owned()).await
{
warn!(
context,
"import_vcard_contact: Could not set biography for {}: {e:#}.", contact.addr
);
}
Ok(id)
}
@@ -1507,18 +1505,6 @@ impl Contact {
&self.addr
}
/// Get authorized name or address.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
pub(crate) fn get_authname_or_addr(&self) -> String {
if !self.authname.is_empty() {
(&self.authname).into()
} else {
(&self.addr).into()
}
}
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
@@ -1564,10 +1550,10 @@ impl Contact {
if show_fallback_icon && !self.id.is_special() && !self.is_key_contact() {
return Ok(Some(chat::get_unencrypted_icon(context).await?));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
if let Some(image_rel) = self.param.get(Param::ProfileImage)
&& !image_rel.is_empty()
{
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
}
Ok(None)
}
@@ -1800,10 +1786,11 @@ WHERE type=? AND id IN (
// also unblock mailinglist
// if the contact is a mailinglist address explicitly created to allow unblocking
if !new_blocking && contact.origin == Origin::MailinglistAddress {
if let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await? {
chat_id.unblock_ex(context, Nosync).await?;
}
if !new_blocking
&& contact.origin == Origin::MailinglistAddress
&& let Some((chat_id, ..)) = chat::get_chat_id_by_grpid(context, &contact.addr).await?
{
chat_id.unblock_ex(context, Nosync).await?;
}
if sync.into() {
@@ -1833,25 +1820,19 @@ WHERE type=? AND id IN (
/// The given profile image is expected to be already in the blob directory
/// as profile images can be set only by receiving messages, this should be always the case, however.
///
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar;
/// this typically happens if we see message with our own profile image.
/// For contact SELF, the image is not saved in the contact-database but as Config::Selfavatar.
pub(crate) async fn set_profile_image(
context: &Context,
contact_id: ContactId,
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
let mut contact = Contact::get_by_id(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar.");
}
context
.set_config_ex(Nosync, Config::Selfavatar, Some(profile_image))
.await?;
} else {
contact.param.set(Param::ProfileImage, profile_image);
}
@@ -1859,13 +1840,9 @@ pub(crate) async fn set_profile_image(
}
AvatarAction::Delete => {
if contact_id == ContactId::SELF {
if was_encrypted {
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.await?;
} else {
info!(context, "Do not use unencrypted selfavatar deletion.");
}
context
.set_config_ex(Nosync, Config::Selfavatar, None)
.await?;
} else {
contact.param.remove(Param::ProfileImage);
}
@@ -1882,22 +1859,16 @@ pub(crate) async fn set_profile_image(
/// Sets contact status.
///
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus. This
/// is only done if message is sent from Delta Chat and it is encrypted, to synchronize signature
/// between Delta Chat devices.
/// For contact SELF, the status is not saved in the contact table, but as Config::Selfstatus.
pub(crate) async fn set_status(
context: &Context,
contact_id: ContactId,
status: String,
encrypted: bool,
has_chat_version: bool,
) -> Result<()> {
if contact_id == ContactId::SELF {
if encrypted && has_chat_version {
context
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.await?;
}
context
.set_config_ex(Nosync, Config::Selfstatus, Some(&status))
.await?;
} else {
let mut contact = Contact::get_by_id(context, contact_id).await?;

View File

@@ -4,7 +4,7 @@ use super::*;
use crate::chat::{Chat, get_chat_contacts, send_text_msg};
use crate::chatlist::Chatlist;
use crate::receive_imf::receive_imf;
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote};
use crate::test_utils::{self, TestContext, TestContextManager, TimeShiftFalsePositiveNote, sync};
#[test]
fn test_contact_id_values() {
@@ -850,8 +850,7 @@ CCCB 5AA9 F6E1 141C 9431
Ok(())
}
/// Tests that status is synchronized when sending encrypted BCC-self messages and not
/// synchronized when the message is not encrypted.
/// Tests that self-status is not synchronized from outgoing messages.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_synchronize_status() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -870,21 +869,12 @@ async fn test_synchronize_status() -> Result<()> {
.await?;
let chat = alice1.create_email_chat(bob).await;
// Alice sends a message to Bob from the first device.
// Alice sends an unencrypted message to Bob from the first device.
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Message is not encrypted.
let message = sent_msg.load_from_db().await;
assert!(!message.get_showpadlock());
// Alice's second devices receives a copy of outgoing message.
alice2.recv_msg(&sent_msg).await;
// Bob receives message.
bob.recv_msg(&sent_msg).await;
// Message was not encrypted, so status is not copied.
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
// Alice sends encrypted message.
@@ -892,17 +882,9 @@ async fn test_synchronize_status() -> Result<()> {
send_text_msg(alice1, chat.id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// Second message is encrypted.
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second devices receives a copy of second outgoing message.
alice2.recv_msg(&sent_msg).await;
assert_eq!(
alice2.get_config(Config::Selfstatus).await?,
Some("New status".to_string())
);
assert_eq!(alice2.get_config(Config::Selfstatus).await?, default_status);
Ok(())
}
@@ -915,9 +897,9 @@ async fn test_selfavatar_changed_event() -> Result<()> {
// Alice has two devices.
let alice1 = &tcm.alice().await;
let alice2 = &tcm.alice().await;
// Bob has one device.
let bob = &tcm.bob().await;
for a in [alice1, alice2] {
a.set_config_bool(Config::SyncMsgs, true).await?;
}
assert_eq!(alice1.get_config(Config::Selfavatar).await?, None);
@@ -933,17 +915,7 @@ async fn test_selfavatar_changed_event() -> Result<()> {
.get_matching(|e| matches!(e, EventType::SelfavatarChanged))
.await;
// Alice sends a message.
let alice1_chat_id = alice1.create_chat(bob).await.id;
send_text_msg(alice1, alice1_chat_id, "Hello".to_string()).await?;
let sent_msg = alice1.pop_sent_msg().await;
// The message is encrypted.
let message = sent_msg.load_from_db().await;
assert!(message.get_showpadlock());
// Alice's second device receives a copy of the outgoing message.
alice2.recv_msg(&sent_msg).await;
sync(alice1, alice2).await;
// Alice's second device applies the selfavatar.
assert!(alice2.get_config(Config::Selfavatar).await?.is_some());

View File

@@ -5,7 +5,7 @@ use std::ffi::OsString;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, OnceLock};
use std::sync::{Arc, OnceLock, Weak};
use std::time::Duration;
use anyhow::{Context as _, Result, bail, ensure};
@@ -46,7 +46,7 @@ use crate::{chatlist_events, stats};
///
/// # Examples
///
/// Creating a new unencrypted database:
/// Creating a new database:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
@@ -61,24 +61,6 @@ use crate::{chatlist_events, stats};
/// drop(context);
/// # });
/// ```
///
/// To use an encrypted database provide a password. If the database does not yet exist it
/// will be created:
///
/// ```
/// # let rt = tokio::runtime::Runtime::new().unwrap();
/// # rt.block_on(async move {
/// use deltachat::context::ContextBuilder;
///
/// let dir = tempfile::tempdir().unwrap();
/// let context = ContextBuilder::new(dir.path().join("db"))
/// .with_password("secret".into())
/// .open()
/// .await
/// .unwrap();
/// drop(context);
/// # });
/// ```
#[derive(Clone, Debug)]
pub struct ContextBuilder {
dbfile: PathBuf,
@@ -150,9 +132,13 @@ impl ContextBuilder {
}
/// Sets the password to unlock the database.
/// Deprecated 2025-11:
/// - Db encryption does nothing with blobs, so fs/disk encryption is recommended.
/// - Isolation from other apps is needed anyway.
///
/// If an encrypted database is used it must be opened with a password. Setting a
/// password on a new database will enable encryption.
#[deprecated(since = "TBD")]
pub fn with_password(mut self, password: String) -> Self {
self.password = Some(password);
self
@@ -180,7 +166,7 @@ impl ContextBuilder {
/// Builds the [`Context`] and opens it.
///
/// Returns error if context cannot be opened with the given passphrase.
/// Returns error if context cannot be opened.
pub async fn open(self) -> Result<Context> {
let password = self.password.clone().unwrap_or_default();
let context = self.build().await?;
@@ -215,6 +201,25 @@ impl Deref for Context {
}
}
/// A weak reference to a [`Context`]
///
/// Can be used to obtain a [`Context`]. An existing weak reference does not prevent the corresponding [`Context`] from being dropped.
#[derive(Clone, Debug)]
pub(crate) struct WeakContext {
inner: Weak<InnerContext>,
}
impl WeakContext {
/// Returns the [`Context`] if it is still available.
pub(crate) fn upgrade(&self) -> Result<Context> {
let inner = self
.inner
.upgrade()
.ok_or_else(|| anyhow::anyhow!("Inner struct has been dropped"))?;
Ok(Context { inner })
}
}
/// Actual context, expensive to clone.
#[derive(Debug)]
pub struct InnerContext {
@@ -399,10 +404,20 @@ impl Context {
Ok(context)
}
/// Returns a weak reference to this [`Context`].
pub(crate) fn get_weak_context(&self) -> WeakContext {
WeakContext {
inner: Arc::downgrade(&self.inner),
}
}
/// Opens the database with the given passphrase.
/// NB: Db encryption is deprecated, so `passphrase` should be empty normally. See
/// [`ContextBuilder::with_password()`] for reasoning.
///
/// Returns true if passphrase is correct, false is passphrase is not correct. Fails on other
/// errors.
#[deprecated(since = "TBD")]
pub async fn open(&self, passphrase: String) -> Result<bool> {
if self.sql.check_passphrase(passphrase.clone()).await? {
self.sql.open(self, passphrase).await?;
@@ -413,6 +428,7 @@ impl Context {
}
/// Changes encrypted database passphrase.
/// Deprecated 2025-11, see [`ContextBuilder::with_password()`] for reasoning.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
Ok(())
@@ -463,7 +479,7 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(3, 0), 3.0)), // Allow at least 1 message every second + a burst of 3.
quota: RwLock::new(None),
new_msgs_notify,
server_id: RwLock::new(None),
@@ -495,12 +511,6 @@ impl Context {
return;
}
if self.is_chatmail().await.unwrap_or_default() {
let mut lock = self.ratelimit.write().await;
// Allow at least 1 message every second + a burst of 3.
*lock = Ratelimit::new(Duration::new(3, 0), 3.0);
}
// The next line is mainly for iOS:
// iOS starts a separate process for receiving notifications and if the user concurrently
// starts the app, the UI process opens the database but waits with calling start_io()
@@ -608,10 +618,9 @@ impl Context {
if self
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
.await
&& let Err(err) = self.update_recent_quota(&mut session).await
{
if let Err(err) = self.update_recent_quota(&mut session).await {
warn!(self, "Failed to update quota: {err:#}.");
}
warn!(self, "Failed to update quota: {err:#}.");
}
}
@@ -808,9 +817,10 @@ impl Context {
/// Returns information about the context as key-value pairs.
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
let l = EnteredLoginParam::load(self).await?;
let l2 = ConfiguredLoginParam::load(self)
.await?
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
let l2 = ConfiguredLoginParam::load(self).await?.map_or_else(
|| "Not configured".to_string(),
|(_transport_id, param)| param.to_string(),
);
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
let chats = get_chat_cnt(self).await?;
let unblocked_msgs = message::get_unblocked_msg_cnt(self).await;

View File

@@ -115,15 +115,13 @@ pub async fn maybe_set_logging_xdc_inner(
filename: Option<&str>,
msg_id: MsgId,
) -> anyhow::Result<()> {
if viewtype == Viewtype::Webxdc {
if let Some(filename) = filename {
if filename.starts_with("debug_logging")
&& filename.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
}
}
if viewtype == Viewtype::Webxdc
&& let Some(filename) = filename
&& filename.starts_with("debug_logging")
&& filename.ends_with(".xdc")
&& chat_id.is_self_talk(context).await?
{
set_debug_logging_xdc(context, Some(msg_id)).await?;
}
Ok(())
}

View File

@@ -57,7 +57,7 @@ impl EncryptHelper {
let ctext = pgp::pk_encrypt(
raw_message,
keyring,
Some(sign_key),
sign_key,
compress,
anonymous_recipients,
)

View File

@@ -451,6 +451,8 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
let t = TestContext::new_alice().await;
const HOUR: i64 = 60 * 60;
let now = time();
let transport_id = 1;
let uidvalidity = 12345;
for (id, timestamp, ephemeral_timestamp) in &[
(900, now - 2 * HOUR, 0),
(1000, now - 23 * HOUR - MIN_DELETE_SERVER_AFTER, 0),
@@ -470,8 +472,8 @@ async fn test_delete_expired_imap_messages() -> Result<()> {
.await?;
t.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, target) VALUES (?,'INBOX',?, 'INBOX');",
(&message_id, id),
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, target, uidvalidity) VALUES (?, ?,'INBOX',?, 'INBOX', ?);",
(transport_id, &message_id, id, uidvalidity),
)
.await?;
}

View File

@@ -417,6 +417,15 @@ pub enum EventType {
chat_id: ChatId,
},
/// One or more transports has changed.
///
/// This event is used for tests to detect when transport
/// synchronization messages arrives.
/// UIs don't need to use it, it is unlikely
/// that user modifies transports on multiple
/// devices simultaneously.
TransportsModified,
/// Event for using in tests, e.g. as a fence between normally generated events.
#[cfg(test)]
Test,

View File

@@ -169,27 +169,28 @@ impl HtmlMsgParser {
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype == mime::TEXT_HTML {
if self.html.is_empty() {
if let Ok(decoded_data) = mail.get_body() {
self.html = decoded_data;
}
}
} else if mimetype == mime::TEXT_PLAIN && self.plain.is_none() {
if let Ok(decoded_data) = mail.get_body() {
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
});
if self.html.is_empty()
&& let Ok(decoded_data) = mail.get_body()
{
self.html = decoded_data;
}
} else if mimetype == mime::TEXT_PLAIN
&& self.plain.is_none()
&& let Ok(decoded_data) = mail.get_body()
{
self.plain = Some(PlainText {
text: decoded_data,
flowed: if let Some(format) = mail.ctype.params.get("format") {
format.as_str().eq_ignore_ascii_case("flowed")
} else {
false
},
delsp: if let Some(delsp) = mail.ctype.params.get("delsp") {
delsp.as_str().eq_ignore_ascii_case("yes")
} else {
false
},
});
}
Ok(())
}
@@ -213,31 +214,29 @@ impl HtmlMsgParser {
MimeMultipartType::Message => Ok(()),
MimeMultipartType::Single => {
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
if mimetype.type_() == mime::IMAGE {
if let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId) {
if let Ok(cid) = parse_message_id(&cid) {
if let Ok(replacement) = mimepart_to_data_url(mail) {
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
if mimetype.type_() == mime::IMAGE
&& let Some(cid) = mail.headers.get_header_value(HeaderDef::ContentId)
&& let Ok(cid) = parse_message_id(&cid)
&& let Ok(replacement) = mimepart_to_data_url(mail)
{
let re_string = format!(
"(<img[^>]*src[^>]*=[^>]*)(cid:{})([^>]*>)",
regex::escape(&cid)
);
match regex::Regex::new(&re_string) {
Ok(re) => {
self.html = re
.replace_all(
&self.html,
format!("${{1}}{replacement}${{3}}").as_str(),
)
.as_ref()
.to_string()
}
Err(e) => warn!(
context,
"Cannot create regex for cid: {} throws {}", re_string, e
),
}
}
Ok(())

View File

@@ -71,10 +71,15 @@ const BODY_PARTIAL: &str = "(FLAGS RFC822.SIZE BODY.PEEK[HEADER])";
#[derive(Debug)]
pub(crate) struct Imap {
/// ID of the transport configuration in the `transports` table.
///
/// This ID is used to namespace records in the `imap` table.
transport_id: u32,
pub(crate) idle_interrupt_receiver: Receiver<()>,
/// Email address.
addr: String,
pub(crate) addr: String,
/// Login parameters.
lp: Vec<ConfiguredServerLoginParam>,
@@ -249,19 +254,21 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
impl Imap {
/// Creates new disconnected IMAP client using the specific login parameters.
///
/// `addr` is used to renew token if OAuth2 authentication is used.
pub fn new(
lp: Vec<ConfiguredServerLoginParam>,
password: String,
proxy_config: Option<ProxyConfig>,
addr: &str,
strict_tls: bool,
oauth2: bool,
pub async fn new(
context: &Context,
transport_id: u32,
param: ConfiguredLoginParam,
idle_interrupt_receiver: Receiver<()>,
) -> Self {
) -> Result<Self> {
let lp = param.imap.clone();
let password = param.imap_password.clone();
let proxy_config = ProxyConfig::load(context).await?;
let addr = &param.addr;
let strict_tls = param.strict_tls(proxy_config.is_some());
let oauth2 = param.oauth2;
let (resync_request_sender, resync_request_receiver) = async_channel::bounded(1);
Imap {
Ok(Imap {
transport_id,
idle_interrupt_receiver,
addr: addr.to_string(),
lp,
@@ -277,7 +284,7 @@ impl Imap {
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
resync_request_sender,
resync_request_receiver,
}
})
}
/// Creates new disconnected IMAP client using configured parameters.
@@ -285,20 +292,10 @@ impl Imap {
context: &Context,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
let param = ConfiguredLoginParam::load(context)
let (transport_id, param) = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
let strict_tls = param.strict_tls(proxy_config.is_some());
let imap = Self::new(
param.imap.clone(),
param.imap_password.clone(),
proxy_config,
&param.addr,
strict_tls,
param.oauth2,
idle_interrupt_receiver,
);
let imap = Self::new(context, transport_id, param, idle_interrupt_receiver).await?;
Ok(imap)
}
@@ -412,9 +409,19 @@ impl Imap {
})
.await
.context("Failed to enable IMAP compression")?;
Session::new(compressed_session, capabilities, resync_request_sender)
Session::new(
compressed_session,
capabilities,
resync_request_sender,
self.transport_id,
)
} else {
Session::new(session, capabilities, resync_request_sender)
Session::new(
session,
capabilities,
resync_request_sender,
self.transport_id,
)
};
// Store server ID in the context to display in account info.
@@ -593,8 +600,9 @@ impl Imap {
folder: &str,
folder_meaning: FolderMeaning,
) -> Result<(usize, bool)> {
let uid_validity = get_uidvalidity(context, folder).await?;
let old_uid_next = get_uid_next(context, folder).await?;
let transport_id = self.transport_id;
let uid_validity = get_uidvalidity(context, transport_id, folder).await?;
let old_uid_next = get_uid_next(context, transport_id, folder).await?;
info!(
context,
"fetch_new_msg_batch({folder}): UIDVALIDITY={uid_validity}, UIDNEXT={old_uid_next}."
@@ -662,12 +670,19 @@ impl Imap {
context
.sql
.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(folder, uid, uidvalidity)
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(transport_id, folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(&message_id, &folder, uid, uid_validity, target),
(
self.transport_id,
&message_id,
&folder,
uid,
uid_validity,
target,
),
)
.await?;
@@ -778,7 +793,7 @@ impl Imap {
prefetch_uid_next < mailbox_uid_next
};
if new_uid_next > old_uid_next {
set_uid_next(context, folder, new_uid_next).await?;
set_uid_next(context, self.transport_id, folder, new_uid_next).await?;
}
info!(context, "{} mails read from \"{}\".", read_cnt, folder);
@@ -858,6 +873,7 @@ impl Session {
let folder_exists = self
.select_with_uidvalidity(context, folder, create)
.await?;
let transport_id = self.transport_id();
if folder_exists {
let mut list = self
.uid_fetch("1:*", RFC724MID_UID)
@@ -890,7 +906,7 @@ impl Session {
msgs.len(),
);
uid_validity = get_uidvalidity(context, folder).await?;
uid_validity = get_uidvalidity(context, transport_id, folder).await?;
} else {
warn!(context, "resync_folder_uids: No folder {folder}.");
uid_validity = 0;
@@ -905,12 +921,12 @@ impl Session {
// This may detect previously undetected moved
// messages, so we update server_folder too.
transaction.execute(
"INSERT INTO imap (rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(folder, uid, uidvalidity)
"INSERT INTO imap (transport_id, rfc724_mid, folder, uid, uidvalidity, target)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(transport_id, folder, uid, uidvalidity)
DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
target=excluded.target",
(rfc724_mid, folder, uid, uid_validity, target),
(transport_id, rfc724_mid, folder, uid, uid_validity, target),
)?;
}
Ok(())
@@ -1232,11 +1248,12 @@ impl Session {
return Ok(());
}
let transport_id = self.transport_id();
let mut updated_chat_ids = BTreeSet::new();
let uid_validity = get_uidvalidity(context, folder)
let uid_validity = get_uidvalidity(context, transport_id, folder)
.await
.with_context(|| format!("failed to get UID validity for folder {folder}"))?;
let mut highest_modseq = get_modseq(context, folder)
let mut highest_modseq = get_modseq(context, transport_id, folder)
.await
.with_context(|| format!("failed to get MODSEQ for folder {folder}"))?;
let mut list = self
@@ -1259,15 +1276,14 @@ impl Session {
continue;
};
let is_seen = fetch.flags().any(|flag| flag == Flag::Seen);
if is_seen {
if let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
if is_seen
&& let Some(chat_id) = mark_seen_by_uid(context, folder, uid_validity, uid)
.await
.with_context(|| {
format!("failed to update seen status for msg {folder}/{uid}")
})?
{
updated_chat_ids.insert(chat_id);
}
{
updated_chat_ids.insert(chat_id);
}
if let Some(modseq) = fetch.modseq {
@@ -1288,7 +1304,7 @@ impl Session {
self.new_mail = true;
}
set_modseq(context, folder, highest_modseq)
set_modseq(context, transport_id, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
if !updated_chat_ids.is_empty() {
@@ -1321,10 +1337,10 @@ impl Session {
while let Some(msg) = list.try_next().await? {
match get_fetch_headers(&msg) {
Ok(headers) => {
if let Some(from) = mimeparser::get_from(&headers) {
if context.is_self_addr(&from.addr).await? {
result.extend(mimeparser::get_recipients(&headers));
}
if let Some(from) = mimeparser::get_from(&headers)
&& context.is_self_addr(&from.addr).await?
{
result.extend(mimeparser::get_recipients(&headers));
}
}
Err(err) => {
@@ -1557,17 +1573,17 @@ impl Session {
.await?;
let mut got_turn_server = false;
for m in metadata {
if m.entry == "/shared/vendor/deltachat/turn" {
if let Some(value) = m.value {
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
if m.entry == "/shared/vendor/deltachat/turn"
&& let Some(value) = m.value
{
match create_ice_servers_from_metadata(context, &value).await {
Ok((parsed_timestamp, parsed_ice_servers)) => {
old_metadata.ice_servers_expiration_timestamp = parsed_timestamp;
old_metadata.ice_servers = parsed_ice_servers;
got_turn_server = false;
}
Err(err) => {
warn!(context, "Failed to parse TURN server metadata: {err:#}.");
}
}
}
@@ -1852,11 +1868,13 @@ impl Imap {
info!(context, "Scanning folder: {:?}", folder);
// Update the delimiter iff there is a different one, but only once.
if let Some(d) = folder.delimiter() {
if delimiter_is_default && !d.is_empty() && delimiter != d {
delimiter = d.to_string();
delimiter_is_default = false;
}
if let Some(d) = folder.delimiter()
&& delimiter_is_default
&& !d.is_empty()
&& delimiter != d
{
delimiter = d.to_string();
delimiter_is_default = false;
}
let folder_meaning = get_folder_meaning_by_attrs(folder.attributes());
@@ -2097,12 +2115,10 @@ async fn needs_move_to_mvbox(
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.eq_ignore_ascii_case("auto-generated"))
.is_some()
&& let Some(from) = mimeparser::get_from(headers)
&& context.is_self_addr(&from.addr).await?
{
if let Some(from) = mimeparser::get_from(headers) {
if context.is_self_addr(&from.addr).await? {
return Ok(true);
}
}
return Ok(true);
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
@@ -2272,12 +2288,13 @@ pub(crate) async fn prefetch_should_download(
// We do not know the Message-ID or the Message-ID is missing (in this case, we create one in
// the further process).
if let Some(chat) = prefetch_get_chat(context, headers).await? {
if chat.typ == Chattype::Group && !chat.id.is_special() {
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
if let Some(chat) = prefetch_get_chat(context, headers).await?
&& chat.typ == Chattype::Group
&& !chat.id.is_special()
{
// This might be a group command, like removing a group member.
// We really need to fetch this to avoid inconsistent group state.
return Ok(true);
}
let maybe_ndn = if let Some(from) = headers.get_header_value(HeaderDef::From_) {
@@ -2417,13 +2434,18 @@ pub(crate) async fn markseen_on_imap_table(context: &Context, message_id: &str)
/// uid_next is the next unique identifier value from the last time we fetched a folder
/// See <https://tools.ietf.org/html/rfc3501#section-2.3.1.1>
/// This function is used to update our uid_next after fetching messages.
pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32) -> Result<()> {
pub(crate) async fn set_uid_next(
context: &Context,
transport_id: u32,
folder: &str,
uid_next: u32,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uid_next) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uid_next=excluded.uid_next",
(folder, uid_next),
"INSERT INTO imap_sync (transport_id, folder, uid_next) VALUES (?, ?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET uid_next=excluded.uid_next",
(transport_id, folder, uid_next),
)
.await?;
Ok(())
@@ -2434,57 +2456,69 @@ pub(crate) async fn set_uid_next(context: &Context, folder: &str, uid_next: u32)
/// This method returns the uid_next from the last time we fetched messages.
/// We can compare this to the current uid_next to find out whether there are new messages
/// and fetch from this value on to get all new messages.
async fn get_uid_next(context: &Context, folder: &str) -> Result<u32> {
async fn get_uid_next(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value("SELECT uid_next FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT uid_next FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
pub(crate) async fn set_uidvalidity(
context: &Context,
transport_id: u32,
folder: &str,
uidvalidity: u32,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, uidvalidity) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
(folder, uidvalidity),
"INSERT INTO imap_sync (transport_id, folder, uidvalidity) VALUES (?,?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET uidvalidity=excluded.uidvalidity",
(transport_id, folder, uidvalidity),
)
.await?;
Ok(())
}
async fn get_uidvalidity(context: &Context, folder: &str) -> Result<u32> {
async fn get_uidvalidity(context: &Context, transport_id: u32, folder: &str) -> Result<u32> {
Ok(context
.sql
.query_get_value(
"SELECT uidvalidity FROM imap_sync WHERE folder=?;",
(folder,),
"SELECT uidvalidity FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
pub(crate) async fn set_modseq(context: &Context, folder: &str, modseq: u64) -> Result<()> {
pub(crate) async fn set_modseq(
context: &Context,
transport_id: u32,
folder: &str,
modseq: u64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO imap_sync (folder, modseq) VALUES (?,?)
ON CONFLICT(folder) DO UPDATE SET modseq=excluded.modseq",
(folder, modseq),
"INSERT INTO imap_sync (transport_id, folder, modseq) VALUES (?,?,?)
ON CONFLICT(transport_id, folder) DO UPDATE SET modseq=excluded.modseq",
(transport_id, folder, modseq),
)
.await?;
Ok(())
}
async fn get_modseq(context: &Context, folder: &str) -> Result<u64> {
async fn get_modseq(context: &Context, transport_id: u32, folder: &str) -> Result<u64> {
Ok(context
.sql
.query_get_value("SELECT modseq FROM imap_sync WHERE folder=?;", (folder,))
.query_get_value(
"SELECT modseq FROM imap_sync WHERE transport_id=? AND folder=?",
(transport_id, folder),
)
.await?
.unwrap_or(0))
}
@@ -2524,11 +2558,11 @@ fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
let mut ranges: Vec<UidRange> = vec![];
for &current in uids {
if let Some(last) = ranges.last_mut() {
if last.end + 1 == current {
last.end = current;
continue;
}
if let Some(last) = ranges.last_mut()
&& last.end + 1 == current
{
last.end = current;
continue;
}
ranges.push(UidRange {

View File

@@ -207,6 +207,7 @@ impl Client {
hostname: &str,
strict_tls: bool,
) -> Result<Self> {
let use_sni = true;
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
let events = context.events.clone();
@@ -215,6 +216,7 @@ impl Client {
strict_tls,
hostname,
addr.port(),
use_sni,
alpn(addr.port()),
logging_stream,
&context.tls_session_store,
@@ -251,6 +253,7 @@ impl Client {
host: &str,
strict_tls: bool,
) -> Result<Self> {
let use_sni = false;
let tcp_stream = connect_tcp_inner(addr).await?;
let account_id = context.get_id();
@@ -275,6 +278,7 @@ impl Client {
strict_tls,
host,
addr.port(),
use_sni,
"",
tcp_stream,
&context.tls_session_store,
@@ -294,6 +298,7 @@ impl Client {
strict_tls: bool,
proxy_config: ProxyConfig,
) -> Result<Self> {
let use_sni = true;
let proxy_stream = proxy_config
.connect(context, domain, port, strict_tls)
.await?;
@@ -301,6 +306,7 @@ impl Client {
strict_tls,
domain,
port,
use_sni,
alpn(port),
proxy_stream,
&context.tls_session_store,
@@ -340,6 +346,7 @@ impl Client {
proxy_config: ProxyConfig,
strict_tls: bool,
) -> Result<Self> {
let use_sni = false;
let proxy_stream = proxy_config
.connect(context, hostname, port, strict_tls)
.await?;
@@ -362,6 +369,7 @@ impl Client {
strict_tls,
hostname,
port,
use_sni,
"",
proxy_stream,
&context.tls_session_store,

View File

@@ -11,17 +11,23 @@ fn test_get_folder_meaning_by_name() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_set_uid_next_validity() {
let t = TestContext::new_alice().await;
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 0);
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 0);
set_uidvalidity(&t.ctx, "Inbox", 7).await.unwrap();
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 7);
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 0);
set_uidvalidity(&t.ctx, 1, "Inbox", 7).await.unwrap();
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 7);
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 0);
set_uid_next(&t.ctx, "Inbox", 5).await.unwrap();
set_uidvalidity(&t.ctx, "Inbox", 6).await.unwrap();
assert_eq!(get_uid_next(&t.ctx, "Inbox").await.unwrap(), 5);
assert_eq!(get_uidvalidity(&t.ctx, "Inbox").await.unwrap(), 6);
// For another transport there is still no UIDVALIDITY set.
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
set_uid_next(&t.ctx, 1, "Inbox", 5).await.unwrap();
set_uidvalidity(&t.ctx, 1, "Inbox", 6).await.unwrap();
assert_eq!(get_uid_next(&t.ctx, 1, "Inbox").await.unwrap(), 5);
assert_eq!(get_uidvalidity(&t.ctx, 1, "Inbox").await.unwrap(), 6);
assert_eq!(get_uid_next(&t.ctx, 2, "Inbox").await.unwrap(), 0);
assert_eq!(get_uidvalidity(&t.ctx, 2, "Inbox").await.unwrap(), 0);
}
#[test]

View File

@@ -5,6 +5,7 @@ use anyhow::Context as _;
use super::session::Session as ImapSession;
use super::{get_uid_next, get_uidvalidity, set_modseq, set_uid_next, set_uidvalidity};
use crate::context::Context;
use crate::ensure_and_debug_assert;
use crate::log::warn;
type Result<T> = std::result::Result<T, Error>;
@@ -34,16 +35,16 @@ impl ImapSession {
/// because no EXPUNGE responses are sent, see
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
if let Some(folder) = &self.selected_folder {
if self.selected_folder_needs_expunge {
info!(context, "Expunge messages in {folder:?}.");
if let Some(folder) = &self.selected_folder
&& self.selected_folder_needs_expunge
{
info!(context, "Expunge messages in {folder:?}.");
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
}
self.close().await.context("IMAP close/expunge failed")?;
info!(context, "Close/expunge succeeded.");
self.selected_folder = None;
self.selected_folder_needs_expunge = false;
self.new_mail = false;
}
Ok(())
}
@@ -54,10 +55,10 @@ impl ImapSession {
async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
// if there is _no_ new folder, we continue as we might want to expunge below.
if let Some(selected_folder) = &self.selected_folder {
if folder == selected_folder {
return Ok(NewlySelected::No);
}
if let Some(selected_folder) = &self.selected_folder
&& folder == selected_folder
{
return Ok(NewlySelected::No);
}
// deselect existing folder, if needed (it's also done implicitly by SELECT, however, without EXPUNGE then)
@@ -129,7 +130,7 @@ impl ImapSession {
context: &Context,
folder: &str,
create: bool,
) -> Result<bool> {
) -> anyhow::Result<bool> {
let newly_selected = if create {
self.select_or_create_folder(context, folder)
.await
@@ -146,15 +147,24 @@ impl ImapSession {
},
}
};
let transport_id = self.transport_id();
// Folders should not be selected when transport_id is not assigned yet
// because we cannot save UID validity then.
ensure_and_debug_assert!(
transport_id > 0,
"Cannot select folder when transport ID is unknown"
);
let mailbox = self
.selected_mailbox
.as_mut()
.with_context(|| format!("No mailbox selected, folder: {folder:?}"))?;
let old_uid_validity = get_uidvalidity(context, folder)
let old_uid_validity = get_uidvalidity(context, transport_id, folder)
.await
.with_context(|| format!("Failed to get old UID validity for folder {folder:?}"))?;
let old_uid_next = get_uid_next(context, folder)
let old_uid_next = get_uid_next(context, transport_id, folder)
.await
.with_context(|| format!("Failed to get old UID NEXT for folder {folder:?}"))?;
@@ -205,7 +215,7 @@ impl ImapSession {
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
);
set_uid_next(context, folder, new_uid_next).await?;
set_uid_next(context, transport_id, folder, new_uid_next).await?;
self.resync_request_sender.try_send(()).ok();
}
@@ -224,21 +234,21 @@ impl ImapSession {
}
// UIDVALIDITY is modified, reset highest seen MODSEQ.
set_modseq(context, folder, 0).await?;
set_modseq(context, transport_id, folder, 0).await?;
// ============== uid_validity has changed or is being set the first time. ==============
let new_uid_next = new_uid_next.unwrap_or_default();
set_uid_next(context, folder, new_uid_next).await?;
set_uidvalidity(context, folder, new_uid_validity).await?;
set_uid_next(context, transport_id, folder, new_uid_next).await?;
set_uidvalidity(context, transport_id, folder, new_uid_validity).await?;
self.new_mail = true;
// Collect garbage entries in `imap` table.
context
.sql
.execute(
"DELETE FROM imap WHERE folder=? AND uidvalidity!=?",
(&folder, new_uid_validity),
"DELETE FROM imap WHERE transport_id=? AND folder=? AND uidvalidity!=?",
(transport_id, &folder, new_uid_validity),
)
.await?;
@@ -247,12 +257,7 @@ impl ImapSession {
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}.",
folder,
new_uid_next,
new_uid_validity,
old_uid_next,
old_uid_validity,
"transport {transport_id}: UID validity for folder {folder} changed from {old_uid_validity}/{old_uid_next} to {new_uid_validity}/{new_uid_next}.",
);
Ok(true)
}

View File

@@ -30,6 +30,8 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
#[derive(Debug)]
pub(crate) struct Session {
transport_id: u32,
pub(super) inner: ImapSession<Box<dyn SessionStream>>,
pub capabilities: Capabilities,
@@ -71,8 +73,10 @@ impl Session {
inner: ImapSession<Box<dyn SessionStream>>,
capabilities: Capabilities,
resync_request_sender: async_channel::Sender<()>,
transport_id: u32,
) -> Self {
Self {
transport_id,
inner,
capabilities,
selected_folder: None,
@@ -84,6 +88,11 @@ impl Session {
}
}
/// Returns ID of the transport for which this session was created.
pub(crate) fn transport_id(&self) -> u32 {
self.transport_id
}
pub fn can_idle(&self) -> bool {
self.capabilities.can_idle
}

View File

@@ -979,11 +979,10 @@ mod tests {
let context1 = &TestContext::new_alice().await;
// `bcc_self` is enabled by default for test contexts. Unset it.
context1.set_config(Config::BccSelf, None).await?;
// Check that the settings are displayed correctly.
assert_eq!(
context1.get_config(Config::BccSelf).await?,
Some("1".to_string())
);
assert_eq!(
context1.get_config(Config::DeleteServerAfter).await?,
Some("0".to_string())

View File

@@ -89,6 +89,7 @@ pub mod securejoin;
mod simplify;
mod smtp;
pub mod stock_str;
pub mod storage_usage;
mod sync;
mod timesmearing;
mod token;

View File

@@ -294,7 +294,7 @@ pub async fn send_locations_to_chat(
.unwrap_or_default();
} else if 0 == seconds && is_sending_locations_before {
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
chat::add_info_msg(context, chat_id, &stock_str).await?;
}
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
@@ -849,7 +849,7 @@ async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
.context("failed to disable location streaming")?;
let stock_str = stock_str::msg_location_disabled(context).await;
chat::add_info_msg(context, chat_id, &stock_str, now).await?;
chat::add_info_msg(context, chat_id, &stock_str).await?;
context.emit_event(EventType::ChatModified(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
}

View File

@@ -638,33 +638,33 @@ impl Message {
pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
if self.viewtype.has_file() {
let file_param = self.param.get_file_path(context)?;
if let Some(path_and_filename) = file_param {
if matches!(
if let Some(path_and_filename) = file_param
&& matches!(
self.viewtype,
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
) && !self.param.exists(Param::Width)
{
let buf = read_file(context, &path_and_filename).await?;
)
&& !self.param.exists(Param::Width)
{
let buf = read_file(context, &path_and_filename).await?;
match get_filemeta(&buf) {
Ok((width, height)) => {
self.param.set_int(Param::Width, width as i32);
self.param.set_int(Param::Height, height as i32);
}
Err(err) => {
self.param.set_int(Param::Width, 0);
self.param.set_int(Param::Height, 0);
warn!(
context,
"Failed to get width and height for {}: {err:#}.",
path_and_filename.display()
);
}
match get_filemeta(&buf) {
Ok((width, height)) => {
self.param.set_int(Param::Width, width as i32);
self.param.set_int(Param::Height, height as i32);
}
Err(err) => {
self.param.set_int(Param::Width, 0);
self.param.set_int(Param::Height, 0);
warn!(
context,
"Failed to get width and height for {}: {err:#}.",
path_and_filename.display()
);
}
}
if !self.id.is_unset() {
self.update_param(context).await?;
}
if !self.id.is_unset() {
self.update_param(context).await?;
}
}
}
@@ -992,14 +992,12 @@ impl Message {
return None;
}
if let Some(filename) = self.get_file(context) {
if let Ok(ref buf) = read_file(context, &filename).await {
if let Ok((typ, headers, _)) = split_armored_data(buf) {
if typ == pgp::armor::BlockType::Message {
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
}
}
}
if let Some(filename) = self.get_file(context)
&& let Ok(ref buf) = read_file(context, &filename).await
&& let Ok((typ, headers, _)) = split_armored_data(buf)
&& typ == pgp::armor::BlockType::Message
{
return headers.get(crate::pgp::HEADER_SETUPCODE).cloned();
}
None
@@ -1224,26 +1222,25 @@ impl Message {
///
/// `References` header is not taken into account.
pub async fn parent(&self, context: &Context) -> Result<Option<Message>> {
if let Some(in_reply_to) = &self.in_reply_to {
if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
let msg = Message::load_from_db_optional(context, msg_id).await?;
return Ok(msg);
}
if let Some(in_reply_to) = &self.in_reply_to
&& let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await?
{
let msg = Message::load_from_db_optional(context, msg_id).await?;
return Ok(msg);
}
Ok(None)
}
/// Returns original message ID for message from "Saved Messages".
pub async fn get_original_msg_id(&self, context: &Context) -> Result<Option<MsgId>> {
if !self.original_msg_id.is_special() {
if let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
{
return if msg.chat_id.is_trash() {
Ok(None)
} else {
Ok(Some(msg.id))
};
}
if !self.original_msg_id.is_special()
&& let Some(msg) = Message::load_from_db_optional(context, self.original_msg_id).await?
{
return if msg.chat_id.is_trash() {
Ok(None)
} else {
Ok(Some(msg.id))
};
}
Ok(None)
}
@@ -1613,10 +1610,10 @@ pub(crate) async fn delete_msg_locally(context: &Context, msg: &Message) -> Resu
.expect("RwLock is poisoned")
.as_ref()
.map(|dl| dl.msg_id);
if let Some(id) = logging_xdc_id {
if id == msg.id {
set_debug_logging_xdc(context, None).await?;
}
if let Some(id) = logging_xdc_id
&& id == msg.id
{
set_debug_logging_xdc(context, None).await?;
}
Ok(())
@@ -1871,6 +1868,33 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
Ok(())
}
/// Checks if the messages with given IDs exist.
///
/// Returns IDs of existing messages.
pub async fn get_existing_msg_ids(context: &Context, ids: &[MsgId]) -> Result<Vec<MsgId>> {
let query_only = true;
let res = context
.sql
.transaction_ex(query_only, |transaction| {
let mut res: Vec<MsgId> = Vec::new();
for id in ids {
if transaction.query_one(
"SELECT COUNT(*) > 0 FROM msgs WHERE id=? AND chat_id!=3",
(id,),
|row| {
let exists: bool = row.get(0)?;
Ok(exists)
},
)? {
res.push(*id);
}
}
Ok(res)
})
.await?;
Ok(res)
}
pub(crate) async fn update_msg_state(
context: &Context,
msg_id: MsgId,

View File

@@ -39,17 +39,16 @@ async fn test_get_width_height() {
let mut has_image = false;
let chatitems = chat::get_chat_msgs(&t, device_chat_id).await.unwrap();
for chatitem in chatitems {
if let ChatItem::Message { msg_id } = chatitem {
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
if msg.get_viewtype() == Viewtype::Image {
has_image = true;
// just check that width/height are inside some reasonable ranges
assert!(msg.get_width() > 100);
assert!(msg.get_height() > 100);
assert!(msg.get_width() < 4000);
assert!(msg.get_height() < 4000);
}
}
if let ChatItem::Message { msg_id } = chatitem
&& let Ok(msg) = Message::load_from_db(&t, msg_id).await
&& msg.get_viewtype() == Viewtype::Image
{
has_image = true;
// just check that width/height are inside some reasonable ranges
assert!(msg.get_width() > 100);
assert!(msg.get_height() > 100);
assert!(msg.get_width() < 4000);
assert!(msg.get_height() < 4000);
}
}
assert!(has_image);
@@ -765,3 +764,27 @@ async fn test_load_unknown_viewtype() -> Result<()> {
assert_eq!(bob_msg.get_viewtype(), Viewtype::Unknown);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_existing_msg_ids() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let msg1_id = tcm.send_recv(alice, bob, "Hello 1!").await.id;
let msg2_id = tcm.send_recv(alice, bob, "Hello 2!").await.id;
let msg3_id = tcm.send_recv(alice, bob, "Hello 3!").await.id;
let msg4_id = tcm.send_recv(alice, bob, "Hello 4!").await.id;
assert_eq!(
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
vec![msg1_id, msg2_id, msg3_id, msg4_id]
);
delete_msgs(bob, &[msg1_id, msg3_id]).await?;
assert_eq!(
get_existing_msg_ids(bob, &[msg1_id, msg2_id, msg3_id, msg4_id]).await?,
vec![msg2_id, msg4_id]
);
Ok(())
}

View File

@@ -9,7 +9,7 @@ use data_encoding::BASE32_NOPAD;
use deltachat_contact_tools::sanitize_bidi_characters;
use iroh_gossip::proto::TopicId;
use mail_builder::headers::HeaderType;
use mail_builder::headers::address::{Address, EmailAddress};
use mail_builder::headers::address::Address;
use mail_builder::mime::MimePart;
use tokio::fs;
@@ -292,11 +292,10 @@ impl MimeFactory {
// In a broadcast channel, only send member-added/removed messages
// to the affected member:
if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
if fp? != fingerprint {
if let Some(fp) = must_have_only_one_recipient(&msg, &chat)
&& fp? != fingerprint {
continue;
}
}
let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
Some(SignedPublicKey::from_slice(public_key_bytes)?)
@@ -347,8 +346,8 @@ impl MimeFactory {
// Row is a tombstone,
// member is not actually part of the group.
if !recipients_contain_addr(&past_members, &addr) {
if let Some(email_to_remove) = email_to_remove {
if email_to_remove == addr {
if let Some(email_to_remove) = email_to_remove
&& email_to_remove == addr {
// This is a "member removed" message,
// we need to notify removed member
// that it was removed.
@@ -365,7 +364,6 @@ impl MimeFactory {
}
}
}
}
if !undisclosed_recipients {
past_members.push((name, addr.clone()));
past_member_timestamps.push(remove_timestamp);
@@ -395,15 +393,14 @@ impl MimeFactory {
"member_fingerprints.len() ({}) < to.len() ({})",
member_fingerprints.len(), to.len());
if to.len() > 1 {
if let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
if to.len() > 1
&& let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
to.remove(position);
member_timestamps.remove(position);
if is_encrypted {
member_fingerprints.remove(position);
}
}
}
member_timestamps.extend(past_member_timestamps);
if is_encrypted {
@@ -467,7 +464,11 @@ impl MimeFactory {
.unwrap_or_default(),
false => "".to_string(),
};
let attach_selfavatar = Self::should_attach_selfavatar(context, &msg).await;
// We don't display avatars for address-contacts, so sending avatars w/o encryption is not
// useful and causes e.g. Outlook to reject a message with a big header, see
// https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
let attach_selfavatar =
Self::should_attach_selfavatar(context, &msg).await && encryption_pubkeys.is_some();
ensure_and_debug_assert!(
member_timestamps.is_empty()
@@ -733,36 +734,35 @@ impl MimeFactory {
));
}
if let Loaded::Message { chat, .. } = &self.loaded {
if chat.typ == Chattype::Group {
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await?
{
headers.push((
"Chat-Group-Member-Timestamps",
mail_builder::headers::raw::Raw::new(
self.member_timestamps
.iter()
.map(|ts| ts.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if let Loaded::Message { chat, .. } = &self.loaded
&& chat.typ == Chattype::Group
{
if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
headers.push((
"Chat-Group-Member-Timestamps",
mail_builder::headers::raw::Raw::new(
self.member_timestamps
.iter()
.map(|ts| ts.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if !self.member_fingerprints.is_empty() {
headers.push((
"Chat-Group-Member-Fpr",
mail_builder::headers::raw::Raw::new(
self.member_fingerprints
.iter()
.map(|fp| fp.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
if !self.member_fingerprints.is_empty() {
headers.push((
"Chat-Group-Member-Fpr",
mail_builder::headers::raw::Raw::new(
self.member_fingerprints
.iter()
.map(|fp| fp.to_string())
.collect::<Vec<String>>()
.join(" "),
)
.into(),
));
}
}
@@ -814,37 +814,34 @@ impl MimeFactory {
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
));
} else if let Loaded::Message { msg, .. } = &self.loaded {
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
));
}
} else if let Loaded::Message { msg, .. } = &self.loaded
&& msg.param.get_cmd() == SystemMessage::SecurejoinMessage
{
let step = msg.param.get(Param::Arg).unwrap_or_default();
if step != "vg-request" && step != "vc-request" {
headers.push((
"Auto-Submitted",
mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
));
}
}
if let Loaded::Message { msg, chat } = &self.loaded {
if chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast {
headers.push((
"Chat-List-ID",
mail_builder::headers::text::Text::new(format!(
"{} <{}>",
chat.name, chat.grpid
))
if let Loaded::Message { msg, chat } = &self.loaded
&& (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
{
headers.push((
"Chat-List-ID",
mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
.into(),
));
));
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup {
if let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET) {
headers.push((
"Chat-Broadcast-Secret",
mail_builder::headers::text::Text::new(secret.to_string()).into(),
));
}
}
if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
&& let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
{
headers.push((
"Chat-Broadcast-Secret",
mail_builder::headers::text::Text::new(secret.to_string()).into(),
));
}
}
@@ -1000,24 +997,7 @@ impl MimeFactory {
} else if header_name == "to" {
protected_headers.push(header.clone());
if is_encrypted {
let mut to_without_names = to
.clone()
.into_iter()
.filter_map(|header| match header {
Address::Address(mb) => Some(Address::Address(EmailAddress {
name: None,
email: mb.email,
})),
_ => None,
})
.collect::<Vec<_>>();
if to_without_names.is_empty() {
to_without_names.push(hidden_recipients());
}
unprotected_headers.push((
original_header_name,
Address::new_list(to_without_names).into(),
));
unprotected_headers.push(("To", hidden_recipients().into()));
} else {
unprotected_headers.push(header.clone());
}
@@ -1043,7 +1023,15 @@ impl MimeFactory {
//
// and the explanation page says
// "The time information deviates too much from the actual time".
let timestamp_offset = rand::random_range(0..1000000);
//
// We also limit the range to 6 days (518400 seconds)
// because with a larger range we got
// error "500 Date header far in the past/future"
// which apparently originates from Symantec Messaging Gateway
// and means the message has a Date that is more
// than 7 days in the past:
// <https://github.com/chatmail/core/issues/7466>
let timestamp_offset = rand::random_range(0..518400);
let protected_timestamp = self.timestamp.saturating_sub(timestamp_offset);
let unprotected_date =
chrono::DateTime::<chrono::Utc>::from_timestamp(protected_timestamp, 0)
@@ -1204,16 +1192,16 @@ impl MimeFactory {
// Set the appropriate Content-Type for the inner message.
for (h, v) in &mut message.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "cipher");
}
*ct = ct_new;
break;
}
}
@@ -1238,7 +1226,7 @@ impl MimeFactory {
// created before we had symmetric encryption,
// we show an error message.
let text = BROADCAST_INCOMPATIBILITY_MSG;
chat::add_info_msg(context, chat.id, text, time()).await?;
chat::add_info_msg(context, chat.id, text).await?;
bail!(text);
}
secret
@@ -1257,10 +1245,10 @@ impl MimeFactory {
// once new core versions are sufficiently deployed.
let anonymous_recipients = false;
if context.get_config_bool(Config::TestHooks).await? {
if let Some(hook) = &*context.pre_encrypt_mime_hook.lock() {
message = hook(context, message);
}
if context.get_config_bool(Config::TestHooks).await?
&& let Some(hook) = &*context.pre_encrypt_mime_hook.lock()
{
message = hook(context, message);
}
let encrypted = if let Some(shared_secret) = shared_secret {
@@ -1348,16 +1336,16 @@ impl MimeFactory {
message
} else {
for (h, v) in &mut message.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
let mut ct_new = ct.clone();
ct_new = ct_new.attribute("protected-headers", "v1");
if use_std_header_protection {
ct_new = ct_new.attribute("hp", "clear");
}
*ct = ct_new;
break;
}
}
@@ -1537,10 +1525,9 @@ impl MimeFactory {
.await?
.unwrap_or_default()
{
placeholdertext = Some(stock_str::msg_group_left_remote(context).await);
placeholdertext = Some("I left the group.".to_string());
} else {
placeholdertext =
Some(stock_str::msg_del_member_remote(context, email_to_remove).await);
placeholdertext = Some(format!("I removed member {email_to_remove}."));
};
if !email_to_remove.is_empty() {
@@ -1563,8 +1550,7 @@ impl MimeFactory {
let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
let fingerprint_to_add = msg.param.get(Param::Arg4).unwrap_or_default();
placeholdertext =
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
placeholdertext = Some(format!("I added member {email_to_add}."));
if !email_to_add.is_empty() {
headers.push((
@@ -1600,7 +1586,7 @@ impl MimeFactory {
"Chat-Content",
mail_builder::headers::text::Text::new("group-avatar-changed").into(),
));
if grpimage.is_none() {
if grpimage.is_none() && is_encrypted {
headers.push((
"Chat-Group-Avatar",
mail_builder::headers::raw::Raw::new("0").into(),
@@ -1727,7 +1713,9 @@ impl MimeFactory {
_ => {}
}
if let Some(grpimage) = grpimage {
if let Some(grpimage) = grpimage
&& is_encrypted
{
info!(context, "setting group image '{}'", grpimage);
let avatar = build_avatar_file(context, grpimage)
.await
@@ -1882,10 +1870,10 @@ impl MimeFactory {
parts.push(msg_kml_part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await? {
if let Some(part) = self.get_location_kml_part(context).await? {
parts.push(part);
}
if location::is_sending_locations_to_chat(context, Some(msg.chat_id)).await?
&& let Some(part) = self.get_location_kml_part(context).await?
{
parts.push(part);
}
// we do not piggyback sync-files to other self-sent-messages

View File

@@ -6,7 +6,8 @@ use std::time::Duration;
use super::*;
use crate::chat::{
self, ChatId, add_contact_to_chat, create_group, remove_contact_from_chat, send_text_msg,
self, ChatId, add_contact_to_chat, create_group, create_group_unencrypted,
remove_contact_from_chat, send_text_msg,
};
use crate::chatlist::Chatlist;
use crate::constants;
@@ -351,7 +352,7 @@ async fn test_subject_in_group() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = tcm.alice().await;
let bob = tcm.bob().await;
let group_id = chat::create_group(&t, "groupname").await.unwrap();
let group_id = create_group(&t, "groupname").await.unwrap();
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
@@ -592,26 +593,6 @@ async fn test_selfavatar_unencrypted() -> anyhow::Result<()> {
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("text/plain").count(), 1);
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(inner.match_indices("Subject:").count(), 0);
assert_eq!(body.match_indices("this is the text!").count(), 1);
// if another message is sent, that one must not contain the avatar
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(3, "\r\n\r\n");
let outer = payload.next().unwrap();
let inner = payload.next().unwrap();
let body = payload.next().unwrap();
assert_eq!(outer.match_indices("multipart/mixed").count(), 1);
assert_eq!(outer.match_indices("Message-ID:").count(), 1);
assert_eq!(outer.match_indices("Subject:").count(), 1);
assert_eq!(outer.match_indices("Autocrypt:").count(), 1);
assert_eq!(outer.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(inner.match_indices("text/plain").count(), 1);
assert_eq!(inner.match_indices("Message-ID:").count(), 1);
assert_eq!(inner.match_indices("Chat-User-Avatar:").count(), 0);
@@ -670,7 +651,7 @@ async fn test_selfavatar_unencrypted_signed() {
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(part.match_indices("From:").count(), 0);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
@@ -684,58 +665,6 @@ async fn test_selfavatar_unencrypted_signed() {
.unwrap();
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert_eq!(alice_contact.is_key_contact(), false);
assert!(
alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some()
);
// if another message is sent, that one must not contain the avatar
let mut msg = Message::new_text("this is the text!".to_string());
let sent_msg = t.send_msg(chat.id, &mut msg).await;
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
let part = payload.next().unwrap();
assert_eq!(part.match_indices("multipart/signed").count(), 1);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
.count(),
1
);
assert_eq!(part.match_indices("From:").count(), 1);
assert_eq!(part.match_indices("Message-ID:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 1);
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
let part = payload.next().unwrap();
assert_eq!(part.match_indices("text/plain").count(), 1);
assert_eq!(body.match_indices("From:").count(), 0);
assert_eq!(part.match_indices("Message-ID:").count(), 1);
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
assert_eq!(part.match_indices("Subject:").count(), 0);
let body = payload.next().unwrap();
assert_eq!(body.match_indices("this is the text!").count(), 1);
bob.recv_msg(&sent_msg).await;
let alice_contact = Contact::get_by_id(&bob.ctx, alice_id).await.unwrap();
assert!(
alice_contact
.get_profile_image(&bob.ctx)
.await
.unwrap()
.is_some()
);
}
/// Test that removed member address does not go into the `To:` field.
@@ -743,17 +672,20 @@ async fn test_selfavatar_unencrypted_signed() {
async fn test_remove_member_bcc() -> Result<()> {
let mut tcm = TestContextManager::new();
// Alice creates a group with Bob and Claire and then removes Bob.
// Alice creates a group with Bob and Charlie and then removes Charlie.
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let charlie = &tcm.charlie().await;
let bob_id = alice.add_or_lookup_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
let charlie_addr = charlie_contact.get_addr();
let alice_addr = alice.get_config(Config::Addr).await?.unwrap();
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
let charlie_addr = charlie.get_config(Config::Addr).await?.unwrap();
let alice_chat_id = create_group(alice, "foo").await?;
let bob_id = alice.add_or_lookup_address_contact_id(bob).await;
let charlie_id = alice.add_or_lookup_address_contact_id(charlie).await;
let alice_chat_id = create_group_unencrypted(alice, "foo").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
@@ -770,8 +702,9 @@ async fn test_remove_member_bcc() -> Result<()> {
for to_addr in to.iter() {
match to_addr {
mailparse::MailAddr::Single(info) => {
// Addresses should be of existing members (Alice and Bob) and not Charlie.
// Addresses should be of existing members and not Charlie.
assert_ne!(info.addr, charlie_addr);
assert!(info.addr == alice_addr || info.addr == bob_addr);
}
mailparse::MailAddr::Group(_) => {
panic!("Group addresses are not expected here");

View File

@@ -273,12 +273,11 @@ impl MimeMessage {
&mut chat_disposition_notification_to,
&mail,
);
headers.retain(|k, _| {
!is_hidden(k) || {
headers_removed.insert(k.to_string());
false
}
});
headers_removed.extend(
headers
.extract_if(|k, _v| is_hidden(k))
.map(|(k, _v)| k.to_string()),
);
// Parse hidden headers.
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
@@ -310,13 +309,14 @@ impl MimeMessage {
// Currently we do not sign unencrypted messages by default.
(&mail, mimetype)
};
if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
if let Some(part) = part.subparts.first() {
for field in &part.headers {
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
headers.insert(key.to_string(), field.get_value());
}
if mimetype.type_() == mime::MULTIPART
&& mimetype.subtype().as_str() == "mixed"
&& let Some(part) = part.subparts.first()
{
for field in &part.headers {
let key = field.get_key().to_lowercase();
if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
headers.insert(key.to_string(), field.get_value());
}
}
}
@@ -679,8 +679,9 @@ impl MimeMessage {
fn parse_system_message_headers(&mut self, context: &Context) {
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
self.parts.retain(|part| {
part.mimetype.is_none()
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
part.mimetype
.as_ref()
.is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
});
if self.parts.len() == 1 {
@@ -805,22 +806,20 @@ impl MimeMessage {
{
part.typ = Viewtype::Voice;
}
if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
if let Some(value) = self.get_header(HeaderDef::ChatContent) {
if value == "sticker" {
part.typ = Viewtype::Sticker;
}
}
}
if part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video
if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
&& let Some(value) = self.get_header(HeaderDef::ChatContent)
&& value == "sticker"
{
if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
}
part.typ = Viewtype::Sticker;
}
if (part.typ == Viewtype::Audio
|| part.typ == Viewtype::Voice
|| part.typ == Viewtype::Video)
&& let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
{
let duration_ms = field_0.parse().unwrap_or_default();
if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
part.param.set_int(Param::Duration, duration_ms);
}
}
@@ -836,38 +835,38 @@ impl MimeMessage {
self.squash_attachment_parts();
}
if !context.get_config_bool(Config::Bot).await? {
if let Some(ref subject) = self.get_subject() {
let mut prepend_subject = true;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.has_chat_version()
|| subject.contains("Chat:")
{
prepend_subject = false
}
if !context.get_config_bool(Config::Bot).await?
&& let Some(ref subject) = self.get_subject()
{
let mut prepend_subject = true;
if !self.decrypting_failed {
let colon = subject.find(':');
if colon == Some(2)
|| colon == Some(3)
|| self.has_chat_version()
|| subject.contains("Chat:")
{
prepend_subject = false
}
}
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.is_mailinglist_message() && !self.has_chat_version() {
prepend_subject = true;
}
// For mailing lists, always add the subject because sometimes there are different topics
// and otherwise it might be hard to keep track:
if self.is_mailinglist_message() && !self.has_chat_version() {
prepend_subject = true;
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(part) = part_with_text {
// Message bubbles are small, so we use en dash to save space. In some
// languages there may be em dashes in the message text added by the author,
// they may look stronger than Subject separation, this is a known thing.
// Anyway, classic email support isn't a priority as of 2025.
part.msg = format!("{} {}", subject, part.msg);
}
if prepend_subject && !subject.is_empty() {
let part_with_text = self
.parts
.iter_mut()
.find(|part| !part.msg.is_empty() && !part.is_reaction);
if let Some(part) = part_with_text {
// Message bubbles are small, so we use en dash to save space. In some
// languages there may be em dashes in the message text added by the author,
// they may look stronger than Subject separation, this is a known thing.
// Anyway, classic email support isn't a priority as of 2025.
part.msg = format!("{} {}", subject, part.msg);
}
}
}
@@ -881,21 +880,22 @@ impl MimeMessage {
self.parse_attachments();
// See if an MDN is requested from the other side
if !self.decrypting_failed && !self.parts.is_empty() {
if let Some(ref dn_to) = self.chat_disposition_notification_to {
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
if !self.decrypting_failed
&& !self.parts.is_empty()
&& let Some(ref dn_to) = self.chat_disposition_notification_to
{
// Check that the message is not outgoing.
let from = &self.from.addr;
if !context.is_self_addr(from).await? {
if from.to_lowercase() == dn_to.addr.to_lowercase() {
if let Some(part) = self.parts.last_mut() {
part.param.set_int(Param::WantsMdn, 1);
}
} else {
warn!(
context,
"{} requested a read receipt to {}, ignoring", from, dn_to.addr
);
}
}
}
@@ -910,10 +910,11 @@ impl MimeMessage {
..Default::default()
};
if let Some(ref subject) = self.get_subject() {
if !self.has_chat_version() && self.webxdc_status_update.is_none() {
part.msg = subject.to_string();
}
if let Some(ref subject) = self.get_subject()
&& !self.has_chat_version()
&& self.webxdc_status_update.is_none()
{
part.msg = subject.to_string();
}
self.do_add_single_part(part);
@@ -952,15 +953,15 @@ impl MimeMessage {
let mut i = 0;
while let Some(part) = self.parts.get_mut(i) {
if let Some(part_filename) = &part.org_filename {
if part_filename == &header_value {
if let Some(blob) = part.param.get(Param::File) {
let res = Some(AvatarAction::Change(blob.to_string()));
self.parts.remove(i);
return Ok(res);
}
break;
if let Some(part_filename) = &part.org_filename
&& part_filename == &header_value
{
if let Some(blob) = part.param.get(Param::File) {
let res = Some(AvatarAction::Change(blob.to_string()));
self.parts.remove(i);
return Ok(res);
}
break;
}
i += 1;
}
@@ -1604,13 +1605,13 @@ impl MimeMessage {
} else if let Some(sender) = self.get_header(HeaderDef::Sender) {
// the `Sender:`-header alone is no indicator for mailing list
// as also used for bot-impersonation via `set_override_sender_name()`
if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
if precedence == "list" || precedence == "bulk" {
// The message belongs to a mailing list, but there is no `ListId:`-header;
// `Sender:`-header is be used to get a unique id.
// This method is used by implementations as Majordomo.
return Some(sender);
}
if let Some(precedence) = self.get_header(HeaderDef::Precedence)
&& (precedence == "list" || precedence == "bulk")
{
// The message belongs to a mailing list, but there is no `ListId:`-header;
// `Sender:`-header is be used to get a unique id.
// This method is used by implementations as Majordomo.
return Some(sender);
}
}
None
@@ -1651,10 +1652,10 @@ impl MimeMessage {
remove_header(headers, "autocrypt-gossip", removed);
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
if secure_join == "vc-request" || secure_join == "vg-request" {
headers.insert("secure-join".to_string(), secure_join);
}
if let Some(secure_join) = remove_header(headers, "secure-join", removed)
&& (secure_join == "vc-request" || secure_join == "vg-request")
{
headers.insert("secure-join".to_string(), secure_join);
}
}
@@ -1679,12 +1680,11 @@ impl MimeMessage {
// See <https://www.rfc-editor.org/rfc/rfc9788.html>.
let has_header_protection = part.ctype.params.contains_key("hp");
headers.retain(|k, _| {
!(has_header_protection || is_protected(k)) || {
headers_removed.insert(k.to_string());
false
}
});
headers_removed.extend(
headers
.extract_if(|k, _v| has_header_protection || is_protected(k))
.map(|(k, _v)| k.to_string()),
);
for field in fields {
// lowercasing all headers is technically not correct, but makes things work better
let key = field.get_key().to_lowercase();
@@ -1889,12 +1889,11 @@ impl MimeMessage {
.iter()
.filter(|p| p.typ == Viewtype::Text)
.count();
if text_part_cnt == 2 {
if let Some(last_part) = self.parts.last() {
if last_part.typ == Viewtype::Text {
self.parts.pop();
}
}
if text_part_cnt == 2
&& let Some(last_part) = self.parts.last()
&& last_part.typ == Viewtype::Text
{
self.parts.pop();
}
}
}
@@ -1947,15 +1946,15 @@ impl MimeMessage {
}
}
if let Some(delivery_report) = &self.delivery_report {
if delivery_report.failure {
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(err) = handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle NDN: {err:#}.");
}
if let Some(delivery_report) = &self.delivery_report
&& delivery_report.failure
{
let error = parts
.iter()
.find(|p| p.typ == Viewtype::Text)
.map(|p| p.msg.clone());
if let Err(err) = handle_ndn(context, delivery_report, error).await {
warn!(context, "Could not handle NDN: {err:#}.");
}
}
}
@@ -2303,14 +2302,14 @@ fn get_attachment_filename(
// `Content-Disposition: ... filename=...`
let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
if desired_filename.is_none() {
if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
warn!(context, "apostrophed encoding invalid: {}", name);
desired_filename = Some(name);
}
if desired_filename.is_none()
&& let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
{
// be graceful and just use the original name.
// some MUA, including Delta Chat up to core1.50,
// use `filename*` mistakenly for simple encoded-words without following rfc2231
warn!(context, "apostrophed encoding invalid: {}", name);
desired_filename = Some(name);
}
// if no filename is set, try `Content-Disposition: ... name=...`
@@ -2383,24 +2382,23 @@ fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<Si
.iter()
.rev()
.find(|h| h.get_key().to_lowercase() == header)
&& let Ok(addrs) = mailparse::addrparse_header(header)
{
if let Ok(addrs) = mailparse::addrparse_header(header) {
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(info) => {
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(info) => {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
mailparse::MailAddr::Group(infos) => {
for info in &infos.addrs {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
mailparse::MailAddr::Group(infos) => {
for info in &infos.addrs {
result.push(SingleInfo {
addr: addr_normalize(&info.addr).to_lowercase(),
display_name: info.display_name.clone(),
});
}
}
}
}
}

View File

@@ -1774,10 +1774,10 @@ async fn test_hp_legacy_display() -> Result<()> {
alice.set_config_bool(Config::TestHooks, true).await?;
*alice.pre_encrypt_mime_hook.lock() = Some(|_, mut mime| {
for (h, v) in &mut mime.headers {
if h == "Content-Type" {
if let mail_builder::headers::HeaderType::ContentType(ct) = v {
*ct = ct.clone().attribute("hp-legacy-display", "1");
}
if h == "Content-Type"
&& let mail_builder::headers::HeaderType::ContentType(ct) = v
{
*ct = ct.clone().attribute("hp-legacy-display", "1");
}
}
mime

View File

@@ -131,11 +131,13 @@ pub(crate) async fn connect_tls_inner(
alpn: &str,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'static> {
let use_sni = true;
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(
strict_tls,
host,
addr.port(),
use_sni,
alpn,
tcp_stream,
tls_session_store,

View File

@@ -229,64 +229,6 @@ pub(crate) async fn update_connect_timestamp(
/// Preloaded DNS results that can be used in case of DNS server failures.
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
HashMap::from([
(
"mail.sangham.net",
vec![
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
],
),
(
"nine.testrun.org",
vec![
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
IpAddr::V4(Ipv4Addr::new(49, 12, 116, 128)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
],
),
(
"disroot.org",
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
),
(
"imap.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
],
),
(
"smtp.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
],
),
(
"mail.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
],
),
(
"smtp.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
],
),
(
"daleth.cafe",
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
),
(
"imap.163.com",
vec![IpAddr::V4(Ipv4Addr::new(111, 124, 203, 45))],
@@ -295,6 +237,21 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"smtp.163.com",
vec![IpAddr::V4(Ipv4Addr::new(103, 129, 252, 45))],
),
(
"newyear.aktivix.org",
vec![IpAddr::V4(Ipv4Addr::new(209, 51, 180, 245))],
),
(
"smtp.aliyun.com",
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
),
(
"imap.aliyun.com",
vec![
IpAddr::V4(Ipv4Addr::new(59, 82, 43, 123)),
IpAddr::V4(Ipv4Addr::new(59, 82, 9, 176)),
],
),
(
"imap.aol.com",
vec![
@@ -302,17 +259,67 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(87, 248, 98, 69)),
],
),
(
"imap.arcor.de",
vec![
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
],
),
(
"smtp.aol.com",
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 31))],
),
(
"mail.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
vec![
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
],
),
(
"imap.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
"mail.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
IpAddr::V4(Ipv4Addr::new(185, 218, 207, 228)),
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
],
),
(
"smtp.autistici.org",
vec![
IpAddr::V4(Ipv4Addr::new(82, 94, 249, 234)),
IpAddr::V4(Ipv4Addr::new(93, 190, 126, 19)),
IpAddr::V4(Ipv4Addr::new(198, 167, 222, 108)),
],
),
(
"imaps.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
],
),
(
"smtpauths.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 176, 232)),
IpAddr::V4(Ipv4Addr::new(16, 62, 15, 25)),
IpAddr::V4(Ipv4Addr::new(16, 63, 183, 216)),
],
),
(
"mail.buzon.uy",
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
),
(
"daleth.cafe",
vec![IpAddr::V4(Ipv4Addr::new(37, 27, 6, 204))],
),
(
"disroot.org",
vec![IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139))],
),
(
"imap.fastmail.com",
@@ -328,6 +335,39 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 60)),
],
),
(
"imap.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 108)),
IpAddr::V4(Ipv4Addr::new(66, 102, 1, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
],
),
(
"mail.ecloud.global",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
),
(
"mail.ende.in.net",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
),
(
"smtp.gmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
],
),
(
"mail.gmx.net",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
],
),
(
"imap.gmx.net",
vec![
@@ -335,6 +375,13 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 186)),
],
),
(
"mail.sangham.net",
vec![
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
],
),
(
"imap.mail.de",
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 16))],
@@ -349,7 +396,7 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.naver.com",
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 153))],
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 233, 34))],
),
(
"imap.ouvaton.coop",
@@ -359,6 +406,57 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"imap.purelymail.com",
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
),
(
"mail.systemausfall.org",
vec![
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
],
),
(
"mail.systemli.org",
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
),
("testrun.org", vec![IpAddr::V4(Ipv4Addr::new(5, 1, 76, 52))]),
(
"nine.testrun.org",
vec![
IpAddr::V4(Ipv4Addr::new(128, 140, 126, 197)),
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
IpAddr::V4(Ipv4Addr::new(216, 144, 228, 100)),
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
IpAddr::V6(Ipv6Addr::new(
0x2001, 0x41d0, 0x701, 0x1100, 0, 0, 0, 0x8ab1,
)),
],
),
(
"secureimap.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
],
),
(
"securesmtp.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
],
),
(
"mail.riseup.net",
vec![
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 171)),
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 170)),
],
),
(
"pimap.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"imap.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 10))],
@@ -367,6 +465,14 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"smtp.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 13))],
),
(
"imap.ukr.net",
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 240))],
),
(
"smtp.ukr.net",
vec![IpAddr::V4(Ipv4Addr::new(212, 42, 75, 250))],
),
(
"imap.web.de",
vec![
@@ -380,33 +486,9 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.zoho.eu",
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25))],
),
(
"imaps.bluewin.ch",
vec![
IpAddr::V4(Ipv4Addr::new(16, 62, 253, 42)),
IpAddr::V4(Ipv4Addr::new(16, 63, 141, 244)),
IpAddr::V4(Ipv4Addr::new(16, 63, 146, 183)),
],
),
(
"mail.buzon.uy",
vec![IpAddr::V4(Ipv4Addr::new(185, 101, 93, 79))],
),
(
"mail.ecloud.global",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 246, 96))],
),
(
"mail.ende.in.net",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 5, 72))],
),
(
"mail.gmx.net",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 168)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 190)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 25)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 206)),
],
),
(
@@ -424,24 +506,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"mail.nubo.coop",
vec![IpAddr::V4(Ipv4Addr::new(79, 99, 201, 10))],
),
(
"mail.riseup.net",
vec![
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
],
),
(
"mail.systemausfall.org",
vec![
IpAddr::V4(Ipv4Addr::new(51, 75, 71, 249)),
IpAddr::V4(Ipv4Addr::new(80, 153, 252, 42)),
],
),
(
"mail.systemli.org",
vec![IpAddr::V4(Ipv4Addr::new(93, 190, 126, 36))],
),
(
"mehl.cloud",
vec![IpAddr::V4(Ipv4Addr::new(95, 217, 223, 172))],
@@ -449,20 +513,14 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
(
"mx.freenet.de",
vec![
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 210)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 211)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 212)),
IpAddr::V4(Ipv4Addr::new(195, 4, 92, 213)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 36)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 34)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 35)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 39)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 37)),
IpAddr::V4(Ipv4Addr::new(194, 97, 208, 38)),
],
),
(
"newyear.aktivix.org",
vec![IpAddr::V4(Ipv4Addr::new(162, 247, 75, 192))],
),
(
"pimap.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"posteo.de",
vec![
@@ -474,26 +532,6 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"psmtp.schulon.org",
vec![IpAddr::V4(Ipv4Addr::new(194, 77, 246, 20))],
),
(
"secureimap.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 114)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 115)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 50)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 51)),
],
),
(
"securesmtp.t-online.de",
vec![
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 110)),
IpAddr::V4(Ipv4Addr::new(194, 25, 134, 46)),
],
),
(
"smtp.aliyun.com",
vec![IpAddr::V4(Ipv4Addr::new(47, 246, 136, 232))],
),
(
"smtp.mail.de",
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 21))],
@@ -501,8 +539,8 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
(
"smtp.mail.ru",
vec![
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
],
),
(
@@ -520,6 +558,20 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
"imap.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
),
(
"imap.migadu.com",
vec![
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 23)),
IpAddr::V4(Ipv4Addr::new(51, 210, 3, 20)),
],
),
(
"smtp.migadu.com",
vec![
IpAddr::V4(Ipv4Addr::new(37, 59, 57, 117)),
IpAddr::V4(Ipv4Addr::new(51, 255, 82, 75)),
],
),
(
"smtp.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
@@ -538,30 +590,48 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
vec![
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
],
),
(
"smtp.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
vec![
IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54)),
IpAddr::V4(Ipv4Addr::new(43, 163, 178, 76)),
],
),
(
"imap.rambler.ru",
vec![
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 168)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 169)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 170)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 171)),
],
),
(
"smtp.rambler.ru",
vec![
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 164)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 165)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 166)),
IpAddr::V4(Ipv4Addr::new(81, 19, 77, 167)),
],
),
(
"stinpriza.net",
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
),
(
"webbox222.server-home.org",
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
),
(
"undernet.uy",
vec![IpAddr::V4(Ipv4Addr::new(200, 40, 115, 74))],
),
(
"imap.vivaldi.net",
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 15))],
@@ -572,17 +642,23 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"imap.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
vec![
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 210)),
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
],
),
(
"smtp.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
vec![
IpAddr::V4(Ipv4Addr::new(151, 189, 176, 206)),
IpAddr::V4(Ipv4Addr::new(178, 15, 69, 206)),
],
),
(
"smtp.web.de",
vec![
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
],
),
(
@@ -599,23 +675,10 @@ static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new
),
(
"smtp.zoho.eu",
vec![IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164))],
),
(
"smtpauths.bluewin.ch",
vec![IpAddr::V4(Ipv4Addr::new(195, 186, 120, 54))],
),
(
"stinpriza.net",
vec![IpAddr::V4(Ipv4Addr::new(5, 9, 122, 184))],
),
(
"undernet.uy",
vec![IpAddr::V4(Ipv4Addr::new(167, 62, 254, 153))],
),
(
"webbox222.server-home.org",
vec![IpAddr::V4(Ipv4Addr::new(91, 203, 111, 88))],
vec![
IpAddr::V4(Ipv4Addr::new(185, 230, 212, 164)),
IpAddr::V4(Ipv4Addr::new(185, 230, 214, 164)),
],
),
])
});

View File

@@ -74,19 +74,33 @@ where
}
"https" => {
let port = parsed_url.port_u16().unwrap_or(443);
let load_cache = true;
let (use_sni, load_cache) = (true, true);
if let Some(proxy_config) = proxy_config_opt {
let proxy_stream = proxy_config
.connect(context, host, port, load_cache)
.await?;
let tls_stream =
wrap_rustls(host, port, "", proxy_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(
host,
port,
use_sni,
"",
proxy_stream,
&context.tls_session_store,
)
.await?;
Box::new(tls_stream)
} else {
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
let tls_stream =
wrap_rustls(host, port, "", tcp_stream, &context.tls_session_store).await?;
let tls_stream = wrap_rustls(
host,
port,
use_sni,
"",
tcp_stream,
&context.tls_session_store,
)
.await?;
Box::new(tls_stream)
}
}

View File

@@ -429,9 +429,11 @@ impl ProxyConfig {
load_cache,
)
.await?;
let use_sni = true;
let tls_stream = wrap_rustls(
&https_config.host,
https_config.port,
use_sni,
"",
tcp_stream,
&context.tls_session_store,

View File

@@ -13,12 +13,14 @@ pub async fn wrap_tls<'a>(
strict_tls: bool,
hostname: &str,
port: u16,
use_sni: bool,
alpn: &str,
stream: impl SessionStream + 'static,
tls_session_store: &TlsSessionStore,
) -> Result<impl SessionStream + 'a> {
if strict_tls {
let tls_stream = wrap_rustls(hostname, port, alpn, stream, tls_session_store).await?;
let tls_stream =
wrap_rustls(hostname, port, use_sni, alpn, stream, tls_session_store).await?;
let boxed_stream: Box<dyn SessionStream> = Box::new(tls_stream);
Ok(boxed_stream)
} else {
@@ -32,6 +34,7 @@ pub async fn wrap_tls<'a>(
};
let tls = async_native_tls::TlsConnector::new()
.min_protocol_version(Some(async_native_tls::Protocol::Tlsv12))
.use_sni(use_sni)
.request_alpns(&alpns)
.danger_accept_invalid_hostnames(true)
.danger_accept_invalid_certs(true);
@@ -90,6 +93,7 @@ impl TlsSessionStore {
pub async fn wrap_rustls<'a>(
hostname: &str,
port: u16,
use_sni: bool,
alpn: &str,
stream: impl SessionStream + 'a,
tls_session_store: &TlsSessionStore,
@@ -117,6 +121,7 @@ pub async fn wrap_rustls<'a>(
let resumption = tokio_rustls::rustls::client::Resumption::store(resumption_store)
.tls12_resumption(tokio_rustls::rustls::client::Tls12Resumption::Disabled);
config.resumption = resumption;
config.enable_sni = use_sni;
let tls = tokio_rustls::TlsConnector::from(Arc::new(config));
let name = rustls_pki_types::ServerName::try_from(hostname)?.to_owned();

View File

@@ -139,10 +139,10 @@ pub(crate) async fn get_oauth2_access_token(
value = &redirect_uri;
} else if value == "$CODE" {
value = code;
} else if value == "$REFRESH_TOKEN" {
if let Some(refresh_token) = refresh_token.as_ref() {
value = refresh_token;
}
} else if value == "$REFRESH_TOKEN"
&& let Some(refresh_token) = refresh_token.as_ref()
{
value = refresh_token;
}
post_param.insert(key, value);
@@ -261,14 +261,12 @@ impl Oauth2 {
if let Some(domain) = addr_normalized
.find('@')
.map(|index| addr_normalized.split_at(index + 1).1)
{
if let Some(oauth2_authorizer) = provider::get_provider_info(domain)
&& let Some(oauth2_authorizer) = provider::get_provider_info(domain)
.and_then(|provider| provider.oauth2_authorizer.as_ref())
{
return Some(match oauth2_authorizer {
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
});
}
{
return Some(match oauth2_authorizer {
Oauth2Authorizer::Yandex => OAUTH2_YANDEX,
});
}
None
}

View File

@@ -165,7 +165,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey
pub async fn pk_encrypt(
plain: Vec<u8>,
public_keys_for_encryption: Vec<SignedPublicKey>,
private_key_for_signing: Option<SignedSecretKey>,
private_key_for_signing: SignedSecretKey,
compress: bool,
anonymous_recipients: bool,
) -> Result<String> {
@@ -187,11 +187,9 @@ pub async fn pk_encrypt(
}
}
if let Some(ref skey) = private_key_for_signing {
msg.sign(&**skey, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
msg.sign(&*private_key_for_signing, Password::empty(), HASH_ALGORITHM);
if compress {
msg.compression(CompressionAlgorithm::ZLIB);
}
let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
@@ -406,7 +404,7 @@ pub async fn symm_encrypt_message(
};
let mut msg = msg.seipd_v2(
&mut rng,
SymmetricKeyAlgorithm::AES128,
SYMMETRIC_KEY_ALGORITHM,
AeadAlgorithm::Ocb,
ChunkSize::C8KiB,
);
@@ -534,7 +532,6 @@ mod tests {
static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
/// A ciphertext encrypted to Alice & Bob, signed by Alice.
async fn ctext_signed() -> &'static String {
@@ -547,28 +544,7 @@ mod tests {
pk_encrypt(
CLEARTEXT.to_vec(),
keyring,
Some(KEYS.alice_secret.clone()),
compress,
anonymous_recipients,
)
.await
.unwrap()
})
.await
}
/// A ciphertext encrypted to Alice & Bob, not signed.
async fn ctext_unsigned() -> &'static String {
let anonymous_recipients = true;
CTEXT_UNSIGNED
.get_or_init(|| async {
let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
let compress = true;
pk_encrypt(
CLEARTEXT.to_vec(),
keyring,
None,
KEYS.alice_secret.clone(),
compress,
anonymous_recipients,
)
@@ -588,16 +564,6 @@ mod tests {
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_encrypt_unsigned() {
assert!(!ctext_unsigned().await.is_empty());
assert!(
ctext_unsigned()
.await
.starts_with("-----BEGIN PGP MESSAGE-----")
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_singed() {
// Check decrypting as Alice
@@ -652,9 +618,9 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_unsigned() {
let decrypt_keyring = vec![KEYS.bob_secret.clone()];
let ctext_unsigned = include_bytes!("../test-data/message/ctext_unsigned.asc");
let (_msg, valid_signatures, content) =
pk_decrypt_and_validate(ctext_unsigned().await.as_bytes(), &decrypt_keyring, &[])
.unwrap();
pk_decrypt_and_validate(ctext_unsigned, &decrypt_keyring, &[]).unwrap();
assert_eq!(content, CLEARTEXT);
assert_eq!(valid_signatures.len(), 0);
}
@@ -744,7 +710,14 @@ mod tests {
let pk_for_encryption = load_self_public_key(alice).await?;
// Encrypt a message, but only to self, not to Bob:
let ctext = pk_encrypt(plain, vec![pk_for_encryption], None, true, true).await?;
let ctext = pk_encrypt(
plain,
vec![pk_for_encryption],
KEYS.alice_secret.clone(),
true,
true,
)
.await?;
// Trying to decrypt it should fail with an OK error message:
let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;

View File

@@ -42,7 +42,7 @@ pub(crate) const DCBACKUP_SCHEME_PREFIX: &str = "DCBACKUP";
/// Version written to Backups and Backup-QR-Codes.
/// Imports will fail when they have a larger version.
pub(crate) const DCBACKUP_VERSION: i32 = 3;
pub(crate) const DCBACKUP_VERSION: i32 = 4;
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]

View File

@@ -991,42 +991,6 @@ Content-Disposition: reaction\n\
Ok(())
}
/// Regression test for reaction resetting self-status.
///
/// Reactions do not contain the status,
/// but should not result in self-status being reset on other devices.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reaction_status_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice1 = tcm.alice().await;
let alice2 = tcm.alice().await;
alice1
.set_config(Config::Selfstatus, Some("New status"))
.await?;
let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
assert_eq!(
alice2.get_config(Config::Selfstatus).await?.as_deref(),
Some("New status")
);
// Alice reacts to own message from second device,
// first device receives rection.
{
send_reaction(&alice2, alice2_msg.id, "👍").await?;
let msg = alice2.pop_sent_msg().await;
alice1.recv_msg_hidden(&msg).await;
}
// Check that the status is still the same.
assert_eq!(
alice1.get_config(Config::Selfstatus).await?.as_deref(),
Some("New status")
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_reaction_multidevice() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -1,6 +1,6 @@
//! Internet Message Format reception pipeline.
use std::collections::{BTreeMap, HashSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::iter;
use std::sync::LazyLock;
@@ -827,6 +827,41 @@ pub(crate) async fn receive_imf_inner(
if let Some(ref sync_items) = mime_parser.sync_items {
if from_id == ContactId::SELF {
if mime_parser.was_encrypted() {
// Receiving encrypted message from self updates primary transport.
let from_addr = &mime_parser.from.addr;
let transport_changed = context
.sql
.transaction(|transaction| {
let transport_exists = transaction.query_row(
"SELECT COUNT(*) FROM transports WHERE addr=?",
(from_addr,),
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)?;
let transport_changed = if transport_exists {
transaction.execute(
"UPDATE config SET value=? WHERE keyname='configured_addr'",
(from_addr,),
)? > 0
} else {
warn!(
context,
"Received sync message from unknown address {from_addr:?}."
);
false
};
Ok(transport_changed)
})
.await?;
if transport_changed {
info!(context, "Primary transport changed to {from_addr:?}.");
context.sql.uncache_raw_config("configured_addr").await;
}
context
.execute_sync_items(sync_items, mime_parser.timestamp_sent)
.await;
@@ -884,53 +919,26 @@ pub(crate) async fn receive_imf_inner(
}
}
if let Some(avatar_action) = &mime_parser.user_avatar {
if from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::AvatarTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_profile_image(
context,
from_id,
avatar_action,
mime_parser.was_encrypted(),
)
.await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
}
}
if let Some(avatar_action) = &mime_parser.user_avatar
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
&& context
.update_contacts_timestamp(from_id, Param::AvatarTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) = contact::set_profile_image(context, from_id, avatar_action).await
{
warn!(context, "receive_imf cannot update profile image: {err:#}.");
};
// Ignore footers from mailinglists as they are often created or modified by the mailinglist software.
if let Some(footer) = &mime_parser.footer {
if !mime_parser.is_mailinglist_message()
&& from_id != ContactId::UNDEFINED
&& context
.update_contacts_timestamp(
from_id,
Param::StatusTimestamp,
mime_parser.timestamp_sent,
)
.await?
{
if let Err(err) = contact::set_status(
context,
from_id,
footer.to_string(),
mime_parser.was_encrypted(),
mime_parser.has_chat_version(),
)
.await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
}
if let Some(footer) = &mime_parser.footer
&& !mime_parser.is_mailinglist_message()
&& !matches!(from_id, ContactId::UNDEFINED | ContactId::SELF)
&& context
.update_contacts_timestamp(from_id, Param::StatusTimestamp, mime_parser.timestamp_sent)
.await?
&& let Err(err) = contact::set_status(context, from_id, footer.to_string()).await
{
warn!(context, "Cannot update contact status: {err:#}.");
}
// Get user-configured server deletion
@@ -1205,19 +1213,19 @@ async fn decide_chat_assignment(
let mut num_recipients = 0;
let mut has_self_addr = false;
for recipient in &mime_parser.recipients {
has_self_addr |= context.is_self_addr(&recipient.addr).await?;
if addr_cmp(&recipient.addr, &mime_parser.from.addr) {
continue;
}
if context.is_self_addr(&recipient.addr).await? {
has_self_addr = true;
}
num_recipients += 1;
}
if from_id != ContactId::SELF && !has_self_addr {
num_recipients += 1;
}
let can_be_11_chat = num_recipients <= 1
&& (from_id != ContactId::SELF
|| !(mime_parser.recipients.is_empty() || has_self_addr)
|| mime_parser.was_encrypted());
let chat_assignment = if should_trash {
ChatAssignment::Trash
@@ -1261,14 +1269,14 @@ async fn decide_chat_assignment(
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
} else if can_be_11_chat {
ChatAssignment::OneOneChat
} else {
ChatAssignment::AdHocGroup
}
} else if mime_parser.get_header(HeaderDef::ChatGroupName).is_some() {
ChatAssignment::AdHocGroup
} else if num_recipients <= 1 {
} else if can_be_11_chat {
ChatAssignment::OneOneChat
} else {
ChatAssignment::AdHocGroup
@@ -1341,8 +1349,8 @@ async fn do_chat_assignment(
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation || test_normal_chat.is_some() {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
} else if (allow_creation || test_normal_chat.is_some())
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
@@ -1353,16 +1361,15 @@ async fn do_chat_assignment(
grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
}
ChatAssignment::MailingListOrBroadcast => {
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header() {
if let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
if let Some(mailinglist_header) = mime_parser.get_mailinglist_header()
&& let Some((new_chat_id, new_chat_id_blocked, new_chat_created)) =
create_or_lookup_mailinglist_or_broadcast(
context,
allow_creation,
@@ -1372,13 +1379,12 @@ async fn do_chat_assignment(
mime_parser,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = new_chat_created;
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = new_chat_created;
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
}
apply_mailinglist_changes(context, mime_parser, new_chat_id).await?;
}
}
ChatAssignment::ExistingChat {
@@ -1413,11 +1419,10 @@ async fn do_chat_assignment(
if chat_id_blocked != Blocked::Not
&& create_blocked != Blocked::Yes
&& !matches!(chat_assignment, ChatAssignment::MailingListOrBroadcast)
&& let Some(chat_id) = chat_id
{
if let Some(chat_id) = chat_id {
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
chat_id.set_blocked(context, create_blocked).await?;
chat_id_blocked = create_blocked;
}
if chat_id.is_none() {
@@ -1441,21 +1446,20 @@ async fn do_chat_assignment(
chat_created = true;
}
if let Some(chat_id) = chat_id {
if chat_id_blocked != Blocked::Not {
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
if create_blocked == Blocked::Request && parent_message.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo)
.await?;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
}
if let Some(chat_id) = chat_id
&& chat_id_blocked != Blocked::Not
{
if chat_id_blocked != create_blocked {
chat_id.set_blocked(context, create_blocked).await?;
}
if create_blocked == Blocked::Request && parent_message.is_some() {
// we do not want any chat to be created implicitly. Because of the origin-scale-up,
// the contact requests will pop up and this should be just fine.
ContactId::scaleup_origin(context, &[from_id], Origin::IncomingReplyTo).await?;
info!(
context,
"Message is a reply to a known message, mark sender as known.",
);
}
}
}
@@ -1476,8 +1480,8 @@ async fn do_chat_assignment(
if let Some((id, blocked)) = chat::get_chat_id_by_grpid(context, grpid).await? {
chat_id = Some(id);
chat_id_blocked = blocked;
} else if allow_creation {
if let Some((new_chat_id, new_chat_id_blocked)) = create_group(
} else if allow_creation
&& let Some((new_chat_id, new_chat_id_blocked)) = create_group(
context,
mime_parser,
is_partial_download.is_some(),
@@ -1488,11 +1492,10 @@ async fn do_chat_assignment(
grpid,
)
.await?
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
{
chat_id = Some(new_chat_id);
chat_id_blocked = new_chat_id_blocked;
chat_created = true;
}
}
ChatAssignment::ExistingChat {
@@ -1574,11 +1577,12 @@ async fn do_chat_assignment(
chat_created = true;
}
}
if chat_id.is_none() && mime_parser.has_chat_version() {
if let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await? {
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
if chat_id.is_none()
&& mime_parser.has_chat_version()
&& let Some(chat) = ChatIdBlocked::lookup_by_contact(context, to_id).await?
{
chat_id = Some(chat.id);
chat_id_blocked = chat.blocked;
}
}
@@ -1598,11 +1602,11 @@ async fn do_chat_assignment(
}
// automatically unblock chat when the user sends a message
if chat_id_blocked != Blocked::Not {
if let Some(chat_id) = chat_id {
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
if chat_id_blocked != Blocked::Not
&& let Some(chat_id) = chat_id
{
chat_id.unblock_ex(context, Nosync).await?;
chat_id_blocked = Blocked::Not;
}
}
let chat_id = chat_id.unwrap_or_else(|| {
@@ -1640,11 +1644,9 @@ async fn add_parts(
// if contact renaming is prevented (for mailinglists and bots),
// we use name from From:-header as override name
if prevent_rename {
if let Some(name) = &mime_parser.from.display_name {
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
}
if prevent_rename && let Some(name) = &mime_parser.from.display_name {
for part in &mut mime_parser.parts {
part.param.set(Param::OverrideSenderDisplayname, name);
}
}
@@ -1801,11 +1803,16 @@ async fn add_parts(
"Updated ephemeral timer to {ephemeral_timer:?} for chat {chat_id}."
);
if mime_parser.is_system_message != SystemMessage::EphemeralTimerChanged {
chat::add_info_msg(
chat::add_info_msg_with_cmd(
context,
chat_id,
&stock_ephemeral_timer_changed(context, ephemeral_timer, from_id).await,
sort_timestamp,
SystemMessage::Unknown,
Some(sort_timestamp),
mime_parser.timestamp_sent,
None,
None,
None,
)
.await?;
}
@@ -1913,8 +1920,8 @@ async fn add_parts(
chat_id,
&group_changes_msg,
cmd,
sort_timestamp,
None,
Some(sort_timestamp),
mime_parser.timestamp_sent,
None,
None,
added_removed_id,
@@ -2270,36 +2277,36 @@ async fn handle_edit_delete(
"Edit message: rfc724_mid {rfc724_mid:?} not found."
);
}
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete) {
if let Some(part) = mime_parser.parts.first() {
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
// deletion requests, so there's no need to support them.
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
let mut modified_chat_ids = HashSet::new();
let mut msg_ids = Vec::new();
} else if let Some(rfc724_mid_list) = mime_parser.get_header(HeaderDef::ChatDelete)
&& let Some(part) = mime_parser.parts.first()
{
// See `message::delete_msgs_ex()`, unlike edit requests, DC doesn't send unencrypted
// deletion requests, so there's no need to support them.
if part.param.get_bool(Param::GuaranteeE2ee).unwrap_or(false) {
let mut modified_chat_ids = HashSet::new();
let mut msg_ids = Vec::new();
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
for rfc724_mid in rfc724_mid_vec {
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(context, "Delete message: Bad sender.");
}
let rfc724_mid_vec: Vec<&str> = rfc724_mid_list.split_whitespace().collect();
for rfc724_mid in rfc724_mid_vec {
if let Some(msg_id) = message::rfc724_mid_exists(context, rfc724_mid).await? {
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
if msg.from_id == from_id {
message::delete_msg_locally(context, &msg).await?;
msg_ids.push(msg.id);
modified_chat_ids.insert(msg.chat_id);
} else {
warn!(context, "Delete message: Database entry does not exist.");
warn!(context, "Delete message: Bad sender.");
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
warn!(context, "Delete message: Database entry does not exist.");
}
} else {
warn!(context, "Delete message: {rfc724_mid:?} not found.");
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
} else {
warn!(context, "Delete message: Not encrypted.");
}
message::delete_msgs_locally_done(context, &msg_ids, modified_chat_ids).await?;
} else {
warn!(context, "Delete message: Not encrypted.");
}
}
Ok(())
@@ -2356,33 +2363,32 @@ async fn save_locations(
let mut send_event = false;
if let Some(message_kml) = &mime_parser.message_kml {
if let Some(newest_location_id) =
if let Some(message_kml) = &mime_parser.message_kml
&& let Some(newest_location_id) =
location::save(context, chat_id, from_id, &message_kml.locations, true).await?
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
send_event = true;
}
{
location::set_msg_location_id(context, msg_id, newest_location_id).await?;
send_event = true;
}
if let Some(location_kml) = &mime_parser.location_kml {
if let Some(addr) = &location_kml.addr {
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
.is_some()
{
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
if let Some(location_kml) = &mime_parser.location_kml
&& let Some(addr) = &location_kml.addr
{
let contact = Contact::get_by_id(context, from_id).await?;
if contact.get_addr().to_lowercase() == addr.to_lowercase() {
if location::save(context, chat_id, from_id, &location_kml.locations, false)
.await?
.is_some()
{
send_event = true;
}
} else {
warn!(
context,
"Address in location.kml {:?} is not the same as the sender address {:?}.",
addr,
contact.get_addr()
);
}
}
if send_event {
@@ -2491,11 +2497,8 @@ async fn lookup_or_create_adhoc_group(
.unwrap_or_else(|| "👥📧".to_string())
});
let to_ids: Vec<ContactId> = to_ids.iter().filter_map(|x| *x).collect();
let mut contact_ids = Vec::with_capacity(to_ids.len() + 1);
contact_ids.extend(&to_ids);
if !contact_ids.contains(&from_id) {
contact_ids.push(from_id);
}
let mut contact_ids = BTreeSet::<ContactId>::from_iter(to_ids.iter().copied());
contact_ids.insert(from_id);
let trans_fn = |t: &mut rusqlite::Transaction| {
t.pragma_update(None, "query_only", "0")?;
t.execute(
@@ -2734,13 +2737,13 @@ async fn update_chats_contacts_timestamps(
to_ids.iter(),
chat_group_member_timestamps.iter().take(to_ids.len()),
) {
if let Some(contact_id) = contact_id {
if Some(*contact_id) != ignored_id {
// It could be that member was already added,
// but updated addition timestamp
// is also a modification worth notifying about.
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
}
if let Some(contact_id) = contact_id
&& Some(*contact_id) != ignored_id
{
// It could be that member was already added,
// but updated addition timestamp
// is also a modification worth notifying about.
modified |= add_statement.execute((chat_id, contact_id, ts))? > 0;
}
}
@@ -3003,13 +3006,14 @@ async fn apply_group_changes(
.copied()
.collect();
if let Some(added_id) = added_id {
if !added_ids.remove(&added_id) && added_id != ContactId::SELF {
// No-op "Member added" message. An exception is self-addition messages because they at
// least must be shown when a chat is created on our side.
info!(context, "No-op 'Member added' message (TRASH)");
better_msg = Some(String::new());
}
if let Some(added_id) = added_id
&& !added_ids.remove(&added_id)
&& added_id != ContactId::SELF
{
// No-op "Member added" message. An exception is self-addition messages because they at
// least must be shown when a chat is created on our side.
info!(context, "No-op 'Member added' message (TRASH)");
better_msg = Some(String::new());
}
if let Some(removed_id) = removed_id {
removed_ids.remove(&removed_id);
@@ -3064,59 +3068,52 @@ async fn apply_chat_name_and_avatar_changes(
Some(_) => Some(chat.name.as_str()),
None => None,
})
{
if let Some(grpname) = mime_parser
&& let Some(grpname) = mime_parser
.get_header(HeaderDef::ChatGroupName)
.map(|grpname| grpname.trim())
.filter(|grpname| grpname.len() < 200)
{
let grpname = &sanitize_single_line(grpname);
{
let grpname = &sanitize_single_line(grpname);
let chat_group_name_timestamp =
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
&& grpname != &chat.name
{
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.await?;
*send_event_chat_modified = true;
}
if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
let old_name = &sanitize_single_line(old_name);
better_msg.get_or_insert(
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
);
}
let chat_group_name_timestamp = chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
// To provide group name consistency, compare names if timestamps are equal.
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
&& chat
.id
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
.await?
&& grpname != &chat.name
{
info!(context, "Updating grpname for chat {}.", chat.id);
context
.sql
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat.id))
.await?;
*send_event_chat_modified = true;
}
if mime_parser
.get_header(HeaderDef::ChatGroupNameChanged)
.is_some()
{
let old_name = &sanitize_single_line(old_name);
better_msg
.get_or_insert(stock_str::msg_grp_name(context, old_name, grpname, from_id).await);
}
}
// ========== Apply chat avatar changes ==========
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg) {
if value == "group-avatar-changed" {
if let Some(avatar_action) = &mime_parser.group_avatar {
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => {
stock_str::msg_grp_img_changed(context, from_id).await
}
});
}
}
if let (Some(value), None) = (mime_parser.get_header(HeaderDef::ChatContent), &better_msg)
&& value == "group-avatar-changed"
&& let Some(avatar_action) = &mime_parser.group_avatar
{
// this is just an explicit message containing the group-avatar,
// apart from that, the group-avatar is send along with various other messages
better_msg.get_or_insert(match avatar_action {
AvatarAction::Delete => stock_str::msg_grp_img_deleted(context, from_id).await,
AvatarAction::Change(_) => stock_str::msg_grp_img_changed(context, from_id).await,
});
}
if let Some(avatar_action) = &mime_parser.group_avatar {
@@ -3297,10 +3294,10 @@ fn compute_mailinglist_name(
// for mailchimp lists, the name in `ListId` is just a long number.
// a usable name for these lists is in the `From` header
// and we can detect these lists by a unique `ListId`-suffix.
if listid.ends_with(".list-id.mcsv.net") {
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
if listid.ends_with(".list-id.mcsv.net")
&& let Some(display_name) = &mime_parser.from.display_name
{
name.clone_from(display_name);
}
// additional names in square brackets in the subject are preferred
@@ -3325,10 +3322,9 @@ fn compute_mailinglist_name(
|| mime_parser.from.addr.starts_with("notifications@")
|| mime_parser.from.addr.starts_with("newsletter@")
|| listid.ends_with(".xt.local"))
&& let Some(display_name) = &mime_parser.from.display_name
{
if let Some(display_name) = &mime_parser.from.display_name {
name.clone_from(display_name);
}
name.clone_from(display_name);
}
// as a last resort, use the ListId as the name
@@ -3476,15 +3472,15 @@ async fn apply_out_broadcast_changes(
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, from_id).await?;
info!(context, "Broadcast leave message (TRASH)");
better_msg = Some("".to_string());
} else if from_id == ContactId::SELF {
if let Some(removed_id) = removed_id {
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
.await?;
} else if from_id == ContactId::SELF
&& let Some(removed_id) = removed_id
{
chat::remove_from_chat_contacts_table_without_trace(context, chat.id, removed_id)
.await?;
better_msg.get_or_insert(
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
);
}
better_msg.get_or_insert(
stock_str::msg_del_member_local(context, removed_id, ContactId::SELF).await,
);
}
}
@@ -3508,14 +3504,14 @@ async fn apply_in_broadcast_changes(
) -> Result<GroupChangesInfo> {
ensure!(chat.typ == Chattype::InBroadcast);
if let Some(part) = mime_parser.parts.first() {
if let Some(error) = &part.error {
warn!(
context,
"Not applying broadcast changes from message with error: {error}"
);
return Ok(GroupChangesInfo::default());
}
if let Some(part) = mime_parser.parts.first()
&& let Some(error) = &part.error
{
warn!(
context,
"Not applying broadcast changes from message with error: {error}"
);
return Ok(GroupChangesInfo::default());
}
let mut send_event_chat_modified = false;
@@ -3531,21 +3527,21 @@ async fn apply_in_broadcast_changes(
)
.await?;
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded) {
if context.is_self_addr(added_addr).await? {
let msg = if chat.is_self_in_chat(context).await? {
// Self is already in the chat.
// Probably Alice has two devices and her second device added us again;
// just hide the message.
info!(context, "No-op broadcast 'Member added' message (TRASH)");
"".to_string()
} else {
stock_str::msg_you_joined_broadcast(context).await
};
if let Some(added_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberAdded)
&& context.is_self_addr(added_addr).await?
{
let msg = if chat.is_self_in_chat(context).await? {
// Self is already in the chat.
// Probably Alice has two devices and her second device added us again;
// just hide the message.
info!(context, "No-op broadcast 'Member added' message (TRASH)");
"".to_string()
} else {
stock_str::msg_you_joined_broadcast(context).await
};
better_msg.get_or_insert(msg);
send_event_chat_modified = true;
}
better_msg.get_or_insert(msg);
send_event_chat_modified = true;
}
if let Some(removed_fpr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemovedFpr) {
@@ -3628,13 +3624,6 @@ async fn create_adhoc_group(
);
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
}
if member_ids.len() < 2 {
info!(
context,
"Not creating ad hoc group with less than 2 members."
);
return Ok(None);
}
let new_chat_id: ChatId = ChatId::create_multiuser_record(
context,
@@ -3756,12 +3745,11 @@ async fn get_previous_message(
context: &Context,
mime_parser: &MimeMessage,
) -> Result<Option<Message>> {
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
if let Some(rfc724mid) = parse_message_ids(field).last() {
if let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await? {
return Message::load_from_db_optional(context, msg_id).await;
}
}
if let Some(field) = mime_parser.get_header(HeaderDef::References)
&& let Some(rfc724mid) = parse_message_ids(field).last()
&& let Some(msg_id) = rfc724_mid_exists(context, rfc724mid).await?
{
return Message::load_from_db_optional(context, msg_id).await;
}
Ok(None)
}

View File

@@ -1964,28 +1964,53 @@ Message content",
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
}
/// Tests that message with hidden recipients is assigned to Saved Messages chat.
/// Tests that an outgoing self-sent unencrypted message doesn't go to the self-chat, but to a
/// proper unencrypted chat instead.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hidden_recipients_self_chat() {
let t = TestContext::new_alice().await;
async fn test_unencrypted_doesnt_goto_self_chat() -> Result<()> {
let mut tcm = TestContextManager::new();
let t = &tcm.alice().await;
let mut chat_id = None;
receive_imf(
&t,
b"Subject: s
for (i, to) in [
"<alice@example.org>",
"<alice@example.org>",
"alice@example.org, alice@example.org",
"hidden-recipients:;",
]
.iter()
.enumerate()
{
receive_imf(
t,
format!(
"Subject: s
Chat-Version: 1.0
Message-ID: <foobar@localhost>
To: hidden-recipients:;
Message-ID: <foobar{i}@localhost>
To: {to}
From: <alice@example.org>
Message content",
false,
)
.await
.unwrap();
Your server is hacked. Have a nice day!"
)
.as_bytes(),
false,
)
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.chat_id, t.get_self_chat().await.id);
assert_eq!(msg.to_id, ContactId::SELF);
let msg = t.get_last_msg().await;
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
if let Some(chat_id) = chat_id {
assert_eq!(msg.chat_id, chat_id);
} else {
chat_id = Some(msg.chat_id);
let chat = Chat::load_from_db(t, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Group);
assert!(!chat.is_encrypted(t).await?);
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -4986,7 +5011,7 @@ async fn test_make_n_send_vcard() -> Result<()> {
Ok(())
}
/// Tests that group is not created if the message
/// Tests that an ad-hoc group is created if the message
/// has no recipients even if it has unencrypted Chat-Group-ID.
///
/// Chat-Group-ID in unencrypted messages should be ignored.
@@ -5005,8 +5030,12 @@ Hello!"
.as_bytes();
let received = receive_imf(t, raw, false).await?.unwrap();
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
assert_eq!(msg.from_id, ContactId::SELF);
assert_eq!(msg.to_id, ContactId::SELF);
let chat = Chat::load_from_db(t, msg.chat_id).await?;
assert_eq!(chat.typ, Chattype::Single);
assert_eq!(chat.typ, Chattype::Group);
assert!(!chat.is_encrypted(t).await?);
assert!(chat.grpid.is_empty());
// Check that the weird group name is sanitzied correctly:
let mail = mailparse::parse_mail(raw).unwrap();
@@ -5017,7 +5046,7 @@ Hello!"
.get_value_raw(),
"Group\n name\u{202B}".as_bytes()
);
assert_eq!(chat.name, "Saved messages");
assert_eq!(chat.name, "Group name");
Ok(())
}
@@ -5154,6 +5183,58 @@ async fn test_dont_reverify_by_self_on_outgoing_msg() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_verify_by_verified_by_unknown() -> Result<()> {
let mut tcm = TestContextManager::new();
let a0 = &tcm.alice().await;
let a1 = &tcm.alice().await;
let bob = &tcm.bob().await;
let fiona = &tcm.fiona().await;
let bob_chat_id = chat::create_group(bob, "Group").await?;
bob.set_chat_protected(bob_chat_id).await;
let qr = get_securejoin_qr(bob, Some(bob_chat_id)).await?;
tcm.exec_securejoin_qr(a0, bob, &qr).await;
let qr = get_securejoin_qr(bob, None).await?;
tcm.exec_securejoin_qr(fiona, bob, &qr).await;
// Bob verifies Fiona for Alice#0.
let bob_fiona_id = bob.add_or_lookup_contact_id(fiona).await;
add_contact_to_chat(bob, bob_chat_id, bob_fiona_id).await?;
let sent_msg = bob.pop_sent_msg().await;
a0.recv_msg(&sent_msg).await;
fiona.recv_msg(&sent_msg).await;
let a0_bob = a0.add_or_lookup_contact(bob).await;
let a0_fiona = a0.add_or_lookup_contact(fiona).await;
assert_eq!(a0_fiona.get_verifier_id(a0).await?, Some(Some(a0_bob.id)));
let chat_id = a0.create_group_with_members("", &[fiona]).await;
a0.set_chat_protected(chat_id).await;
a1.recv_msg(&a0.send_text(chat_id, "Hi").await).await;
let a1_fiona = a1.add_or_lookup_contact(fiona).await;
assert_eq!(a1_fiona.get_verifier_id(a1).await?, Some(None));
let some_time_to_regossip = Duration::from_secs(20 * 24 * 3600);
SystemTime::shift(some_time_to_regossip);
let fiona_chat_id = fiona.get_last_msg().await.chat_id;
fiona.set_chat_protected(fiona_chat_id).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
let a1_bob = a1.add_or_lookup_contact(bob).await;
// There was a bug that Bob is verified by Fiona on Alice's other device.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(None));
SystemTime::shift(some_time_to_regossip);
tcm.execute_securejoin(a1, fiona).await;
a1.recv_msg(&fiona.send_text(fiona_chat_id, "Hi").await)
.await;
// But now Bob's verifier id must be updated because Fiona is verified by a known verifier
// (moreover, directly), so Alice has reverse verification chains on her devices.
assert_eq!(a1_bob.get_verifier_id(a1).await?, Some(Some(a1_fiona.id)));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sanitize_filename_in_received() -> Result<()> {
let alice = &TestContext::new_alice().await;

View File

@@ -1,5 +1,4 @@
use std::cmp;
use std::iter::{self, once};
use std::num::NonZeroUsize;
use anyhow::{Context as _, Error, Result, bail};
@@ -26,6 +25,7 @@ use crate::smtp::{Smtp, send_smtp_messages};
use crate::sql;
use crate::stats::maybe_send_stats;
use crate::tools::{self, duration_to_str, maybe_add_time_based_warnings, time, time_elapsed};
use crate::transport::ConfiguredLoginParam;
use crate::{constants, stats};
pub(crate) mod connectivity;
@@ -145,14 +145,14 @@ impl SchedulerState {
InnerSchedulerState::Started(_) => {
let new_state = InnerSchedulerState::Paused {
started: true,
pause_guards_count: NonZeroUsize::new(1).unwrap(),
pause_guards_count: NonZeroUsize::MIN,
};
Self::do_stop(&mut inner, context, new_state).await;
}
InnerSchedulerState::Stopped => {
*inner = InnerSchedulerState::Paused {
started: false,
pause_guards_count: NonZeroUsize::new(1).unwrap(),
pause_guards_count: NonZeroUsize::MIN,
};
}
InnerSchedulerState::Paused {
@@ -183,7 +183,7 @@ impl SchedulerState {
ref started,
ref mut pause_guards_count,
} => {
if *pause_guards_count == NonZeroUsize::new(1).unwrap() {
if *pause_guards_count == NonZeroUsize::MIN {
match *started {
true => SchedulerState::do_start(&mut inner, &context).await,
false => *inner = InnerSchedulerState::Stopped,
@@ -212,21 +212,25 @@ impl SchedulerState {
/// Indicate that the network likely has come back.
pub(crate) async fn maybe_network(&self) {
let inner = self.inner.read().await;
let (inbox, oboxes) = match *inner {
let (inboxes, oboxes) = match *inner {
InnerSchedulerState::Started(ref scheduler) => {
scheduler.maybe_network();
let inbox = scheduler.inbox.conn_state.state.connectivity.clone();
let inboxes = scheduler
.inboxes
.iter()
.map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>();
let oboxes = scheduler
.oboxes
.iter()
.map(|b| b.conn_state.state.connectivity.clone())
.collect::<Vec<_>>();
(inbox, oboxes)
(inboxes, oboxes)
}
_ => return,
};
drop(inner);
connectivity::idle_interrupted(inbox, oboxes);
connectivity::idle_interrupted(inboxes, oboxes);
}
/// Indicate that the network likely is lost.
@@ -331,7 +335,8 @@ struct SchedBox {
/// Job and connection scheduler.
#[derive(Debug)]
pub(crate) struct Scheduler {
inbox: SchedBox,
/// Inboxes, one per transport.
inboxes: Vec<SchedBox>,
/// Optional boxes -- mvbox.
oboxes: Vec<SchedBox>,
smtp: SmtpConnectionState,
@@ -474,17 +479,17 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
}
// Update quota no more than once a minute.
if ctx.quota_needs_update(60).await {
if let Err(err) = ctx.update_recent_quota(&mut session).await {
warn!(ctx, "Failed to update quota: {:#}.", err);
}
if ctx.quota_needs_update(60).await
&& let Err(err) = ctx.update_recent_quota(&mut session).await
{
warn!(ctx, "Failed to update quota: {:#}.", err);
}
if let Ok(()) = imap.resync_request_receiver.try_recv() {
if let Err(err) = session.resync_folders(ctx).await {
warn!(ctx, "Failed to resync folders: {:#}.", err);
imap.resync_request_sender.try_send(()).ok();
}
if let Ok(()) = imap.resync_request_receiver.try_recv()
&& let Err(err) = session.resync_folders(ctx).await
{
warn!(ctx, "Failed to resync folders: {:#}.", err);
imap.resync_request_sender.try_send(()).ok();
}
maybe_add_time_based_warnings(ctx).await;
@@ -573,12 +578,19 @@ async fn fetch_idle(
mvbox.as_deref().unwrap_or(&watch_folder)
}
};
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
if ctx
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default()
== connection.addr
{
session
.send_sync_msgs(ctx, syncbox)
.await
.context("fetch_idle: send_sync_msgs")
.log_err(ctx)
.ok();
}
session
.store_seen_flags_on_imap(ctx)
@@ -857,34 +869,40 @@ impl Scheduler {
let (ephemeral_interrupt_send, ephemeral_interrupt_recv) = channel::bounded(1);
let (location_interrupt_send, location_interrupt_recv) = channel::bounded(1);
let mut inboxes = Vec::new();
let mut oboxes = Vec::new();
let mut start_recvs = Vec::new();
let (conn_state, inbox_handlers) = ImapConnectionState::new(ctx).await?;
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
conn_state,
handle,
};
start_recvs.push(inbox_start_recv);
if ctx.should_watch_mvbox().await? {
let (conn_state, handlers) = ImapConnectionState::new(ctx).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
meaning,
for (transport_id, configured_login_param) in ConfiguredLoginParam::load_all(ctx).await? {
let (conn_state, inbox_handlers) =
ImapConnectionState::new(ctx, transport_id, configured_login_param.clone()).await?;
let (inbox_start_send, inbox_start_recv) = oneshot::channel();
let handle = {
let ctx = ctx.clone();
task::spawn(inbox_loop(ctx, inbox_start_send, inbox_handlers))
};
let inbox = SchedBox {
meaning: FolderMeaning::Inbox,
conn_state,
handle,
});
start_recvs.push(start_recv);
};
inboxes.push(inbox);
start_recvs.push(inbox_start_recv);
if ctx.should_watch_mvbox().await? {
let (conn_state, handlers) =
ImapConnectionState::new(ctx, transport_id, configured_login_param).await?;
let (start_send, start_recv) = oneshot::channel();
let ctx = ctx.clone();
let meaning = FolderMeaning::Mvbox;
let handle = task::spawn(simple_imap_loop(ctx, start_send, handlers, meaning));
oboxes.push(SchedBox {
meaning,
conn_state,
handle,
});
start_recvs.push(start_recv);
}
}
let smtp_handle = {
@@ -910,7 +928,7 @@ impl Scheduler {
let recently_seen_loop = RecentlySeenLoop::new(ctx.clone());
let res = Self {
inbox,
inboxes,
oboxes,
smtp,
smtp_handle,
@@ -930,8 +948,8 @@ impl Scheduler {
Ok(res)
}
fn boxes(&self) -> iter::Chain<iter::Once<&SchedBox>, std::slice::Iter<'_, SchedBox>> {
once(&self.inbox).chain(self.oboxes.iter())
fn boxes(&self) -> impl Iterator<Item = &SchedBox> {
self.inboxes.iter().chain(self.oboxes.iter())
}
fn maybe_network(&self) {
@@ -949,7 +967,9 @@ impl Scheduler {
}
fn interrupt_inbox(&self) {
self.inbox.conn_state.interrupt();
for b in &self.inboxes {
b.conn_state.interrupt();
}
}
fn interrupt_oboxes(&self) {
@@ -989,7 +1009,7 @@ impl Scheduler {
let timeout_duration = std::time::Duration::from_secs(30);
let tracker = TaskTracker::new();
for b in once(self.inbox).chain(self.oboxes) {
for b in self.inboxes.into_iter().chain(self.oboxes.into_iter()) {
let context = context.clone();
tracker.spawn(async move {
tokio::time::timeout(timeout_duration, b.handle)
@@ -1095,12 +1115,17 @@ pub(crate) struct ImapConnectionState {
impl ImapConnectionState {
/// Construct a new connection.
async fn new(context: &Context) -> Result<(Self, ImapConnectionHandlers)> {
async fn new(
context: &Context,
transport_id: u32,
login_param: ConfiguredLoginParam,
) -> Result<(Self, ImapConnectionHandlers)> {
let stop_token = CancellationToken::new();
let (idle_interrupt_sender, idle_interrupt_receiver) = channel::bounded(1);
let handlers = ImapConnectionHandlers {
connection: Imap::new_configured(context, idle_interrupt_receiver).await?,
connection: Imap::new(context, transport_id, login_param, idle_interrupt_receiver)
.await?,
stop_token: stop_token.clone(),
};

View File

@@ -201,19 +201,20 @@ impl ConnectivityStore {
/// Set all folder states to InterruptingIdle in case they were `Idle` before.
/// Called during `dc_maybe_network()` to make sure that `all_work_done()`
/// returns false immediately after `dc_maybe_network()`.
pub(crate) fn idle_interrupted(inbox: ConnectivityStore, oboxes: Vec<ConnectivityStore>) {
let mut connectivity_lock = inbox.0.lock();
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Idle
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
pub(crate) fn idle_interrupted(inboxes: Vec<ConnectivityStore>, oboxes: Vec<ConnectivityStore>) {
for inbox in inboxes {
let mut connectivity_lock = inbox.0.lock();
// For the inbox, we also have to set the connectivity to InterruptingIdle if it was
// NotConfigured before: If all folders are NotConfigured, dc_get_connectivity()
// returns Connected. But after dc_maybe_network(), dc_get_connectivity() must not
// return Connected until DC is completely done with fetching folders; this also
// includes scan_folders() which happens on the inbox thread.
if *connectivity_lock == DetailedConnectivity::Idle
|| *connectivity_lock == DetailedConnectivity::NotConfigured
{
*connectivity_lock = DetailedConnectivity::InterruptingIdle;
}
}
drop(connectivity_lock);
for state in oboxes {
let mut connectivity_lock = state.0.lock();

View File

@@ -118,7 +118,7 @@ pub async fn get_securejoin_qr(context: &Context, chat: Option<ChatId>) -> Resul
chat.id,
);
let text = BROADCAST_INCOMPATIBILITY_MSG;
add_info_msg(context, chat.id, text, time()).await?;
add_info_msg(context, chat.id, text).await?;
bail!(text.to_string());
}
}

View File

@@ -127,7 +127,7 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
QrInvite::Group { .. } => {
let joining_chat_id = joining_chat_id(context, &invite, private_chat_id).await?;
let msg = stock_str::secure_join_started(context, invite.contact_id()).await;
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
chat::add_info_msg(context, joining_chat_id, &msg).await?;
Ok(joining_chat_id)
}
QrInvite::Broadcast { .. } => {
@@ -144,32 +144,23 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
}
// If we were not in the broadcast channel before, show a 'please wait' info message.
// Since we don't have any specific stock string for this,
// use the generic `Establishing guaranteed end-to-end encryption, please wait…`
if !is_contact_in_chat(context, joining_chat_id, ContactId::SELF).await? {
let msg = stock_str::securejoin_wait(context).await;
chat::add_info_msg(context, joining_chat_id, &msg, time()).await?;
let msg =
stock_str::secure_join_broadcast_started(context, invite.contact_id()).await;
chat::add_info_msg(context, joining_chat_id, &msg).await?;
}
Ok(joining_chat_id)
}
QrInvite::Contact { .. } => {
// For setup-contact the BobState already ensured the 1:1 chat exists because it
// uses it to send the handshake messages.
// Calculate the sort timestamp before checking the chat protection status so that if we
// race with its change, we don't add our message below the protection message.
let sort_to_bottom = true;
let (received, incoming) = (false, false);
let ts_sort = private_chat_id
.calc_sort_timestamp(context, 0, sort_to_bottom, received, incoming)
.await?;
let ts_start = time();
chat::add_info_msg_with_cmd(
context,
private_chat_id,
&stock_str::securejoin_wait(context).await,
SystemMessage::SecurejoinWait,
ts_sort,
Some(ts_start),
None,
time(),
None,
None,
None,
@@ -243,7 +234,7 @@ pub(super) async fn handle_auth_required(
let contact_id = invite.contact_id();
let msg = stock_str::secure_join_replies(context, contact_id).await;
let chat_id = joining_chat_id(context, &invite, chat_id).await?;
chat::add_info_msg(context, chat_id, &msg, time()).await?;
chat::add_info_msg(context, chat_id, &msg).await?;
}
}

View File

@@ -199,19 +199,17 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
})
.collect::<Vec<&str>>()
.join("\n");
if l_last > 1 {
if let Some(line) = lines.get(l_last - 1) {
if is_empty_line(line) {
l_last -= 1
}
}
if l_last > 1
&& let Some(line) = lines.get(l_last - 1)
&& is_empty_line(line)
{
l_last -= 1
}
if l_last > 1 {
if let Some(line) = lines.get(l_last - 1) {
if is_quoted_headline(line) {
l_last -= 1
}
}
if l_last > 1
&& let Some(line) = lines.get(l_last - 1)
&& is_quoted_headline(line)
{
l_last -= 1
}
(lines.get(..l_last).unwrap_or(lines), Some(quoted_text))
} else {

View File

@@ -89,7 +89,7 @@ impl Smtp {
}
self.connectivity.set_connecting(context);
let lp = ConfiguredLoginParam::load(context)
let (_transport_id, lp) = ConfiguredLoginParam::load(context)
.await?
.context("Not configured")?;
let proxy_config = ProxyConfig::load(context).await?;
@@ -307,24 +307,23 @@ pub(crate) async fn smtp_send(
Ok(()) => SendResult::Success,
};
if let SendResult::Failure(err) = &status {
if let Some(msg_id) = msg_id {
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) =
message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
if let SendResult::Failure(err) = &status
&& let Some(msg_id) = msg_id
{
// We couldn't send the message, so mark it as failed
match Message::load_from_db(context, msg_id).await {
Ok(mut msg) => {
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
{
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
}
}
Err(err) => {
error!(
context,
"Failed to load {msg_id} to mark it as failed: {err:#}."
);
}
}
}
status
@@ -444,11 +443,11 @@ pub(crate) async fn send_msg_to_smtp(
chat_id,
&text,
crate::mimeparser::SystemMessage::InvalidUnencryptedMail,
Some(timestamp_sort),
timestamp_sort,
None,
None,
None,
None,
)
.await?;
};

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