Compare commits

...

149 Commits

Author SHA1 Message Date
link2xt
e4ebb91712 chore(release): prepare for 1.142.12 2024-09-02 23:57:32 +00:00
iequidoo
eb3c1b3c25 fix: Display Config::MdnsEnabled as true by default (#5948) 2024-09-02 23:51:51 +00:00
link2xt
fcf3dbbad4 chore(release): prepare for 1.142.11 2024-08-30 22:37:12 +00:00
link2xt
d344cc3bdd fix: set backward verification when observing vc-contact-confirm or vg-member-added (#5930)
Documentation comment says forward and backward verification is set,
but the code was not doing it.
`vc-contact-confirm` and `vg-member-added` messages
indicate that other device finished securejoin protocol
so we know Bob has our key marked as verified.
2024-08-30 19:51:26 +00:00
link2xt
2dd85afdc2 chore(release): prepare for 1.142.10 2024-08-26 18:53:03 +00:00
Hocuri
cdeca9ed9d fix: Only include one From: header in securejoin messages (#5917)
This fixes the bug that sometimes made QR scans fail.

The problem was:

When sorting headers into unprotected/hidden/protected, the From: header
was added twice for all messages: Once into unprotected_headers and once
into protected_headers. For messages that are `is_encrypted && verified
|| is_securejoin_message`, the display name is removed before pushing it
into unprotected_headers.

Later, duplicate headers are removed from unprotected_headers right
before prepending unprotected_headers to the message. But since the
unencrypted From: header got modified a bit when removing the display
name, it's not exactly the same anymore, so it's not removed from
unprotected_headers and consequently added again.
2024-08-26 20:44:26 +02:00
link2xt
495337743a chore(release): prepare for 1.142.9 2024-08-24 21:42:49 +00:00
link2xt
775edab7b1 feat: update preloaded DNS cache 2024-08-24 21:37:56 +00:00
iequidoo
fe9fa17005 fix: Fix skip_smtp_greeting() (#5911)
- Skip lines starting with "220-" (w/o whitespace at the end).
- Don't forget to clear the buffer before reading the next line.
2024-08-24 14:15:29 -03:00
link2xt
0d0f556f21 chore(release): prepare for 1.142.8 2024-08-21 12:44:16 +00:00
link2xt
0e365395bf fix: do not panic on unknown CertificateChecks values 2024-08-21 12:27:42 +00:00
link2xt
8538a3c148 chore(release): prepare for 1.142.7 2024-08-17 16:00:42 +00:00
link2xt
cb4b992204 fix: do not request ALPN on standard ports and when using STARTTLS
Apparently some providers fail TLS connection
with "no_application_protocol" alert
even when requesting "imap" protocol for IMAP connection
and "smtp" protocol for SMTP connection.

Fixes <https://github.com/deltachat/deltachat-core-rust/issues/5892>.
2024-08-17 15:56:26 +00:00
link2xt
af4d54ab50 fix: do not save "Automatic" into configured_imap_certificate_checks
configured_imap_certificate_checks=0 means
accept invalid certificates unless provider database
says otherwise or SOCKS5 is enabled.
It should not be saved into the database anymore.

This bug was introduced in
<https://github.com/deltachat/deltachat-core-rust/pull/5854>
(commit 6b4532a08e)
and affects released core 1.142.4, 1.142.5 and 1.142.6.

Fix reverts faulty fix from
<https://github.com/deltachat/deltachat-core-rust/pull/5886>
(commit a268946f8d)
which changed the way configured_imap_certificate_checks=0
is interpreted and introduced problems
for existing setups with configured_imap_certificate_checks=0:
<https://github.com/deltachat/deltachat-core-rust/issues/5889>.

Existing test from previous fix is not reverted
and still applies.
Regression test is added to check that
configured_imap_certificate_checks
is not "0" for new accounts.
2024-08-17 15:18:06 +00:00
link2xt
1faff84905 build: update rpgp from 0.13.1 to 0.13.2
This fixes the problem with old core (<1.136.0)
not being able to decrypt messages
produced with the new core
when using Ed25519 keys.

The issue is described in
<https://github.com/deltachat/deltachat-core-rust/issues/5881>
2024-08-17 11:31:22 +00:00
iequidoo
62fde21d9a fix: Create a group unblocked for bot even if 1:1 chat is blocked (#5514) 2024-08-16 13:14:29 -03:00
iequidoo
6f3729a00f test: Protected group for bot is auto-accepted 2024-08-16 13:14:29 -03:00
iequidoo
fbf66ba02b feat(jsonrpc): Add ContactObject::e2ee_avail
This can be helpful for the chatmail case, the app can warn the user at least.
2024-08-15 14:41:09 -03:00
link2xt
ed74f4d1d9 chore(release): prepare for 1.142.6 2024-08-15 16:57:56 +00:00
link2xt
a268946f8d fix: default to strict TLS checks if not configured
If user has not set any settings manually
and provider is not configured,
default to strict TLS checks.

Bug was introduced in
<https://github.com/deltachat/deltachat-core-rust/pull/5854>
(commit 6b4532a08e)
and affects released core 1.142.4 and 1.142.5.

The problem only affects accounts configured
using these core versions with provider
not in the provider database or when using advanced settings.
2024-08-15 16:45:48 +00:00
link2xt
7432c6de84 chore(deltachat-rpc-client): fix ruff 0.6.0 warnings 2024-08-15 16:20:02 +00:00
link2xt
7fe9342d0d docs: tweak changelog 2024-08-15 02:16:11 +00:00
B. Petersen
a0e89e4d4e chore(release): prepare for 1.142.5 2024-08-15 02:13:59 +00:00
Hocuri
0c3a476449 fix: Increase timeout for QR generation to 60s (#5882)
On big accounts, it can take more than 10s, so that QR generation
failed.
2024-08-14 22:46:48 +02:00
B. Petersen
de517c15ff chore: update provider database 2024-08-14 21:58:37 +02:00
link2xt
b83d5b0dbf docs: document new mdns_enabled behavior 2024-08-12 20:47:27 +00:00
dependabot[bot]
27924a259f Merge pull request #5871 from deltachat/dependabot/github_actions/actions/setup-node-4 2024-08-11 18:11:13 +00:00
dependabot[bot]
530256b1bf chore(deps): bump actions/setup-node from 2 to 4
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 2 to 4.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-11 02:29:57 +00:00
dependabot[bot]
23d15d7485 Merge pull request #5872 from deltachat/dependabot/github_actions/dependabot/fetch-metadata-2.2.0 2024-08-11 02:28:55 +00:00
dependabot[bot]
3c38d2e105 Merge pull request #5873 from deltachat/dependabot/github_actions/horochx/deploy-via-scp-1.1.0 2024-08-11 02:28:09 +00:00
iequidoo
a53ffcf5e3 fix: store_seen_flags_on_imap: Skip to next messages if couldn't select folder (#5870)
`imap::Session::store_seen_flags_on_imap()` handles messages from multiple folders, so not being
able to select one folder mustn't fail the whole function.
2024-08-10 17:39:24 -03:00
iequidoo
22366cf246 fix: Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" (#5870)
It appeared that some servers require namespace-style names for folders created via IMAP, like
"INBOX.DeltaChat". This partially reverts 05c256dd5b.
2024-08-10 17:39:24 -03:00
dependabot[bot]
ddc2b86875 Merge pull request #5874 from deltachat/dependabot/cargo/serde-1.0.205 2024-08-09 20:51:14 +00:00
dependabot[bot]
9e966615f2 Merge pull request #5875 from deltachat/dependabot/cargo/regex-1.10.6 2024-08-09 20:50:40 +00:00
dependabot[bot]
3335fc727d chore(cargo): bump regex from 1.10.5 to 1.10.6
Bumps [regex](https://github.com/rust-lang/regex) from 1.10.5 to 1.10.6.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.5...1.10.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 19:14:31 +00:00
dependabot[bot]
00d7b38e02 chore(cargo): bump serde from 1.0.204 to 1.0.205
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.204 to 1.0.205.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.204...v1.0.205)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 19:14:15 +00:00
dependabot[bot]
2a8a98c432 chore(deps): bump horochx/deploy-via-scp from 1.0.1 to 1.1.0
Bumps [horochx/deploy-via-scp](https://github.com/horochx/deploy-via-scp) from 1.0.1 to 1.1.0.
- [Release notes](https://github.com/horochx/deploy-via-scp/releases)
- [Commits](https://github.com/horochx/deploy-via-scp/compare/v1.0.1...1.1.0)

---
updated-dependencies:
- dependency-name: horochx/deploy-via-scp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 19:14:02 +00:00
dependabot[bot]
13841491d4 chore(deps): bump dependabot/fetch-metadata from 1.1.1 to 2.2.0
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 1.1.1 to 2.2.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](https://github.com/dependabot/fetch-metadata/compare/v1.1.1...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-09 19:13:59 +00:00
link2xt
2137c05cd6 ci: configure Dependabot to update GitHub Actions 2024-08-09 19:13:31 +00:00
link2xt
6519630d46 chore(release): prepare for 1.142.4 2024-08-09 17:30:54 +00:00
iequidoo
7c6d6a4b12 fix: Still send MDNs from bots by default
Fixup for 5ce44ade1. It is good that read receipts are sent by bots to see the bot received the
message. Thanks to @adbenitez for pointing this out.
2024-08-09 13:58:29 -03:00
link2xt
745b33f174 build: use --locked with cargo install
`cargo install` ignores lockfile by default.
Without lockfile current build fails
due to iroh-net 0.21.0 depending on `derive_more` 1.0.0-beta.6
but failing to compile with `derive_more` 1.0.0.-beta.7.
This particular error will be fixed by upgrading to iroh 0.22.0,
but using lockfile will avoid similar problems in the future.
2024-08-09 16:22:19 +00:00
link2xt
153188db20 feat: allow autoconfig when SOCKS5 is enabled
Since HTTP module supports SOCKS5 now,
there is no reason not to request autoconfig XML
and outlook configuration anymore.
2024-08-09 15:06:27 +00:00
link2xt
4a2ebd0c81 feat: allow using OAuth 2 with SOCKS5
SOCKS5 for HTTP requests is supported since
fa198c3b5e
(PR <https://github.com/deltachat/deltachat-core-rust/pull/4017>)
2024-08-09 15:06:27 +00:00
link2xt
e701709645 chore(cargo): update iroh from 0.21 to 0.22 (#5860) 2024-08-09 14:06:22 +00:00
Daniel Kahn Gillmor
1ca835f34d Point to active Header Protection draft
The old draft was expired and abandoned, and the new draft should be
possible to generate cleanly without breaking compatibility with old
clients.
2024-08-09 15:53:24 +02:00
link2xt
1c021ae5ca ci: update Rust to 1.80.1 2024-08-09 07:49:05 +00:00
link2xt
479a4c2880 chore: update provider database 2024-08-09 03:34:58 +00:00
iequidoo
5ce44ade17 feat: Disable MDNs for bots by default
- To avoid receiving undecryptable MDNs by bots and replying to them if the bot's key changes.
- MDNs from bots don't look useful in general, usually the user expects some reply from the bot, not
  just that the message is read.
2024-08-09 00:11:47 -03:00
link2xt
f03ffa7641 refactor: pass address to moz_autoconfigure() instead of LoginParam 2024-08-08 00:15:08 +00:00
link2xt
b44185948d refactor: remove param_addr_urlencoded argument from get_autoconfig()
It can be calculated inside the function.
2024-08-08 00:15:08 +00:00
link2xt
6b4532a08e refactor: merge imap_certificate_checks and smtp_certificate_checks 2024-08-07 18:08:39 +00:00
iequidoo
86ad5506e3 feat: Always move outgoing auto-generated messages to the mvbox
Recently there are many questions on the Delta Chat forum why some unexpected encrypted messages
appear in Inbox. Seems they are mainly sync messages, though that also obviously happens to
SecureJoin messages. Anyway, regardless of the `MvboxMove` setting, auto-generated outgoing messages
should be moved to the DeltaChat folder so as not to complicate co-using Delta Chat with other MUAs.
2024-08-06 11:33:22 -03:00
iequidoo
6513349c09 feat: Add Config::FixIsChatmail
Add a config option preventing autoconfiguring `IsChatmail` for tests.
2024-08-06 11:33:22 -03:00
link2xt
92685189aa ci: update EmbarkStudios/cargo-deny-action action
v1 is not going to be updated to cargo-deny 0.16.0
because of breaking changes in cargo-deny.
2024-08-06 14:11:56 +00:00
link2xt
3b76622cf1 chore: fix typo s/webdxc/webxdc/ 2024-08-06 05:53:54 +00:00
link2xt
c5a524d3c6 refactor: derive Default for CertificateChecks 2024-08-06 02:32:55 +00:00
link2xt
17eb85b9cd build: downgrade Tokio to 1.38 to fix Android compilation 2024-08-05 17:10:11 +00:00
link2xt
3c688360fb chore(release): prepare for 1.142.3 2024-08-04 04:11:52 +00:00
link2xt
9f220768c2 build: do not disable "vendored" feature in the workspace
This fixes `nix build .#python-docs`
2024-08-04 03:17:42 +00:00
link2xt
fd183c6ee5 chore: remove direct "quinn" dependency 2024-08-04 02:31:42 +00:00
link2xt
9788fb16e8 chore(cargo): update rusqlite and libsqlite3-sys
SQLCipher does not allow passing empty key
since version v4.5.5,
so PRAGMA calls are wrapped into if's.
2024-08-03 23:02:08 +00:00
link2xt
39ed587959 Revert "chore(cargo): update rusqlite"
This reverts commit 1b92d18777.
2024-08-03 19:19:55 +00:00
link2xt
c4327a0558 Fix cargo warnings about default-features
Otherwise cargo emits these warnings:
warning: .../deltachat-core-rust/deltachat-ffi/Cargo.toml: `default-features` is ignored for deltachat, since `default-features` was not specified for `workspace.dependencies.deltachat`, this could become a hard error in the future
warning: .../deltachat-core-rust/deltachat-rpc-server/Cargo.toml: `default-features` is ignored for deltachat, since `default-features` was not specified for `workspace.dependencies.deltachat`, this could become a hard error in the future
warning: .../deltachat-core-rust/deltachat-rpc-server/Cargo.toml: `default-features` is ignored for deltachat-jsonrpc, since `default-features` was not specified for `workspace.dependencies.deltachat-jsonrpc`, this could become a hard error in the future
2024-08-03 19:08:47 +00:00
link2xt
1b92d18777 chore(cargo): update rusqlite 2024-08-03 19:08:29 +00:00
link2xt
a67503ae4a chore: remove backtrace dependency
It is not used directly by `deltachat` crate.
2024-08-02 23:06:30 +00:00
link2xt
c54f39bea0 chore: remove sha2 dependency
It is not used since ce6ec64069
2024-08-02 23:06:30 +00:00
dependabot[bot]
ff3138fa43 Merge pull request #5830 from deltachat/dependabot/cargo/env_logger-0.11.5 2024-08-02 21:39:16 +00:00
dependabot[bot]
09d46942ca Merge pull request #5832 from deltachat/dependabot/cargo/tokio-1.39.2 2024-08-02 19:51:34 +00:00
dependabot[bot]
84e365d263 Merge pull request #5833 from deltachat/dependabot/cargo/uuid-1.10.0 2024-08-02 19:50:28 +00:00
dependabot[bot]
b31bcf5561 Merge pull request #5836 from deltachat/dependabot/cargo/quick-xml-0.36.1 2024-08-02 19:43:48 +00:00
link2xt
da50d682e1 chore(release): prepare for 1.142.2 2024-08-02 17:05:43 +00:00
link2xt
094d310f5c feat: sort DNS results by successful connection timestamp (#5818) 2024-08-02 16:53:16 +00:00
dependabot[bot]
642eaf92d7 chore(cargo): bump serde from 1.0.203 to 1.0.204
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.203 to 1.0.204.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.203...v1.0.204)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 13:39:06 -03:00
link2xt
76c032a2c4 fix: reset configured_provider on reconfiguration 2024-08-02 16:31:07 +00:00
dependabot[bot]
a74b04d175 chore(cargo): bump quoted_printable from 0.5.0 to 0.5.1
Bumps [quoted_printable](https://github.com/staktrace/quoted-printable) from 0.5.0 to 0.5.1.
- [Commits](https://github.com/staktrace/quoted-printable/compare/v0.5.0...v0.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 13:14:21 -03:00
dependabot[bot]
c9448feafc chore(cargo): bump env_logger from 0.11.3 to 0.11.5
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.3 to 0.11.5.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.11.3...v0.11.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 15:51:02 +00:00
dependabot[bot]
8314f3e30c chore(cargo): bump syn from 2.0.68 to 2.0.72
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.68 to 2.0.72.
- [Release notes](https://github.com/dtolnay/syn/releases)
- [Commits](https://github.com/dtolnay/syn/compare/2.0.68...2.0.72)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 12:45:02 -03:00
dependabot[bot]
935da2db49 Merge pull request #5838 from deltachat/dependabot/cargo/thiserror-1.0.63 2024-08-02 15:41:01 +00:00
dependabot[bot]
b5e95fa1ef chore(cargo): bump human-panic from 2.0.0 to 2.0.1
Bumps [human-panic](https://github.com/rust-cli/human-panic) from 2.0.0 to 2.0.1.
- [Changelog](https://github.com/rust-cli/human-panic/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/human-panic/compare/v2.0.0...v2.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 12:40:06 -03:00
dependabot[bot]
b60d8356cb chore(cargo): bump serde_json from 1.0.120 to 1.0.122
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.120 to 1.0.122.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.120...v1.0.122)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-02 12:39:19 -03:00
link2xt
ee7a7a2f9d fix: fix compilation on iOS 2024-08-02 15:22:19 +00:00
dependabot[bot]
b5eb824346 Merge pull request #5835 from deltachat/dependabot/cargo/toml-0.8.15 2024-08-02 15:20:33 +00:00
dependabot[bot]
41867b89a0 chore(cargo): bump thiserror from 1.0.61 to 1.0.63
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.61 to 1.0.63.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.61...1.0.63)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:58:46 +00:00
dependabot[bot]
7e7aa7aba0 chore(cargo): bump quick-xml from 0.35.0 to 0.36.1
Bumps [quick-xml](https://github.com/tafia/quick-xml) from 0.35.0 to 0.36.1.
- [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.35.0...v0.36.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:57:59 +00:00
dependabot[bot]
fd1dab7c7b chore(cargo): bump toml from 0.8.14 to 0.8.15
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.14 to 0.8.15.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.14...toml-v0.8.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:57:38 +00:00
dependabot[bot]
a69f9f01b3 chore(cargo): bump uuid from 1.9.1 to 1.10.0
Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.9.1 to 1.10.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.9.1...1.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:56:35 +00:00
dependabot[bot]
c808ed1368 chore(cargo): bump tokio from 1.38.0 to 1.39.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.38.0 to 1.39.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.38.0...tokio-1.39.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 21:56:05 +00:00
link2xt
21be85071a feat: try only the full email address if username is unspecified
Previously Delta Chat tried to use local part of email address as well.
This configuration is very uncommon,
but trying it doubled the time of configuration try
in the worst case, e.g. when the password is typed in incorrectly.
2024-08-01 17:08:23 +00:00
iequidoo
a30c6ae1f7 refactor: Don't update message state to OutMdnRcvd anymore
This state can be computed from the `msgs_mdns` table without significant overhead as we have an
index by msg_id there.
2024-07-31 21:04:24 -03:00
link2xt
0324884124 build: use workspace dependencies to make cargo-deny 0.15.1 happy 2024-07-31 16:45:12 +00:00
link2xt
ad225b12c2 chore(cargo): update bytemuck from 0.14.3 to 0.16.3
Version 0.14.3 is yanked.
2024-07-31 05:20:09 +00:00
link2xt
0dd5e5ab7d fix: do not reset is_chatmail config on failed reconfiguration
Instead, always set it at the end of successful configuration.
2024-07-31 02:36:36 +00:00
link2xt
490f41cda8 fix: await the tasks after aborting them 2024-07-30 20:31:22 +00:00
link2xt
c163438eaf chore(release): prepare for 1.142.1 2024-07-30 15:40:06 +00:00
link2xt
ef925b0948 refactor: move DNS resolution into IMAP and SMTP connect code 2024-07-30 02:12:05 +00:00
link2xt
0fceb270ca fix: explicitly close the database on account removal 2024-07-30 00:22:03 +00:00
link2xt
4ec5d12213 refactor(imap): unify IMAP connection setup in Client::connect()
All functions like Client::connect_secure() are now private
and every new connection is established in Client::connect().
2024-07-29 15:16:40 +00:00
link2xt
d9c0e47581 refactor(smtp): unify SMTP connection setup between TLS and STARTTLS
Single function smtp::connect::connect_stream
returns a stream of a single `dyn` type
that can be a TLS, STARTTLS or plaintext
connection established over SOCKS5 or directly.
2024-07-29 15:16:40 +00:00
link2xt
8ec4a8ad46 refactor: replace {IMAP,SMTP,HTTP}_TIMEOUT with a single constant
This change also increases HTTP timeout from 30 seconds to 60 seconds.
2024-07-29 15:16:40 +00:00
link2xt
40d355209b refactor: pass single ALPN around instead of ALPN list
This way there is always exactly one ALPN ("imap" or "smtp").
2024-07-29 15:16:40 +00:00
iequidoo
354702fcab fix: imex::import_backup: Ignore errors from delete_and_reset_all_device_msgs()
They are not a good reason to fail the whole import. Anyway `delete_and_reset_all_device_msgs()`
isn't retried after restarting the program.
2024-07-28 12:51:25 -03:00
iequidoo
bfc7ae1eff fix: Sql::import: Detach backup db if any step of the import fails
Otherwise we continue to work with an incompletely imported db... but only until restart -- after
that all changes to the db are lost.
2024-07-28 12:51:25 -03:00
iequidoo
cccefe15b3 fix: import_backup_stream: Fix progress stucking at 0
Fix the progress calculation, before `total_size.checked_div(file_size)` was giving 0 if `total_size
< file_size`.
2024-07-28 12:51:25 -03:00
iequidoo
bb4236ffed fix: imex::import_backup: Unpack all blobs before importing a db (#4307)
This way we can't get an account with missing blobs if there's not enough disk space.

Also delete already unpacked files if all files weren't unpacked successfully. Still, there are some
minor problems remaining:
- If a db wasn't imported successfully, unpacked blobs aren't deleted because we don't know at which
  step the import failed and whether the db will reference the blobs after restart.
- If `delete_and_reset_all_device_msgs()` fails, the whole `import_backup()` fails also, but after a
  restart delete_and_reset_all_device_msgs() isn't retried. Probably errors from it should be
  ignored at all.
2024-07-28 12:51:25 -03:00
link2xt
14d57e780b feat: report first error instead of the last on connection failure
First result has higher priority
as it is the one prioritized by DNS
or used recently, while the last
tried server may never work at all.
2024-07-27 23:00:05 +00:00
link2xt
76a43c8de6 feat: try next DNS resolution result if TLS setup fails
Previously Delta Chat tried all DNS resolution results
in sequence until TCP connection is established successfully,
then tried to establish TLS on top of the TCP connection.
If establishing TLS fails, the whole
connection establishment procedure failed
without trying next DNS resolution results.

In particular, in a scenario
where DNS returns incorrect result
pointing e.g. to a server
that listens on the TCP port
but does not have correpsponding TLS certificate,
Delta Chat now will fall back to the cached result
and connect successfully.
2024-07-27 23:00:05 +00:00
link2xt
b807435c42 refactor: add net/dns submodule 2024-07-27 23:00:05 +00:00
link2xt
3b040fd4b5 ci: update Rust to 1.80.0 2024-07-26 23:57:21 +00:00
link2xt
b9b9ed197e chore(cargo): update iroh from 0.20.0 to 0.21.0 2024-07-26 23:07:59 +00:00
bjoern
03523ab589 feat: do not reveal sender's language in read receipts (#5802)
while adapting strings for the recent change about read receipts,
https://github.com/deltachat/deltachat-core-rust/pull/5712 , it turns
out in discussions eg. at
https://github.com/deltachat/deltachat-android/issues/3179 that
untranslated english for the read receipts seem to be sufficient or even
better:

- do not reveal the sender's language
- unexpected languages are confusing - even if you chat in english, you
may get Chinese read receipts
- many clients do not show the text anyways, iirc, eg. Outlook display
the read receipts in context, and Delta Chat of course as well
- afaik, we're leaving comparable `multipart/report` untranslated as
well (sync, but also webxdc updates are practically english only)
- less code, fewer translations needed :)
2024-07-26 21:07:30 +02:00
link2xt
c4efe59a12 chore(cargo): update time from 0.3.34 to 0.3.36 2024-07-26 16:21:20 +00:00
link2xt
d46f53a004 fix(smtp): use DNS cache for implicit TLS connections
load_cache argument to connect_tcp() should only be false
if strict TLS checks are disabled or TLS is not used.
2024-07-25 01:25:49 +00:00
link2xt
5fb5fd4318 chore(release): prepare for 1.142.0 2024-07-23 05:02:53 +00:00
link2xt
a3cb58484f feat: use [...] for protected subject
This subject is going to be standardized in
<https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/>
and is already used in K-9 Mail:
<https://github.com/thunderbird/thunderbird-android/pull/8014>
2024-07-23 04:50:40 +00:00
iequidoo
04fd2cdcab fix: Reject message with forged From even if no valid signatures are found
There are many reasons why we may fail to find valid signatures in a message, e.g. we don't yet know
a public key attached in the same message, anyway, if From is forged, the message must be rejected.

Also always take the displayname from encrypted From, even if no valid signatures are found.
2024-07-22 20:22:46 -03:00
link2xt
a710c034e4 feat: do not show the address in invite QR code SVG
Addresses take space and sometimes
do not fit. We generally want
to deemphasize addresses in the UI,
especially randomized chatmail
addresses.
2024-07-22 22:39:48 +00:00
iequidoo
bd651d9ef3 feat: Set summary thumbnail path for WebXDCs to "webxdc-icon://last-msg-id" (#5782)
This is a hint for apps that a WebXDC icon should be shown in the summary, e.g. in the
chatlist. Otherwise it's not clear when it should be shown, e.g. it shouldn't be shown in a reaction
summary.
2024-07-22 18:25:15 -03:00
link2xt
7f3e8f9796 feat: promote fallback DNS results to cached on successful use
For hardcoded built-in DNS results
there is no cache entry in `dns_cache` table
so they cannot be prioritized if DNS resolution
never returned these results yet.
If there is no entry, a new one should be created.
SQL UPSERT does this.
2024-07-22 20:08:07 +00:00
link2xt
837311abce chore(cargo): update image crate to 0.25.2
This version deprecated `image::io::Reader`,
requires changes to avoid warnings.
2024-07-22 20:02:58 +00:00
iequidoo
c596ee0256 fix: Emit MsgsChanged if the number of unnoticed archived chats could decrease (#5768)
Follow-up to 3cf78749df "Emit DC_EVENT_MSGS_CHANGED for
DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with unread messages increases (#3940)".

In general we don't want to make an extra db query to know if a noticied chat is
archived. Emitting events should be cheap, better to allow false-positive `MsgsChanged` events.
2024-07-22 16:26:20 -03:00
dependabot[bot]
5815d8f1dd chore(deps): bump openssl from 0.10.60 to 0.10.66 in /fuzz
Bumps [openssl](https://github.com/sfackler/rust-openssl) from 0.10.60 to 0.10.66.
- [Release notes](https://github.com/sfackler/rust-openssl/releases)
- [Commits](https://github.com/sfackler/rust-openssl/compare/openssl-v0.10.60...openssl-v0.10.66)

---
updated-dependencies:
- dependency-name: openssl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 16:06:18 -03:00
iequidoo
2675e7b2e1 chore(cargo): Update openssl to v0.10.66 2024-07-22 11:24:29 +00:00
link2xt
8f400dda85 feat: use custom DNS resolver for HTTP(S) 2024-07-21 23:04:53 +00:00
link2xt
2a605b93cd test: add test for get_http_response JSON-RPC call 2024-07-21 23:04:53 +00:00
iequidoo
e4d65b2f3b fix: Call send_sync_msg() only from the SMTP loop (#5780)
`Context::send_sync_msg()` mustn't be called from multiple tasks in parallel to avoid sending the
same sync items twice because sync items are removed from the db only after successful
sending. Let's guarantee this by calling `send_sync_msg()` only from the SMTP loop. Before
`send_sync_msg()` could be called in parallel from the SMTP loop and another task doing
e.g. `chat::sync()` which led to `test_multidevice_sync_chat` being flaky because of events
triggered by duplicated sync messages.
2024-07-21 12:10:06 -03:00
link2xt
87a45e88dc fix: correct copy-pasted DCACCOUNT parsing errors message
Apparently error message was copy-pasted from DCWEBRTC handling code.
2024-07-20 12:26:13 +00:00
link2xt
d6d90db957 feat: new BACKUP2 transfer protocol
New protocol streams .tar into iroh-net
stream without traversing all the files first.
Reception over old backup protocol
is still supported to allow
transferring backups from old devices
to new ones, but not vice versa.
2024-07-19 03:16:57 +00:00
iequidoo
eb669afb8f feat: Don't unarchive a group on a member removal except SELF (#5618) 2024-07-17 17:25:10 -03:00
iequidoo
d1cf80001e feat: Don't create ad-hoc group on a member removal message (#5618)
The "Chat-Group-Member-Removed" header is added to ad-hoc group messages as well, so we should check
for its presense before creating an ad-hoc group as we do for DC-style groups.
2024-07-17 17:25:10 -03:00
link2xt
307d11f503 api(deltachat-jsonrpc): add pinned property to FullChat and BasicChat 2024-07-17 10:12:39 +00:00
link2xt
73f527e772 fix: randomize avatar blob filenames to work around caching 2024-07-15 22:48:39 +00:00
iequidoo
5143ebece1 refactor: Reduce boilerplate for migration version increment 2024-07-15 15:39:10 -03:00
iequidoo
9996c2db80 feat: Limit the size of aggregated WebXDC update to 100 KiB (#4825)
Before, update sending might be delayed due to rate limits and later merged into large
messages. This is undesirable for apps that want to send large files over WebXDC updates because the
message with aggregated update may be too large for actual sending and hit the provider limit or
require multiple attempts on a flaky SMTP connection.

So, don't aggregate updates if the size of an aggregated update will exceed the limit of 100
KiB. This is a soft limit, so it may be exceeded if a single update is larger and it limits only the
update JSON size, so the message with all envelopes still may be larger. Also the limit may be
exceeded when updates are sent together with the WebXDC instance when resending it as the instance
size isn't accounted to not complicate the code. At least this is not worse than the previous
behaviour when all updates were attached.
2024-07-13 16:24:44 -03:00
link2xt
0f26da4028 feat(iroh): pass direct addresses from Endpoint to Gossip 2024-07-13 14:36:35 +00:00
iequidoo
a3dd37b011 feat(jsonrpc): Allow to set message quote text without referencing quoted message (#5695)
Bridge bots like matterdelta need to set a quoted text without referencing the quoted message, this
makes easier bridging messages from other platforms to Delta Chat or even bridging Delta Chat groups
in different accounts where you can not set a quoted message by the message id from another account.
2024-07-12 15:08:45 -03:00
iequidoo
6b11b0ea8d fix: Message::set_quote: Don't forget to remove Param::ProtectQuote 2024-07-12 15:08:45 -03:00
link2xt
faad7d5843 feat: request smtp ALPN for SMTP TLS connections
Even though SMTP ALPN is not officially registered (unlike IMAP),
it is an obvious choice that will allow
to multiplex SMTP and other protocols on the same TLS port.
2024-07-11 20:57:45 +00:00
link2xt
ef0d6d0c90 build(node): pin node-gyp to version 10.1
Newer node-gyp uses newer gyp
which requires newer python
that is not available in Debian 10
container that is
used for building node prebuilds
with old glibc.
2024-07-11 08:59:26 +00:00
link2xt
bd83fb3d38 feat: set imap ALPN when connecting to IMAP servers
IMAP has a registered protocol ID
listed at <https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids>

Requesting specific ALPN on the client
should allow the server to
multiplex multiple protocols on the same
port and dispatch
requests to the correct backend on the proxy such as HAProxy.
2024-07-11 05:28:32 +00:00
link2xt
f84e603318 refactor: return error from export_backup() without logging
The error is logged by `imex` caller.
2024-07-10 21:14:07 +00:00
link2xt
d77459e4fc refactor: move TempPathGuard into tools and use instead of DeleteOnDrop 2024-07-10 20:13:42 +00:00
link2xt
2c14bd353f refactor: move key transfer into its own submodule
`create_setup_code` and `render_setup_file`
are now hidden from public API,
so deltachat-repl does not have "export-setup"
debug command anymore.
2024-07-10 04:29:10 +00:00
iequidoo
0860508a1d feat: Contact::create_ex: Don't send sync message if nothing changed (#5705)
Follow-up to 5fa7cff46. Let's still not send a sync message if the contact wasn't modified. This is
not very important, but just for consistency with the `chat::rename_ex()` behaviour.
2024-07-10 00:14:23 -03:00
iequidoo
f81daa16b3 feat: Add email address and fingerprint to exported key file names (#5694)
This way it's clearer which key is which and also adding the key fingerprint to the file name avoids
overwriting another previously exported key. I think this is better than adding an incremental
number as we do for backups, there's no need to export a key several times to different files.
2024-07-10 00:13:02 -03:00
iequidoo
436b00e3cb feat: Report better error from DcKey::from_asc() (#5539)
If no matching key packet was found, report which key is needed to make it clear to the user.
2024-07-09 21:38:20 -03:00
link2xt
4d52aa8b7f chore(cargo): update hashlink to remove allocator-api2 dependency 2024-07-09 23:17:21 +00:00
dignifiedquire
c2d5488663 fix: only add node addrs with actual information 2024-07-09 22:11:30 +00:00
link2xt
cc51d51a78 chore(cargo): update iroh from 0.17 to 0.20 2024-07-09 22:11:30 +00:00
96 changed files with 6876 additions and 2743 deletions

View File

@@ -7,3 +7,10 @@ updates:
commit-message:
prefix: "chore(cargo)"
open-pull-requests-limit: 50
# Keep GitHub Actions up to date.
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -24,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.79.0
RUSTUP_TOOLCHAIN: 1.80.1
steps:
- uses: actions/checkout@v4
with:
@@ -59,7 +59,7 @@ jobs:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
with:
arguments: --all-features --workspace
command: check
@@ -95,11 +95,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.79.0
rust: 1.80.1
- os: windows-latest
rust: 1.79.0
rust: 1.80.1
- os: macos-latest
rust: 1.79.0
rust: 1.80.1
# Minimum Supported Rust Version = 1.77.0
- os: ubuntu-latest

View File

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

View File

@@ -31,7 +31,7 @@ jobs:
mv docs js
- name: Upload
uses: horochx/deploy-via-scp@v1.0.1
uses: horochx/deploy-via-scp@1.1.0
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}

View File

@@ -74,7 +74,7 @@ jobs:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: '18'
- name: npm install

View File

@@ -1,5 +1,260 @@
# Changelog
## [1.142.12] - 2024-09-02
### Fixes
- Display Config::MdnsEnabled as true by default ([#5948](https://github.com/deltachat/deltachat-core-rust/pull/5948)).
## [1.142.11] - 2024-08-30
### Fixes
- Set backward verification when observing vc-contact-confirm or `vg-member-added` ([#5930](https://github.com/deltachat/deltachat-core-rust/pull/5930)).
## [1.142.10] - 2024-08-26
### Fixes
- Only include one From: header in securejoin messages ([#5917](https://github.com/deltachat/deltachat-core-rust/pull/5917)).
## [1.142.9] - 2024-08-24
### Fixes
- Fix reading of multiline SMTP greetings ([#5911](https://github.com/deltachat/deltachat-core-rust/pull/5911)).
### Features / Changes
- Update preloaded DNS cache.
## [1.142.8] - 2024-08-21
### Fixes
- Do not panic on unknown CertificateChecks values.
## [1.142.7] - 2024-08-17
### Fixes
- Do not save "Automatic" into configured_imap_certificate_checks. **This fixes regression introduced in core 1.142.4. Versions 1.142.4..1.142.6 should not be used in releases.**
- Create a group unblocked for bot even if 1:1 chat is blocked ([#5514](https://github.com/deltachat/deltachat-core-rust/pull/5514)).
- Update rpgp from 0.13.1 to 0.13.2 to fix "unable to decrypt" errors when sending messages to old Delta Chat clients and using Ed25519 keys to encrypt.
- Do not request ALPN on standard ports and when using STARTTLS.
### Features / Changes
- jsonrpc: Add ContactObject::e2ee_avail.
### Tests
- Protected group for bot is auto-accepted.
## [1.142.6] - 2024-08-15
### Fixes
- Default to strict TLS checks if not configured.
### Miscellaneous Tasks
- deltachat-rpc-client: Fix ruff 0.6.0 warnings.
## [1.142.5] - 2024-08-14
### Fixes
- Still try to create "INBOX.DeltaChat" if couldn't create "DeltaChat" ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
- `store_seen_flags_on_imap`: Skip to next messages if couldn't select folder ([#5870](https://github.com/deltachat/deltachat-core-rust/pull/5870)).
- Increase timeout for QR generation to 60s ([#5882](https://github.com/deltachat/deltachat-core-rust/pull/5882)).
### Documentation
- Document new `mdns_enabled` behavior (bots do not send MDNs by default).
### CI
- Configure Dependabot to update GitHub Actions.
### Miscellaneous Tasks
- cargo: Bump regex from 1.10.5 to 1.10.6.
- cargo: Bump serde from 1.0.204 to 1.0.205.
- deps: Bump horochx/deploy-via-scp from 1.0.1 to 1.1.0.
- deps: Bump dependabot/fetch-metadata from 1.1.1 to 2.2.0.
- deps: Bump actions/setup-node from 2 to 4.
- Update provider database.
## [1.142.4] - 2024-08-09
### Build system
- Downgrade Tokio to 1.38 to fix Android compilation.
- Use `--locked` with `cargo install`.
### Features / Changes
- Add Config::FixIsChatmail.
- Always move outgoing auto-generated messages to the mvbox.
- Disable requesting MDNs for bots by default.
- Allow using OAuth 2 with SOCKS5.
- Allow autoconfig when SOCKS5 is enabled.
- Update provider database.
- cargo: Update iroh from 0.21 to 0.22 ([#5860](https://github.com/deltachat/deltachat-core-rust/pull/5860)).
### CI
- Update Rust to 1.80.1.
- Update EmbarkStudios/cargo-deny-action.
### Documentation
- Point to active Header Protection draft
### Refactor
- Derive `Default` for `CertificateChecks`.
- Merge imap_certificate_checks and smtp_certificate_checks.
- Remove param_addr_urlencoded argument from get_autoconfig().
- Pass address to moz_autoconfigure() instead of LoginParam.
## [1.142.3] - 2024-08-04
### Build system
- cargo: Update rusqlite and libsqlite3-sys.
- Fix cargo warnings about default-features
- Do not disable "vendored" feature in the workspace.
- cargo: Bump quick-xml from 0.35.0 to 0.36.1.
- cargo: Bump uuid from 1.9.1 to 1.10.0.
- cargo: Bump tokio from 1.38.0 to 1.39.2.
- cargo: Bump env_logger from 0.11.3 to 0.11.5.
- Remove sha2 dependency.
- Remove `backtrace` dependency.
- Remove direct "quinn" dependency.
## [1.142.2] - 2024-08-02
### Features / Changes
- Try only the full email address if username is unspecified.
- Sort DNS results by successful connection timestamp ([#5818](https://github.com/deltachat/deltachat-core-rust/pull/5818)).
### Fixes
- Await the tasks after aborting them.
- Do not reset is_chatmail config on failed reconfiguration.
- Fix compilation on iOS.
- Reset configured_provider on reconfiguration.
### Refactor
- Don't update message state to `OutMdnRcvd` anymore.
### Build system
- Use workspace dependencies to make cargo-deny 0.15.1 happy.
- cargo: Update bytemuck from 0.14.3 to 0.16.3.
- cargo: Bump toml from 0.8.14 to 0.8.15.
- cargo: Bump serde_json from 1.0.120 to 1.0.122.
- cargo: Bump human-panic from 2.0.0 to 2.0.1.
- cargo: Bump thiserror from 1.0.61 to 1.0.63.
- cargo: Bump syn from 2.0.68 to 2.0.72.
- cargo: Bump quoted_printable from 0.5.0 to 0.5.1.
- cargo: Bump serde from 1.0.203 to 1.0.204.
## [1.142.1] - 2024-07-30
### Features / Changes
- Do not reveal sender's language in read receipts ([#5802](https://github.com/deltachat/deltachat-core-rust/pull/5802)).
- Try next DNS resolution result if TLS setup fails.
- Report first error instead of the last on connection failure.
### Fixes
- smtp: Use DNS cache for implicit TLS connections.
- Imex::import_backup: Unpack all blobs before importing a db ([#4307](https://github.com/deltachat/deltachat-core-rust/pull/4307)).
- Import_backup_stream: Fix progress stucking at 0.
- Sql::import: Detach backup db if any step of the import fails.
- Imex::import_backup: Ignore errors from delete_and_reset_all_device_msgs().
- Explicitly close the database on account removal.
### Miscellaneous Tasks
- cargo: Update time from 0.3.34 to 0.3.36.
- cargo: Update iroh from 0.20.0 to 0.21.0.
### Refactor
- Add net/dns submodule.
- Pass single ALPN around instead of ALPN list.
- Replace {IMAP,SMTP,HTTP}_TIMEOUT with a single constant.
- smtp: Unify SMTP connection setup between TLS and STARTTLS.
- imap: Unify IMAP connection setup in Client::connect().
- Move DNS resolution into IMAP and SMTP connect code.
### CI
- Update Rust to 1.80.0.
## [1.142.0] - 2024-07-23
### API-Changes
- deltachat-jsonrpc: Add `pinned` property to `FullChat` and `BasicChat`.
- deltachat-jsonrpc: Allow to set message quote text without referencing quoted message ([#5695](https://github.com/deltachat/deltachat-core-rust/pull/5695)).
### Features / Changes
- cargo: Update iroh from 0.17 to 0.20.
- iroh: Pass direct addresses from Endpoint to Gossip.
- New BACKUP2 transfer protocol.
- Use `[...]` instead of `...` for protected subject.
- Add email address and fingerprint to exported key file names ([#5694](https://github.com/deltachat/deltachat-core-rust/pull/5694)).
- Request `imap` ALPN for IMAP TLS connections and `smtp` ALPN for SMTP TLS connections.
- Limit the size of aggregated WebXDC update to 100 KiB ([#4825](https://github.com/deltachat/deltachat-core-rust/pull/4825)).
- Don't create ad-hoc group on a member removal message ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)).
- Don't unarchive a group on a member removal except SELF ([#5618](https://github.com/deltachat/deltachat-core-rust/pull/5618)).
- Use custom DNS resolver for HTTP(S).
- Promote fallback DNS results to cached on successful use.
- Set summary thumbnail path for WebXDCs to "webxdc-icon://last-msg-id" ([#5782](https://github.com/deltachat/deltachat-core-rust/pull/5782)).
- Do not show the address in invite QR code SVG.
- Report better error from DcKey::from_asc() ([#5539](https://github.com/deltachat/deltachat-core-rust/pull/5539)).
- Contact::create_ex: Don't send sync message if nothing changed ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)).
### Fixes
- `Message::set_quote`: Don't forget to remove `Param::ProtectQuote`.
- Randomize avatar blob filenames to work around caching.
- Correct copy-pasted DCACCOUNT parsing errors message.
- Call `send_sync_msg()` only from the SMTP loop ([#5780](https://github.com/deltachat/deltachat-core-rust/pull/5780)).
- Emit MsgsChanged if the number of unnoticed archived chats could decrease ([#5768](https://github.com/deltachat/deltachat-core-rust/pull/5768)).
- Reject message with forged From even if no valid signatures are found.
### Refactor
- Move key transfer into its own submodule.
- Move TempPathGuard into `tools` and use instead of `DeleteOnDrop`.
- Return error from export_backup() without logging.
- Reduce boilerplate for migration version increment.
### Tests
- Add test for `get_http_response` JSON-RPC call.
### Build system
- node: Pin node-gyp to version 10.1.
### Miscellaneous Tasks
- cargo: Update hashlink to remove allocator-api2 dependency.
- cargo: Update openssl to v0.10.66.
- deps: Bump openssl from 0.10.60 to 0.10.66 in /fuzz.
- cargo: Update `image` crate to 0.25.2.
## [1.141.2] - 2024-07-09
### Features / Changes
@@ -24,7 +279,7 @@
- Protect from reusing migration versions ([#5719](https://github.com/deltachat/deltachat-core-rust/pull/5719)).
- Move `quota_needs_update` calculation to a separate function ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)).
### Other
### Documentation
- Document vCards in the specification ([#5724](https://github.com/deltachat/deltachat-core-rust/pull/5724))
@@ -4538,3 +4793,16 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.141.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.2...v1.141.0
[1.141.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.0...v1.141.1
[1.141.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.1...v1.141.2
[1.142.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.141.2...v1.142.0
[1.142.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.0...v1.142.1
[1.142.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.1...v1.142.2
[1.142.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.2...v1.142.3
[1.142.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.3...v1.142.4
[1.142.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.4...v1.142.5
[1.142.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.5...v1.142.6
[1.142.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.6...v1.142.7
[1.142.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.7...v1.142.8
[1.142.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.8...v1.142.9
[1.142.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.9..v1.142.10
[1.142.11]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.10..v1.142.11
[1.142.12]: https://github.com/deltachat/deltachat-core-rust/compare/v1.142.11..v1.142.12

759
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.141.2"
version = "1.142.12"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.77"
@@ -34,7 +34,7 @@ strip = true
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
deltachat-contact-tools = { workspace = true }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
@@ -45,7 +45,6 @@ async-imap = { version = "0.9.7", default-features = false, features = ["runtime
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
backtrace = "0.3"
base64 = { workspace = true }
brotli = { version = "6", default-features=false, features = ["std"] }
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
@@ -61,9 +60,8 @@ hickory-resolver = "0.24"
humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
iroh-net = "0.17.0"
iroh-gossip = { version = "0.17.0", features = ["net"] }
quinn = "0.10.0"
iroh-net = { version = "0.22.0", default-features = false }
iroh-gossip = { version = "0.22.0", default-features = false, features = ["net"] }
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = { workspace = true }
@@ -75,20 +73,19 @@ num-traits = { workspace = true }
once_cell = { workspace = true }
percent-encoding = "2.3"
parking_lot = "0.12"
pgp = { version = "0.13", default-features = false }
pgp = { version = "0.13.2", default-features = false }
qrcodegen = "1.7.0"
quick-xml = "0.35"
quick-xml = "0.36"
quoted_printable = "0.5"
rand = { workspace = true }
regex = { workspace = true }
reqwest = { version = "0.11.27", features = ["json"] }
reqwest = { version = "0.12.5", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] }
rust-hsluv = "0.1"
sanitize-filename = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1.13.2"
strum = "0.26"
strum_macros = "0.26"
@@ -163,6 +160,9 @@ ansi_term = "0.12.1"
async-channel = "2.3.1"
base64 = "0.22"
chrono = { version = "0.4.38", default-features = false }
deltachat-contact-tools = { path = "deltachat-contact-tools" }
deltachat-jsonrpc = { path = "deltachat-jsonrpc" }
deltachat = { path = "." }
futures = "0.3.30"
futures-lite = "2.3.0"
libc = "0.2"
@@ -171,13 +171,20 @@ num-traits = "0.2"
once_cell = "1.18.0"
rand = "0.8"
regex = "1.10"
rusqlite = "0.31"
rusqlite = "0.32"
sanitize-filename = "0.5"
serde_json = "1"
serde = "1.0"
tempfile = "3.10.1"
thiserror = "1"
tokio = "1.38.0"
# 1.38 is the latest version before `mio` dependency update
# that broke compilation with Android NDK r23c and r24.
# Version 1.39.0 cannot be compiled using these NDKs,
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
# for details.
tokio = "~1.38.1"
tokio-util = "0.7.11"
tracing-subscriber = "0.3"
yerpc = "0.6.2"
@@ -190,3 +197,6 @@ vendored = [
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

View File

@@ -30,13 +30,13 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ cargo run -p deltachat-repl -- ~/deltachat-db
$ cargo run --locked -p deltachat-repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
Optionally, install `deltachat-repl` binary with
```
$ cargo install --path deltachat-repl/
$ cargo install --locked --path deltachat-repl/
```
and run as
```

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.141.2"
version = "1.142.12"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -14,8 +14,8 @@ name = "deltachat"
crate-type = ["cdylib", "staticlib"]
[dependencies]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
deltachat = { workspace = true, default-features = false }
deltachat-jsonrpc = { workspace = true, optional = true }
libc = { workspace = true }
human-panic = { version = "2", default-features = false }
num-traits = { workspace = true }
@@ -29,6 +29,6 @@ yerpc = { workspace = true, features = ["anyhow_expose"] }
[features]
default = ["vendored"]
vendored = ["deltachat/vendored"]
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
jsonrpc = ["dep:deltachat-jsonrpc"]

View File

@@ -409,7 +409,7 @@ char* dc_get_blobdir (const dc_context_t* context);
* - `socks5_user` = SOCKS5 proxy username
* - `socks5_password` = SOCKS5 proxy password
* - `imap_certificate_checks` = how to check IMAP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = how to check SMTP certificates, one of the @ref DC_CERTCK flags, defaults to #DC_CERTCK_AUTO (0)
* - `smtp_certificate_checks` = deprecated option, should be set to the same value as `imap_certificate_checks` but ignored by the new core
* - `displayname` = Own name to use when sending messages. MUAs are allowed to spread this way e.g. using CC, defaults to empty
* - `selfstatus` = Own status to display, e.g. in e-mail footers, defaults to empty
* - `selfavatar` = File containing avatar. Will immediately be copied to the
@@ -420,7 +420,8 @@ char* dc_get_blobdir (const dc_context_t* context);
* and also recoded to a reasonable size.
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
* - `mdns_enabled` = 0=do not send or request read receipts,
* 1=send and request read receipts (default)
* 1=send and request read receipts
* default=send and request read receipts, only send but not reuqest if `bot` is set
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
* 1=send a copy of outgoing messages to self.
* Sending messages to self is needed for a proper multi-account setup,
@@ -2504,6 +2505,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_BACKUP 251
#define DC_QR_BACKUP2 252
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
@@ -2550,6 +2552,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_BACKUP:
* - DC_QR_BACKUP2:
* ask the user if they want to set up a new device.
* If so, pass the qr-code to dc_receive_backup().
*
@@ -6648,6 +6651,8 @@ void dc_event_unref(dc_event_t* event);
/// "Message opened"
///
/// Used in subjects of outgoing read receipts.
///
/// @deprecated Deprecated 2024-07-26
#define DC_STR_READRCPT 31
/// "The message '%1$s' you sent was displayed on the screen of the recipient."
@@ -6655,7 +6660,7 @@ void dc_event_unref(dc_event_t* event);
/// Used as message text of outgoing read receipts.
/// - %1$s will be replaced by the subject of the displayed message
///
/// @deprecated Deprecated 2024-06-23, use DC_STR_READRCPT_MAILBODY2 instead.
/// @deprecated Deprecated 2024-06-23
#define DC_STR_READRCPT_MAILBODY 32
/// @deprecated Deprecated, this string is no longer needed.
@@ -7374,11 +7379,6 @@ void dc_event_unref(dc_event_t* event);
/// Used as info message.
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
/// "The message is a receipt notification."
///
/// Used as message text of outgoing read receipts.
#define DC_STR_READRCPT_MAILBODY2 192
/// "Contact". Deprecated, currently unused.
#define DC_STR_CONTACT 200

View File

@@ -4364,7 +4364,7 @@ pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provid
let ctx = &*ffi_provider.context;
let provider = &mut ffi_provider.provider;
block_on(provider)
.context("Failed to await BackupProvider")
.context("Failed to await backup provider")
.log_err(ctx)
.set_last_error(ctx)
.ok();
@@ -4418,7 +4418,7 @@ trait ResultExt<T, E> {
/// Like `log_err()`, but:
/// - returns the default value instead of an Err value.
/// - emits an error instead of a warning for an [Err] result. This means
/// that the error will be shown to the user in a small pop-up.
/// that the error will be shown to the user in a small pop-up.
fn unwrap_or_log_default(self, context: &context::Context, message: &str) -> T;
}

View File

@@ -50,6 +50,7 @@ impl Lot {
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::Backup { .. } => None,
Qr::Backup2 { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
@@ -102,6 +103,7 @@ impl Lot {
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup { .. } => LotState::QrBackup,
Qr::Backup2 { .. } => LotState::QrBackup2,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
@@ -127,6 +129,7 @@ impl Lot {
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup { .. } => Default::default(),
Qr::Backup2 { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
@@ -177,6 +180,8 @@ pub enum LotState {
QrBackup = 251,
QrBackup2 = 252,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.141.2"
version = "1.142.12"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -14,8 +14,8 @@ required-features = ["webserver"]
[dependencies]
anyhow = { workspace = true }
deltachat = { path = ".." }
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
deltachat = { workspace = true }
deltachat-contact-tools = { workspace = true }
num-traits = { workspace = true }
schemars = "0.8.21"
serde = { workspace = true, features = ["derive"] }
@@ -33,7 +33,7 @@ base64 = { workspace = true }
# optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] }
env_logger = { version = "0.11.3", optional = true }
env_logger = { version = "0.11.5", optional = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }

View File

@@ -1672,10 +1672,10 @@ impl CommandApi {
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
/// but will fail after 60 seconds to avoid deadlocks.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = tokio::time::timeout(
Duration::from_secs(10),
Duration::from_secs(60),
self.inner_get_backup_qr(account_id),
)
.await
@@ -1691,13 +1691,13 @@ impl CommandApi {
///
/// This call will block until the QR code is ready,
/// even if there is no concurrent call to [`CommandApi::provide_backup`],
/// but will fail after 10 seconds to avoid deadlocks.
/// but will fail after 60 seconds to avoid deadlocks.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = tokio::time::timeout(
Duration::from_secs(10),
Duration::from_secs(60),
self.inner_get_backup_qr(account_id),
)
.await

View File

@@ -32,6 +32,7 @@ pub struct FullChat {
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
// subtitle - will be moved to frontend because it uses translation functions
chat_type: u32,
is_unpromoted: bool,
@@ -104,6 +105,7 @@ impl FullChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),
@@ -153,6 +155,7 @@ pub struct BasicChat {
is_protected: bool,
profile_image: Option<String>, //BLOBS ?
archived: bool,
pinned: bool,
chat_type: u32,
is_unpromoted: bool,
is_self_talk: bool,
@@ -180,6 +183,7 @@ impl BasicChat {
is_protected: chat.is_protected(),
profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
is_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(),

View File

@@ -19,6 +19,7 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS
name_and_addr: String,
is_blocked: bool,
e2ee_avail: bool,
/// True if the contact can be added to verified groups.
///
@@ -79,6 +80,7 @@ impl ContactObject {
profile_image, //BLOBS
name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(),
e2ee_avail: contact.e2ee_avail(context).await?,
is_verified,
is_profile_verified,
verifier_id,

View File

@@ -577,7 +577,9 @@ pub struct MessageData {
pub file: Option<String>,
pub location: Option<(f64, f64)>,
pub override_sender_name: Option<String>,
/// Quoted message id. Takes preference over `quoted_text` (see below).
pub quoted_message_id: Option<u32>,
pub quoted_text: Option<String>,
}
impl MessageData {
@@ -613,6 +615,9 @@ impl MessageData {
),
)
.await?;
} else if let Some(text) = self.quoted_text {
let protect = false;
message.set_quote_text(Some((text, protect)));
}
Ok(message)
}

View File

@@ -35,6 +35,11 @@ pub enum QrObject {
Backup {
ticket: String,
},
Backup2 {
auth_token: String,
node_addr: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
@@ -132,6 +137,14 @@ impl From<Qr> for QrObject {
Qr::Backup { ticket } => QrObject::Backup {
ticket: ticket.to_string(),
},
Qr::Backup2 {
ref node_addr,
auth_token,
} => QrObject::Backup2 {
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
auth_token,
},
Qr::WebrtcInstance {
domain,
instance_pattern,

View File

@@ -58,5 +58,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.141.2"
"version": "1.142.12"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.141.2"
version = "1.142.12"
license = "MPL-2.0"
edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
@@ -8,7 +8,7 @@ repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies]
ansi_term = { workspace = true }
anyhow = { workspace = true }
deltachat = { path = "..", features = ["internals"]}
deltachat = { workspace = true, features = ["internals"]}
dirs = "5"
log = { workspace = true }
rusqlite = { workspace = true }

View File

@@ -339,7 +339,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
receive-backup <qr>\n\
export-keys\n\
import-keys\n\
export-setup\n\
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
reset <flags>\n\
stop\n\
@@ -504,17 +503,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"import-keys" => {
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
}
"export-setup" => {
let setup_code = create_setup_code(&context);
let file_name = blobdir.join("autocrypt-setup-message.html");
let file_content = render_setup_file(&context, &setup_code).await?;
fs::write(&file_name, file_content).await?;
println!(
"Setup message written to: {}\nSetup code: {}",
file_name.display(),
&setup_code,
);
}
"poke" => {
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
}

View File

@@ -152,7 +152,7 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 14] = [
const IMEX_COMMANDS: [&str; 13] = [
"initiate-key-transfer",
"get-setupcodebegin",
"continue-key-transfer",
@@ -163,7 +163,6 @@ const IMEX_COMMANDS: [&str; 14] = [
"receive-backup",
"export-keys",
"import-keys",
"export-setup",
"poke",
"reset",
"stop",

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
version = "1.141.2"
version = "1.142.12"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",

View File

@@ -114,13 +114,13 @@ class ACFactory:
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
@pytest.fixture()
@pytest.fixture
def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
yield rpc_server
@pytest.fixture()
@pytest.fixture
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))

View File

@@ -210,6 +210,7 @@ def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
alice_second_device.clear_all_events()
alice_chat_bob.pin()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().pinned
alice_second_device.clear_all_events()
alice_chat_bob.mute()

View File

@@ -12,10 +12,11 @@ import threading
import time
import pytest
from deltachat_rpc_client import EventType
@pytest.fixture()
@pytest.fixture
def path_to_webxdc(request):
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
assert p.exists()

View File

@@ -1,6 +1,7 @@
import logging
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
@@ -30,17 +31,46 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
bob2.export_self_keys(tmp_path)
logging.info("Bob imports a key")
bob.import_self_keys(tmp_path / "private-key-default.asc")
bob.import_self_keys(tmp_path)
assert bob.get_config("key_id") == "2"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert not bob_contact_alice_snapshot.is_verified
def test_qr_setup_contact_svg(acfactory) -> None:
alice = acfactory.new_configured_account()
_, _, domain = alice.get_config("addr").rpartition("@")
_qr_code, svg = alice.get_qr_code_svg()
# Test that email address is in SVG
# when we have no display name.
# Check only the domain name, because
# long address may be split over multiple lines
# and not matched.
assert domain in svg
alice.set_config("displayname", "Alice")
# Test that display name is used
# in SVG and no address is visible.
_qr_code, svg = alice.get_qr_code_svg()
assert domain not in svg
assert "Alice" in svg
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect):
def test_qr_securejoin(acfactory, protect, tmp_path):
alice, bob = acfactory.get_online_accounts(2)
# Setup second device for Alice
# to test observing securejoin protocol.
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
@@ -74,6 +104,14 @@ def test_qr_securejoin(acfactory, protect):
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
# Start second Alice device.
# Alice observes securejoin protocol and verifies Bob on second device.
alice2.start_io()
alice2.wait_for_securejoin_inviter_success()
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
assert alice2_contact_bob_snapshot.is_verified
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""

View File

@@ -1,12 +1,15 @@
import base64
import concurrent.futures
import json
import logging
import os
import socket
import subprocess
import time
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import Contact, EventType, Message, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
@@ -68,6 +71,18 @@ def test_configure_starttls(acfactory) -> None:
assert account.is_configured()
def test_configure_ip(acfactory) -> None:
account = acfactory.new_preconfigured_account()
domain = account.get_config("addr").rsplit("@")[-1]
ip_address = socket.gethostbyname(domain)
# This should fail TLS check.
account.set_config("mail_server", ip_address)
with pytest.raises(JsonRpcError):
account.configure()
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@@ -103,12 +118,12 @@ def test_account(acfactory) -> None:
assert alice.get_chatlist(snapshot=True)
assert alice.get_qr_code()
assert alice.get_fresh_messages()
assert alice.get_next_messages()
# Test sending empty message.
assert len(bob.wait_next_messages()) == 0
alice_chat_bob.send_text("")
messages = bob.wait_next_messages()
assert bob.get_next_messages() == messages
assert len(messages) == 1
message = messages[0]
snapshot = message.get_snapshot()
@@ -613,3 +628,31 @@ def test_markseen_contact_request(acfactory, tmp_path):
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN
def test_get_http_response(acfactory):
alice = acfactory.new_configured_account()
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
assert http_response["mimetype"] == "text/html"
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
def test_configured_imap_certificate_checks(acfactory):
alice = acfactory.new_configured_account()
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
# Certificate checks should be configured (not None)
assert configured_certificate_checks
# 0 is the value old Delta Chat core versions used
# to mean user entered "imap_certificate_checks=0" (Automatic)
# and configuration failed to use strict TLS checks
# so it switched strict TLS checks off.
#
# New versions of Delta Chat are not disabling TLS checks
# unless users explicitly disables them
# or provider database says provider has invalid certificates.
#
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
# This test is a regression test to prevent this happening again.
assert configured_certificate_checks != "0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.141.2"
version = "1.142.12"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -10,8 +10,8 @@ keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"]
[dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
deltachat-jsonrpc = { workspace = true }
deltachat = { workspace = true }
anyhow = { workspace = true }
futures-lite = { workspace = true }

View File

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

View File

@@ -54,9 +54,9 @@ skip = [
{ name = "fastrand", version = "1.9.0" },
{ name = "futures-lite", version = "1.13.0" },
{ name = "getrandom", version = "<0.2" },
{ name = "h2", version = "0.3.26" },
{ name = "http-body", version = "0.4.6" },
{ name = "http", version = "0.2.12" },
{ name = "hyper-rustls", version = "0.24.2" },
{ name = "hyper", version = "0.14.28" },
{ name = "idna", version = "0.4.0" },
{ name = "netlink-packet-core", version = "0.5.0" },
@@ -74,7 +74,6 @@ skip = [
{ name = "redox_syscall", version = "0.3.5" },
{ name = "regex-automata", version = "0.1.10" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "reqwest", version = "0.11.27" },
{ name = "ring", version = "0.16.20" },
{ name = "rustls-pemfile", version = "1.0.4" },
{ name = "rustls", version = "0.21.11" },

2675
fuzz/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -128,6 +128,7 @@ module.exports = {
DC_QR_ASK_VERIFYCONTACT: 200,
DC_QR_ASK_VERIFYGROUP: 202,
DC_QR_BACKUP: 251,
DC_QR_BACKUP2: 252,
DC_QR_ERROR: 400,
DC_QR_FPR_MISMATCH: 220,
DC_QR_FPR_OK: 210,
@@ -266,7 +267,6 @@ module.exports = {
DC_STR_REACTED_BY: 177,
DC_STR_READRCPT: 31,
DC_STR_READRCPT_MAILBODY: 32,
DC_STR_READRCPT_MAILBODY2: 192,
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
DC_STR_REPLY_NOUN: 90,

View File

@@ -128,6 +128,7 @@ export enum C {
DC_QR_ASK_VERIFYCONTACT = 200,
DC_QR_ASK_VERIFYGROUP = 202,
DC_QR_BACKUP = 251,
DC_QR_BACKUP2 = 252,
DC_QR_ERROR = 400,
DC_QR_FPR_MISMATCH = 220,
DC_QR_FPR_OK = 210,
@@ -266,7 +267,6 @@ export enum C {
DC_STR_REACTED_BY = 177,
DC_STR_READRCPT = 31,
DC_STR_READRCPT_MAILBODY = 32,
DC_STR_READRCPT_MAILBODY2 = 192,
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
DC_STR_REPLY_NOUN = 90,

View File

@@ -11,7 +11,7 @@
"chai": "~4.3.10",
"chai-as-promised": "^7.1.1",
"mocha": "^8.2.1",
"node-gyp": "^10.0.0",
"node-gyp": "~10.1.0",
"prebuildify": "^5.0.1",
"prebuildify-ci": "^1.0.5",
"prettier": "^3.0.3",
@@ -55,5 +55,5 @@
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
},
"types": "node/dist/index.d.ts",
"version": "1.141.2"
"version": "1.142.12"
}

View File

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

View File

@@ -484,6 +484,16 @@ def test_move_works_on_self_sent(acfactory):
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_move_sync_msgs(acfactory):
ac1 = acfactory.new_online_configuring_account(bcc_self=True, sync_msgs=True, fix_is_chatmail=True)
acfactory.bring_accounts_online()
ac1.set_config("displayname", "Alice")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac1.set_config("displayname", "Bob")
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
def test_forward_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
@@ -1562,8 +1572,6 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
# check progress events for import
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
assert imex_tracker.wait_progress(500, progress_upper_limit=749)
assert imex_tracker.wait_progress(750, progress_upper_limit=999)
assert imex_tracker.wait_progress(1000)
assert_account_is_proper(ac1)

View File

@@ -1 +1 @@
2024-07-09
2024-09-02

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.79.0
RUST_VERSION=1.80.1
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -3,4 +3,4 @@ set -euo pipefail
tox -c deltachat-rpc-client -e py --devenv venv
venv/bin/pip install --upgrade pip
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cargo install --path deltachat-rpc-server/ --root "$PWD/venv" --debug
cargo install --locked --path deltachat-rpc-server/ --root "$PWD/venv" --debug
PATH="$PWD/venv/bin:$PATH" tox -c deltachat-rpc-client

View File

@@ -6,7 +6,7 @@ set -euo pipefail
export TZ=UTC
# Provider database revision.
REV=828e5ddc7e6609b582fbd7f063cc3f60b580ce96
REV=05c1b2029da74718e4bdc3799a46e29c4f794dc7
CORE_ROOT="$PWD"
TMP="$(mktemp -d)"

View File

@@ -166,6 +166,19 @@ impl Accounts {
.remove(&id)
.with_context(|| format!("no account with id {id}"))?;
ctx.stop_io().await;
// Explicitly close the database
// to make sure the database file is closed
// and can be removed on Windows.
// If some spawned task tries to use the database afterwards,
// it will fail.
//
// Previously `stop_io()` aborted the tasks without awaiting them
// and this resulted in keeping `Context` clones inside
// `Future`s that were not dropped. This bug is fixed now,
// but explicitly closing the database ensures that file is freed
// even if not all `Context` references are dropped.
ctx.sql.close().await;
drop(ctx);
if let Some(cfg) = self.config.get_account(id) {

View File

@@ -12,7 +12,7 @@ use anyhow::{format_err, Context as _, Result};
use base64::Engine as _;
use futures::StreamExt;
use image::codecs::jpeg::JpegEncoder;
use image::io::Reader as ImageReader;
use image::ImageReader;
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;

View File

@@ -49,6 +49,7 @@ use crate::tools::{
create_smeared_timestamps, get_abs_path, gm2local_offset, smeared_time, time, IsNoneOrEmpty,
SystemTime,
};
use crate::webxdc::StatusUpdateSerial;
/// An chat item, such as a message or a marker.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -1934,7 +1935,7 @@ impl Chat {
self.param.remove(Param::Unpromoted);
self.update_param(context).await?;
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
// when the group-creation message is actually sent though SMTP -
// when the group creation message is actually sent through SMTP --
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
context
.sync_qr_code_tokens(Some(self.id))
@@ -2239,7 +2240,7 @@ pub(crate) async fn sync(context: &Context, id: SyncId, action: SyncAction) -> R
context
.add_sync_item(SyncData::AlterChat { id, action })
.await?;
context.send_sync_msg().await?;
context.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -3267,35 +3268,25 @@ pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()>
context.emit_event(EventType::MsgsNoticed(chat_id_in_archive));
chatlist_events::emit_chatlist_item_changed(context, chat_id_in_archive);
}
chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK);
} else {
let exists = context
.sql
.exists(
"SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;",
(MessageState::InFresh, chat_id),
)
.await?;
if !exists {
return Ok(());
}
context
.sql
.execute(
"UPDATE msgs
SET state=?
WHERE state=?
AND hidden=0
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?;
} else if context
.sql
.execute(
"UPDATE msgs
SET state=?
WHERE state=?
AND hidden=0
AND chat_id=?;",
(MessageState::InNoticed, MessageState::InFresh, chat_id),
)
.await?
== 0
{
return Ok(());
}
context.emit_event(EventType::MsgsNoticed(chat_id));
chatlist_events::emit_chatlist_item_changed(context, chat_id);
context.on_archived_chats_maybe_noticed();
Ok(())
}
@@ -3358,6 +3349,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
context,
"Marking chats as noticed because there are newer outgoing messages: {changed_chats:?}."
);
context.on_archived_chats_maybe_noticed();
}
for c in changed_chats {
@@ -3737,12 +3729,14 @@ pub(crate) async fn add_contact_to_chat_ex(
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
chat.param.remove(Param::Unpromoted);
chat.update_param(context).await?;
let _ = context
if context
.sync_qr_code_tokens(Some(chat_id))
.await
.log_err(context)
.is_ok()
&& context.send_sync_msg().await.log_err(context).is_ok();
{
context.scheduler.interrupt_smtp().await;
}
}
if context.is_self_addr(contact.get_addr()).await? {
@@ -4272,9 +4266,39 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
msg.timestamp_sort = create_smeared_timestamp(context);
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
context.scheduler.interrupt_smtp().await;
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
continue;
}
if msg.viewtype == Viewtype::Webxdc {
let conn_fn = |conn: &mut rusqlite::Connection| {
let range = conn.query_row(
"SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \
FROM msgs_status_updates WHERE msg_id=?",
(msg.id,),
|row| {
let min_id: StatusUpdateSerial = row.get(0)?;
let max_id: StatusUpdateSerial = row.get(1)?;
Ok((min_id, max_id))
},
)?;
if range.0 > range.1 {
return Ok(());
};
// `first_serial` must be decreased, otherwise if `Context::flush_status_updates()`
// runs in parallel, it would miss the race and instead of resending just remove the
// updates thinking that they have been already sent.
conn.execute(
"INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) \
VALUES(?, ?, ?, '') \
ON CONFLICT(msg_id) \
DO UPDATE SET first_serial=min(first_serial - 1, excluded.first_serial)",
(msg.id, range.0, range.1),
)?;
Ok(())
};
context.sql.call_write(conn_fn).await?;
}
context.scheduler.interrupt_smtp().await;
}
Ok(())
}
@@ -4664,6 +4688,14 @@ impl Context {
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
}
}
/// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
/// archived chats could decrease. In general we don't want to make an extra db query to know if
/// a noticied chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
/// is ok.
pub(crate) fn on_archived_chats_maybe_noticed(&self) {
self.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
}
}
#[cfg(test)]
@@ -5835,7 +5867,27 @@ mod tests {
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2);
// mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well
t.evtracker.clear_events();
marknoticed_chat(&t, claire_chat_id).await?;
let ev = t
.evtracker
.get_matching(|ev| {
matches!(
ev,
EventType::MsgsChanged {
chat_id: DC_CHAT_ID_ARCHIVED_LINK,
..
}
)
})
.await;
assert_eq!(
ev,
EventType::MsgsChanged {
chat_id: DC_CHAT_ID_ARCHIVED_LINK,
msg_id: MsgId::new(0),
}
);
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2);
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0);
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1);

View File

@@ -82,11 +82,13 @@ impl Chatlist {
/// not needed when DC_GCL_ARCHIVED_ONLY is already set)
/// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
/// is added as needed.
///
/// `query`: An optional query for filtering the list. Only chats matching this query
/// are returned. When `is:unread` is contained in the query, the chatlist is
/// filtered such that only chats with unread messages show up.
/// are returned. When `is:unread` is contained in the query, the chatlist is
/// filtered such that only chats with unread messages show up.
///
/// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
/// are returned.
/// are returned.
pub async fn try_load(
context: &Context,
listflags: usize,

View File

@@ -13,7 +13,7 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use tokio::fs;
use crate::blob::BlobObject;
use crate::constants::{self, DC_VERSION_STR};
use crate::constants;
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
@@ -59,7 +59,10 @@ pub enum Config {
/// IMAP server security (e.g. TLS, STARTTLS).
MailSecurity,
/// How to check IMAP server TLS certificates.
/// How to check TLS certificates.
///
/// "IMAP" in the name is for compatibility,
/// this actually applies to both IMAP and SMTP connections.
ImapCertificateChecks,
/// SMTP server hostname.
@@ -77,7 +80,9 @@ pub enum Config {
/// SMTP server security (e.g. TLS, STARTTLS).
SendSecurity,
/// How to check SMTP server TLS certificates.
/// Deprecated option for backwards compatibilty.
///
/// Certificate checks for SMTP are actually controlled by `imap_certificate_checks` config.
SmtpCertificateChecks,
/// Whether to use OAuth 2.
@@ -131,7 +136,8 @@ pub enum Config {
#[strum(props(default = "0"))]
SentboxWatch,
/// True if chat messages should be moved to a separate folder.
/// True if chat messages should be moved to a separate folder. Auto-sent messages like sync
/// ones are moved there anyway.
#[strum(props(default = "1"))]
MvboxMove,
@@ -209,7 +215,12 @@ pub enum Config {
/// Configured IMAP server security (e.g. TLS, STARTTLS).
ConfiguredMailSecurity,
/// How to check IMAP server TLS certificates.
/// Configured TLS certificate checks.
/// This option is saved on successful configuration
/// and should not be modified manually.
///
/// This actually applies to both IMAP and SMTP connections,
/// but has "IMAP" in the name for backwards compatibility.
ConfiguredImapCertificateChecks,
/// Configured SMTP server hostname.
@@ -224,7 +235,9 @@ pub enum Config {
/// Configured SMTP server port.
ConfiguredSendPort,
/// How to check SMTP server TLS certificates.
/// Deprecated, stored for backwards compatibility.
///
/// ConfiguredImapCertificateChecks is actually used.
ConfiguredSmtpCertificateChecks,
/// Whether OAuth 2 is used with configured provider.
@@ -257,6 +270,9 @@ pub enum Config {
/// True if account is a chatmail account.
IsChatmail,
/// True if `IsChatmail` mustn't be autoconfigured. For tests.
FixIsChatmail,
/// True if account is muted.
IsMuted,
@@ -382,9 +398,6 @@ impl Config {
/// multiple users are sharing an account. Another example is `Self::SyncMsgs` itself which
/// mustn't be controlled by other devices.
pub(crate) fn is_synced(&self) -> bool {
// NB: We don't restart IO from the synchronisation code, so `MvboxMove` isn't effective
// immediately if `ConfiguredMvboxFolder` is unset, but only after a reconnect (see
// `Imap::prepare()`).
matches!(
self,
Self::Displayname
@@ -398,10 +411,7 @@ 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 | Config::SentboxWatch
)
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
}
}
@@ -427,7 +437,7 @@ impl Context {
.into_owned()
})
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
Config::SysConfigKeys => Some(get_config_keys_string()),
_ => self.sql.get_raw_config(key.as_ref()).await?,
@@ -485,7 +495,8 @@ impl Context {
/// Returns true if movebox ("DeltaChat" folder) should be watched.
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
Ok(self.get_config_bool(Config::MvboxMove).await?
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
|| self.get_config_bool(Config::OnlyFetchMvbox).await?
|| !self.get_config_bool(Config::IsChatmail).await?)
}
/// Returns true if sentbox ("Sent" folder) should be watched.
@@ -504,6 +515,19 @@ impl Context {
&& !self.get_config_bool(Config::Bot).await?)
}
/// Returns whether MDNs should be requested.
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
match self.config_exists(Config::MdnsEnabled).await? {
true => self.get_config_bool(Config::MdnsEnabled).await,
false => Ok(!self.get_config_bool(Config::Bot).await?),
}
}
/// Returns whether MDNs should be sent.
pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
self.get_config_bool(Config::MdnsEnabled).await
}
/// Gets configured "delete_server_after" value.
///
/// `None` means never delete the message, `Some(0)` means delete
@@ -688,7 +712,7 @@ impl Context {
{
return Ok(());
}
Box::pin(self.send_sync_msg()).await.log_err(self).ok();
self.scheduler.interrupt_smtp().await;
Ok(())
}
@@ -953,6 +977,21 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mdns_default_behaviour() -> Result<()> {
let t = &TestContext::new_alice().await;
assert!(t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
// The setting should be displayed correctly.
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
t.set_config_bool(Config::Bot, true).await?;
assert!(!t.should_request_mdns().await?);
assert!(t.should_send_mdns().await?);
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sync() -> Result<()> {
let alice0 = TestContext::new_alice().await;
@@ -979,7 +1018,6 @@ mod tests {
// Reset to default. Test that it's not synced because defaults may differ across client
// versions.
alice0.set_config(Config::MdnsEnabled, None).await?;
assert_eq!(alice0.get_config_bool(Config::MdnsEnabled).await?, true);
alice0.set_config_bool(Config::MdnsEnabled, false).await?;
sync(&alice0, &alice1).await;
assert_eq!(alice1.get_config_bool(Config::MdnsEnabled).await?, false);
@@ -1054,7 +1092,8 @@ mod tests {
let status = "Synced via usual message";
alice0.set_config(Config::Selfstatus, Some(status)).await?;
alice0.pop_sent_msg().await; // Sync message
alice0.send_sync_msg().await?;
alice0.pop_sent_msg().await;
let status1 = "Synced via sync message";
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
tcm.send_recv(alice0, alice1, "hi Alice!").await;
@@ -1077,7 +1116,8 @@ mod tests {
alice0
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
.await?;
alice0.pop_sent_msg().await; // Sync message
alice0.send_sync_msg().await?;
alice0.pop_sent_msg().await;
let file = alice1.dir.path().join("avatar.jpg");
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
tokio::fs::write(&file, bytes).await?;

View File

@@ -113,10 +113,6 @@ impl Context {
let mut param = LoginParam::load_candidate_params(self).await?;
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
// Reset our knowledge about whether the server is a chatmail server.
// We will update it when we connect to IMAP.
self.set_config_internal(Config::IsChatmail, None).await?;
let success = configure(self, &mut param).await;
self.set_config_internal(Config::NotifyAboutWrongPw, None)
.await?;
@@ -193,10 +189,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
// Step 1: Load the parameters and check email-address and password
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
// socks5 requests, this can work with socks5 too. OAuth is always set either for both
// IMAP and SMTP or not at all.
if param.imap.oauth2 && !socks5_enabled {
// OAuth is always set either for both IMAP and SMTP or not at all.
if param.imap.oauth2 {
// the used oauth2 addr may differ, check this.
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
progress!(ctx, 10);
@@ -216,7 +210,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
let parsed = EmailAddress::new(&param.addr).context("Bad email-address")?;
let param_domain = parsed.domain;
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
// Step 2: Autoconfig
progress!(ctx, 200);
@@ -267,7 +260,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
}
},
strict_tls: Some(provider.opt.strict_tls),
})
.collect();
@@ -282,19 +274,28 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
} else {
// Try receiving autoconfig
info!(ctx, "no offline autoconfig found");
param_autoconfig = if socks5_enabled {
// Currently we can't do http requests through socks5, to not leak
// the ip, just don't do online autoconfig
info!(ctx, "socks5 enabled, skipping autoconfig");
None
} else {
get_autoconfig(ctx, param, &param_domain, &param_addr_urlencoded).await
}
param_autoconfig = get_autoconfig(ctx, param, &param_domain).await;
}
} else {
param_autoconfig = None;
}
let user_strict_tls = match param.certificate_checks {
CertificateChecks::Automatic => None,
CertificateChecks::Strict => Some(true),
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
};
let provider_strict_tls = param.provider.map(|provider| provider.opt.strict_tls);
let strict_tls = user_strict_tls.or(provider_strict_tls).unwrap_or(true);
// Do not save `CertificateChecks::Automatic` into `configured_imap_certificate_checks`.
param.certificate_checks = if strict_tls {
CertificateChecks::Strict
} else {
CertificateChecks::AcceptInvalidCertificates
};
progress!(ctx, 500);
let mut servers = param_autoconfig.unwrap_or_default();
@@ -308,7 +309,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
port: param.imap.port,
socket: param.imap.security,
username: param.imap.user.clone(),
strict_tls: None,
})
}
if !servers
@@ -321,24 +321,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
port: param.smtp.port,
socket: param.smtp.security,
username: param.smtp.user.clone(),
strict_tls: None,
})
}
// respect certificate setting from function parameters
for server in &mut servers {
let certificate_checks = match server.protocol {
Protocol::Imap => param.imap.certificate_checks,
Protocol::Smtp => param.smtp.certificate_checks,
};
server.strict_tls = match certificate_checks {
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
CertificateChecks::Strict => Some(true),
CertificateChecks::Automatic => server.strict_tls,
};
}
let servers = expand_param_vector(servers, &param.addr, &param_domain);
progress!(ctx, 550);
@@ -354,9 +339,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
.filter(|params| params.protocol == Protocol::Smtp)
.cloned()
.collect();
let provider_strict_tls = param
.provider
.map_or(socks5_config.is_some(), |provider| provider.opt.strict_tls);
let smtp_config_task = task::spawn(async move {
let mut smtp_configured = false;
@@ -366,18 +348,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
smtp_param.server.clone_from(&smtp_server.hostname);
smtp_param.port = smtp_server.port;
smtp_param.security = smtp_server.socket;
smtp_param.certificate_checks = match smtp_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_smtp_one_param(
&context_smtp,
&smtp_param,
&socks5_config,
&smtp_addr,
provider_strict_tls,
strict_tls,
&mut smtp,
)
.await
@@ -413,18 +390,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
param.imap.server.clone_from(&imap_server.hostname);
param.imap.port = imap_server.port;
param.imap.security = imap_server.socket;
param.imap.certificate_checks = match imap_server.strict_tls {
Some(true) => CertificateChecks::Strict,
Some(false) => CertificateChecks::AcceptInvalidCertificates,
None => CertificateChecks::Automatic,
};
match try_imap_one_param(
ctx,
&param.imap,
&param.socks5_config,
&param.addr,
provider_strict_tls,
strict_tls,
)
.await
{
@@ -458,8 +430,22 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 900);
if imap_session.is_chatmail() {
ctx.set_config(Config::IsChatmail, Some("1")).await?;
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::SentboxWatch, None).await?;
ctx.set_config(Config::MvboxMove, Some("0")).await?;
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
@@ -467,8 +453,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
}
let create_mvbox = ctx.should_watch_mvbox().await?;
let create_mvbox = !is_chatmail;
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
.await?;
@@ -519,14 +504,15 @@ async fn get_autoconfig(
ctx: &Context,
param: &LoginParam,
param_domain: &str,
param_addr_urlencoded: &str,
) -> Option<Vec<ServerParams>> {
let param_addr_urlencoded = utf8_percent_encode(&param.addr, NON_ALPHANUMERIC).to_string();
if let Ok(res) = moz_autoconfigure(
ctx,
&format!(
"https://autoconfig.{param_domain}/mail/config-v1.1.xml?emailaddress={param_addr_urlencoded}"
),
param,
&param.addr,
)
.await
{
@@ -541,7 +527,7 @@ async fn get_autoconfig(
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
&param_domain, &param_addr_urlencoded
),
param,
&param.addr,
)
.await
{
@@ -577,7 +563,7 @@ async fn get_autoconfig(
if let Ok(res) = moz_autoconfigure(
ctx,
&format!("https://autoconfig.thunderbird.net/v1.1/{}", &param_domain),
param,
&param.addr,
)
.await
{
@@ -592,15 +578,15 @@ async fn try_imap_one_param(
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
strict_tls: bool,
) -> Result<(Imap, ImapSession), ConfigurationError> {
let inf = format!(
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
"imap: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
strict_tls,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
@@ -612,7 +598,7 @@ async fn try_imap_one_param(
let (_s, r) = async_channel::bounded(1);
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
let mut imap = match Imap::new(param, socks5_config.clone(), addr, strict_tls, r) {
Err(err) => {
info!(context, "failure: {:#}", err);
return Err(ConfigurationError {
@@ -643,16 +629,16 @@ async fn try_smtp_one_param(
param: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
strict_tls: bool,
smtp: &mut Smtp,
) -> Result<(), ConfigurationError> {
let inf = format!(
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
"smtp: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
param.user,
param.server,
param.port,
param.security,
param.certificate_checks,
strict_tls,
param.oauth2,
if let Some(socks5_config) = socks5_config {
socks5_config.to_string()
@@ -663,7 +649,7 @@ async fn try_smtp_one_param(
info!(context, "Trying: {}", inf);
if let Err(err) = smtp
.connect(context, param, socks5_config, addr, provider_strict_tls)
.connect(context, param, socks5_config, addr, strict_tls)
.await
{
info!(context, "SMTP failure: {err:#}.");

View File

@@ -9,7 +9,6 @@ use quick_xml::events::{BytesStart, Event};
use super::{Error, ServerParams};
use crate::context::Context;
use crate::login_param::LoginParam;
use crate::net::read_url;
use crate::provider::{Protocol, Socket};
@@ -248,7 +247,6 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
hostname: server.hostname,
port: server.port,
username: server.username,
strict_tls: None,
})
})
.collect();
@@ -258,11 +256,11 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
pub(crate) async fn moz_autoconfigure(
context: &Context,
url: &str,
param_in: &LoginParam,
addr: &str,
) -> Result<Vec<ServerParams>, Error> {
let xml_raw = read_url(context, url).await?;
let res = parse_serverparams(&param_in.addr, &xml_raw);
let res = parse_serverparams(addr, &xml_raw);
if let Err(err) = &res {
warn!(
context,

View File

@@ -187,7 +187,6 @@ fn protocols_to_serverparams(protocols: Vec<ProtocolTag>) -> Vec<ServerParams> {
hostname: protocol.server,
port: protocol.port,
username: String::new(),
strict_tls: None,
})
})
.collect()

View File

@@ -22,31 +22,18 @@ pub(crate) struct ServerParams {
/// Username, empty if unknown.
pub username: String,
/// Whether TLS certificates should be strictly checked or not, `None` for automatic.
pub strict_tls: Option<bool>,
}
impl ServerParams {
fn expand_usernames(self, addr: &str) -> Vec<ServerParams> {
let mut res = Vec::new();
if self.username.is_empty() {
res.push(Self {
vec![Self {
username: addr.to_string(),
..self.clone()
});
if let Some(at) = addr.find('@') {
res.push(Self {
username: addr.split_at(at).0.to_string(),
..self
});
}
}]
} else {
res.push(self)
vec![self]
}
res
}
fn expand_hostnames(self, param_domain: &str) -> Vec<ServerParams> {
@@ -135,14 +122,6 @@ impl ServerParams {
vec![self]
}
}
fn expand_strict_tls(self) -> Vec<ServerParams> {
vec![Self {
// Strict if not set by the user or provider database.
strict_tls: Some(self.strict_tls.unwrap_or(true)),
..self
}]
}
}
/// Expands vector of `ServerParams`, replacing placeholders with
@@ -155,9 +134,7 @@ pub(crate) fn expand_param_vector(
v.into_iter()
// The order of expansion is important.
//
// Ports are expanded the last, so they are changed the first. Username is only changed if
// default value (address with domain) didn't work for all available hosts and ports.
.flat_map(|params| params.expand_strict_tls().into_iter())
// Ports are expanded the last, so they are changed the first.
.flat_map(|params| params.expand_usernames(addr).into_iter())
.flat_map(|params| params.expand_hostnames(domain).into_iter())
.flat_map(|params| params.expand_ports().into_iter())
@@ -177,7 +154,6 @@ mod tests {
port: 0,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -191,7 +167,6 @@ mod tests {
port: 993,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -202,7 +177,6 @@ mod tests {
port: 123,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: None,
}],
"foobar@example.net",
"example.net",
@@ -217,7 +191,6 @@ mod tests {
port: 123,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
},
ServerParams {
protocol: Protocol::Smtp,
@@ -225,12 +198,10 @@ mod tests {
port: 123,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: Some(true)
},
],
);
// Test that strict_tls is not expanded for plaintext connections.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Smtp,
@@ -238,7 +209,6 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -251,7 +221,6 @@ mod tests {
port: 123,
socket: Socket::Plain,
username: "foobar".to_string(),
strict_tls: Some(true)
}],
);
@@ -263,7 +232,6 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -277,7 +245,6 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -285,7 +252,6 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Imap,
@@ -293,7 +259,6 @@ mod tests {
port: 10480,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
}
],
);
@@ -307,7 +272,6 @@ mod tests {
port: 0,
socket: Socket::Automatic,
username: "foobar".to_string(),
strict_tls: Some(true),
}],
"foobar@example.net",
"example.net",
@@ -321,7 +285,6 @@ mod tests {
port: 465,
socket: Socket::Ssl,
username: "foobar".to_string(),
strict_tls: Some(true)
},
ServerParams {
protocol: Protocol::Smtp,
@@ -329,7 +292,45 @@ mod tests {
port: 587,
socket: Socket::Starttls,
username: "foobar".to_string(),
strict_tls: Some(true)
},
],
);
// Test that email address is used as the default username.
// We do not try other usernames
// such as the local part of the address
// as this is very uncommon configuration
// and not worth doubling the number of candidates to try.
// If such configuration is used, email provider
// should provide XML autoconfig or
// be added to the provider database as an exception.
let v = expand_param_vector(
vec![ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 0,
socket: Socket::Automatic,
username: "".to_string(),
}],
"foobar@example.net",
"example.net",
);
assert_eq!(
v,
vec![
ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 993,
socket: Socket::Ssl,
username: "foobar@example.net".to_string(),
},
ServerParams {
protocol: Protocol::Imap,
hostname: "example.net".to_string(),
port: 143,
socket: Socket::Starttls,
username: "foobar@example.net".to_string(),
},
],
);

View File

@@ -209,7 +209,7 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
// Key for the folder configuration version (see below).
pub(crate) const DC_FOLDERS_CONFIGURED_KEY: &str = "folders_configured";
// this value can be increased if the folder configuration is changed and must be redone on next program start
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 5;
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting

View File

@@ -642,7 +642,7 @@ impl Contact {
set_blocked(context, Nosync, contact_id, false).await?;
}
if sync.into() {
if sync.into() && sth_modified != Modifier::None {
chat::sync(
context,
chat::SyncId::ContactAddr(addr.to_string()),
@@ -747,7 +747,7 @@ impl Contact {
/// - "name": name passed as function argument, belonging to the given origin
/// - "row_name": current name used in the database, typically set to "name"
/// - "row_authname": name as authorized from a contact, set only through a From-header
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
///
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
pub(crate) async fn add_or_lookup(
@@ -997,7 +997,7 @@ impl Contact {
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
/// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
/// `query` is a string to filter the list.
/// `query` is a string to filter the list.
pub async fn get_all(
context: &Context,
listflags: u32,
@@ -1402,6 +1402,17 @@ impl Contact {
self.status.as_str()
}
/// Returns whether end-to-end encryption to the contact is available.
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
if self.id == ContactId::SELF {
return Ok(true);
}
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
return Ok(false);
};
Ok(peerstate.peek_key(false).is_some())
}
/// Returns true if the contact
/// can be added to verified chats,
/// i.e. has a verified key
@@ -1913,8 +1924,13 @@ impl RecentlySeenLoop {
.unwrap();
}
pub(crate) fn abort(self) {
pub(crate) async fn abort(self) {
self.handle.abort();
// Await aborted task to ensure the `Future` is dropped
// with all resources moved inside such as the `Context`
// reference to `InnerContext`.
self.handle.await.ok();
}
}
@@ -2668,6 +2684,8 @@ mod tests {
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
assert_eq!(encrinfo, "No encryption");
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(!contact.e2ee_avail(&alice).await?);
let bob = TestContext::new_bob().await;
let chat_alice = bob
@@ -2691,6 +2709,8 @@ bob@example.net:
CCCB 5AA9 F6E1 141C 9431
65F1 DB18 B18C BCF7 0487"
);
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
assert!(contact.e2ee_avail(&alice).await?);
Ok(())
}

View File

@@ -814,6 +814,12 @@ impl Context {
}
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
res.insert(
"fix_is_chatmail",
self.get_config_bool(Config::FixIsChatmail)
.await?
.to_string(),
);
res.insert(
"is_muted",
self.get_config_bool(Config::IsMuted).await?.to_string(),

View File

@@ -11,6 +11,7 @@ pub enum HeaderDef {
Date,
From_,
To,
AutoSubmitted,
/// Carbon copy.
Cc,

View File

@@ -32,11 +32,10 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
use crate::context::Context;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
use crate::mimeparser;
use crate::oauth2::get_oauth2_access_token;
use crate::provider::Socket;
use crate::receive_imf::{
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
};
@@ -232,20 +231,13 @@ impl Imap {
lp: &ServerLoginParam,
socks5_config: Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
strict_tls: bool,
idle_interrupt_receiver: Receiver<()>,
) -> Result<Self> {
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
bail!("Incomplete IMAP connection parameters");
}
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let imap = Imap {
idle_interrupt_receiver,
addr: addr.to_string(),
@@ -273,17 +265,11 @@ impl Imap {
}
let param = LoginParam::load_configured_params(context).await?;
// the trailing underscore is correct
let imap = Self::new(
&param.imap,
param.socks5_config.clone(),
&param.addr,
param
.provider
.map_or(param.socks5_config.is_some(), |provider| {
provider.opt.strict_tls
}),
param.strict_tls(),
idle_interrupt_receiver,
)?;
Ok(imap)
@@ -342,52 +328,16 @@ impl Imap {
);
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
let connection_res: Result<Client> =
if self.lp.security == Socket::Starttls || self.lp.security == Socket::Plain {
let imap_server: &str = self.lp.server.as_ref();
let imap_port = self.lp.port;
let connection_res = Client::connect(
context,
self.lp.server.as_ref(),
self.lp.port,
self.strict_tls,
self.socks5_config.clone(),
self.lp.security,
)
.await;
if let Some(socks5_config) = &self.socks5_config {
if self.lp.security == Socket::Starttls {
Client::connect_starttls_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
self.strict_tls,
)
.await
} else {
Client::connect_insecure_socks5(
context,
imap_server,
imap_port,
socks5_config.clone(),
)
.await
}
} else if self.lp.security == Socket::Starttls {
Client::connect_starttls(context, imap_server, imap_port, self.strict_tls).await
} else {
Client::connect_insecure(context, imap_server, imap_port).await
}
} else {
let imap_server: &str = self.lp.server.as_ref();
let imap_port = self.lp.port;
if let Some(socks5_config) = &self.socks5_config {
Client::connect_secure_socks5(
context,
imap_server,
imap_port,
self.strict_tls,
socks5_config.clone(),
)
.await
} else {
Client::connect_secure(context, imap_server, imap_port, self.strict_tls).await
}
};
let client = connection_res?;
self.conn_backoff_ms = BACKOFF_MIN_MS;
self.ratelimit.send();
@@ -483,7 +433,11 @@ impl Imap {
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
.await?;
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
let create_mvbox = true;
let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
false => session.is_chatmail(),
true => context.get_config_bool(Config::IsChatmail).await?,
};
let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
self.configure_folders(context, &mut session, create_mvbox)
.await?;
}
@@ -1091,18 +1045,12 @@ impl Session {
.await?;
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
self.select_with_uidvalidity(context, &folder)
.await
.context("failed to select folder")?;
if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
if let Err(err) = self.select_with_uidvalidity(context, &folder).await {
warn!(context, "store_seen_flags_on_imap: Failed to select {folder}, will retry later: {err:#}.");
} else if let Err(err) = self.add_flag_finalized_with_set(&uid_set, "\\Seen").await {
warn!(
context,
"Cannot mark messages {} in folder {} as seen, will retry later: {}.",
uid_set,
folder,
err
);
"Cannot mark messages {uid_set} in {folder} as seen, will retry later: {err:#}.");
} else {
info!(
context,
@@ -1201,6 +1149,9 @@ impl Session {
set_modseq(context, folder, highest_modseq)
.await
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
if !updated_chat_ids.is_empty() {
context.on_archived_chats_maybe_noticed();
}
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
@@ -1523,7 +1474,7 @@ impl Session {
} else if !context.push_subscriber.heartbeat_subscribed().await {
let context = context.clone();
// Subscribe for heartbeat notifications.
tokio::spawn(async move { context.push_subscriber.subscribe().await });
tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
}
Ok(())
@@ -1553,8 +1504,8 @@ impl Session {
/// Attempts to configure mvbox.
///
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create
/// `folders[0]`. This method does not use LIST command to ensure that
/// Tries to find any folder examining `folders` in the order they go. If none is found, tries
/// to create any folder in the same order. This method does not use LIST command to ensure that
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
///
@@ -1588,16 +1539,17 @@ impl Session {
if !create_mvbox {
return Ok(None);
}
let Some(folder) = folders.first() else {
return Ok(None);
};
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
// the variants here.
for folder in folders {
match self.select_with_uidvalidity(context, folder).await {
Ok(_) => {
info!(context, "MVBOX-folder {} created.", folder);
return Ok(Some(folder));
}
Err(err) => {
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
}
}
}
Ok(None)
@@ -1845,6 +1797,20 @@ async fn needs_move_to_mvbox(
context: &Context,
headers: &[mailparse::MailHeader<'_>],
) -> Result<bool> {
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
if !context.get_config_bool(Config::IsChatmail).await?
&& has_chat_version
&& headers
.get_header_value(HeaderDef::AutoSubmitted)
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
.is_some()
{
if let Some(from) = mimeparser::get_from(headers) {
if context.is_self_addr(&from.addr).await? {
return Ok(true);
}
}
}
if !context.get_config_bool(Config::MvboxMove).await? {
return Ok(false);
}
@@ -1858,7 +1824,7 @@ async fn needs_move_to_mvbox(
return Ok(false);
}
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
if has_chat_version {
Ok(true)
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
match parent.is_dc_message {

View File

@@ -1,24 +1,23 @@
use std::{
ops::{Deref, DerefMut},
time::Duration,
};
use std::net::SocketAddr;
use std::ops::{Deref, DerefMut};
use anyhow::{Context as _, Result};
use anyhow::{bail, format_err, Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::Session as ImapSession;
use fast_socks5::client::Socks5Stream;
use tokio::io::BufWriter;
use super::capabilities::Capabilities;
use super::session::Session;
use crate::context::Context;
use crate::net::connect_tcp;
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::session::SessionStream;
use crate::net::tls::wrap_tls;
use crate::net::update_connection_history;
use crate::net::{connect_tcp_inner, connect_tls_inner};
use crate::provider::Socket;
use crate::socks::Socks5Config;
use fast_socks5::client::Socks5Stream;
/// IMAP connection, write and read timeout.
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(60);
use crate::tools::time;
#[derive(Debug)]
pub(crate) struct Client {
@@ -39,6 +38,16 @@ impl DerefMut for Client {
}
}
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static [&'static str] {
if port == 993 {
// Do not request ALPN on standard port.
&[]
} else {
&["imap"]
}
}
/// Determine server capabilities.
///
/// If server supports ID capability, send our client ID.
@@ -98,14 +107,67 @@ impl Client {
Ok(Session::new(session, capabilities))
}
pub async fn connect_secure(
pub async fn connect(
context: &Context,
hostname: &str,
host: &str,
port: u16,
strict_tls: bool,
socks5_config: Option<Socks5Config>,
security: Socket,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
if let Some(socks5_config) = socks5_config {
let client = match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => {
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
.await?
}
Socket::Starttls => {
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
.await?
}
Socket::Plain => {
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
}
};
Ok(client)
} else {
let mut first_error = None;
let load_cache =
strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
for resolved_addr in
lookup_host_with_cache(context, host, port, "imap", load_cache).await?
{
let res = match security {
Socket::Automatic => bail!("IMAP port security is not configured"),
Socket::Ssl => Client::connect_secure(resolved_addr, host, strict_tls).await,
Socket::Starttls => {
Client::connect_starttls(resolved_addr, host, strict_tls).await
}
Socket::Plain => Client::connect_insecure(resolved_addr).await,
};
match res {
Ok(client) => {
let ip_addr = resolved_addr.ip().to_string();
if load_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "imap", host, port, &ip_addr, time())
.await?;
return Ok(client);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}
}
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -116,8 +178,8 @@ impl Client {
Ok(client)
}
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
let buffered_stream = BufWriter::new(tcp_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -128,17 +190,12 @@ impl Client {
Ok(client)
}
pub async fn connect_starttls(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<Self> {
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
let tcp_stream = connect_tcp_inner(addr).await?;
// Run STARTTLS command and convert the client back into a stream.
let buffered_tcp_stream = BufWriter::new(tcp_stream);
let mut client = ImapClient::new(buffered_tcp_stream);
let mut client = async_imap::Client::new(buffered_tcp_stream);
let _greeting = client
.read_response()
.await
@@ -150,7 +207,7 @@ impl Client {
let buffered_tcp_stream = client.into_inner();
let tcp_stream = buffered_tcp_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
@@ -160,7 +217,7 @@ impl Client {
Ok(client)
}
pub async fn connect_secure_socks5(
async fn connect_secure_socks5(
context: &Context,
domain: &str,
port: u16,
@@ -168,9 +225,9 @@ impl Client {
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
.connect(context, domain, port, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?;
let buffered_stream = BufWriter::new(tls_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -181,15 +238,13 @@ impl Client {
Ok(client)
}
pub async fn connect_insecure_socks5(
async fn connect_insecure_socks5(
context: &Context,
domain: &str,
port: u16,
socks5_config: Socks5Config,
) -> Result<Self> {
let socks5_stream = socks5_config
.connect(context, domain, port, IMAP_TIMEOUT, false)
.await?;
let socks5_stream = socks5_config.connect(context, domain, port, false).await?;
let buffered_stream = BufWriter::new(socks5_stream);
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
let mut client = Client::new(session_stream);
@@ -200,7 +255,7 @@ impl Client {
Ok(client)
}
pub async fn connect_starttls_socks5(
async fn connect_starttls_socks5(
context: &Context,
hostname: &str,
port: u16,
@@ -208,7 +263,7 @@ impl Client {
strict_tls: bool,
) -> Result<Self> {
let socks5_stream = socks5_config
.connect(context, hostname, port, IMAP_TIMEOUT, strict_tls)
.connect(context, hostname, port, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
@@ -225,7 +280,7 @@ impl Client {
let buffered_socks5_stream = client.into_inner();
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufWriter::new(tls_stream);

View File

@@ -9,7 +9,8 @@ use tokio::time::timeout;
use super::session::Session;
use super::Imap;
use crate::context::Context;
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
use crate::imap::FolderMeaning;
use crate::net::TIMEOUT;
use crate::tools::{self, time_elapsed};
/// Timeout after which IDLE is finished
@@ -51,7 +52,7 @@ impl Session {
// At this point IDLE command was sent and we received a "+ idling" response. We will now
// read from the stream without getting any data for up to `IDLE_TIMEOUT`. If we don't
// disable read timeout, we would get a timeout after `IMAP_TIMEOUT`, which is a lot
// disable read timeout, we would get a timeout after `crate::net::TIMEOUT`, which is a lot
// shorter than `IDLE_TIMEOUT`.
handle.as_mut().set_read_timeout(None);
let (idle_wait, interrupt) = handle.wait_with_timeout(IDLE_TIMEOUT);
@@ -93,7 +94,7 @@ impl Session {
.await
.with_context(|| format!("{folder}: IMAP IDLE protocol timed out"))?
.with_context(|| format!("{folder}: IMAP IDLE failed"))?;
session.as_mut().set_read_timeout(Some(IMAP_TIMEOUT));
session.as_mut().set_read_timeout(Some(TIMEOUT));
self.inner = session;
// Fetch mail once we exit IDLE.

View File

@@ -24,6 +24,7 @@ const PREFETCH_FLAGS: &str = "(UID INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIE
FROM \
IN-REPLY-TO REFERENCES \
CHAT-VERSION \
AUTO-SUBMITTED \
AUTOCRYPT-SETUP-MESSAGE\
)])";

View File

@@ -1,41 +1,35 @@
//! # Import/export module.
use std::any::Any;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use ::pgp::types::KeyTrait;
use anyhow::{bail, ensure, format_err, Context as _, Result};
use deltachat_contact_tools::EmailAddress;
use futures::StreamExt;
use futures::TryStreamExt;
use futures_lite::FutureExt;
use rand::{thread_rng, Rng};
use tokio::fs::{self, File};
use tokio_tar::Archive;
use crate::blob::{BlobDirContents, BlobObject};
use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::blob::BlobDirContents;
use crate::chat::{self, delete_and_reset_all_device_msgs};
use crate::context::Context;
use crate::e2ee;
use crate::events::EventType;
use crate::key::{
self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey,
};
use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey};
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::message::{Message, Viewtype};
use crate::pgp;
use crate::sql;
use crate::stock_str;
use crate::tools::{
create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file,
create_folder, delete_file, get_filesuffix_lc, read_file, time, write_file, TempPathGuard,
};
mod key_transfer;
mod transfer;
pub use key_transfer::{continue_key_transfer, initiate_key_transfer};
pub use transfer::{get_backup, BackupProvider};
// Name of the database file in the backup.
@@ -47,12 +41,13 @@ pub(crate) const BLOBS_BACKUP_NAME: &str = "blobs_backup";
#[repr(u32)]
pub enum ImexMode {
/// Export all private keys and all public keys of the user to the
/// directory given as `path`. The default key is written to the files `public-key-default.asc`
/// and `private-key-default.asc`, if there are more keys, they are written to files as
/// `public-key-<id>.asc` and `private-key-<id>.asc`
/// directory given as `path`. The default key is written to the files
/// `{public,private}-key-<addr>-default-<fingerprint>.asc`, if there are more keys, they are
/// written to files as `{public,private}-key-<addr>-<id>-<fingerprint>.asc`.
ExportSelfKeys = 1,
/// Import private keys found in the directory given as `path`.
/// Import private keys found in `path` if it is a directory, otherwise import a private key
/// from `path`.
/// The last imported key is made the default keys unless its name contains the string `legacy`.
/// Public keys are not imported.
ImportSelfKeys = 2,
@@ -141,117 +136,6 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
}
}
/// Initiates key transfer via Autocrypt Setup Message.
///
/// Returns setup code.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
/* encrypting may also take a while ... */
let setup_file_blob = BlobObject::create(
context,
"autocrypt-setup-message.html",
setup_file_content.as_bytes(),
)
.await?;
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
let mut msg = Message {
viewtype: Viewtype::File,
..Default::default()
};
msg.param.set(Param::File, setup_file_blob.as_name());
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.force_plaintext();
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::send_msg(context, chat_id, &mut msg).await?;
// no maybe_add_bcc_self_device_msg() here.
// the ui shows the dialog with the setup code on this device,
// it would be too much noise to have two things popping up at the same time.
// maybe_add_bcc_self_device_msg() is called on the other device
// once the transfer is completed.
Ok(setup_code)
}
/// Renders HTML body of a setup file message.
///
/// The `passphrase` must be at least 2 characters long.
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
passphrase_begin
} else {
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = load_self_secret_key(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes())
.await?
.replace('\n', "\r\n");
let replacement = format!(
concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
passphrase_begin
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
Ok(format!(
concat!(
"<!DOCTYPE html>\r\n",
"<html>\r\n",
" <head>\r\n",
" <title>{}</title>\r\n",
" </head>\r\n",
" <body>\r\n",
" <h1>{}</h1>\r\n",
" <p>{}</p>\r\n",
" <pre>\r\n{}\r\n</pre>\r\n",
" </body>\r\n",
"</html>\r\n"
),
msg_subj, msg_subj, msg_body_html, pgp_msg
))
}
/// Creates a new setup code for Autocrypt Setup Message.
pub fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut rng = thread_rng();
let mut ret = String::new();
for i in 0..9 {
loop {
random_val = rng.gen();
if random_val as usize <= 60000 {
break;
}
}
random_val = (random_val as usize % 10000) as u16;
ret += &format!(
"{}{:04}",
if 0 != i { "-" } else { "" },
random_val as usize
);
}
ret
}
async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
@@ -265,36 +149,6 @@ async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
Ok(())
}
/// Continue key transfer via Autocrypt Setup Message.
///
/// `msg_id` is the ID of the received Autocrypt Setup Message.
/// `setup_code` is the code entered by the user.
pub async fn continue_key_transfer(
context: &Context,
msg_id: MsgId,
setup_code: &str,
) -> Result<()> {
ensure!(!msg_id.is_special(), "wrong id");
let msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.is_setupmessage(),
"Message is no Autocrypt Setup Message."
);
if let Some(filename) = msg.get_file(context) {
let file = open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(&sc, file).await?;
set_self_key(context, &armored_key, true).await?;
maybe_add_bcc_self_device_msg(context).await?;
Ok(())
} else {
bail!("Message is no Autocrypt Setup Message.");
}
}
async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Result<()> {
// try hard to only modify key-state
let (private_key, header) = SignedSecretKey::from_asc(armored)?;
@@ -345,29 +199,6 @@ async fn set_self_key(context: &Context, armored: &str, set_default: bool) -> Re
Ok(())
}
async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
passphrase: &str,
file: T,
) -> Result<String> {
let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
let plain_text = std::string::String::from_utf8(plain_bytes)?;
Ok(plain_text)
}
fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if c.is_ascii_digit() {
out.push(c);
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
out += "-"
}
}
}
out
}
async fn imex_inner(
context: &Context,
what: ImexMode,
@@ -438,51 +269,126 @@ async fn import_backup(
context.get_dbfile().display()
);
import_backup_stream(context, backup_file, file_size, passphrase).await?;
Ok(())
}
/// Imports backup by reading a tar file from a stream.
///
/// `file_size` is used to calculate the progress
/// and emit progress events.
/// Ideally it is the sum of the entry
/// sizes without the header overhead,
/// but can be estimated as tar file size
/// in which case the progress is underestimated
/// and may not reach 99.9% by the end of import.
/// Underestimating is better than
/// overestimating because the progress
/// jumps to 100% instead of getting stuck at 99.9%
/// for some time.
pub(crate) async fn import_backup_stream<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> Result<()> {
import_backup_stream_inner(context, backup_file, file_size, passphrase)
.await
.0
}
async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
context: &Context,
backup_file: R,
file_size: u64,
passphrase: String,
) -> (Result<()>,) {
let mut archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
let mut last_progress = 0;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress != last_progress && progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
let mut entries = match archive.entries() {
Ok(entries) => entries,
Err(e) => return (Err(e).context("Failed to get archive entries"),),
};
let mut blobs = Vec::new();
// We already emitted ImexProgress(10) above
let mut last_progress = 10;
const PROGRESS_MIGRATIONS: u128 = 999;
let mut total_size: u64 = 0;
let mut res: Result<()> = loop {
let mut f = match entries.try_next().await {
Ok(Some(f)) => f,
Ok(None) => break Ok(()),
Err(e) => break Err(e).context("Failed to get next entry"),
};
total_size += match f.header().entry_size() {
Ok(size) => size,
Err(e) => break Err(e).context("Failed to get entry size"),
};
let max = PROGRESS_MIGRATIONS - 1;
let progress = std::cmp::min(
max * u128::from(total_size) / std::cmp::max(u128::from(file_size), 1),
max,
);
if progress > last_progress {
context.emit_event(EventType::ImexProgress(progress as usize));
last_progress = progress;
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
// async_tar can't unpack to a specified file name, so we just unpack to the blobdir and then move the unpacked file.
f.unpack_in(context.get_blobdir()).await?;
let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
context
.sql
.import(&unpacked_database, passphrase.clone())
.await
.context("cannot import unpacked database")?;
fs::remove_file(unpacked_database)
.await
.context("cannot remove unpacked database")?;
} else {
// async_tar will unpack to blobdir/BLOBS_BACKUP_NAME, so we move the file afterwards.
f.unpack_in(context.get_blobdir()).await?;
let from_path = context.get_blobdir().join(f.path()?);
if from_path.is_file() {
if let Some(name) = from_path.file_name() {
fs::rename(&from_path, context.get_blobdir().join(name)).await?;
} else {
warn!(context, "No file name");
let path = match f.path() {
Ok(path) => path.to_path_buf(),
Err(e) => break Err(e).context("Failed to get entry path"),
};
if let Err(e) = f.unpack_in(context.get_blobdir()).await {
break Err(e).context("Failed to unpack file");
}
if path.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
continue;
}
// async_tar unpacked to $BLOBDIR/BLOBS_BACKUP_NAME/, so we move the file afterwards.
let from_path = context.get_blobdir().join(&path);
if from_path.is_file() {
if let Some(name) = from_path.file_name() {
let to_path = context.get_blobdir().join(name);
if let Err(e) = fs::rename(&from_path, &to_path).await {
blobs.push(from_path);
break Err(e).context("Failed to move file to blobdir");
}
blobs.push(to_path);
} else {
warn!(context, "No file name");
}
}
};
if res.is_err() {
for blob in blobs {
fs::remove_file(&blob).await.log_err(context).ok();
}
}
context.sql.run_migrations(context).await?;
delete_and_reset_all_device_msgs(context).await?;
Ok(())
let unpacked_database = context.get_blobdir().join(DBFILE_BACKUP_NAME);
if res.is_ok() {
res = context
.sql
.import(&unpacked_database, passphrase.clone())
.await
.context("cannot import unpacked database");
}
fs::remove_file(unpacked_database)
.await
.context("cannot remove unpacked database")
.log_err(context)
.ok();
if res.is_ok() {
context.emit_event(EventType::ImexProgress(PROGRESS_MIGRATIONS as usize));
res = context.sql.run_migrations(context).await;
}
if res.is_ok() {
delete_and_reset_all_device_msgs(context)
.await
.log_err(context)
.ok();
}
(res,)
}
/*******************************************************************************
@@ -530,8 +436,8 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
let now = time();
let self_addr = context.get_primary_self_addr().await?;
let (temp_db_path, temp_path, dest_path) = get_next_backup_path(dir, &self_addr, now)?;
let _d1 = DeleteOnDrop(temp_db_path.clone());
let _d2 = DeleteOnDrop(temp_path.clone());
let temp_db_path = TempPathGuard::new(temp_db_path);
let temp_path = TempPathGuard::new(temp_path);
export_database(context, &temp_db_path, passphrase, now)
.await
@@ -544,52 +450,40 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
dest_path.display(),
);
let res = export_backup_inner(context, &temp_db_path, &temp_path).await;
match &res {
Ok(_) => {
fs::rename(temp_path, &dest_path).await?;
context.emit_event(EventType::ImexFileWritten(dest_path));
}
Err(e) => {
error!(context, "backup failed: {}", e);
}
}
res
}
struct DeleteOnDrop(PathBuf);
impl Drop for DeleteOnDrop {
fn drop(&mut self) {
let file = self.0.clone();
// Not using `tools::delete_file` here because it would send a DeletedBlobFile event
// Hack to avoid panic in nested runtime calls of tokio
std::fs::remove_file(file).ok();
}
let file = File::create(&temp_path).await?;
let blobdir = BlobDirContents::new(context).await?;
export_backup_stream(context, &temp_db_path, blobdir, file)
.await
.context("Exporting backup to file failed")?;
fs::rename(temp_path, &dest_path).await?;
context.emit_event(EventType::ImexFileWritten(dest_path));
Ok(())
}
async fn export_backup_inner(
context: &Context,
/// Exports the database and blobs into a stream.
pub(crate) async fn export_backup_stream<'a, W>(
context: &'a Context,
temp_db_path: &Path,
temp_path: &Path,
) -> Result<()> {
let file = File::create(temp_path).await?;
let mut builder = tokio_tar::Builder::new(file);
blobdir: BlobDirContents<'a>,
writer: W,
) -> Result<()>
where
W: tokio::io::AsyncWrite + tokio::io::AsyncWriteExt + Unpin + Send + 'static,
{
let mut builder = tokio_tar::Builder::new(writer);
builder
.append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME)
.await?;
let blobdir = BlobDirContents::new(context).await?;
let mut last_progress = 0;
let mut last_progress = 10;
for (i, blob) in blobdir.iter().enumerate() {
let mut file = File::open(blob.to_abs_path()).await?;
let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name());
builder.append_file(path_in_archive, &mut file).await?;
let progress = 1000 * i / blobdir.len();
if progress != last_progress && progress > 10 && progress < 1000 {
let progress = std::cmp::min(1000 * i / blobdir.len(), 999);
if progress > last_progress {
context.emit_event(EventType::ImexProgress(progress));
last_progress = progress;
}
@@ -695,12 +589,12 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
},
)
.await?;
let self_addr = context.get_primary_self_addr().await?;
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default == 0);
if let Ok(key) = public_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
error!(context, "Failed to export public key: {:#}.", err);
export_errors += 1;
}
@@ -708,7 +602,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
export_errors += 1;
}
if let Ok(key) = private_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
if let Err(err) = export_key_to_asc_file(context, dir, &self_addr, id, &key).await {
error!(context, "Failed to export private key: {:#}.", err);
export_errors += 1;
}
@@ -721,46 +615,43 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
Ok(())
}
/*******************************************************************************
* Classic key export
******************************************************************************/
/// Returns the exported key file name inside `dir`.
async fn export_key_to_asc_file<T>(
context: &Context,
dir: &Path,
addr: &str,
id: Option<i64>,
key: &T,
) -> Result<()>
) -> Result<String>
where
T: DcKey + Any,
T: DcKey,
{
let file_name = {
let any_key = key as &dyn Any;
let kind = if any_key.downcast_ref::<SignedPublicKey>().is_some() {
"public"
} else if any_key.downcast_ref::<SignedSecretKey>().is_some() {
"private"
} else {
"unknown"
let kind = match T::is_private() {
false => "public",
true => "private",
};
let id = id.map_or("default".into(), |i| i.to_string());
dir.join(format!("{}-key-{}.asc", kind, &id))
let fp = DcKey::fingerprint(key).hex();
format!("{kind}-key-{addr}-{id}-{fp}.asc")
};
let path = dir.join(&file_name);
info!(
context,
"Exporting key {:?} to {}",
"Exporting key {:?} to {}.",
key.key_id(),
file_name.display()
path.display()
);
// Delete the file if it already exists.
delete_file(context, &file_name).await.ok();
delete_file(context, &path).await.ok();
let content = key.to_asc(None).into_bytes();
write_file(context, &file_name, &content)
write_file(context, &path, &content)
.await
.with_context(|| format!("cannot write key to {}", file_name.display()))?;
context.emit_event(EventType::ImexFileWritten(file_name));
Ok(())
.with_context(|| format!("cannot write key to {}", path.display()))?;
context.emit_event(EventType::ImexFileWritten(path));
Ok(file_name)
}
/// Exports the database to *dest*, encrypted using *passphrase*.
@@ -819,92 +710,57 @@ async fn export_database(
mod tests {
use std::time::Duration;
use ::pgp::armor::BlockType;
use tokio::task;
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::{alice_keypair, TestContext, TestContextManager};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
let t = TestContext::new_alice().await;
let msg = render_setup_file(&t, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
assert!(msg.contains("<title>Autocrypt Setup Message</title"));
assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\r\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\r\n"));
for line in msg.rsplit_terminator('\n') {
assert!(line.ends_with('\r'));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new_alice().await;
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
let msg = render_setup_file(&t, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let setupcode = create_setup_code(&t);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
}
use crate::config::Config;
use crate::test_utils::{alice_keypair, TestContext};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_public_key_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().public;
let blobdir = Path::new("$BLOBDIR");
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
.await
.is_ok());
.unwrap();
assert!(filename.starts_with("public-key-a@b-default-"));
assert!(filename.ends_with(".asc"));
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/public-key-default.asc");
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_private_key_to_asc_file() {
async fn test_import_private_key_exported_to_asc_file() {
let context = TestContext::new().await;
let key = alice_keypair().secret;
let blobdir = Path::new("$BLOBDIR");
assert!(export_key_to_asc_file(&context.ctx, blobdir, None, &key)
let filename = export_key_to_asc_file(&context.ctx, blobdir, "a@b", None, &key)
.await
.is_ok());
.unwrap();
let fingerprint = filename
.strip_prefix("private-key-a@b-default-")
.unwrap()
.strip_suffix(".asc")
.unwrap();
assert_eq!(fingerprint, DcKey::fingerprint(&key).hex());
let blobdir = context.ctx.get_blobdir().to_str().unwrap();
let filename = format!("{blobdir}/private-key-default.asc");
let filename = format!("{blobdir}/{filename}");
let bytes = tokio::fs::read(&filename).await.unwrap();
assert_eq!(bytes, key.to_asc(None).into_bytes());
let alice = &TestContext::new_alice().await;
if let Err(err) = imex(alice, ImexMode::ImportSelfKeys, Path::new(&filename), None).await {
panic!("got error on import: {err:#}");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_key() {
async fn test_export_and_import_key_from_dir() {
let export_dir = tempfile::tempdir().unwrap();
let context = TestContext::new_alice().await;
@@ -930,12 +786,6 @@ mod tests {
{
panic!("got error on import: {err:#}");
}
let keyfile = export_dir.path().join("private-key-default.asc");
let context3 = TestContext::new_alice().await;
if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await {
panic!("got error on import: {err:#}");
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -1080,137 +930,4 @@ mod tests {
Ok(())
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
let norm =
normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234 ");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
}
/* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
with an "encrypted session key", see RFC 4880. The code is in S_EM_SETUPCODE */
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../test-data/message/stress.txt");
// Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
const S_PLAINTEXT_SETUPFILE: &str =
include_str!("../test-data/message/plaintext-autocrypt-setup.txt");
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_split_and_decrypt() {
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
assert_eq!(typ, BlockType::Message);
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
assert!(!headers.contains_key(HEADER_AUTOCRYPT));
assert!(!base64.is_empty());
let setup_file = S_EM_SETUPFILE.to_string();
let decrypted =
decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
.await
.unwrap();
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
assert_eq!(typ, BlockType::PrivateKey);
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
assert!(!headers.contains_key(HEADER_SETUPCODE));
}
/// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
/// decrypted.
///
/// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_plaintext_autocrypt_setup_message() {
let setup_file = S_PLAINTEXT_SETUPFILE.to_string();
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
assert!(decrypt_setup_file(
incorrect_setupcode,
std::io::Cursor::new(setup_file.as_bytes()),
)
.await
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer() -> Result<()> {
let alice = TestContext::new_alice().await;
let setup_code = initiate_key_transfer(&alice).await?;
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
// Alice sets up a second device.
let alice2 = TestContext::new().await;
alice2.set_name("alice2");
alice2.configure_addr("alice@example.org").await;
alice2.recv_msg(&sent).await;
let msg = alice2.get_last_msg().await;
assert!(msg.is_setupmessage());
// Send a message that cannot be decrypted because the keys are
// not synchronized yet.
let sent = alice2.send_text(msg.chat_id, "Test").await;
let trashed_message = alice.recv_msg_opt(&sent).await;
assert!(trashed_message.is_none());
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
// Transfer the key.
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
// Alice sends a message to self from the new device.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_eq!(alice.get_last_msg().await.get_text(), "Test");
Ok(())
}
/// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
/// This prevents Bob from tricking Alice into changing the key
/// by sending her an Autocrypt Setup Message as long as Alice's server
/// does not allow to forge the `From:` header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer_non_self_sent() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let _setup_code = initiate_key_transfer(&alice).await?;
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
let rcvd = bob.recv_msg(&sent).await;
assert!(!rcvd.is_setupmessage());
Ok(())
}
/// Tests reception of Autocrypt Setup Message from K-9 6.802.
///
/// Unlike Autocrypt Setup Message sent by Delta Chat,
/// this message does not contain `Autocrypt-Prefer-Encrypt` header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer_k_9() -> Result<()> {
let t = &TestContext::new().await;
t.configure_addr("autocrypt@nine.testrun.org").await;
let raw = include_bytes!("../test-data/message/k-9-autocrypt-setup-message.eml");
let received = receive_imf(t, raw, false).await?.unwrap();
let setup_code = "0655-9868-8252-5455-4232-5158-1237-5333-2638";
continue_key_transfer(t, *received.msg_ids.last().unwrap(), setup_code).await?;
Ok(())
}
}

372
src/imex/key_transfer.rs Normal file
View File

@@ -0,0 +1,372 @@
//! # Key transfer via Autocrypt Setup Message.
use rand::{thread_rng, Rng};
use anyhow::{bail, ensure, Result};
use crate::blob::BlobObject;
use crate::chat::{self, ChatId};
use crate::config::Config;
use crate::contact::ContactId;
use crate::context::Context;
use crate::imex::maybe_add_bcc_self_device_msg;
use crate::imex::set_self_key;
use crate::key::{load_self_secret_key, DcKey};
use crate::message::{Message, MsgId, Viewtype};
use crate::mimeparser::SystemMessage;
use crate::param::Param;
use crate::pgp;
use crate::stock_str;
use crate::tools::open_file_std;
/// Initiates key transfer via Autocrypt Setup Message.
///
/// Returns setup code.
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
let setup_code = create_setup_code(context);
/* this may require a keypair to be created. this may take a second ... */
let setup_file_content = render_setup_file(context, &setup_code).await?;
/* encrypting may also take a while ... */
let setup_file_blob = BlobObject::create(
context,
"autocrypt-setup-message.html",
setup_file_content.as_bytes(),
)
.await?;
let chat_id = ChatId::create_for_contact(context, ContactId::SELF).await?;
let mut msg = Message {
viewtype: Viewtype::File,
..Default::default()
};
msg.param.set(Param::File, setup_file_blob.as_name());
msg.subject = stock_str::ac_setup_msg_subject(context).await;
msg.param
.set(Param::MimeType, "application/autocrypt-setup");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
msg.force_plaintext();
msg.param.set_int(Param::SkipAutocrypt, 1);
chat::send_msg(context, chat_id, &mut msg).await?;
// no maybe_add_bcc_self_device_msg() here.
// the ui shows the dialog with the setup code on this device,
// it would be too much noise to have two things popping up at the same time.
// maybe_add_bcc_self_device_msg() is called on the other device
// once the transfer is completed.
Ok(setup_code)
}
/// Continue key transfer via Autocrypt Setup Message.
///
/// `msg_id` is the ID of the received Autocrypt Setup Message.
/// `setup_code` is the code entered by the user.
pub async fn continue_key_transfer(
context: &Context,
msg_id: MsgId,
setup_code: &str,
) -> Result<()> {
ensure!(!msg_id.is_special(), "wrong id");
let msg = Message::load_from_db(context, msg_id).await?;
ensure!(
msg.is_setupmessage(),
"Message is no Autocrypt Setup Message."
);
if let Some(filename) = msg.get_file(context) {
let file = open_file_std(context, filename)?;
let sc = normalize_setup_code(setup_code);
let armored_key = decrypt_setup_file(&sc, file).await?;
set_self_key(context, &armored_key, true).await?;
maybe_add_bcc_self_device_msg(context).await?;
Ok(())
} else {
bail!("Message is no Autocrypt Setup Message.");
}
}
/// Renders HTML body of a setup file message.
///
/// The `passphrase` must be at least 2 characters long.
pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<String> {
let passphrase_begin = if let Some(passphrase_begin) = passphrase.get(..2) {
passphrase_begin
} else {
bail!("Passphrase must be at least 2 chars long.");
};
let private_key = load_self_secret_key(context).await?;
let ac_headers = match context.get_config_bool(Config::E2eeEnabled).await? {
false => None,
true => Some(("Autocrypt-Prefer-Encrypt", "mutual")),
};
let private_key_asc = private_key.to_asc(ac_headers);
let encr = pgp::symm_encrypt(passphrase, private_key_asc.as_bytes())
.await?
.replace('\n', "\r\n");
let replacement = format!(
concat!(
"-----BEGIN PGP MESSAGE-----\r\n",
"Passphrase-Format: numeric9x4\r\n",
"Passphrase-Begin: {}"
),
passphrase_begin
);
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
let msg_body = stock_str::ac_setup_msg_body(context).await;
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
Ok(format!(
concat!(
"<!DOCTYPE html>\r\n",
"<html>\r\n",
" <head>\r\n",
" <title>{}</title>\r\n",
" </head>\r\n",
" <body>\r\n",
" <h1>{}</h1>\r\n",
" <p>{}</p>\r\n",
" <pre>\r\n{}\r\n</pre>\r\n",
" </body>\r\n",
"</html>\r\n"
),
msg_subj, msg_subj, msg_body_html, pgp_msg
))
}
/// Creates a new setup code for Autocrypt Setup Message.
fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut rng = thread_rng();
let mut ret = String::new();
for i in 0..9 {
loop {
random_val = rng.gen();
if random_val as usize <= 60000 {
break;
}
}
random_val = (random_val as usize % 10000) as u16;
ret += &format!(
"{}{:04}",
if 0 != i { "-" } else { "" },
random_val as usize
);
}
ret
}
async fn decrypt_setup_file<T: std::io::Read + std::io::Seek>(
passphrase: &str,
file: T,
) -> Result<String> {
let plain_bytes = pgp::symm_decrypt(passphrase, file).await?;
let plain_text = std::string::String::from_utf8(plain_bytes)?;
Ok(plain_text)
}
fn normalize_setup_code(s: &str) -> String {
let mut out = String::new();
for c in s.chars() {
if c.is_ascii_digit() {
out.push(c);
if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() {
out += "-"
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
use crate::test_utils::{TestContext, TestContextManager};
use ::pgp::armor::BlockType;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file() {
let t = TestContext::new_alice().await;
let msg = render_setup_file(&t, "hello").await.unwrap();
println!("{}", &msg);
// Check some substrings, indicating things got substituted.
assert!(msg.contains("<title>Autocrypt Setup Message</title"));
assert!(msg.contains("<h1>Autocrypt Setup Message</h1>"));
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to"));
assert!(msg.contains("-----BEGIN PGP MESSAGE-----\r\n"));
assert!(msg.contains("Passphrase-Format: numeric9x4\r\n"));
assert!(msg.contains("Passphrase-Begin: he\r\n"));
assert!(msg.contains("-----END PGP MESSAGE-----\r\n"));
for line in msg.rsplit_terminator('\n') {
assert!(line.ends_with('\r'));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_setup_file_newline_replace() {
let t = TestContext::new_alice().await;
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
.await
.unwrap();
let msg = render_setup_file(&t, "pw").await.unwrap();
println!("{}", &msg);
assert!(msg.contains("<p>hello<br>there</p>"));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_setup_code() {
let t = TestContext::new().await;
let setupcode = create_setup_code(&t);
assert_eq!(setupcode.len(), 44);
assert_eq!(setupcode.chars().nth(4).unwrap(), '-');
assert_eq!(setupcode.chars().nth(9).unwrap(), '-');
assert_eq!(setupcode.chars().nth(14).unwrap(), '-');
assert_eq!(setupcode.chars().nth(19).unwrap(), '-');
assert_eq!(setupcode.chars().nth(24).unwrap(), '-');
assert_eq!(setupcode.chars().nth(29).unwrap(), '-');
assert_eq!(setupcode.chars().nth(34).unwrap(), '-');
assert_eq!(setupcode.chars().nth(39).unwrap(), '-');
}
#[test]
fn test_normalize_setup_code() {
let norm = normalize_setup_code("123422343234423452346234723482349234");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
let norm =
normalize_setup_code("\t1 2 3422343234- foo bar-- 423-45 2 34 6234723482349234 ");
assert_eq!(norm, "1234-2234-3234-4234-5234-6234-7234-8234-9234");
}
/* S_EM_SETUPFILE is a AES-256 symm. encrypted setup message created by Enigmail
with an "encrypted session key", see RFC 4880. The code is in S_EM_SETUPCODE */
const S_EM_SETUPCODE: &str = "1742-0185-6197-1303-7016-8412-3581-4441-0597";
const S_EM_SETUPFILE: &str = include_str!("../../test-data/message/stress.txt");
// Autocrypt Setup Message payload "encrypted" with plaintext algorithm.
const S_PLAINTEXT_SETUPFILE: &str =
include_str!("../../test-data/message/plaintext-autocrypt-setup.txt");
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_split_and_decrypt() {
let buf_1 = S_EM_SETUPFILE.as_bytes().to_vec();
let (typ, headers, base64) = split_armored_data(&buf_1).unwrap();
assert_eq!(typ, BlockType::Message);
assert!(S_EM_SETUPCODE.starts_with(headers.get(HEADER_SETUPCODE).unwrap()));
assert!(!headers.contains_key(HEADER_AUTOCRYPT));
assert!(!base64.is_empty());
let setup_file = S_EM_SETUPFILE.to_string();
let decrypted =
decrypt_setup_file(S_EM_SETUPCODE, std::io::Cursor::new(setup_file.as_bytes()))
.await
.unwrap();
let (typ, headers, _base64) = split_armored_data(decrypted.as_bytes()).unwrap();
assert_eq!(typ, BlockType::PrivateKey);
assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
assert!(!headers.contains_key(HEADER_SETUPCODE));
}
/// Tests that Autocrypt Setup Message encrypted with "plaintext" algorithm cannot be
/// decrypted.
///
/// According to <https://datatracker.ietf.org/doc/html/rfc4880#section-13.4>
/// "Implementations MUST NOT use plaintext in Symmetrically Encrypted Data packets".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decrypt_plaintext_autocrypt_setup_message() {
let setup_file = S_PLAINTEXT_SETUPFILE.to_string();
let incorrect_setupcode = "0000-0000-0000-0000-0000-0000-0000-0000-0000";
assert!(decrypt_setup_file(
incorrect_setupcode,
std::io::Cursor::new(setup_file.as_bytes()),
)
.await
.is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer() -> Result<()> {
let alice = TestContext::new_alice().await;
let setup_code = initiate_key_transfer(&alice).await?;
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
// Alice sets up a second device.
let alice2 = TestContext::new().await;
alice2.set_name("alice2");
alice2.configure_addr("alice@example.org").await;
alice2.recv_msg(&sent).await;
let msg = alice2.get_last_msg().await;
assert!(msg.is_setupmessage());
// Send a message that cannot be decrypted because the keys are
// not synchronized yet.
let sent = alice2.send_text(msg.chat_id, "Test").await;
let trashed_message = alice.recv_msg_opt(&sent).await;
assert!(trashed_message.is_none());
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
// Transfer the key.
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
// Alice sends a message to self from the new device.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_eq!(alice.get_last_msg().await.get_text(), "Test");
Ok(())
}
/// Tests that Autocrypt Setup Messages is only clickable if it is self-sent.
/// This prevents Bob from tricking Alice into changing the key
/// by sending her an Autocrypt Setup Message as long as Alice's server
/// does not allow to forge the `From:` header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer_non_self_sent() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let _setup_code = initiate_key_transfer(&alice).await?;
// Get Autocrypt Setup Message.
let sent = alice.pop_sent_msg().await;
let rcvd = bob.recv_msg(&sent).await;
assert!(!rcvd.is_setupmessage());
Ok(())
}
/// Tests reception of Autocrypt Setup Message from K-9 6.802.
///
/// Unlike Autocrypt Setup Message sent by Delta Chat,
/// this message does not contain `Autocrypt-Prefer-Encrypt` header.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_transfer_k_9() -> Result<()> {
let t = &TestContext::new().await;
t.configure_addr("autocrypt@nine.testrun.org").await;
let raw = include_bytes!("../../test-data/message/k-9-autocrypt-setup-message.eml");
let received = receive_imf(t, raw, false).await?.unwrap();
let setup_code = "0655-9868-8252-5455-4232-5158-1237-5333-2638";
continue_key_transfer(t, *received.msg_ids.last().unwrap(), setup_code).await?;
Ok(())
}
}

View File

@@ -1,17 +1,12 @@
//! Transfer a backup to an other device.
//!
//! This module provides support for using n0's iroh tool to initiate transfer of a backup
//! to another device using a QR code.
//!
//! Using the iroh terminology there are two parties to this:
//! This module provides support for using [iroh](https://iroh.computer/)
//! to initiate transfer of a backup to another device using a QR code.
//!
//! There are two parties to this:
//! - The *Provider*, which starts a server and listens for connections.
//! - The *Getter*, which connects to the server and retrieves the data.
//!
//! Iroh is designed around the idea of verifying hashes, the downloads are verified as
//! they are retrieved. The entire transfer is initiated by requesting the data of a single
//! root hash.
//!
//! Both the provider and the getter are authenticated:
//!
//! - The provider is known by its *peer ID*.
@@ -21,24 +16,30 @@
//! Both these are transferred in the QR code offered to the getter. This ensures that the
//! getter can not connect to an impersonated provider and the provider does not offer the
//! download to an impersonated getter.
//!
//! Protocol starts by getter opening a bidirectional QUIC stream
//! to the provider and sending authentication token.
//! Provider verifies received authentication token,
//! sends the size of all files in a backup (database and all blobs)
//! as an unsigned 64-bit big endian integer and streams the backup in tar format.
//! Getter receives the backup and acknowledges successful reception
//! by sending a single byte.
//! Provider closes the endpoint after receiving an acknowledgment.
use std::future::Future;
use std::net::Ipv4Addr;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::task::Poll;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
use async_channel::Receiver;
use futures_lite::StreamExt;
use iroh::blobs::Collection;
use iroh::get::DataStream;
use iroh::progress::ProgressEmitter;
use iroh::protocol::AuthToken;
use iroh::provider::{DataSource, Event, Provider, Ticket};
use iroh::Hash;
use iroh_old as iroh;
use iroh_net::relay::RelayMode;
use iroh_net::Endpoint;
use iroh_old;
use iroh_old::blobs::Collection;
use iroh_old::get::DataStream;
use iroh_old::progress::ProgressEmitter;
use iroh_old::provider::Ticket;
use tokio::fs::{self, File};
use tokio::io::{self, AsyncWriteExt, BufWriter};
use tokio::sync::broadcast::error::RecvError;
@@ -47,19 +48,22 @@ use tokio::task::{JoinHandle, JoinSet};
use tokio_stream::wrappers::ReadDirStream;
use tokio_util::sync::CancellationToken;
use crate::blob::BlobDirContents;
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
use crate::context::Context;
use crate::imex::BlobDirContents;
use crate::message::{Message, Viewtype};
use crate::qr::{self, Qr};
use crate::stock_str::backup_transfer_msg_body;
use crate::tools::time;
use crate::{e2ee, EventType};
use crate::tools::{create_id, time, TempPathGuard};
use crate::EventType;
use super::{export_database, DBFILE_BACKUP_NAME};
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
const MAX_CONCURRENT_DIALS: u8 = 16;
/// ALPN protocol identifier for the backup transfer protocol.
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
/// Provide or send a backup of this device.
///
/// This creates a backup of the current device and starts a service which offers another
@@ -70,15 +74,21 @@ const MAX_CONCURRENT_DIALS: u8 = 16;
///
/// This starts a task which acquires the global "ongoing" mutex. If you need to stop the
/// task use the [`Context::stop_ongoing`] mechanism.
///
/// The task implements [`Future`] and awaiting it will complete once a transfer has been
/// either completed or aborted.
#[derive(Debug)]
pub struct BackupProvider {
/// The supervisor task, run by [`BackupProvider::watch_provider`].
/// iroh-net endpoint.
_endpoint: Endpoint,
/// iroh-net address.
node_addr: iroh_net::NodeAddr,
/// Authentication token that should be submitted
/// to retrieve the backup.
auth_token: String,
/// Handle for the task accepting backup transfer requests.
handle: JoinHandle<Result<()>>,
/// The ticket to retrieve the backup collection.
ticket: Ticket,
/// Guard to cancel the provider on drop.
_drop_guard: tokio_util::sync::DropGuard,
}
@@ -95,9 +105,13 @@ impl BackupProvider {
///
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
pub async fn prepare(context: &Context) -> Result<Self> {
e2ee::ensure_secret_key_exists(context)
.await
.context("Private key not available, aborting backup export")?;
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder()
.alpns(vec![BACKUP_ALPN.to_vec()])
.relay_mode(relay_mode)
.bind(0)
.await?;
let node_addr = endpoint.node_addr().await?;
// Acquire global "ongoing" mutex.
let cancel_token = context.alloc_ongoing().await?;
@@ -105,195 +119,153 @@ impl BackupProvider {
let context_dir = context
.get_blobdir()
.parent()
.ok_or_else(|| anyhow!("Context dir not found"))?;
.context("Context dir not found")?;
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
if fs::metadata(&dbfile).await.is_ok() {
fs::remove_file(&dbfile).await?;
warn!(context, "Previous database export deleted");
}
let dbfile = TempPathGuard::new(dbfile);
let res = tokio::select! {
biased;
res = Self::prepare_inner(context, &dbfile) => {
match res {
Ok(slf) => Ok(slf),
Err(err) => {
error!(context, "Failed to set up second device setup: {:#}", err);
Err(err)
},
}
},
_ = cancel_token.recv() => Err(format_err!("cancelled")),
};
let (provider, ticket) = match res {
Ok((provider, ticket)) => (provider, ticket),
Err(err) => {
context.free_ongoing().await;
return Err(err);
}
};
// Authentication token that receiver should send us to receive a backup.
let auth_token = create_id();
let passphrase = String::new();
export_database(context, &dbfile, passphrase, time())
.await
.context("Database export failed")?;
context.emit_event(EventType::ImexProgress(300));
let drop_token = CancellationToken::new();
let handle = {
let context = context.clone();
let drop_token = drop_token.clone();
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
tokio::spawn(async move {
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
Self::accept_loop(
context.clone(),
endpoint,
auth_token,
cancel_token,
drop_token,
dbfile,
)
.await;
info!(context, "Finished accept loop.");
context.free_ongoing().await;
// Explicit drop to move the guards into this future
drop(paused_guard);
drop(dbfile);
res
Ok(())
})
};
Ok(Self {
_endpoint: endpoint,
node_addr,
auth_token,
handle,
ticket,
_drop_guard: drop_token.drop_guard(),
})
}
/// Creates the provider task.
///
/// Having this as a function makes it easier to cancel it when needed.
async fn prepare_inner(context: &Context, dbfile: &Path) -> Result<(Provider, Ticket)> {
// Generate the token up front: we also use it to encrypt the database.
let token = AuthToken::generate();
context.emit_event(SendProgress::Started.into());
export_database(context, dbfile, token.to_string(), time())
.await
.context("Database export failed")?;
context.emit_event(SendProgress::DatabaseExported.into());
async fn handle_connection(
context: Context,
conn: iroh_net::endpoint::Connecting,
auth_token: String,
dbfile: Arc<TempPathGuard>,
) -> Result<()> {
let conn = conn.await?;
let (mut send_stream, mut recv_stream) = conn.accept_bi().await?;
// Now we can be sure IO is not running.
let mut files = vec![DataSource::with_name(
dbfile.to_owned(),
format!("db/{DBFILE_BACKUP_NAME}"),
)];
let blobdir = BlobDirContents::new(context).await?;
for blob in blobdir.iter() {
let path = blob.to_abs_path();
let name = format!("blob/{}", blob.as_file_name());
files.push(DataSource::with_name(path, name));
// Read authentication token from the stream.
let mut received_auth_token = vec![0u8; auth_token.len()];
recv_stream.read_exact(&mut received_auth_token).await?;
if received_auth_token.as_slice() != auth_token.as_bytes() {
warn!(context, "Received wrong backup authentication token.");
return Ok(());
}
// Start listening.
let (db, hash) = iroh::provider::create_collection(files).await?;
context.emit_event(SendProgress::CollectionCreated.into());
let provider = Provider::builder(db)
.bind_addr((Ipv4Addr::UNSPECIFIED, 0).into())
.auth_token(token)
.spawn()?;
context.emit_event(SendProgress::ProviderListening.into());
info!(context, "Waiting for remote to connect");
let ticket = provider.ticket(hash)?;
Ok((provider, ticket))
info!(context, "Received valid backup authentication token.");
let blobdir = BlobDirContents::new(&context).await?;
let mut file_size = 0;
file_size += dbfile.metadata()?.len();
for blob in blobdir.iter() {
file_size += blob.to_abs_path().metadata()?.len()
}
send_stream.write_all(&file_size.to_be_bytes()).await?;
export_backup_stream(&context, &dbfile, blobdir, send_stream)
.await
.context("Failed to write backup into QUIC stream")?;
info!(context, "Finished writing backup into QUIC stream.");
let mut buf = [0u8; 1];
info!(context, "Waiting for acknowledgment.");
recv_stream.read_exact(&mut buf).await?;
info!(context, "Received backup reception acknowledgement.");
context.emit_event(EventType::ImexProgress(1000));
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(&context).await;
add_device_msg(&context, None, Some(&mut msg)).await?;
Ok(())
}
/// Supervises the iroh [`Provider`], terminating it when needed.
///
/// This will watch the provider and terminate it when:
///
/// - A transfer is completed, successful or unsuccessful.
/// - An event could not be observed to protect against not knowing of a completed event.
/// - The ongoing process is cancelled.
///
/// The *cancel_token* is the handle for the ongoing process mutex, when this completes
/// we must cancel this operation.
async fn watch_provider(
context: &Context,
mut provider: Provider,
cancel_token: Receiver<()>,
async fn accept_loop(
context: Context,
endpoint: Endpoint,
auth_token: String,
cancel_token: async_channel::Receiver<()>,
drop_token: CancellationToken,
) -> Result<()> {
let mut events = provider.subscribe();
let mut total_size = 0;
let mut current_size = 0;
let res = loop {
dbfile: TempPathGuard,
) {
let dbfile = Arc::new(dbfile);
loop {
tokio::select! {
biased;
res = &mut provider => {
break res.context("BackupProvider failed");
},
maybe_event = events.recv() => {
match maybe_event {
Ok(event) => {
match event {
Event::ClientConnected { ..} => {
context.emit_event(SendProgress::ClientConnected.into());
}
Event::RequestReceived { .. } => {
}
Event::TransferCollectionStarted { total_blobs_size, .. } => {
total_size = total_blobs_size;
context.emit_event(SendProgress::TransferInProgress {
current_size,
total_size,
}.into());
}
Event::TransferBlobCompleted { size, .. } => {
current_size += size;
context.emit_event(SendProgress::TransferInProgress {
current_size,
total_size,
}.into());
}
Event::TransferCollectionCompleted { .. } => {
context.emit_event(SendProgress::TransferInProgress {
current_size: total_size,
total_size
}.into());
provider.shutdown();
}
Event::TransferAborted { .. } => {
provider.shutdown();
break Err(anyhow!("BackupProvider transfer aborted"));
}
}
}
Err(broadcast::error::RecvError::Closed) => {
// We should never see this, provider.join() should complete
// first.
}
Err(broadcast::error::RecvError::Lagged(_)) => {
// We really shouldn't be lagging, if we did we may have missed
// a completion event.
provider.shutdown();
break Err(anyhow!("Missed events from BackupProvider"));
conn = endpoint.accept() => {
if let Some(conn) = conn {
// Got a new in-progress connection.
let context = context.clone();
let auth_token = auth_token.clone();
let dbfile = dbfile.clone();
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).await {
warn!(context, "Error while handling backup connection: {err:#}.");
} else {
info!(context, "Backup transfer finished successfully.");
break;
}
} else {
break;
}
},
_ = cancel_token.recv() => {
provider.shutdown();
break Err(anyhow!("BackupProvider cancelled"));
},
context.emit_event(EventType::ImexProgress(0));
break;
}
_ = drop_token.cancelled() => {
provider.shutdown();
break Err(anyhow!("BackupProvider dropped"));
context.emit_event(EventType::ImexProgress(0));
break;
}
}
};
match &res {
Ok(_) => {
context.emit_event(SendProgress::Completed.into());
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(context).await;
add_device_msg(context, None, Some(&mut msg)).await?;
}
Err(err) => {
error!(context, "Backup transfer failure: {err:#}");
context.emit_event(SendProgress::Failed.into())
}
}
res
}
/// Returns a QR code that allows fetching this backup.
///
/// This QR code can be passed to [`get_backup`] on a (different) device.
pub fn qr(&self) -> Qr {
Qr::Backup {
ticket: self.ticket.clone(),
Qr::Backup2 {
node_addr: self.node_addr.clone(),
auth_token: self.auth_token.clone(),
}
}
}
@@ -301,92 +273,14 @@ impl BackupProvider {
impl Future for BackupProvider {
type Output = Result<()>;
/// Waits for the backup transfer to complete.
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handle).poll(cx)?
}
}
/// A guard which will remove the path when dropped.
///
/// It implements [`Deref`] it it can be used as a `&Path`.
#[derive(Debug)]
struct TempPathGuard {
path: PathBuf,
}
impl TempPathGuard {
fn new(path: PathBuf) -> Self {
Self { path }
}
}
impl Drop for TempPathGuard {
fn drop(&mut self) {
let path = self.path.clone();
tokio::spawn(async move {
fs::remove_file(&path).await.ok();
});
}
}
impl Deref for TempPathGuard {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.path
}
}
/// Create [`EventType::ImexProgress`] events using readable names.
///
/// Plus you get warnings if you don't use all variants.
#[derive(Debug)]
enum SendProgress {
Failed,
Started,
DatabaseExported,
CollectionCreated,
ProviderListening,
ClientConnected,
TransferInProgress { current_size: u64, total_size: u64 },
Completed,
}
impl From<SendProgress> for EventType {
fn from(source: SendProgress) -> Self {
use SendProgress::*;
let num: u16 = match source {
Failed => 0,
Started => 100,
DatabaseExported => 300,
CollectionCreated => 350,
ProviderListening => 400,
ClientConnected => 450,
TransferInProgress {
current_size,
total_size,
} => {
// the range is 450..=950
450 + ((current_size as f64 / total_size as f64) * 500.).floor() as u16
}
Completed => 1000,
};
Self::ImexProgress(num.into())
}
}
/// Contacts a backup provider and receives the backup from it.
///
/// This uses a QR code to contact another instance of deltachat which is providing a backup
/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in
/// the QR code and retrieve the backup.
///
/// This is a long running operation which will only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts one specific variant of it. It
/// does avoid having [`iroh::provider::Ticket`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
/// Retrieves backup from a legacy backup provider using iroh 0.4.
pub async fn get_legacy_backup(context: &Context, qr: Qr) -> Result<()> {
ensure!(
matches!(qr, Qr::Backup { .. }),
"QR code for backup must be of type DCBACKUP"
@@ -412,6 +306,64 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
res
}
pub async fn get_backup2(
context: &Context,
node_addr: iroh_net::NodeAddr,
auth_token: String,
) -> Result<()> {
let relay_mode = RelayMode::Disabled;
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind(0).await?;
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
info!(context, "Sending backup authentication token.");
send_stream.write_all(auth_token.as_bytes()).await?;
let passphrase = String::new();
info!(context, "Starting to read backup from the stream.");
let mut file_size_buf = [0u8; 8];
recv_stream.read_exact(&mut file_size_buf).await?;
let file_size = u64::from_be_bytes(file_size_buf);
import_backup_stream(context, recv_stream, file_size, passphrase)
.await
.context("Failed to import backup from QUIC stream")?;
info!(context, "Finished importing backup from the stream.");
context.emit_event(EventType::ImexProgress(1000));
// Send an acknowledgement, but ignore the errors.
// We have imported backup successfully already.
send_stream.write_all(b".").await.ok();
send_stream.finish().await.ok();
info!(context, "Sent backup reception acknowledgment.");
Ok(())
}
/// Contacts a backup provider and receives the backup from it.
///
/// This uses a QR code to contact another instance of deltachat which is providing a backup
/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in
/// the QR code and retrieve the backup.
///
/// This is a long running operation which will return only when completed.
///
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variants of it. It
/// does avoid having [`iroh_old::provider::Ticket`] in the primary API however, without
/// having to revert to untyped bytes.
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
match qr {
Qr::Backup { .. } => get_legacy_backup(context, qr).await?,
Qr::Backup2 {
node_addr,
auth_token,
} => get_backup2(context, node_addr, auth_token).await?,
_ => bail!("QR code for backup must be of type DCBACKUP or DCBACKUP2"),
}
Ok(())
}
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
let ticket = match qr {
Qr::Backup { ticket } => ticket,
@@ -458,7 +410,7 @@ async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()
// Perform the transfer.
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
let stats = iroh::get::run_ticket(
let stats = iroh_old::get::run_ticket(
ticket,
keylog,
MAX_CONCURRENT_DIALS,
@@ -490,7 +442,7 @@ async fn on_blob(
progress: &ProgressEmitter,
jobs: &Mutex<JoinSet<()>>,
ticket: &Ticket,
_hash: Hash,
_hash: iroh_old::Hash,
mut reader: DataStream,
name: String,
) -> Result<DataStream> {
@@ -672,24 +624,6 @@ mod tests {
.await;
}
#[test]
fn test_send_progress() {
let cases = [
((0, 100), 450),
((10, 100), 500),
((50, 100), 700),
((100, 100), 950),
];
for ((current_size, total_size), progress) in cases {
let out = EventType::from(SendProgress::TransferInProgress {
current_size,
total_size,
});
assert_eq!(out, EventType::ImexProgress(progress));
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_drop_provider() {
let mut tcm = TestContextManager::new();

View File

@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
use std::fmt;
use std::io::Cursor;
use anyhow::{ensure, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use deltachat_contact_tools::EmailAddress;
use num_traits::FromPrimitive;
@@ -46,7 +46,14 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
/// the ASCII-armored representation.
fn from_asc(data: &str) -> Result<(Self, BTreeMap<String, String>)> {
let bytes = data.as_bytes();
let (key, headers) = Self::from_armor_single(Cursor::new(bytes)).context("rPGP error")?;
let res = Self::from_armor_single(Cursor::new(bytes));
let (key, headers) = match res {
Err(pgp::errors::Error::NoMatchingPacket) => match Self::is_private() {
true => bail!("No private key packet found"),
false => bail!("No public key packet found"),
},
_ => res.context("rPGP error")?,
};
let headers = headers
.into_iter()
.map(|(key, values)| {
@@ -89,6 +96,8 @@ pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone {
fn fingerprint(&self) -> Fingerprint {
Fingerprint::new(KeyTrait::fingerprint(self))
}
fn is_private() -> bool;
}
pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPublicKey> {
@@ -187,6 +196,10 @@ impl DcKey for SignedPublicKey {
.unwrap_or_default();
std::string::String::from_utf8(buf).unwrap_or_default()
}
fn is_private() -> bool {
false
}
}
impl DcKey for SignedSecretKey {
@@ -202,6 +215,10 @@ impl DcKey for SignedSecretKey {
.unwrap_or_default();
std::string::String::from_utf8(buf).unwrap_or_default()
}
fn is_private() -> bool {
true
}
}
/// Deltachat extension trait for secret keys.

View File

@@ -2,7 +2,7 @@
use std::fmt;
use anyhow::{ensure, Result};
use anyhow::{ensure, Context as _, Result};
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
use crate::context::Context;
@@ -10,7 +10,7 @@ use crate::provider::Socket;
use crate::provider::{get_provider_by_id, Provider};
use crate::socks::Socks5Config;
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
#[repr(u32)]
#[strum(serialize_all = "snake_case")]
pub enum CertificateChecks {
@@ -30,6 +30,7 @@ pub enum CertificateChecks {
/// means that provider database setting should be taken.
/// If there is no provider database setting for certificate checks,
/// `Automatic` is the same as `Strict`.
#[default]
Automatic = 0,
Strict = 1,
@@ -41,12 +42,6 @@ pub enum CertificateChecks {
AcceptInvalidCertificates = 3,
}
impl Default for CertificateChecks {
fn default() -> Self {
Self::Automatic
}
}
/// Login parameters for a single server, either IMAP or SMTP
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct ServerLoginParam {
@@ -56,10 +51,6 @@ pub struct ServerLoginParam {
pub port: u16,
pub security: Socket,
pub oauth2: bool,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: CertificateChecks,
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
@@ -69,6 +60,10 @@ pub struct LoginParam {
pub smtp: ServerLoginParam,
pub provider: Option<&'static Provider>,
pub socks5_config: Option<Socks5Config>,
/// TLS options: whether to allow invalid certificates and/or
/// invalid hostnames
pub certificate_checks: CertificateChecks,
}
impl LoginParam {
@@ -130,10 +125,15 @@ impl LoginParam {
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
// The setting is named `imap_certificate_checks`
// for backwards compatibility,
// but now it is a global setting applied to all protocols,
// while `smtp_certificate_checks` is ignored.
let key = &format!("{prefix}imap_certificate_checks");
let imap_certificate_checks =
let certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
num_traits::FromPrimitive::from_i32(certificate_checks)
.with_context(|| format!("Invalid {key} value"))?
} else {
Default::default()
};
@@ -157,14 +157,6 @@ impl LoginParam {
.and_then(num_traits::FromPrimitive::from_i32)
.unwrap_or_default();
let key = &format!("{prefix}smtp_certificate_checks");
let smtp_certificate_checks =
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default()
} else {
Default::default()
};
let key = &format!("{prefix}server_flags");
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
@@ -186,7 +178,6 @@ impl LoginParam {
port: mail_port as u16,
security: mail_security,
oauth2,
certificate_checks: imap_certificate_checks,
},
smtp: ServerLoginParam {
server: send_server,
@@ -195,8 +186,8 @@ impl LoginParam {
port: send_port as u16,
security: send_security,
oauth2,
certificate_checks: smtp_certificate_checks,
},
certificate_checks,
provider,
socks5_config,
})
@@ -227,7 +218,7 @@ impl LoginParam {
.await?;
let key = &format!("{prefix}imap_certificate_checks");
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
sql.set_raw_config_int(key, self.certificate_checks as i32)
.await?;
let key = &format!("{prefix}send_server");
@@ -247,8 +238,9 @@ impl LoginParam {
sql.set_raw_config_int(key, self.smtp.security as i32)
.await?;
// This is only saved for compatibility reasons, but never loaded.
let key = &format!("{prefix}smtp_certificate_checks");
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
sql.set_raw_config_int(key, self.certificate_checks as i32)
.await?;
// The OAuth2 flag is either set for both IMAP and SMTP or not at all.
@@ -259,13 +251,25 @@ impl LoginParam {
};
sql.set_raw_config_int(key, server_flags).await?;
if let Some(provider) = self.provider {
let key = &format!("{prefix}provider");
sql.set_raw_config(key, Some(provider.id)).await?;
}
let key = &format!("{prefix}provider");
sql.set_raw_config(key, self.provider.map(|provider| provider.id))
.await?;
Ok(())
}
pub fn strict_tls(&self) -> bool {
let user_strict_tls = match self.certificate_checks {
CertificateChecks::Automatic => None,
CertificateChecks::Strict => Some(true),
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
};
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
user_strict_tls
.or(provider_strict_tls)
.unwrap_or(self.socks5_config.is_some())
}
}
impl fmt::Display for LoginParam {
@@ -275,7 +279,7 @@ impl fmt::Display for LoginParam {
write!(
f,
"{} imap:{}:{}:{}:{}:{}:cert_{}:{} smtp:{}:{}:{}:{}:{}:cert_{}:{}",
"{} imap:{}:{}:{}:{}:{}:{} smtp:{}:{}:{}:{}:{}:{} cert_{}",
unset_empty(&self.addr),
unset_empty(&self.imap.user),
if !self.imap.password.is_empty() {
@@ -286,7 +290,6 @@ impl fmt::Display for LoginParam {
unset_empty(&self.imap.server),
self.imap.port,
self.imap.security,
self.imap.certificate_checks,
if self.imap.oauth2 {
"OAUTH2"
} else {
@@ -301,12 +304,12 @@ impl fmt::Display for LoginParam {
unset_empty(&self.smtp.server),
self.smtp.port,
self.smtp.security,
self.smtp.certificate_checks,
if self.smtp.oauth2 {
"OAUTH2"
} else {
"AUTH_NORMAL"
},
self.certificate_checks
)
}
}
@@ -347,7 +350,6 @@ mod tests {
port: 123,
security: Socket::Starttls,
oauth2: false,
certificate_checks: CertificateChecks::Strict,
},
smtp: ServerLoginParam {
server: "smtp.example.com".to_string(),
@@ -356,16 +358,24 @@ mod tests {
port: 456,
security: Socket::Ssl,
oauth2: false,
certificate_checks: CertificateChecks::AcceptInvalidCertificates,
},
provider: get_provider_by_id("example.com"),
// socks5_config is not saved by `save_to_database`, using default value
socks5_config: None,
certificate_checks: CertificateChecks::Strict,
};
param.save_as_configured_params(&t).await?;
let loaded = LoginParam::load_configured_params(&t).await?;
assert_eq!(param, loaded);
// Remove provider.
let param = LoginParam {
provider: None,
..param
};
param.save_as_configured_params(&t).await?;
let loaded = LoginParam::load_configured_params(&t).await?;
assert_eq!(param, loaded);
Ok(())
}

View File

@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use tokio::{fs, io};
use crate::blob::BlobObject;
use crate::chat::{Chat, ChatId, ChatIdBlocked};
use crate::chat::{Chat, ChatId, ChatIdBlocked, ChatVisibility};
use crate::chatlist_events;
use crate::config::Config;
use crate::constants::{
@@ -81,7 +81,20 @@ impl MsgId {
pub async fn get_state(self, context: &Context) -> Result<MessageState> {
let result = context
.sql
.query_get_value("SELECT state FROM msgs WHERE id=?", (self,))
.query_row_optional(
concat!(
"SELECT m.state, mdns.msg_id",
" FROM msgs m LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE id=?",
" LIMIT 1",
),
(self,),
|row| {
let state: MessageState = row.get(0)?;
let mdn_msg_id: Option<MsgId> = row.get(1)?;
Ok(state.with_mdns(mdn_msg_id.is_some()))
},
)
.await?
.unwrap_or_default();
Ok(result)
@@ -519,6 +532,7 @@ impl Message {
" m.ephemeral_timestamp AS ephemeral_timestamp,",
" m.type AS type,",
" m.state AS state,",
" mdns.msg_id AS mdn_msg_id,",
" m.download_state AS download_state,",
" m.error AS error,",
" m.msgrmsg AS msgrmsg,",
@@ -529,11 +543,16 @@ impl Message {
" m.hidden AS hidden,",
" m.location_id AS location,",
" c.blocked AS blocked",
" FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id",
" WHERE m.id=? AND chat_id!=3;"
" FROM msgs m",
" LEFT JOIN chats c ON c.id=m.chat_id",
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE m.id=? AND chat_id!=3",
" LIMIT 1",
),
(id,),
|row| {
let state: MessageState = row.get("state")?;
let mdn_msg_id: Option<MsgId> = row.get("mdn_msg_id")?;
let text = match row.get_ref("txt")? {
rusqlite::types::ValueRef::Text(buf) => {
match String::from_utf8(buf.to_vec()) {
@@ -568,7 +587,7 @@ impl Message {
ephemeral_timer: row.get("ephemeral_timer")?,
ephemeral_timestamp: row.get("ephemeral_timestamp")?,
viewtype: row.get("type")?,
state: row.get("state")?,
state: state.with_mdns(mdn_msg_id.is_some()),
download_state: row.get("download_state")?,
error: Some(row.get::<_, String>("error")?)
.filter(|error| !error.is_empty()),
@@ -1157,6 +1176,27 @@ impl Message {
Ok(())
}
/// Sets message quote text.
///
/// If `text` is `Some((text_str, protect))`, `protect` specifies whether `text_str` should only
/// be sent encrypted. If it should, but the message is unencrypted, `text_str` is replaced with
/// "...".
pub fn set_quote_text(&mut self, text: Option<(String, bool)>) {
let Some((text, protect)) = text else {
self.param.remove(Param::Quote);
self.param.remove(Param::ProtectQuote);
return;
};
self.param.set(Param::Quote, text);
self.param.set_optional(
Param::ProtectQuote,
match protect {
true => Some("1"),
false => None,
},
);
}
/// Sets message quote.
///
/// Message-Id is used to set Reply-To field, message text is used for quote.
@@ -1173,31 +1213,27 @@ impl Message {
);
self.in_reply_to = Some(quote.rfc724_mid.clone());
if quote
.param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default()
{
self.param.set(Param::ProtectQuote, "1");
}
let text = quote.get_text();
self.param.set(
Param::Quote,
if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote
.get_summary(context, None)
.await?
.truncated_text(500)
.to_string()
} else {
text
},
);
let text = if text.is_empty() {
// Use summary, similar to "Image" to avoid sending empty quote.
quote
.get_summary(context, None)
.await?
.truncated_text(500)
.to_string()
} else {
text
};
self.set_quote_text(Some((
text,
quote
.param
.get_bool(Param::GuaranteeE2ee)
.unwrap_or_default(),
)));
} else {
self.in_reply_to = None;
self.param.remove(Param::Quote);
self.set_quote_text(None);
}
Ok(())
@@ -1336,7 +1372,7 @@ pub enum MessageState {
OutDelivered = 26,
/// Outgoing message read by the recipient (two checkmarks; this
/// requires goodwill on the receiver's side)
/// requires goodwill on the receiver's side). Not used in the db for new messages.
OutMdnRcvd = 28,
}
@@ -1379,6 +1415,14 @@ impl MessageState {
OutPreparing | OutDraft | OutPending | OutFailed | OutDelivered | OutMdnRcvd
)
}
/// Returns adjusted message state if the message has MDNs.
pub(crate) fn with_mdns(self, has_mdns: bool) -> Self {
if self == MessageState::OutDelivered && has_mdns {
return MessageState::OutMdnRcvd;
}
self
}
}
/// Returns contacts that sent read receipts and the time of reading.
@@ -1647,6 +1691,7 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
m.param AS param,
m.from_id AS from_id,
m.rfc724_mid AS rfc724_mid,
c.archived AS archived,
c.blocked AS blocked
FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id
WHERE m.id IN ({}) AND m.chat_id>9",
@@ -1660,16 +1705,20 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
let param: Params = row.get::<_, String>("param")?.parse().unwrap_or_default();
let from_id: ContactId = row.get("from_id")?;
let rfc724_mid: String = row.get("rfc724_mid")?;
let visibility: ChatVisibility = row.get("archived")?;
let blocked: Option<Blocked> = row.get("blocked")?;
let ephemeral_timer: EphemeralTimer = row.get("ephemeral_timer")?;
Ok((
id,
chat_id,
state,
param,
from_id,
rfc724_mid,
blocked.unwrap_or_default(),
(
id,
chat_id,
state,
param,
from_id,
rfc724_mid,
visibility,
blocked.unwrap_or_default(),
),
ephemeral_timer,
))
},
@@ -1677,25 +1726,28 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
)
.await?;
if msgs.iter().any(
|(_id, _chat_id, _state, _param, _from_id, _rfc724_mid, _blocked, ephemeral_timer)| {
*ephemeral_timer != EphemeralTimer::Disabled
},
) {
if msgs
.iter()
.any(|(_, ephemeral_timer)| *ephemeral_timer != EphemeralTimer::Disabled)
{
start_ephemeral_timers_msgids(context, &msg_ids)
.await
.context("failed to start ephemeral timers")?;
}
let mut updated_chat_ids = BTreeSet::new();
let mut archived_chats_maybe_noticed = false;
for (
id,
curr_chat_id,
curr_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
curr_blocked,
(
id,
curr_chat_id,
curr_state,
curr_param,
curr_from_id,
curr_rfc724_mid,
curr_visibility,
curr_blocked,
),
_curr_ephemeral_timer,
) in msgs
{
@@ -1717,28 +1769,31 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
if curr_blocked == Blocked::Not
&& curr_param.get_bool(Param::WantsMdn).unwrap_or_default()
&& curr_param.get_cmd() == SystemMessage::Unknown
&& context.should_send_mdns().await?
{
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if mdns_enabled {
context
.sql
.execute(
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
(id, curr_from_id, curr_rfc724_mid),
)
.await
.context("failed to insert into smtp_mdns")?;
context.scheduler.interrupt_smtp().await;
}
context
.sql
.execute(
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
(id, curr_from_id, curr_rfc724_mid),
)
.await
.context("failed to insert into smtp_mdns")?;
context.scheduler.interrupt_smtp().await;
}
updated_chat_ids.insert(curr_chat_id);
}
archived_chats_maybe_noticed |=
curr_state == MessageState::InFresh && curr_visibility == ChatVisibility::Archived;
}
for updated_chat_id in updated_chat_ids {
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
}
if archived_chats_maybe_noticed {
context.on_archived_chats_maybe_noticed();
}
Ok(())
}
@@ -1748,6 +1803,10 @@ pub(crate) async fn update_msg_state(
msg_id: MsgId,
state: MessageState,
) -> Result<()> {
ensure!(
state != MessageState::OutMdnRcvd,
"Update msgs_mdns table instead!"
);
ensure!(state != MessageState::OutFailed, "use set_msg_failed()!");
let error_subst = match state >= MessageState::OutPending {
true => ", error=''",
@@ -2325,12 +2384,23 @@ mod tests {
// Alice quotes encrypted message in unencrypted chat.
let mut msg = Message::new(Viewtype::Text);
msg.set_quote(alice, Some(&alice_received_message)).await?;
msg.set_text("unencrypted".to_string());
chat::send_msg(alice, alice_group, &mut msg).await?;
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
assert_eq!(bob_received_message.get_showpadlock(), false);
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_quote(alice, Some(&alice_received_message)).await?;
msg1.set_quote(alice, Some(&msg)).await?;
chat::send_msg(alice, alice_group, &mut msg1).await?;
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
assert_eq!(bob_received_message.get_showpadlock(), false);
Ok(())
}
@@ -2524,9 +2594,6 @@ mod tests {
let payload = alice.pop_sent_msg().await;
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;

View File

@@ -31,6 +31,7 @@ use crate::tools::IsNoneOrEmpty;
use crate::tools::{
create_outgoing_rfc724_mid, create_smeared_timestamp, remove_subject_prefix, time,
};
use crate::webxdc::StatusUpdateSerial;
use crate::{location, peer_channels};
// attachments of 25 mb brutto should work on the majority of providers
@@ -185,7 +186,7 @@ impl MimeFactory {
if !msg.is_system_message()
&& msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
&& context.get_config_bool(Config::MdnsEnabled).await?
&& context.should_request_mdns().await?
{
req_mdn = true;
}
@@ -453,7 +454,7 @@ impl MimeFactory {
};
stock_str::subject_for_new_contact(context, self_name).await
}
Loaded::Mdn { .. } => stock_str::read_rcpt(context).await,
Loaded::Mdn { .. } => "Receipt Notification".to_string(), // untranslated to no reveal sender's language
};
Ok(subject)
@@ -671,7 +672,7 @@ impl MimeFactory {
})
}
}
Loaded::Mdn { .. } => self.render_mdn(context).await?,
Loaded::Mdn { .. } => self.render_mdn()?,
};
let get_content_type_directives_header = || {
@@ -725,18 +726,18 @@ impl MimeFactory {
} else if header_name == "autocrypt" {
unprotected_headers.push(header.clone());
} else if header_name == "from" {
protected_headers.push(header.clone());
if is_encrypted && verified || is_securejoin_message {
unprotected_headers.push(
Header::new_with_value(
header.name,
vec![Address::new_mailbox(self.from_addr.clone())],
)
.unwrap(),
);
} else {
unprotected_headers.push(header);
// Unencrypted securejoin messages should _not_ include the display name:
if is_encrypted || !is_securejoin_message {
protected_headers.push(header.clone());
}
unprotected_headers.push(
Header::new_with_value(
header.name,
vec![Address::new_mailbox(self.from_addr.clone())],
)
.unwrap(),
);
} else if header_name == "to" {
protected_headers.push(header.clone());
if is_encrypted {
@@ -773,7 +774,7 @@ impl MimeFactory {
match header_name.as_str() {
"subject" => {
unprotected_headers.push(Header::new(header.name, "...".to_string()));
unprotected_headers.push(Header::new(header.name, "[...]".to_string()));
}
"date"
| "in-reply-to"
@@ -901,12 +902,11 @@ impl MimeFactory {
.fold(message, |message, header| message.header(header.clone()));
if skip_autocrypt || !context.get_config_bool(Config::SignUnencrypted).await? {
let protected: HashSet<Header> = HashSet::from_iter(protected_headers.into_iter());
for h in unprotected_headers.split_off(0) {
if !protected.contains(&h) {
unprotected_headers.push(h);
}
}
// Deduplicate unprotected headers that also are in the protected headers:
let protected: HashSet<&str> =
HashSet::from_iter(protected_headers.iter().map(|h| h.name.as_str()));
unprotected_headers.retain(|h| !protected.contains(&h.name.as_str()));
message
} else {
let message = message.header(get_content_type_directives_header());
@@ -1369,8 +1369,13 @@ impl MimeFactory {
} else if msg.viewtype == Viewtype::Webxdc {
let topic = peer_channels::create_random_topic();
headers.push(create_iroh_header(context, topic, msg.id).await?);
if let Some(json) = context
.render_webxdc_status_update_object(msg.id, None)
if let (Some(json), _) = context
.render_webxdc_status_update_object(
msg.id,
StatusUpdateSerial::MIN,
StatusUpdateSerial::MAX,
None,
)
.await?
{
parts.push(context.build_status_update_part(&json));
@@ -1394,7 +1399,7 @@ impl MimeFactory {
}
/// Render an MDN
async fn render_mdn(&mut self, context: &Context) -> Result<PartBuilder> {
fn render_mdn(&mut self) -> Result<PartBuilder> {
// RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report
//
@@ -1420,16 +1425,15 @@ impl MimeFactory {
"multipart/report; report-type=disposition-notification".to_string(),
));
// first body part: always human-readable, always REQUIRED by RFC 6522
let message_text = format!(
"{}\r\n",
format_flowed(&stock_str::read_rcpt_mail_body(context).await)
);
// first body part: always human-readable, always REQUIRED by RFC 6522.
// untranslated to no reveal sender's language.
// moreover, translations in unknown languages are confusing, and clients may not display them at all
let text_part = PartBuilder::new().header((
"Content-Type".to_string(),
"text/plain; charset=utf-8; format=flowed; delsp=no".to_string(),
));
let text_part = self.add_message_text(text_part, message_text);
let text_part =
self.add_message_text(text_part, "This is a receipt notification.\r\n".to_string());
message = message.child(text_part.build());
// second body part: machine-readable, always REQUIRED by RFC 6522
@@ -2505,6 +2509,7 @@ mod tests {
.await?;
let sent = bob.send_msg(chat, &mut msg).await;
assert!(msg.get_showpadlock());
assert!(sent.payload.contains("\r\nSubject: [...]\r\n"));
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");

View File

@@ -11,6 +11,7 @@ use deltachat_derive::{FromSql, ToSql};
use format_flowed::unformat_flowed;
use lettre_email::mime::Mime;
use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
use rand::distributions::{Alphanumeric, DistString};
use crate::aheader::{Aheader, EncryptPreference};
use crate::blob::BlobObject;
@@ -27,10 +28,7 @@ use crate::dehtml::dehtml;
use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
use crate::message::{
self, get_vcard_summary, set_msg_failed, update_msg_state, Message, MessageState, MsgId,
Viewtype,
};
use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::simplify::{simplify, SimplifiedText};
@@ -395,13 +393,10 @@ impl MimeMessage {
&mail.headers,
);
if let (Some(inner_from), true) = (inner_from, !signatures.is_empty()) {
if addr_cmp(&inner_from.addr, &from.addr) {
from_is_signed = true;
from = inner_from;
} else {
// There is a From: header in the encrypted &
// signed part, but it doesn't match the outer one.
if let Some(inner_from) = inner_from {
if !addr_cmp(&inner_from.addr, &from.addr) {
// There is a From: header in the encrypted
// part, but it doesn't match the outer one.
// This _might_ be because the sender's mail server
// replaced the sending address, e.g. in a mailing list.
// Or it's because someone is doing some replay attack.
@@ -410,7 +405,7 @@ impl MimeMessage {
// so we return an error below.
warn!(
context,
"From header in signed part doesn't match the outer one",
"From header in encrypted part doesn't match the outer one",
);
// Return an error from the parser.
@@ -419,6 +414,8 @@ impl MimeMessage {
// as if the MIME structure is broken.
bail!("From header is forged");
}
from = inner_from;
from_is_signed = !signatures.is_empty();
}
}
if signatures.is_empty() {
@@ -785,7 +782,15 @@ impl MimeMessage {
.collect::<String>()
.strip_prefix("base64:")
{
match BlobObject::store_from_base64(context, base64, "avatar").await {
// Add random suffix to the filename
// to prevent the UI from accidentally using
// cached "avatar.jpg".
let suffix = Alphanumeric
.sample_string(&mut rand::thread_rng(), 7)
.to_lowercase();
match BlobObject::store_from_base64(context, base64, &format!("avatar-{suffix}")).await
{
Ok(path) => Some(AvatarAction::Change(path)),
Err(err) => {
warn!(
@@ -2149,24 +2154,32 @@ async fn handle_mdn(
return Ok(());
}
let Some((msg_id, chat_id, msg_state)) = context
let Some((msg_id, chat_id, has_mdns, is_dup)) = context
.sql
.query_row_optional(
concat!(
"SELECT",
" m.id AS msg_id,",
" c.id AS chat_id,",
" m.state AS state",
" FROM msgs m LEFT JOIN chats c ON m.chat_id=c.id",
" mdns.contact_id AS mdn_contact",
" FROM msgs m ",
" LEFT JOIN chats c ON m.chat_id=c.id",
" LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
" WHERE rfc724_mid=? AND from_id=1",
" ORDER BY m.id"
" ORDER BY msg_id DESC, mdn_contact=? DESC",
" LIMIT 1",
),
(&rfc724_mid,),
(&rfc724_mid, from_id),
|row| {
let msg_id: MsgId = row.get("msg_id")?;
let chat_id: ChatId = row.get("chat_id")?;
let msg_state: MessageState = row.get("state")?;
Ok((msg_id, chat_id, msg_state))
let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
Ok((
msg_id,
chat_id,
mdn_contact.is_some(),
mdn_contact == Some(from_id),
))
},
)
.await?
@@ -2178,28 +2191,17 @@ async fn handle_mdn(
return Ok(());
};
if !context
.sql
.exists(
"SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=? AND contact_id=?",
(msg_id, from_id),
)
.await?
{
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
(msg_id, from_id, timestamp_sent),
)
.await?;
if is_dup {
return Ok(());
}
if msg_state == MessageState::OutPreparing
|| msg_state == MessageState::OutPending
|| msg_state == MessageState::OutDelivered
{
update_msg_state(context, msg_id, MessageState::OutMdnRcvd).await?;
context
.sql
.execute(
"INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
(msg_id, from_id, timestamp_sent),
)
.await?;
if !has_mdns {
context.emit_event(EventType::MsgRead { chat_id, msg_id });
// note(treefit): only matters if it is the last message in chat (but probably too expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, chat_id);
@@ -2307,7 +2309,7 @@ mod tests {
chat,
chatlist::Chatlist,
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
message::MessengerMessage,
message::{MessageState, MessengerMessage},
receive_imf::receive_imf,
test_utils::{TestContext, TestContextManager},
tools::time,

View File

@@ -1,206 +1,88 @@
//! # Common network utilities.
use std::net::{IpAddr, SocketAddr};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::net::SocketAddr;
use std::pin::Pin;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{format_err, Context as _, Result};
use tokio::net::{lookup_host, TcpStream};
use async_native_tls::TlsStream;
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_io_timeout::TimeoutStream;
use crate::context::Context;
use crate::tools::time;
pub(crate) mod dns;
pub(crate) mod http;
pub(crate) mod session;
pub(crate) mod tls;
use dns::lookup_host_with_cache;
pub use http::{read_url, read_url_blob, Response as HttpResponse};
use tls::wrap_tls;
async fn connect_tcp_inner(addr: SocketAddr, timeout_val: Duration) -> Result<TcpStream> {
let tcp_stream = timeout(timeout_val, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
Ok(tcp_stream)
}
async fn lookup_host_with_timeout(
hostname: &str,
port: u16,
timeout_val: Duration,
) -> Result<Vec<SocketAddr>> {
let res = timeout(timeout_val, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?;
Ok(res.collect())
}
/// Looks up hostname and port using DNS and updates the address resolution cache.
/// Connection, write and read timeout.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end
/// or entries from fallback cache if there are no cached addresses.
async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Vec<SocketAddr>> {
/// This constant should be more than the largest expected RTT.
pub(crate) const TIMEOUT: Duration = Duration::from_secs(60);
/// TTL for caches in seconds.
pub(crate) const CACHE_TTL: u64 = 30 * 24 * 60 * 60;
/// Removes connection history entries after `CACHE_TTL`.
pub(crate) async fn prune_connection_history(context: &Context) -> Result<()> {
let now = time();
let mut resolved_addrs = match lookup_host_with_timeout(hostname, port, timeout_val).await {
Ok(res) => res,
Err(err) => {
warn!(
context,
"DNS resolution for {}:{} failed: {:#}.", hostname, port, err
);
Vec::new()
}
};
context
.sql
.execute(
"DELETE FROM connection_history
WHERE ? > timestamp + ?",
(now, CACHE_TTL),
)
.await?;
Ok(())
}
for addr in &resolved_addrs {
let ip_string = addr.ip().to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}
pub(crate) async fn update_connection_history(
context: &Context,
alpn: &str,
host: &str,
port: u16,
addr: &str,
now: i64,
) -> Result<()> {
context
.sql
.execute(
"INSERT INTO connection_history (host, port, alpn, addr, timestamp)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (host, port, alpn, addr)
DO UPDATE SET timestamp=excluded.timestamp",
(host, port, alpn, addr, now),
)
.await?;
Ok(())
}
info!(context, "Resolved {}:{} into {}.", hostname, port, &addr);
// Update the cache.
context
.sql
.execute(
"INSERT INTO dns_cache
(hostname, address, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (hostname, address)
DO UPDATE SET timestamp=excluded.timestamp",
(hostname, ip_string, now),
)
.await?;
}
if load_cache {
for cached_address in context
.sql
.query_map(
"SELECT address
FROM dns_cache
WHERE hostname = ?
AND ? < timestamp + 30 * 24 * 3600
ORDER BY timestamp DESC",
(hostname, now),
|row| {
let address: String = row.get(0)?;
Ok(address)
},
|rows| {
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
},
)
.await?
{
match IpAddr::from_str(&cached_address) {
Ok(ip_addr) => {
let addr = SocketAddr::new(ip_addr, port);
if !resolved_addrs.contains(&addr) {
resolved_addrs.push(addr);
}
}
Err(err) => {
warn!(
context,
"Failed to parse cached address {:?}: {:#}.", cached_address, err
);
}
}
}
if resolved_addrs.is_empty() {
// Load hardcoded cache if everything else fails.
//
// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
//
// In the future we may pre-resolve all provider database addresses
// and build them in.
match hostname {
"mail.sangham.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0xc17, 0x798c, 0, 0, 0, 1)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(159, 69, 186, 85)),
port,
));
}
"nine.testrun.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236)),
port,
));
}
"disroot.org" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(178, 21, 23, 139)),
port,
));
}
"mail.riseup.net" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 70)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(198, 252, 153, 71)),
port,
));
}
"imap.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x400c, 0xc1f, 0, 0, 0, 0x6d)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 108)),
port,
));
}
"smtp.gmail.com" => {
resolved_addrs.push(SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0x2a00, 0x1450, 0x4013, 0xc04, 0, 0, 0, 0x6c)),
port,
));
resolved_addrs.push(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(142, 250, 110, 109)),
port,
));
}
_ => {}
}
}
}
Ok(resolved_addrs)
pub(crate) async fn load_connection_timestamp(
context: &Context,
alpn: &str,
host: &str,
port: u16,
addr: &str,
) -> Result<Option<i64>> {
let timestamp = context
.sql
.query_get_value(
"SELECT timestamp FROM connection_history
WHERE host = ?
AND port = ?
AND alpn = ?
AND addr = ?",
(host, port, alpn, addr),
)
.await?;
Ok(timestamp)
}
/// Returns a TCP connection stream with read/write timeouts set
@@ -208,7 +90,37 @@ async fn lookup_host_with_cache(
///
/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet
/// to the network, which is important to reduce the latency of interactive protocols such as IMAP.
///
pub(crate) async fn connect_tcp_inner(
addr: SocketAddr,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
// Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(TIMEOUT));
timeout_stream.set_read_timeout(Some(TIMEOUT));
Ok(Box::pin(timeout_stream))
}
/// Attempts to establish TLS connection
/// given the result of the hostname to address resolution.
pub(crate) async fn connect_tls_inner(
addr: SocketAddr,
host: &str,
strict_tls: bool,
alpn: &[&str],
) -> Result<TlsStream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream = connect_tcp_inner(addr).await?;
let tls_stream = wrap_tls(strict_tls, host, alpn, tcp_stream).await?;
Ok(tls_stream)
}
/// If `load_cache` is true, may use cached DNS results.
/// Because the cache may be poisoned with incorrect results by networks hijacking DNS requests,
/// this option should only be used when connection is authenticated,
@@ -219,57 +131,24 @@ pub(crate) async fn connect_tcp(
context: &Context,
host: &str,
port: u16,
timeout_val: Duration,
load_cache: bool,
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let mut tcp_stream = None;
let mut last_error = None;
let mut first_error = None;
for resolved_addr in
lookup_host_with_cache(context, host, port, timeout_val, load_cache).await?
{
match connect_tcp_inner(resolved_addr, timeout_val).await {
for resolved_addr in lookup_host_with_cache(context, host, port, "", load_cache).await? {
match connect_tcp_inner(resolved_addr).await {
Ok(stream) => {
tcp_stream = Some(stream);
// Maximize priority of this cached entry.
context
.sql
.execute(
"UPDATE dns_cache
SET timestamp = ?
WHERE address = ?",
(time(), resolved_addr.ip().to_string()),
)
.await?;
break;
return Ok(stream);
}
Err(err) => {
warn!(
context,
"Failed to connect to {}: {:#}.", resolved_addr, err
);
last_error = Some(err);
first_error.get_or_insert(err);
}
}
}
let tcp_stream = match tcp_stream {
Some(tcp_stream) => tcp_stream,
None => {
return Err(
last_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}"))
);
}
};
// Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?;
let mut timeout_stream = TimeoutStream::new(tcp_stream);
timeout_stream.set_write_timeout(Some(timeout_val));
timeout_stream.set_read_timeout(Some(timeout_val));
let pinned_stream = Box::pin(timeout_stream);
Ok(pinned_stream)
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}

870
src/net/dns.rs Normal file
View File

@@ -0,0 +1,870 @@
//! DNS resolution and cache.
use anyhow::{Context as _, Result};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use tokio::net::lookup_host;
use tokio::time::timeout;
use super::load_connection_timestamp;
use crate::context::Context;
use crate::tools::time;
use once_cell::sync::Lazy;
/// Inserts entry into DNS cache
/// or updates existing one with a new timestamp.
async fn update_cache(context: &Context, host: &str, addr: &str, now: i64) -> Result<()> {
context
.sql
.execute(
"INSERT INTO dns_cache
(hostname, address, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (hostname, address)
DO UPDATE SET timestamp=excluded.timestamp",
(host, addr, now),
)
.await?;
Ok(())
}
pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
let now = time();
context
.sql
.execute(
"DELETE FROM dns_cache
WHERE ? > timestamp + ?",
(now, super::CACHE_TTL),
)
.await?;
Ok(())
}
/// Looks up the hostname and updates DNS cache
/// on success.
async fn lookup_host_and_update_cache(
context: &Context,
hostname: &str,
port: u16,
now: i64,
) -> Result<Vec<SocketAddr>> {
let res: Vec<SocketAddr> = timeout(super::TIMEOUT, lookup_host((hostname, port)))
.await
.context("DNS lookup timeout")?
.context("DNS lookup failure")?
.collect();
for addr in &res {
let ip_string = addr.ip().to_string();
if ip_string == hostname {
// IP address resolved into itself, not interesting to cache.
continue;
}
info!(context, "Resolved {hostname}:{port} into {addr}.");
// Update the cache.
update_cache(context, hostname, &ip_string, now).await?;
}
Ok(res)
}
// Updates timestamp of the cached entry
// or inserts a new one if cached entry does not exist.
//
// This function should be called when a successful TLS
// connection is established with strict TLS checks.
//
// This increases priority of existing cached entries
// and copies fallback addresses from built-in cache
// into database cache on successful use.
//
// Unlike built-in cache,
// database cache is used even if DNS
// resolver returns a non-empty
// (but potentially incorrect and unusable) result.
pub(crate) async fn update_connect_timestamp(
context: &Context,
host: &str,
address: &str,
) -> Result<()> {
if host == address {
return Ok(());
}
context
.sql
.execute(
"INSERT INTO dns_cache (hostname, address, timestamp)
VALUES (?, ?, ?)
ON CONFLICT (hostname, address)
DO UPDATE SET timestamp=excluded.timestamp",
(host, address, time()),
)
.await?;
Ok(())
}
static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::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))],
),
(
"smtp.163.com",
vec![IpAddr::V4(Ipv4Addr::new(103, 129, 252, 45))],
),
(
"imap.aol.com",
vec![
IpAddr::V4(Ipv4Addr::new(212, 82, 101, 33)),
IpAddr::V4(Ipv4Addr::new(87, 248, 98, 69)),
],
),
(
"smtp.aol.com",
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 31))],
),
(
"mail.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
),
(
"imap.arcor.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
),
(
"imap.fastmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 43)),
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 58)),
],
),
(
"smtp.fastmail.com",
vec![
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 45)),
IpAddr::V4(Ipv4Addr::new(103, 168, 172, 60)),
],
),
(
"imap.gmx.net",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 170)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 186)),
],
),
(
"imap.mail.de",
vec![IpAddr::V4(Ipv4Addr::new(62, 201, 172, 16))],
),
(
"smtp.mailbox.org",
vec![IpAddr::V4(Ipv4Addr::new(185, 97, 174, 196))],
),
(
"imap.mailbox.org",
vec![IpAddr::V4(Ipv4Addr::new(185, 97, 174, 199))],
),
(
"imap.naver.com",
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 153))],
),
(
"imap.ouvaton.coop",
vec![IpAddr::V4(Ipv4Addr::new(194, 36, 166, 20))],
),
(
"imap.purelymail.com",
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
),
(
"imap.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 10))],
),
(
"smtp.tiscali.it",
vec![IpAddr::V4(Ipv4Addr::new(213, 205, 33, 13))],
),
(
"imap.web.de",
vec![
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 162)),
IpAddr::V4(Ipv4Addr::new(212, 227, 17, 178)),
],
),
(
"imap.ziggo.nl",
vec![IpAddr::V4(Ipv4Addr::new(84, 116, 6, 3))],
),
(
"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)),
],
),
(
"mail.infomaniak.com",
vec![
IpAddr::V4(Ipv4Addr::new(83, 166, 143, 44)),
IpAddr::V4(Ipv4Addr::new(83, 166, 143, 45)),
],
),
(
"mail.mymagenta.at",
vec![IpAddr::V4(Ipv4Addr::new(80, 109, 253, 241))],
),
(
"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))],
),
(
"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)),
],
),
(
"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![
IpAddr::V4(Ipv4Addr::new(185, 67, 36, 168)),
IpAddr::V4(Ipv4Addr::new(185, 67, 36, 169)),
],
),
(
"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))],
),
(
"smtp.mail.ru",
vec![
IpAddr::V4(Ipv4Addr::new(217, 69, 139, 160)),
IpAddr::V4(Ipv4Addr::new(94, 100, 180, 160)),
],
),
(
"imap.mail.yahoo.com",
vec![
IpAddr::V4(Ipv4Addr::new(87, 248, 103, 8)),
IpAddr::V4(Ipv4Addr::new(212, 82, 101, 24)),
],
),
(
"smtp.mail.yahoo.com",
vec![IpAddr::V4(Ipv4Addr::new(87, 248, 97, 36))],
),
(
"imap.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
),
(
"smtp.mailo.com",
vec![IpAddr::V4(Ipv4Addr::new(213, 182, 54, 20))],
),
(
"smtp.naver.com",
vec![IpAddr::V4(Ipv4Addr::new(125, 209, 238, 155))],
),
(
"smtp.ouvaton.coop",
vec![IpAddr::V4(Ipv4Addr::new(194, 36, 166, 20))],
),
(
"smtp.purelymail.com",
vec![IpAddr::V4(Ipv4Addr::new(18, 204, 123, 63))],
),
(
"imap.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
),
(
"smtp.qq.com",
vec![IpAddr::V4(Ipv4Addr::new(43, 129, 255, 54))],
),
(
"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, 170)),
],
),
(
"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)),
],
),
(
"imap.vivaldi.net",
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 15))],
),
(
"smtp.vivaldi.net",
vec![IpAddr::V4(Ipv4Addr::new(31, 209, 137, 12))],
),
(
"imap.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 230))],
),
(
"smtp.vodafonemail.de",
vec![IpAddr::V4(Ipv4Addr::new(2, 207, 150, 234))],
),
(
"smtp.web.de",
vec![
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 108)),
IpAddr::V4(Ipv4Addr::new(213, 165, 67, 124)),
],
),
(
"imap.yandex.com",
vec![IpAddr::V4(Ipv4Addr::new(77, 88, 21, 125))],
),
(
"smtp.yandex.com",
vec![IpAddr::V4(Ipv4Addr::new(77, 88, 21, 158))],
),
(
"smtp.ziggo.nl",
vec![IpAddr::V4(Ipv4Addr::new(84, 116, 6, 3))],
),
(
"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))],
),
])
});
/// Load hardcoded cache if everything else fails.
///
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
///
/// In the future we may pre-resolve all provider database addresses
/// and build them in.
fn load_hardcoded_cache(hostname: &str, port: u16) -> Vec<SocketAddr> {
if let Some(ips) = DNS_PRELOAD.get(hostname) {
ips.iter().map(|ip| SocketAddr::new(*ip, port)).collect()
} else {
Vec::new()
}
}
async fn lookup_cache(
context: &Context,
host: &str,
port: u16,
alpn: &str,
now: i64,
) -> Result<Vec<SocketAddr>> {
let mut res = Vec::new();
for cached_address in context
.sql
.query_map(
"SELECT dns_cache.address
FROM dns_cache
LEFT JOIN connection_history
ON dns_cache.hostname = connection_history.host
AND dns_cache.address = connection_history.addr
AND connection_history.port = ?
AND connection_history.alpn = ?
WHERE dns_cache.hostname = ?
AND ? < dns_cache.timestamp + ?
ORDER BY IFNULL(connection_history.timestamp, dns_cache.timestamp) DESC
LIMIT 50",
(port, alpn, host, now, super::CACHE_TTL),
|row| {
let address: String = row.get(0)?;
Ok(address)
},
|rows| {
rows.collect::<std::result::Result<Vec<String>, _>>()
.map_err(Into::into)
},
)
.await?
{
match IpAddr::from_str(&cached_address) {
Ok(ip_addr) => {
let addr = SocketAddr::new(ip_addr, port);
res.push(addr);
}
Err(err) => {
warn!(
context,
"Failed to parse cached address {:?}: {:#}.", cached_address, err
);
}
}
}
Ok(res)
}
/// Sorts DNS resolution results by connection timestamp in descending order
/// so IP addresses that we recently connected to successfully are tried first.
async fn sort_by_connection_timestamp(
context: &Context,
input: Vec<SocketAddr>,
alpn: &str,
host: &str,
) -> Result<Vec<SocketAddr>> {
let mut res: Vec<(Option<i64>, SocketAddr)> = Vec::new();
for addr in input {
let timestamp =
load_connection_timestamp(context, alpn, host, addr.port(), &addr.ip().to_string())
.await?;
res.push((timestamp, addr));
}
res.sort_by_key(|(ts, _addr)| std::cmp::Reverse(*ts));
Ok(res.into_iter().map(|(_ts, addr)| addr).collect())
}
/// Looks up hostname and port using DNS and updates the address resolution cache.
///
/// `alpn` is used to sort DNS results by the time we have successfully
/// connected to the IP address using given `alpn`.
/// If result sorting is not needed or `alpn` is unknown,
/// pass empty string here, e.g. for HTTP requests
/// or when resolving the IP address of SOCKS proxy.
///
/// If `load_cache` is true, appends cached results not older than 30 days to the end
/// or entries from fallback cache if there are no cached addresses.
pub(crate) async fn lookup_host_with_cache(
context: &Context,
hostname: &str,
port: u16,
alpn: &str,
load_cache: bool,
) -> Result<Vec<SocketAddr>> {
let now = time();
let mut resolved_addrs = match lookup_host_and_update_cache(context, hostname, port, now).await
{
Ok(res) => res,
Err(err) => {
warn!(
context,
"DNS resolution for {hostname}:{port} failed: {err:#}."
);
Vec::new()
}
};
if !alpn.is_empty() {
resolved_addrs =
sort_by_connection_timestamp(context, resolved_addrs, alpn, hostname).await?;
}
if load_cache {
for addr in lookup_cache(context, hostname, port, alpn, now).await? {
if !resolved_addrs.contains(&addr) {
resolved_addrs.push(addr);
}
}
if resolved_addrs.is_empty() {
return Ok(load_hardcoded_cache(hostname, port));
}
}
Ok(resolved_addrs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::net::update_connection_history;
use crate::test_utils::TestContext;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_sort_by_connection_timestamp() {
let alice = &TestContext::new_alice().await;
let now = time();
let ipv6_addr = IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2));
let ipv4_addr = IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236));
assert_eq!(
sort_by_connection_timestamp(
alice,
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
],
"imap",
"nine.testrun.org"
)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
]
);
update_connection_history(
alice,
"imap",
"nine.testrun.org",
993,
"116.202.233.236",
now,
)
.await
.unwrap();
assert_eq!(
sort_by_connection_timestamp(
alice,
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
],
"imap",
"nine.testrun.org"
)
.await
.unwrap(),
vec![
SocketAddr::new(ipv4_addr, 993),
SocketAddr::new(ipv6_addr, 993),
]
);
assert_eq!(
sort_by_connection_timestamp(
alice,
vec![
SocketAddr::new(ipv6_addr, 465),
SocketAddr::new(ipv4_addr, 465)
],
"smtp",
"nine.testrun.org"
)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 465),
SocketAddr::new(ipv4_addr, 465),
]
);
update_connection_history(
alice,
"smtp",
"nine.testrun.org",
465,
"116.202.233.236",
now,
)
.await
.unwrap();
assert_eq!(
sort_by_connection_timestamp(
alice,
vec![
SocketAddr::new(ipv6_addr, 465),
SocketAddr::new(ipv4_addr, 465)
],
"smtp",
"nine.testrun.org"
)
.await
.unwrap(),
vec![
SocketAddr::new(ipv4_addr, 465),
SocketAddr::new(ipv6_addr, 465),
]
);
update_connection_history(
alice,
"imap",
"nine.testrun.org",
993,
"2a01:4f8:241:4ce8::2",
now,
)
.await
.unwrap();
assert_eq!(
sort_by_connection_timestamp(
alice,
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
],
"imap",
"nine.testrun.org"
)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_lookup_cache() {
let alice = &TestContext::new_alice().await;
let ipv4_addr = IpAddr::V4(Ipv4Addr::new(116, 202, 233, 236));
let ipv6_addr = IpAddr::V6(Ipv6Addr::new(0x2a01, 0x4f8, 0x241, 0x4ce8, 0, 0, 0, 2));
let now = time();
assert!(lookup_cache(alice, "nine.testrun.org", 587, "smtp", now)
.await
.unwrap()
.is_empty());
update_cache(alice, "nine.testrun.org", "116.202.233.236", now)
.await
.unwrap();
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 587, "smtp", now)
.await
.unwrap(),
vec![SocketAddr::new(ipv4_addr, 587)]
);
// Cache should be returned for other ports and no ALPN as well,
// port and ALPN should only affect the order
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 443, "", now)
.await
.unwrap(),
vec![SocketAddr::new(ipv4_addr, 443)]
);
update_cache(alice, "nine.testrun.org", "2a01:4f8:241:4ce8::2", now + 30)
.await
.unwrap();
// New DNS cache entry should go first.
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 443, "", now + 60)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 443),
SocketAddr::new(ipv4_addr, 443)
],
);
// After successful connection to SMTP over port 465 using IPv4 address,
// IPv4 address has higher priority.
update_connection_history(
alice,
"smtp",
"nine.testrun.org",
465,
"116.202.233.236",
now + 100,
)
.await
.unwrap();
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 465, "smtp", now + 120)
.await
.unwrap(),
vec![
SocketAddr::new(ipv4_addr, 465),
SocketAddr::new(ipv6_addr, 465)
]
);
// For other ports and ALPNs order remains the same.
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 993, "imap", now + 120)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
],
);
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 465, "imap", now + 120)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 465),
SocketAddr::new(ipv4_addr, 465)
],
);
assert_eq!(
lookup_cache(alice, "nine.testrun.org", 993, "smtp", now + 120)
.await
.unwrap(),
vec![
SocketAddr::new(ipv6_addr, 993),
SocketAddr::new(ipv4_addr, 993)
],
);
}
}

View File

@@ -1,16 +1,15 @@
//! # HTTP module.
use std::time::Duration;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use mime::Mime;
use once_cell::sync::Lazy;
use crate::context::Context;
use crate::net::lookup_host_with_cache;
use crate::socks::Socks5Config;
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
reqwest::tls::Certificate::from_der(include_bytes!(
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
@@ -60,8 +59,13 @@ pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
}
async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Response> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let client = get_client(socks5_config)?;
// It is safe to use cached IP addresses
// for HTTPS URLs, but for HTTP URLs
// better resolve from scratch each time to prevent
// cache poisoning attacks from having lasting effects.
let load_cache = url.starts_with("https://");
let client = get_client(context, load_cache).await?;
let mut url = url.to_string();
// Follow up to 10 http-redirects
@@ -86,10 +90,56 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
Err(anyhow!("Followed 10 redirections"))
}
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
struct CustomResolver {
context: Context,
/// Whether to return cached results or not.
/// If resolver can be used for URLs
/// without TLS, e.g. HTTP URLs from HTML email,
/// this must be false. If TLS is used
/// and certificate hostnames are checked,
/// it is safe to load cache.
load_cache: bool,
}
impl CustomResolver {
fn new(context: Context, load_cache: bool) -> Self {
Self {
context,
load_cache,
}
}
}
impl reqwest::dns::Resolve for CustomResolver {
fn resolve(&self, hostname: reqwest::dns::Name) -> reqwest::dns::Resolving {
let context = self.context.clone();
let load_cache = self.load_cache;
Box::pin(async move {
let port = 443; // Actual port does not matter.
let socket_addrs =
lookup_host_with_cache(&context, hostname.as_str(), port, "", load_cache).await;
match socket_addrs {
Ok(socket_addrs) => {
let addrs: reqwest::dns::Addrs = Box::new(socket_addrs.into_iter());
Ok(addrs)
}
Err(err) => Err(err.into()),
}
})
}
}
pub(crate) async fn get_client(context: &Context, load_cache: bool) -> Result<reqwest::Client> {
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let resolver = Arc::new(CustomResolver::new(context.clone(), load_cache));
let builder = reqwest::ClientBuilder::new()
.timeout(HTTP_TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
.timeout(super::TIMEOUT)
.add_root_certificate(LETSENCRYPT_ROOT.clone())
.dns_resolver(resolver);
let builder = if let Some(socks5_config) = socks5_config {
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;

View File

@@ -14,9 +14,10 @@ static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
.unwrap()
});
pub fn build_tls(strict_tls: bool) -> TlsConnector {
pub fn build_tls(strict_tls: bool, alpns: &[&str]) -> TlsConnector {
let tls_builder = TlsConnector::new()
.min_protocol_version(Some(Protocol::Tlsv12))
.request_alpns(alpns)
.add_root_certificate(LETSENCRYPT_ROOT.clone());
if strict_tls {
@@ -31,9 +32,10 @@ pub fn build_tls(strict_tls: bool) -> TlsConnector {
pub async fn wrap_tls<T: AsyncRead + AsyncWrite + Unpin>(
strict_tls: bool,
hostname: &str,
alpn: &[&str],
stream: T,
) -> Result<TlsStream<T>> {
let tls = build_tls(strict_tls);
let tls = build_tls(strict_tls, alpn);
let tls_stream = tls.connect(hostname, stream).await?;
Ok(tls_stream)
}
@@ -46,7 +48,7 @@ mod tests {
fn test_build_tls() {
// we are using some additional root certificates.
// make sure, they do not break construction of TlsConnector
let _ = build_tls(true);
let _ = build_tls(false);
let _ = build_tls(true, &[]);
let _ = build_tls(false, &[]);
}
}

View File

@@ -10,7 +10,6 @@ use crate::config::Config;
use crate::context::Context;
use crate::provider;
use crate::provider::Oauth2Authorizer;
use crate::socks::Socks5Config;
use crate::tools::time;
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
@@ -159,8 +158,12 @@ pub(crate) async fn get_oauth2_access_token(
}
// ... and POST
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let client = crate::net::http::get_client(socks5_config)?;
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = crate::net::http::get_client(context, load_cache).await?;
let response: Response = match client.post(post_url).form(&post_param).send().await {
Ok(resp) => match resp.json().await {
@@ -290,8 +293,12 @@ impl Oauth2 {
// "verified_email": true,
// "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg"
// }
let socks5_config = Socks5Config::from_database(&context.sql).await.ok()?;
let client = match crate::net::http::get_client(socks5_config) {
// All OAuth URLs are hardcoded HTTPS URLs,
// so it is safe to load DNS cache.
let load_cache = true;
let client = match crate::net::http::get_client(context, load_cache).await {
Ok(cl) => cl,
Err(err) => {
warn!(context, "failed to get HTTP client: {}", err);

View File

@@ -25,15 +25,17 @@
use anyhow::{anyhow, Context as _, Result};
use email::Header;
use iroh_gossip::net::{Gossip, JoinTopicFut, GOSSIP_ALPN};
use iroh_gossip::proto::{Event as IrohEvent, TopicId};
use futures_lite::StreamExt;
use iroh_gossip::net::{Event, Gossip, GossipEvent, JoinOptions, GOSSIP_ALPN};
use iroh_gossip::proto::TopicId;
use iroh_net::key::{PublicKey, SecretKey};
use iroh_net::relay::{RelayMap, RelayUrl};
use iroh_net::{relay::RelayMode, Endpoint};
use iroh_net::{NodeAddr, NodeId};
use parking_lot::Mutex;
use std::collections::{BTreeSet, HashMap};
use std::env;
use tokio::sync::RwLock;
use tokio::sync::{oneshot, RwLock};
use tokio::task::JoinHandle;
use url::Url;
@@ -58,6 +60,9 @@ pub struct Iroh {
/// [Gossip] needed for iroh peer channels.
pub(crate) gossip: Gossip,
/// Sequence numbers for gossip channels.
pub(crate) sequence_numbers: Mutex<HashMap<TopicId, i32>>,
/// Topics for which an advertisement has already been sent.
pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
@@ -82,7 +87,7 @@ impl Iroh {
&self,
ctx: &Context,
msg_id: MsgId,
) -> Result<Option<JoinTopicFut>> {
) -> Result<Option<oneshot::Receiver<()>>> {
let topic = get_iroh_topic_for_msg(ctx, msg_id)
.await?
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
@@ -93,56 +98,54 @@ impl Iroh {
// Otherwise we would receive every message twice or more times.
let mut iroh_channels = self.iroh_channels.write().await;
let seq = if let Some(channel_state) = iroh_channels.get(&topic) {
if channel_state.subscribe_loop.is_some() {
return Ok(None);
}
channel_state.seq_number
} else {
0
};
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
info!(
ctx,
"IROH_REALTIME: Joining gossip with peers: {:?}",
peers.iter().map(|p| p.node_id).collect::<Vec<_>>()
);
// Connect to all peers
for peer in &peers {
self.endpoint.add_node_addr(peer.clone())?;
if iroh_channels.contains_key(&topic) {
return Ok(None);
}
let connect_future = self
let peers = get_iroh_gossip_peers(ctx, msg_id).await?;
let node_ids = peers.iter().map(|p| p.node_id).collect::<Vec<_>>();
info!(
ctx,
"IROH_REALTIME: Joining gossip with peers: {:?}", node_ids,
);
// Inform iroh of potentially new node addresses
for node_addr in &peers {
if !node_addr.info.is_empty() {
self.endpoint.add_node_addr(node_addr.clone())?;
}
}
let (join_tx, join_rx) = oneshot::channel();
let (gossip_sender, gossip_receiver) = self
.gossip
.join(topic, peers.into_iter().map(|addr| addr.node_id).collect())
.await?;
.join_with_opts(topic, JoinOptions::with_bootstrap(node_ids))
.split();
let ctx = ctx.clone();
let gossip = self.gossip.clone();
let subscribe_loop = tokio::spawn(async move {
if let Err(e) = subscribe_loop(&ctx, gossip, topic, msg_id).await {
if let Err(e) = subscribe_loop(&ctx, gossip_receiver, topic, msg_id, join_tx).await {
warn!(ctx, "subscribe_loop failed: {e}")
}
});
iroh_channels.insert(topic, ChannelState::new(seq, subscribe_loop));
iroh_channels.insert(topic, ChannelState::new(subscribe_loop, gossip_sender));
Ok(Some(connect_future))
Ok(Some(join_rx))
}
/// Add gossip peers to realtime channel if it is already active.
pub async fn maybe_add_gossip_peers(&self, topic: TopicId, peers: Vec<NodeAddr>) -> Result<()> {
if let Some(state) = self.iroh_channels.read().await.get(&topic) {
if state.subscribe_loop.is_some() {
for peer in &peers {
self.endpoint.add_node_addr(peer.clone())?;
}
self.gossip
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
.await?;
if self.iroh_channels.read().await.get(&topic).is_some() {
for peer in &peers {
self.endpoint.add_node_addr(peer.clone())?;
}
self.gossip
.join(topic, peers.into_iter().map(|peer| peer.node_id).collect())
.await?;
}
Ok(())
}
@@ -159,11 +162,16 @@ impl Iroh {
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
self.join_and_subscribe_gossip(ctx, msg_id).await?;
let seq_num = self.get_and_incr(&topic).await;
let seq_num = self.get_and_incr(&topic);
let mut iroh_channels = self.iroh_channels.write().await;
let state = iroh_channels
.get_mut(&topic)
.context("Just created state does not exist")?;
data.extend(seq_num.to_le_bytes());
data.extend(self.public_key.as_bytes());
self.gossip.broadcast(topic, data.into()).await?;
state.sender.broadcast(data.into()).await?;
if env::var("REALTIME_DEBUG").is_ok() {
info!(ctx, "Sent realtime data");
@@ -172,30 +180,33 @@ impl Iroh {
Ok(())
}
async fn get_and_incr(&self, topic: &TopicId) -> i32 {
let mut seq = 0;
if let Some(state) = self.iroh_channels.write().await.get_mut(topic) {
seq = state.seq_number;
state.seq_number = state.seq_number.wrapping_add(1)
}
seq
fn get_and_incr(&self, topic: &TopicId) -> i32 {
let mut sequence_numbers = self.sequence_numbers.lock();
let entry = sequence_numbers.entry(*topic).or_default();
*entry = entry.wrapping_add(1);
*entry
}
/// Get the iroh [NodeAddr] without direct IP addresses.
pub(crate) async fn get_node_addr(&self) -> Result<NodeAddr> {
let mut addr = self.endpoint.my_addr().await?;
let mut addr = self.endpoint.node_addr().await?;
addr.info.direct_addresses = BTreeSet::new();
Ok(addr)
}
/// Leave the realtime channel for a given topic.
pub(crate) async fn leave_realtime(&self, topic: TopicId) -> Result<()> {
if let Some(channel) = &mut self.iroh_channels.write().await.get_mut(&topic) {
if let Some(subscribe_loop) = channel.subscribe_loop.take() {
subscribe_loop.abort();
}
if let Some(channel) = self.iroh_channels.write().await.remove(&topic) {
// Dropping the last GossipTopic results in quitting the topic.
// It is split into GossipReceiver and GossipSender.
// GossipSender (`channel.sender`) is dropped automatically.
// Subscribe loop owns GossipReceiver.
// Aborting it and waiting for it to be dropped
// drops the receiver.
channel.subscribe_loop.abort();
let _ = channel.subscribe_loop.await;
}
self.gossip.quit(topic).await?;
Ok(())
}
}
@@ -203,23 +214,23 @@ impl Iroh {
/// Single gossip channel state.
#[derive(Debug)]
pub(crate) struct ChannelState {
/// Sequence number for the gossip channel.
seq_number: i32,
/// The subscribe loop handle.
subscribe_loop: Option<JoinHandle<()>>,
subscribe_loop: JoinHandle<()>,
sender: iroh_gossip::net::GossipSender,
}
impl ChannelState {
fn new(seq_number: i32, subscribe_loop: JoinHandle<()>) -> Self {
fn new(subscribe_loop: JoinHandle<()>, sender: iroh_gossip::net::GossipSender) -> Self {
Self {
seq_number,
subscribe_loop: Some(subscribe_loop),
subscribe_loop,
sender,
}
}
}
impl Context {
/// Create magic endpoint and gossip.
/// Create iroh endpoint and gossip.
async fn init_peer_channels(&self) -> Result<Iroh> {
let secret_key = SecretKey::generate();
let public_key = secret_key.public();
@@ -246,7 +257,7 @@ impl Context {
.await?;
// create gossip
let my_addr = endpoint.my_addr().await?;
let my_addr = endpoint.node_addr().await?;
let gossip = Gossip::from_endpoint(endpoint.clone(), Default::default(), &my_addr.info);
// spawn endpoint loop that forwards incoming connections to the gossiper
@@ -254,10 +265,12 @@ impl Context {
// Shuts down on deltachat shutdown
tokio::spawn(endpoint_loop(context, endpoint.clone(), gossip.clone()));
tokio::spawn(gossip_direct_address_loop(endpoint.clone(), gossip.clone()));
Ok(Iroh {
endpoint,
gossip,
sequence_numbers: Mutex::new(HashMap::new()),
iroh_channels: RwLock::new(HashMap::new()),
public_key,
})
@@ -272,6 +285,15 @@ impl Context {
}
}
/// Loop to update direct addresses of the gossip.
async fn gossip_direct_address_loop(endpoint: Endpoint, gossip: Gossip) -> Result<()> {
let mut stream = endpoint.direct_addresses();
while let Some(addrs) = stream.next().await {
gossip.update_direct_addresses(&addrs)?;
}
Ok(())
}
/// Cache a peers [NodeId] for one topic.
pub(crate) async fn iroh_add_peer_for_topic(
ctx: &Context,
@@ -358,7 +380,7 @@ pub(crate) async fn get_iroh_topic_for_msg(
pub async fn send_webxdc_realtime_advertisement(
ctx: &Context,
msg_id: MsgId,
) -> Result<Option<JoinTopicFut>> {
) -> Result<Option<oneshot::Receiver<()>>> {
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
return Ok(None);
}
@@ -440,11 +462,11 @@ async fn handle_connection(
let conn = conn.await?;
let peer_id = iroh_net::endpoint::get_remote_node_id(&conn)?;
match alpn.as_bytes() {
match alpn.as_slice() {
GOSSIP_ALPN => gossip
.handle_connection(conn)
.await
.context(format!("Connection to {peer_id} with ALPN {alpn} failed"))?,
.context(format!("Gossip connection to {peer_id} failed"))?,
_ => warn!(
context,
"Ignoring connection from {peer_id}: unsupported ALPN protocol"
@@ -455,32 +477,50 @@ async fn handle_connection(
async fn subscribe_loop(
context: &Context,
gossip: Gossip,
mut stream: iroh_gossip::net::GossipReceiver,
topic: TopicId,
msg_id: MsgId,
join_tx: oneshot::Sender<()>,
) -> Result<()> {
let mut stream = gossip.subscribe(topic).await?;
loop {
let event = stream.recv().await?;
let mut join_tx = Some(join_tx);
while let Some(event) = stream.try_next().await? {
match event {
IrohEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
Event::Gossip(event) => match event {
GossipEvent::Joined(nodes) => {
if let Some(join_tx) = join_tx.take() {
// Try to notify that at least one peer joined,
// but ignore the error if receiver is dropped and nobody listens.
join_tx.send(()).ok();
}
for node in nodes {
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
}
GossipEvent::NeighborUp(node) => {
info!(context, "IROH_REALTIME: NeighborUp: {}", node.to_string());
iroh_add_peer_for_topic(context, msg_id, topic, node, None).await?;
}
GossipEvent::NeighborDown(_node) => {}
GossipEvent::Received(message) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: message
.content
.get(0..message.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
},
Event::Lagged => {
warn!(context, "Gossip lost some messages");
}
IrohEvent::Received(event) => {
info!(context, "IROH_REALTIME: Received realtime data");
context.emit_event(EventType::WebxdcRealtimeData {
msg_id,
data: event
.content
.get(0..event.content.len() - 4 - PUBLIC_KEY_LENGTH)
.context("too few bytes in iroh message")?
.into(),
});
}
_ => (),
};
}
Ok(())
}
#[cfg(test)]
@@ -528,10 +568,10 @@ mod tests {
assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
let webxdc = alice.pop_sent_msg().await;
let bob_webdxc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webdxc.get_viewtype(), Viewtype::Webxdc);
let bob_webxdc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
bob_webdxc.chat_id.accept(bob).await.unwrap();
bob_webxdc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
@@ -542,7 +582,7 @@ mod tests {
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webdxc.id)
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
.await
.unwrap()
.into_iter()
@@ -556,7 +596,7 @@ mod tests {
);
bob_iroh
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap()
@@ -584,7 +624,7 @@ mod tests {
}
// Bob sends ephemeral message
bob_iroh
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -616,7 +656,7 @@ mod tests {
);
bob_iroh
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice 2".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice 2".as_bytes().to_vec())
.await
.unwrap();
@@ -674,10 +714,10 @@ mod tests {
assert_eq!(alice_webxdc.get_viewtype(), Viewtype::Webxdc);
let webxdc = alice.pop_sent_msg().await;
let bob_webdxc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webdxc.get_viewtype(), Viewtype::Webxdc);
let bob_webxdc = bob.recv_msg(&webxdc).await;
assert_eq!(bob_webxdc.get_viewtype(), Viewtype::Webxdc);
bob_webdxc.chat_id.accept(bob).await.unwrap();
bob_webxdc.chat_id.accept(bob).await.unwrap();
// Alice advertises herself.
send_webxdc_realtime_advertisement(alice, alice_webxdc.id)
@@ -688,7 +728,7 @@ mod tests {
let bob_iroh = bob.get_or_try_init_peer_channel().await.unwrap();
// Bob adds alice to gossip peers.
let members = get_iroh_gossip_peers(bob, bob_webdxc.id)
let members = get_iroh_gossip_peers(bob, bob_webxdc.id)
.await
.unwrap()
.into_iter()
@@ -702,7 +742,7 @@ mod tests {
);
bob_iroh
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap()
@@ -729,11 +769,32 @@ mod tests {
}
}
// TODO: check that seq number is persisted
leave_webxdc_realtime(bob, bob_webdxc.id).await.unwrap();
let bob_topic = get_iroh_topic_for_msg(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap();
let bob_sequence_number = bob
.iroh
.get()
.unwrap()
.sequence_numbers
.lock()
.get(&bob_topic)
.copied();
leave_webxdc_realtime(bob, bob_webxdc.id).await.unwrap();
let bob_sequence_number_after = bob
.iroh
.get()
.unwrap()
.sequence_numbers
.lock()
.get(&bob_topic)
.copied();
// Check that sequence number is persisted when leaving the channel.
assert_eq!(bob_sequence_number, bob_sequence_number_after);
bob_iroh
.join_and_subscribe_gossip(bob, bob_webdxc.id)
.join_and_subscribe_gossip(bob, bob_webxdc.id)
.await
.unwrap()
.unwrap()
@@ -741,7 +802,7 @@ mod tests {
.unwrap();
bob_iroh
.send_webxdc_realtime_data(bob, bob_webdxc.id, "bob -> alice".as_bytes().to_vec())
.send_webxdc_realtime_data(bob, bob_webxdc.id, "bob -> alice".as_bytes().to_vec())
.await
.unwrap();
@@ -771,7 +832,7 @@ mod tests {
.await
.unwrap()
.unwrap();
assert!(if let Some(state) = alice
assert!(alice
.iroh
.get()
.unwrap()
@@ -779,11 +840,7 @@ mod tests {
.read()
.await
.get(&topic)
{
state.subscribe_loop.is_none()
} else {
false
});
.is_none());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -520,7 +520,7 @@ static P_FREENET_DE: Provider = Provider {
static P_GMAIL: Provider = Provider {
id: "gmail",
status: Status::Preparation,
before_login_hint: "For Gmail accounts, you need to create an app-password if you have \"2-Step Verification\" enabled. If this setting is not available, you need to enable \"less secure apps\".",
before_login_hint: "For Gmail accounts, you need to have \"2-Step Verification\" enabled and create an app-password.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/gmail",
server: &[
@@ -1037,6 +1037,20 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
port: 587,
username_pattern: Email,
},
Server {
protocol: Imap,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "nine.testrun.org",
port: 443,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
config_defaults: Some(&[ConfigDefault {
@@ -1260,14 +1274,14 @@ static P_RISEUP_NET: Provider = Provider {
socket: Ssl,
hostname: "mail.riseup.net",
port: 993,
username_pattern: Emaillocalpart,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Ssl,
hostname: "mail.riseup.net",
port: 465,
username_pattern: Emaillocalpart,
username_pattern: Email,
},
],
opt: ProviderOptions::new(),
@@ -1301,6 +1315,37 @@ static P_SONIC: Provider = Provider {
oauth2_authorizer: None,
};
// stinpriza.net.md: stinpriza.net, stinpriza.eu, el-hoyo.net
static P_STINPRIZA_NET: Provider = Provider {
id: "stinpriza.net",
status: Status::Ok,
before_login_hint: "",
after_login_hint: "",
overview_page: "https://providers.delta.chat/stinpriza-net",
server: &[
Server {
protocol: Imap,
socket: Starttls,
hostname: "stinpriza.net",
port: 143,
username_pattern: Email,
},
Server {
protocol: Smtp,
socket: Starttls,
hostname: "stinpriza.net",
port: 587,
username_pattern: Email,
},
],
opt: ProviderOptions {
strict_tls: true,
..ProviderOptions::new()
},
config_defaults: None,
oauth2_authorizer: None,
};
// systemausfall.org.md: systemausfall.org, solidaris.me
static P_SYSTEMAUSFALL_ORG: Provider = Provider {
id: "systemausfall.org",
@@ -1555,11 +1600,13 @@ static P_VIVALDI: Provider = Provider {
// vk.com.md: vk.com
static P_VK_COM: Provider = Provider {
id: "vk.com",
status: Status::Broken,
before_login_hint: "К сожалению, VK Почта не поддерживает работу с Delta Chat. См. https://help.vk.mail.ru/vkmail/questions/client",
status: Status::Preparation,
before_login_hint: "Вам необходимо сгенерировать \"пароль для внешнего приложения\" в веб-интерфейсе mail.ru https://account.mail.ru/user/2-step-auth/passwords/ чтобы vk.com работал с Delta Chat.",
after_login_hint: "",
overview_page: "https://providers.delta.chat/vk-com",
server: &[
Server { protocol: Imap, socket: Ssl, hostname: "imap.mail.ru", port: 993, username_pattern: Email },
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.mail.ru", port: 465, username_pattern: Email },
],
opt: ProviderOptions::new(),
config_defaults: None,
@@ -1757,7 +1804,7 @@ static P_ZOHO: Provider = Provider {
oauth2_authorizer: None,
};
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 528] = [
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 531] = [
("163.com", &P_163),
("aktivix.org", &P_AKTIVIX_ORG),
("aliyun.com", &P_ALIYUN),
@@ -2212,6 +2259,9 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 528] = [
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic.net", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("stinpriza.eu", &P_STINPRIZA_NET),
("el-hoyo.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("solidaris.me", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
@@ -2343,6 +2393,7 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
("riseup.net", &P_RISEUP_NET),
("rogers.com", &P_ROGERS_COM),
("sonic", &P_SONIC),
("stinpriza.net", &P_STINPRIZA_NET),
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
("systemli.org", &P_SYSTEMLI_ORG),
("t-online", &P_T_ONLINE),
@@ -2366,4 +2417,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
});
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 8, 14).unwrap());

View File

@@ -48,7 +48,7 @@ impl PushSubscriber {
/// Subscribes for heartbeat notifications with previously set device token.
#[cfg(target_os = "ios")]
pub(crate) async fn subscribe(&self) -> Result<()> {
pub(crate) async fn subscribe(&self, context: &Context) -> Result<()> {
use crate::net::http;
let mut state = self.inner.write().await;
@@ -61,8 +61,9 @@ impl PushSubscriber {
return Ok(());
};
let socks5_config = None;
let response = http::get_client(socks5_config)?
let load_cache = true;
let response = http::get_client(context, load_cache)
.await?
.post("https://notifications.delta.chat/register")
.body(format!("{{\"token\":\"{token}\"}}"))
.send()
@@ -77,7 +78,7 @@ impl PushSubscriber {
/// Placeholder to skip subscribing to heartbeat notifications outside iOS.
#[cfg(not(target_os = "ios"))]
pub(crate) async fn subscribe(&self) -> Result<()> {
pub(crate) async fn subscribe(&self, _context: &Context) -> Result<()> {
let mut state = self.inner.write().await;
state.heartbeat_subscribed = true;
Ok(())

View File

@@ -20,7 +20,6 @@ use crate::events::EventType;
use crate::key::Fingerprint;
use crate::message::Message;
use crate::peerstate::Peerstate;
use crate::socks::Socks5Config;
use crate::token;
use crate::tools::validate_id;
use iroh_old as iroh;
@@ -37,8 +36,13 @@ const VCARD_SCHEME: &str = "BEGIN:VCARD";
const SMTP_SCHEME: &str = "SMTP:";
const HTTP_SCHEME: &str = "http://";
const HTTPS_SCHEME: &str = "https://";
/// Legacy backup transfer based on iroh 0.4.
pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:";
/// Backup transfer based on iroh-net.
pub(crate) const DCBACKUP2_SCHEME: &str = "DCBACKUP2:";
/// Scanned QR code.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Qr {
@@ -106,7 +110,7 @@ pub enum Qr {
domain: String,
},
/// Provides a backup that can be retrieve.
/// Provides a backup that can be retrieved using legacy iroh 0.4.
///
/// This contains all the data needed to connect to a device and download a backup from
/// it to configure the receiving device with the same account.
@@ -120,6 +124,15 @@ pub enum Qr {
ticket: iroh::provider::Ticket,
},
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
Backup2 {
/// Iroh node address.
node_addr: iroh_net::NodeAddr,
/// Authentication token.
auth_token: String,
},
/// Ask the user if they want to use the given service for video chats.
WebrtcInstance {
/// Server domain name.
@@ -266,6 +279,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
decode_webrtc_instance(context, qr)?
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) {
decode_backup(qr)?
} else if starts_with_ignore_case(qr, DCBACKUP2_SCHEME) {
decode_backup2(qr)?
} else if qr.starts_with(MAILTO_SCHEME) {
decode_mailto(context, qr).await?
} else if qr.starts_with(SMTP_SCHEME) {
@@ -295,6 +310,13 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
pub fn format_backup(qr: &Qr) -> Result<String> {
match qr {
Qr::Backup { ref ticket } => Ok(format!("{DCBACKUP_SCHEME}{ticket}")),
Qr::Backup2 {
ref node_addr,
ref auth_token,
} => {
let node_addr = serde_json::to_string(node_addr)?;
Ok(format!("{DCBACKUP2_SCHEME}{auth_token}&{node_addr}"))
}
_ => Err(anyhow!("Not a backup QR code")),
}
}
@@ -487,7 +509,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
Ok(Qr::Account {
domain: url
.host_str()
.context("can't extract WebRTC instance domain")?
.context("can't extract account setup domain")?
.to_string(),
})
} else {
@@ -529,6 +551,24 @@ fn decode_backup(qr: &str) -> Result<Qr> {
Ok(Qr::Backup { ticket })
}
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
fn decode_backup2(qr: &str) -> Result<Qr> {
let payload = qr
.strip_prefix(DCBACKUP2_SCHEME)
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
let (auth_token, node_addr) = payload
.split_once('&')
.context("Backup QR code has no separator")?;
let auth_token = auth_token.to_string();
let node_addr = serde_json::from_str::<iroh_net::NodeAddr>(node_addr)
.context("Invalid node addr in backup QR code")?;
Ok(Qr::Backup2 {
node_addr,
auth_token,
})
}
#[derive(Debug, Deserialize)]
struct CreateAccountSuccessResponse {
/// Email address.
@@ -549,8 +589,16 @@ struct CreateAccountErrorResponse {
#[allow(clippy::indexing_slicing)]
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
let url_str = &qr[DCACCOUNT_SCHEME.len()..];
let socks5_config = Socks5Config::from_database(&context.sql).await?;
let response = crate::net::http::get_client(socks5_config)?
if !url_str.starts_with(HTTPS_SCHEME) {
bail!("DCACCOUNT QR codes must use HTTPS scheme");
}
// As only HTTPS is used, it is safe to load DNS cache.
let load_cache = true;
let response = crate::net::http::get_client(context, load_cache)
.await?
.post(url_str)
.send()
.await?;
@@ -611,7 +659,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;
context.send_sync_msg().await?;
}
Qr::WithdrawVerifyGroup {
invitenumber,
@@ -623,7 +670,6 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
context
.sync_qr_code_token_deletion(invitenumber, authcode)
.await?;
context.send_sync_msg().await?;
}
Qr::ReviveVerifyContact {
invitenumber,
@@ -633,7 +679,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
token::save(context, token::Namespace::InviteNumber, None, &invitenumber).await?;
token::save(context, token::Namespace::Auth, None, &authcode).await?;
context.sync_qr_code_tokens(None).await?;
context.send_sync_msg().await?;
context.scheduler.interrupt_smtp().await;
}
Qr::ReviveVerifyGroup {
invitenumber,
@@ -653,7 +699,7 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
.await?;
token::save(context, token::Namespace::Auth, chat_id, &authcode).await?;
context.sync_qr_code_tokens(chat_id).await?;
context.send_sync_msg().await?;
context.scheduler.interrupt_smtp().await;
}
Qr::Login { address, options } => {
configure_from_login_qr(context, &address, options).await?
@@ -1359,9 +1405,12 @@ mod tests {
ctx.ctx.get_config(Config::SendUser).await?,
Some("SendUser".to_owned())
);
// `sc` option is actually ignored and `ic` is used instead
// because `smtp_certificate_checks` is deprecated.
assert_eq!(
ctx.ctx.get_config(Config::SmtpCertificateChecks).await?,
Some("3".to_owned())
Some("1".to_owned())
);
assert_eq!(
ctx.ctx.get_config(Config::SendSecurity).await?,

View File

@@ -39,9 +39,6 @@ pub enum LoginOptions {
/// IMAP socket security.
imap_security: Option<Socket>,
/// IMAP certificate checks.
imap_certificate_checks: Option<CertificateChecks>,
/// SMTP host.
smtp_host: Option<String>,
@@ -57,8 +54,8 @@ pub enum LoginOptions {
/// SMTP socket security.
smtp_security: Option<Socket>,
/// SMTP certificate checks.
smtp_certificate_checks: Option<CertificateChecks>,
/// Certificate checks.
certificate_checks: Option<CertificateChecks>,
},
}
@@ -107,14 +104,13 @@ pub(super) fn decode_login(qr: &str) -> Result<Qr> {
imap_username: parameter_map.get("iu").map(|s| s.to_owned()),
imap_password: parameter_map.get("ipw").map(|s| s.to_owned()),
imap_security: parse_socket_security(parameter_map.get("is"))?,
imap_certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
smtp_host: parameter_map.get("sh").map(|s| s.to_owned()),
smtp_port: parse_port(parameter_map.get("sp"))
.context("could not parse smtp port")?,
smtp_username: parameter_map.get("su").map(|s| s.to_owned()),
smtp_password: parameter_map.get("spw").map(|s| s.to_owned()),
smtp_security: parse_socket_security(parameter_map.get("ss"))?,
smtp_certificate_checks: parse_certificate_checks(parameter_map.get("sc"))?,
certificate_checks: parse_certificate_checks(parameter_map.get("ic"))?,
},
Some(Ok(v)) => LoginOptions::UnsuportedVersion(v),
Some(Err(_)) => bail!("version could not be parsed as number E6"),
@@ -177,13 +173,12 @@ pub(crate) async fn configure_from_login_qr(
imap_username,
imap_password,
imap_security,
imap_certificate_checks,
smtp_host,
smtp_port,
smtp_username,
smtp_password,
smtp_security,
smtp_certificate_checks,
certificate_checks,
} => {
context
.set_config_internal(Config::MailPw, Some(&mail_pw))
@@ -216,14 +211,6 @@ pub(crate) async fn configure_from_login_qr(
.set_config_internal(Config::MailSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = imap_certificate_checks {
let code = value
.to_u32()
.context("could not convert imap certificate checks value to number")?;
context
.set_config_internal(Config::ImapCertificateChecks, Some(&code.to_string()))
.await?;
}
if let Some(value) = smtp_host {
context
.set_config_internal(Config::SendServer, Some(&value))
@@ -252,10 +239,13 @@ pub(crate) async fn configure_from_login_qr(
.set_config_internal(Config::SendSecurity, Some(&code.to_string()))
.await?;
}
if let Some(value) = smtp_certificate_checks {
if let Some(value) = certificate_checks {
let code = value
.to_u32()
.context("could not convert smtp certificate checks value to number")?;
.context("could not convert certificate checks value to number")?;
context
.set_config_internal(Config::ImapCertificateChecks, Some(&code.to_string()))
.await?;
context
.set_config_internal(Config::SmtpCertificateChecks, Some(&code.to_string()))
.await?;
@@ -284,13 +274,12 @@ mod test {
imap_username: None,
imap_password: None,
imap_security: None,
imap_certificate_checks: None,
smtp_host: None,
smtp_port: None,
smtp_username: None,
smtp_password: None,
smtp_security: None,
smtp_certificate_checks: None,
certificate_checks: None,
}
};
}
@@ -392,13 +381,12 @@ mod test {
imap_username: Some("max".to_owned()),
imap_password: Some("87654".to_owned()),
imap_security: Some(Socket::Ssl),
imap_certificate_checks: Some(CertificateChecks::Strict),
smtp_host: Some("mail.host.tld".to_owned()),
smtp_port: Some(3000),
smtp_username: Some("max@host.tld".to_owned()),
smtp_password: Some("3242HS".to_owned()),
smtp_security: Some(Socket::Plain),
smtp_certificate_checks: Some(CertificateChecks::AcceptInvalidCertificates),
certificate_checks: Some(CertificateChecks::Strict),
}
);
} else {

View File

@@ -58,7 +58,7 @@ async fn generate_verification_qr(context: &Context) -> Result<String> {
)
}
/// Renders a [`Qr::Backup`] QR code as an SVG image.
/// Renders a [`Qr::Backup2`] QR code as an SVG image.
pub async fn generate_backup_qr(context: &Context, qr: &Qr) -> Result<String> {
let content = qr::format_backup(qr)?;
let (avatar, displayname, _addr, color) = self_info(context).await?;

View File

@@ -632,9 +632,9 @@ pub(crate) async fn receive_imf_inner(
/// Also returns whether it is blocked or not and its origin.
///
/// * `prevent_rename`: if true, the display_name of this contact will not be changed. Useful for
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change every time the user gets a new email from
/// a mailing list.
/// mailing lists: In some mailing lists, many users write from the same address but with different
/// display names. We don't want the display name to change every time the user gets a new email from
/// a mailing list.
///
/// Returns `None` if From field does not contain a valid contact address.
pub async fn from_field_to_contact_id(
@@ -807,7 +807,7 @@ async fn add_parts(
// 1:1 chat is blocked, but the contact is not.
// This happens when 1:1 chat is hidden
// during scanning of a group invitation code.
Blocked::Request
create_blocked_default
}
}
}
@@ -1664,7 +1664,13 @@ RETURNING id
replace_msg_id.trash(context, on_server).await?;
}
chat_id.unarchive_if_not_muted(context, state).await?;
let unarchive = match mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) {
Some(addr) => context.is_self_addr(addr).await?,
None => true,
};
if unarchive {
chat_id.unarchive_if_not_muted(context, state).await?;
}
info!(
context,
@@ -1824,7 +1830,7 @@ async fn lookup_chat_or_create_adhoc_group(
Ok(Some((new_chat_id, new_chat_id_blocked)))
} else if allow_creation {
// Try to create an ad hoc group.
if let Some(new_chat_id) = create_adhoc_group(
create_adhoc_group(
context,
mime_parser,
create_blocked,
@@ -1833,12 +1839,7 @@ async fn lookup_chat_or_create_adhoc_group(
is_partial_download,
)
.await
.context("Could not create ad hoc group")?
{
Ok(Some((new_chat_id, create_blocked)))
} else {
Ok(None)
}
.context("Could not create ad hoc group")
} else {
Ok(None)
}
@@ -2512,7 +2513,7 @@ async fn create_adhoc_group(
from_id: ContactId,
to_ids: &[ContactId],
is_partial_download: bool,
) -> Result<Option<ChatId>> {
) -> Result<Option<(ChatId, Blocked)>> {
if is_partial_download {
// Partial download may be an encrypted message with protected Subject header.
//
@@ -2551,7 +2552,16 @@ async fn create_adhoc_group(
);
return Ok(None);
}
if mime_parser
.get_header(HeaderDef::ChatGroupMemberRemoved)
.is_some()
{
info!(
context,
"Message removes member from unknown ad-hoc group (TRASH)."
);
return Ok(Some((DC_CHAT_ID_TRASH, Blocked::Not)));
}
if member_ids.len() < 3 {
return Ok(None);
}
@@ -2583,7 +2593,7 @@ async fn create_adhoc_group(
chatlist_events::emit_chatlist_changed(context);
chatlist_events::emit_chatlist_item_changed(context, new_chat_id);
Ok(Some(new_chat_id))
Ok(Some((new_chat_id, create_blocked)))
}
#[derive(Debug, PartialEq, Eq)]

View File

@@ -14,6 +14,7 @@ use crate::contact;
use crate::download::MIN_DOWNLOAD_LIMIT;
use crate::imap::prefetch_should_download;
use crate::imex::{imex, ImexMode};
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
use crate::tools::{time, SystemTime};
@@ -3275,6 +3276,46 @@ async fn test_auto_accept_group_for_bots() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_auto_accept_protected_group_for_bots() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(Config::Bot, Some("1")).await.unwrap();
mark_as_verified(alice, bob).await;
mark_as_verified(bob, alice).await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let sent = alice.send_text(group_id, "Hello!").await;
let msg = bob.recv_msg(&sent).await;
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
assert!(!chat.is_contact_request());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_bot_accepts_another_group_after_qr_scan() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
bob.set_config(Config::Bot, Some("1")).await?;
let group_id = chat::create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
let qr = get_securejoin_qr(alice, Some(group_id)).await?;
tcm.exec_securejoin_qr(bob, alice, &qr).await;
let group_id = alice
.create_group_with_members(ProtectionStatus::Protected, "Group", &[bob])
.await;
let sent = alice.send_text(group_id, "Hello!").await;
let msg = bob.recv_msg(&sent).await;
let chat = chat::Chat::load_from_db(bob, msg.chat_id).await?;
assert!(!chat.is_contact_request());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_as_bot() -> Result<()> {
let mut tcm = TestContextManager::new();
@@ -3559,6 +3600,39 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from_and_no_valid_signatures() -> Result<()> {
let t = &TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
let received_msg = receive_imf(t, raw, false).await?.unwrap();
assert!(!received_msg.from_is_signed);
let msg = t.get_last_msg().await;
assert!(!msg.chat_id.is_trash());
assert!(!msg.get_showpadlock());
let t = &TestContext::new_bob().await;
let raw = String::from_utf8(raw.to_vec())?.replace("alice@example.org", "clarice@example.org");
let received_msg = receive_imf(t, raw.as_bytes(), false).await?.unwrap();
assert!(received_msg.chat_id.is_trash());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_wrong_from_name_and_no_valid_signatures() -> Result<()> {
let t = &TestContext::new_bob().await;
let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_signed.eml");
let raw = String::from_utf8(raw.to_vec())?.replace("From: Alice", "From: A");
let received_msg = receive_imf(t, raw.as_bytes(), false).await?.unwrap();
assert!(!received_msg.from_is_signed);
let msg = t.get_last_msg().await;
assert!(!msg.chat_id.is_trash());
assert!(!msg.get_showpadlock());
let contact = Contact::get_by_id(t, msg.from_id).await?;
assert_eq!(contact.get_authname(), "Alice");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> {
let t = TestContext::new_bob().await;
@@ -4658,6 +4732,67 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_dont_create_adhoc_group_on_member_removal() -> Result<()> {
let mut tcm = TestContextManager::new();
let bob = &tcm.bob().await;
async fn get_chat_cnt(ctx: &Context) -> Result<usize> {
ctx.sql
.count("SELECT COUNT(*) FROM chats WHERE id>9", ())
.await
}
let chat_cnt = get_chat_cnt(bob).await?;
receive_imf(
bob,
b"From: Alice <alice@example.org>\n\
To: <bob@example.net>, <charlie@example.com>\n\
Chat-Version: 1.0\n\
Subject: subject\n\
Message-ID: <first@example.org>\n\
Date: Sun, 14 Nov 2021 00:10:00 +0000\
Content-Type: text/plain
Chat-Group-Member-Removed: charlie@example.com",
false,
)
.await?;
assert_eq!(get_chat_cnt(bob).await?, chat_cnt);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unarchive_on_member_removal() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let bob_id = Contact::create(alice, "", "bob@example.net").await?;
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
let msg = alice.pop_sent_msg().await;
bob.recv_msg(&msg).await;
let bob_chat_id = bob.get_last_msg().await.chat_id;
bob_chat_id
.set_visibility(bob, ChatVisibility::Archived)
.await?;
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
let msg = alice.pop_sent_msg().await;
bob.recv_msg(&msg).await;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.get_visibility(), ChatVisibility::Archived);
remove_contact_from_chat(alice, alice_chat_id, bob_id).await?;
let msg = alice.pop_sent_msg().await;
bob.recv_msg(&msg).await;
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
assert_eq!(bob_chat.get_visibility(), ChatVisibility::Normal);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_forged_from() -> Result<()> {
let mut tcm = TestContextManager::new();

View File

@@ -110,13 +110,14 @@ impl SchedulerState {
// to allow for clean shutdown.
context.new_msgs_notify.notify_one();
if let Some(debug_logging) = context
let debug_logging = context
.debug_logging
.read()
.write()
.expect("RwLock is poisoned")
.as_ref()
{
.take();
if let Some(debug_logging) = debug_logging {
debug_logging.loop_handle.abort();
debug_logging.loop_handle.await.ok();
}
let prev_state = std::mem::replace(&mut *inner, new_state);
context.emit_event(EventType::ConnectivityChanged);
@@ -465,11 +466,13 @@ pub async fn convert_folder_meaning(
}
async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session) -> Result<Session> {
ctx.set_config_internal(
Config::IsChatmail,
crate::config::from_bool(session.is_chatmail()),
)
.await?;
if !ctx.get_config_bool(Config::FixIsChatmail).await? {
ctx.set_config_internal(
Config::IsChatmail,
crate::config::from_bool(session.is_chatmail()),
)
.await?;
}
// Update quota no more than once a minute.
if ctx.quota_needs_update(60).await {
@@ -974,9 +977,16 @@ impl Scheduler {
.await
.log_err(context)
.ok();
// Abort tasks, then await them to ensure the `Future` is dropped.
// Just aborting the task may keep resources such as `Context` clone
// moved into it indefinitely, resulting in database not being
// closed etc.
self.ephemeral_handle.abort();
self.ephemeral_handle.await.ok();
self.location_handle.abort();
self.recently_seen_loop.abort();
self.location_handle.await.ok();
self.recently_seen_loop.abort().await;
}
}

View File

@@ -98,6 +98,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
let group_name_urlencoded = utf8_percent_encode(group_name, NON_ALPHANUMERIC).to_string();
if sync_token {
context.sync_qr_code_tokens(Some(chat.id)).await?;
context.scheduler.interrupt_smtp().await;
}
format!(
"OPENPGP4FPR:{}#a={}&g={}&x={}&i={}&s={}",
@@ -112,6 +113,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option<ChatId>) -> Resu
// parameters used: a=n=i=s=
if sync_token {
context.sync_qr_code_tokens(None).await?;
context.scheduler.interrupt_smtp().await;
}
format!(
"OPENPGP4FPR:{}#a={}&n={}&i={}&s={}",
@@ -635,6 +637,10 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore);
};
peerstate.set_verified(key.clone(), fingerprint, addr)?;
if matches!(step, "vg-member-added" | "vc-contact-confirm") {
peerstate.backward_verified_key_id =
Some(context.get_config_i64(Config::KeyId).await?).filter(|&id| id > 0);
}
peerstate.prefer_encrypt = EncryptPreference::Mutual;
peerstate.save_to_db(&context.sql).await?;
@@ -841,6 +847,7 @@ mod tests {
);
let sent = bob.pop_sent_msg().await;
assert!(!sent.payload.contains("Bob Examplenet"));
assert_eq!(sent.recipient(), EmailAddress::new(alice_addr).unwrap());
let msg = alice.parse_msg(&sent).await;
assert!(!msg.was_encrypted());
@@ -858,6 +865,7 @@ mod tests {
);
let sent = alice.pop_sent_msg().await;
assert!(!sent.payload.contains("Alice Exampleorg"));
let msg = bob.parse_msg(&sent).await;
assert!(msg.was_encrypted());
assert_eq!(

View File

@@ -1,13 +1,11 @@
//! # SMTP transport module.
mod connect;
pub mod send;
use std::time::Duration;
use anyhow::{bail, format_err, Context as _, Error, Result};
use async_smtp::response::{Category, Code, Detail};
use async_smtp::{self as smtp, EmailAddress, SmtpTransport};
use tokio::io::BufStream;
use tokio::task;
use crate::chat::{add_info_msg_with_cmd, ChatId};
@@ -15,24 +13,18 @@ use crate::config::Config;
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::events::EventType;
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
use crate::login_param::{LoginParam, ServerLoginParam};
use crate::message::Message;
use crate::message::{self, MsgId};
use crate::mimefactory::MimeFactory;
use crate::net::connect_tcp;
use crate::net::session::SessionBufStream;
use crate::net::tls::wrap_tls;
use crate::oauth2::get_oauth2_access_token;
use crate::provider::Socket;
use crate::scheduler::connectivity::ConnectivityStore;
use crate::socks::Socks5Config;
use crate::sql;
use crate::stock_str::unencrypted_email;
use crate::tools::{self, time_elapsed};
/// SMTP connection, write and read timeout.
const SMTP_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Default)]
pub(crate) struct Smtp {
/// SMTP connection.
@@ -102,128 +94,11 @@ impl Smtp {
&lp.smtp,
&lp.socks5_config,
&lp.addr,
lp.provider.map_or(lp.socks5_config.is_some(), |provider| {
provider.opt.strict_tls
}),
lp.strict_tls(),
)
.await
}
async fn connect_secure_socks5(
&self,
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, SMTP_TIMEOUT, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream).await?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
async fn connect_starttls_socks5(
&self,
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, SMTP_TIMEOUT, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true).without_greeting();
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
async fn connect_insecure_socks5(
&self,
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, SMTP_TIMEOUT, false)
.await?;
let buffered_stream = BufStream::new(socks5_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
async fn connect_secure(
&self,
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let tcp_stream = connect_tcp(context, hostname, port, SMTP_TIMEOUT, false).await?;
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
async fn connect_starttls(
&self,
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let tcp_stream = connect_tcp(context, hostname, port, SMTP_TIMEOUT, strict_tls).await?;
// Run STARTTLS command and convert the client back into a stream.
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, BufStream::new(tcp_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true).without_greeting();
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
async fn connect_insecure(
&self,
context: &Context,
hostname: &str,
port: u16,
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
let tcp_stream = connect_tcp(context, hostname, port, SMTP_TIMEOUT, false).await?;
let buffered_stream = BufStream::new(tcp_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
let client = smtp::SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, session_stream).await?;
Ok(transport)
}
/// Connect using the provided login params.
pub async fn connect(
&mut self,
@@ -231,7 +106,7 @@ impl Smtp {
lp: &ServerLoginParam,
socks5_config: &Option<Socks5Config>,
addr: &str,
provider_strict_tls: bool,
strict_tls: bool,
) -> Result<()> {
if self.is_connected() {
warn!(context, "SMTP already connected.");
@@ -250,55 +125,17 @@ impl Smtp {
let domain = &lp.server;
let port = lp.port;
let strict_tls = match lp.certificate_checks {
CertificateChecks::Automatic => provider_strict_tls,
CertificateChecks::Strict => true,
CertificateChecks::AcceptInvalidCertificates
| CertificateChecks::AcceptInvalidCertificates2 => false,
};
let mut transport = if let Some(socks5_config) = socks5_config {
match lp.security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => {
self.connect_secure_socks5(
context,
domain,
port,
strict_tls,
socks5_config.clone(),
)
.await?
}
Socket::Starttls => {
self.connect_starttls_socks5(
context,
domain,
port,
strict_tls,
socks5_config.clone(),
)
.await?
}
Socket::Plain => {
self.connect_insecure_socks5(context, domain, port, socks5_config.clone())
.await?
}
}
} else {
match lp.security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => {
self.connect_secure(context, domain, port, strict_tls)
.await?
}
Socket::Starttls => {
self.connect_starttls(context, domain, port, strict_tls)
.await?
}
Socket::Plain => self.connect_insecure(context, domain, port).await?,
}
};
let session_stream = connect::connect_stream(
context,
domain,
port,
strict_tls,
socks5_config.clone(),
lp.security,
)
.await?;
let client = smtp::SmtpClient::new().smtp_utf8(true).without_greeting();
let mut transport = SmtpTransport::new(client, session_stream).await?;
// Authenticate.
{
@@ -794,9 +631,7 @@ async fn send_mdn_rfc724_mid(
/// Tries to send a single MDN. Returns true if more MDNs should be sent.
async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
if !mdns_enabled {
// User has disabled MDNs.
if !context.should_send_mdns().await? {
context.sql.execute("DELETE FROM smtp_mdns", []).await?;
return Ok(false);
}

226
src/smtp/connect.rs Normal file
View File

@@ -0,0 +1,226 @@
//! SMTP connection establishment.
use std::net::SocketAddr;
use anyhow::{bail, format_err, Context as _, Result};
use async_smtp::{SmtpClient, SmtpTransport};
use tokio::io::BufStream;
use crate::context::Context;
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
use crate::net::session::SessionBufStream;
use crate::net::tls::wrap_tls;
use crate::net::update_connection_history;
use crate::net::{connect_tcp_inner, connect_tls_inner};
use crate::provider::Socket;
use crate::socks::Socks5Config;
use crate::tools::time;
/// Converts port number to ALPN list.
fn alpn(port: u16) -> &'static [&'static str] {
if port == 465 {
// Do not request ALPN on standard port.
&[]
} else {
&["smtp"]
}
}
/// Returns TLS, STARTTLS or plaintext connection
/// using SOCKS5 or direct connection depending on the given configuration.
///
/// Connection is returned after skipping the welcome message
/// and is ready for sending commands. Because SMTP STARTTLS
/// does not send welcome message over TLS connection
/// after establishing it, welcome message is always ignored
/// to unify the result regardless of whether TLS or STARTTLS is used.
pub(crate) async fn connect_stream(
context: &Context,
host: &str,
port: u16,
strict_tls: bool,
socks5_config: Option<Socks5Config>,
security: Socket,
) -> Result<Box<dyn SessionBufStream>> {
if let Some(socks5_config) = socks5_config {
let stream = match security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => {
connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone())
.await?
}
Socket::Starttls => {
connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone())
.await?
}
Socket::Plain => {
connect_insecure_socks5(context, host, port, socks5_config.clone()).await?
}
};
Ok(stream)
} else {
let mut first_error = None;
let load_cache = strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
for resolved_addr in lookup_host_with_cache(context, host, port, "smtp", load_cache).await?
{
let res = match security {
Socket::Automatic => bail!("SMTP port security is not configured"),
Socket::Ssl => connect_secure(resolved_addr, host, strict_tls).await,
Socket::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
Socket::Plain => connect_insecure(resolved_addr).await,
};
match res {
Ok(stream) => {
let ip_addr = resolved_addr.ip().to_string();
if load_cache {
update_connect_timestamp(context, host, &ip_addr).await?;
}
update_connection_history(context, "smtp", host, port, &ip_addr, time())
.await?;
return Ok(stream);
}
Err(err) => {
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
first_error.get_or_insert(err);
}
}
}
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
}
}
/// Reads and ignores SMTP greeting.
///
/// This function is used to unify
/// TLS, STARTTLS and plaintext connection setup
/// by skipping the greeting in case of TLS
/// and STARTTLS connection setup.
async fn skip_smtp_greeting<R: tokio::io::AsyncBufReadExt + Unpin>(stream: &mut R) -> Result<()> {
let mut line = String::with_capacity(512);
loop {
line.clear();
let read = stream.read_line(&mut line).await?;
if read == 0 {
bail!("Unexpected EOF while reading SMTP greeting.");
}
if line.starts_with("220-") {
continue;
} else if line.starts_with("220 ") {
return Ok(());
} else {
bail!("Unexpected greeting: {line:?}.");
}
}
}
async fn connect_secure_socks5(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, strict_tls)
.await?;
let tls_stream = wrap_tls(strict_tls, hostname, alpn(port), socks5_stream).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_starttls_socks5(
context: &Context,
hostname: &str,
port: u16,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, strict_tls)
.await?;
// Run STARTTLS command and convert the client back into a stream.
let client = SmtpClient::new().smtp_utf8(true);
let transport = SmtpTransport::new(client, BufStream::new(socks5_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, hostname, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_insecure_socks5(
context: &Context,
hostname: &str,
port: u16,
socks5_config: Socks5Config,
) -> Result<Box<dyn SessionBufStream>> {
let socks5_stream = socks5_config
.connect(context, hostname, port, false)
.await?;
let mut buffered_stream = BufStream::new(socks5_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_secure(
addr: SocketAddr,
hostname: &str,
strict_tls: bool,
) -> Result<Box<dyn SessionBufStream>> {
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
let mut buffered_stream = BufStream::new(tls_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_starttls(
addr: SocketAddr,
host: &str,
strict_tls: bool,
) -> Result<Box<dyn SessionBufStream>> {
let tcp_stream = connect_tcp_inner(addr).await?;
// Run STARTTLS command and convert the client back into a stream.
let client = async_smtp::SmtpClient::new().smtp_utf8(true);
let transport = async_smtp::SmtpTransport::new(client, BufStream::new(tcp_stream)).await?;
let tcp_stream = transport.starttls().await?.into_inner();
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
.await
.context("STARTTLS upgrade failed")?;
let buffered_stream = BufStream::new(tls_stream);
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
async fn connect_insecure(addr: SocketAddr) -> Result<Box<dyn SessionBufStream>> {
let tcp_stream = connect_tcp_inner(addr).await?;
let mut buffered_stream = BufStream::new(tcp_stream);
skip_smtp_greeting(&mut buffered_stream).await?;
let session_stream: Box<dyn SessionBufStream> = Box::new(buffered_stream);
Ok(session_stream)
}
#[cfg(test)]
mod tests {
use tokio::io::BufReader;
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_skip_smtp_greeting() -> Result<()> {
let greeting = b"220-server261.web-hosting.com ESMTP Exim 4.96.2 #2 Sat, 24 Aug 2024 12:25:53 -0400 \r\n\
220-We do not authorize the use of this system to transport unsolicited,\r\n\
220 and/or bulk e-mail.\r\n";
let mut buffered_stream = BufReader::new(&greeting[..]);
skip_smtp_greeting(&mut buffered_stream).await
}
}

View File

@@ -2,7 +2,6 @@
use std::fmt;
use std::pin::Pin;
use std::time::Duration;
use anyhow::Result;
use fast_socks5::client::{Config, Socks5Stream};
@@ -76,11 +75,9 @@ impl Socks5Config {
context: &Context,
target_host: &str,
target_port: u16,
timeout_val: Duration,
load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
let tcp_stream =
connect_tcp(context, &self.host, self.port, timeout_val, load_dns_cache).await?;
let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache).await?;
let authentication_method = if let Some((username, password)) = self.user_password.as_ref()
{

View File

@@ -18,6 +18,8 @@ use crate::imex::BLOBS_BACKUP_NAME;
use crate::location::delete_orphaned_poi_locations;
use crate::log::LogExt;
use crate::message::{Message, MsgId, Viewtype};
use crate::net::dns::prune_dns_cache;
use crate::net::prune_connection_history;
use crate::param::{Param, Params};
use crate::peerstate::Peerstate;
use crate::stock_str;
@@ -103,9 +105,11 @@ impl Sql {
// Test that the key is correct using a single connection.
let connection = Connection::open(&self.dbfile)?;
connection
.pragma_update(None, "key", &passphrase)
.context("failed to set PRAGMA key")?;
if !passphrase.is_empty() {
connection
.pragma_update(None, "key", &passphrase)
.context("Failed to set PRAGMA key")?;
}
let key_is_correct = connection
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.is_ok();
@@ -126,7 +130,7 @@ impl Sql {
}
/// Closes all underlying Sqlite connections.
async fn close(&self) {
pub(crate) async fn close(&self) {
let _ = self.pool.write().await.take();
// drop closes the connection
}
@@ -137,46 +141,50 @@ impl Sql {
.to_str()
.with_context(|| format!("path {path:?} is not valid unicode"))?
.to_string();
let res = self
.call_write(move |conn| {
// Check that backup passphrase is correct before resetting our database.
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
.context("failed to attach backup database")?;
if let Err(err) = conn
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.context("backup passphrase is not correct")
{
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
return Err(err);
}
// Reset the database without reopening it. We don't want to reopen the database because we
// don't have main database passphrase at this point.
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
// Without resetting import may fail due to existing tables.
// Keep `config_cache` locked all the time the db is imported so that nobody can use invalid
// values from there. And clear it immediately so as not to forget in case of errors.
let mut config_cache = self.config_cache.write().await;
config_cache.clear();
self.call_write(move |conn| {
// Check that backup passphrase is correct before resetting our database.
conn.execute("ATTACH DATABASE ? AS backup KEY ?", (path_str, passphrase))
.context("failed to attach backup database")?;
let res = conn
.query_row("SELECT count(*) FROM sqlite_master", [], |_row| Ok(()))
.context("backup passphrase is not correct");
// Reset the database without reopening it. We don't want to reopen the database because we
// don't have main database passphrase at this point.
// See <https://sqlite.org/c3ref/c_dbconfig_enable_fkey.html> for documentation.
// Without resetting import may fail due to existing tables.
let res = res.and_then(|_| {
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, true)
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")?;
.context("failed to set SQLITE_DBCONFIG_RESET_DATABASE")
});
let res = res.and_then(|_| {
conn.execute("VACUUM", [])
.context("failed to vacuum the database")?;
.context("failed to vacuum the database")
});
let res = res.and(
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_RESET_DATABASE, false)
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE")?;
let res = conn
.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
Ok(())
})
.context("failed to import from attached backup database");
.context("failed to unset SQLITE_DBCONFIG_RESET_DATABASE"),
);
let res = res.and_then(|_| {
conn.query_row("SELECT sqlcipher_export('main', 'backup')", [], |_row| {
Ok(())
})
.context("failed to import from attached backup database")
});
let res = res.and(
conn.execute("DETACH DATABASE backup", [])
.context("failed to detach backup database")?;
res?;
Ok(())
})
.await;
// The config cache is wrong now that we have a different database
self.config_cache.write().await.clear();
res
.context("failed to detach backup database"),
);
res?;
Ok(())
})
.await
}
/// Creates a new connection pool.
@@ -318,8 +326,10 @@ impl Sql {
let pool = lock.take().context("SQL connection pool is not open")?;
let conn = pool.get().await?;
conn.pragma_update(None, "rekey", passphrase.clone())
.context("failed to set PRAGMA rekey")?;
if !passphrase.is_empty() {
conn.pragma_update(None, "rekey", passphrase.clone())
.context("Failed to set PRAGMA rekey")?;
}
drop(pool);
*lock = Some(Self::new_pool(&self.dbfile, passphrase.to_string())?);
@@ -350,12 +360,12 @@ impl Sql {
///
/// 1. As mentioned above, SQLite's locking mechanism is non-async and sleeps in a loop.
/// 2. If there are other write transactions, we block the db connection until
/// upgraded. If some reader comes then, it has to get the next, less used connection with a
/// worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
/// upgraded. If some reader comes then, it has to get the next, less used connection with a
/// worse per-connection page cache (SQLite allows one write and any number of reads in parallel).
/// 3. If a transaction is blocked for more than `busy_timeout`, it fails with SQLITE_BUSY.
/// 4. If upon a successful upgrade to a write transaction the db has been modified,
/// the transaction has to be rolled back and retried, which means extra work in terms of
/// CPU/battery.
/// the transaction has to be rolled back and retried, which means extra work in terms of
/// CPU/battery.
///
/// The only pro of making write transactions DEFERRED w/o the external locking would be some
/// parallelism between them.
@@ -691,7 +701,9 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
conn.pragma_update(None, "temp_store", "memory")?;
}
conn.pragma_update(None, "key", passphrase)?;
if !passphrase.is_empty() {
conn.pragma_update(None, "key", passphrase)?;
}
// Try to enable auto_vacuum. This will only be
// applied if the database is new or after successful
// VACUUM, which usually happens before backup export.
@@ -783,6 +795,17 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
.log_err(context)
.ok();
prune_connection_history(context)
.await
.context("Failed to prune connection history")
.log_err(context)
.ok();
prune_dns_cache(context)
.await
.context("Failed to prune DNS cache")
.log_err(context)
.ok();
// Delete POI locations
// which don't have corresponding message.
delete_orphaned_poi_locations(context)

View File

@@ -11,6 +11,7 @@ use crate::imap;
use crate::message::MsgId;
use crate::provider::get_provider_by_domain;
use crate::sql::Sql;
use crate::tools::inc_and_check;
const DBVERSION: i32 = 68;
const VERSION_CFG: &str = "dbversion";
@@ -941,10 +942,9 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
sql.execute_migration("ALTER TABLE msgs ADD COLUMN txt_normalized TEXT", 115)
.await?;
}
let migration_version: i32 = 115;
let mut migration_version: i32 = 115;
let migration_version: i32 = migration_version + 1;
ensure!(migration_version == 116, "Fix the number here");
inc_and_check(&mut migration_version, 116)?;
if dbversion < migration_version {
// Whether the message part doesn't need to be stored on the server. If all parts are marked
// deleted, a server-side deletion is issued.
@@ -955,6 +955,22 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
.await?;
}
inc_and_check(&mut migration_version, 117)?;
if dbversion < migration_version {
sql.execute_migration(
"CREATE TABLE connection_history (
host TEXT NOT NULL, -- server hostname
port INTEGER NOT NULL, -- server port
alpn TEXT NOT NULL, -- ALPN such as smtp or imap
addr TEXT NOT NULL, -- IP address
timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
UNIQUE (host, port, alpn, addr)
) STRICT",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -80,9 +80,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Fingerprints"))]
FingerPrints = 30,
#[strum(props(fallback = "Return receipt"))]
ReadRcpt = 31,
#[strum(props(fallback = "End-to-end encryption preferred"))]
E2ePreferred = 34,
@@ -440,9 +437,6 @@ pub enum StockMessage {
fallback = "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
))]
SecurejoinWaitTimeout = 191,
#[strum(props(fallback = "This message is a receipt notification."))]
ReadRcptMailBody = 192,
}
impl StockMessage {
@@ -795,16 +789,6 @@ pub(crate) async fn finger_prints(context: &Context) -> String {
translated(context, StockMessage::FingerPrints).await
}
/// Stock string: `Return receipt`.
pub(crate) async fn read_rcpt(context: &Context) -> String {
translated(context, StockMessage::ReadRcpt).await
}
/// Stock string: `This message is a receipt notification.`.
pub(crate) async fn read_rcpt_mail_body(context: &Context) -> String {
translated(context, StockMessage::ReadRcptMailBody).await
}
/// Stock string: `Group image deleted.`.
pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
if by_contact == ContactId::SELF {
@@ -859,10 +843,10 @@ pub(crate) async fn setup_contact_qr_description(
display_name: &str,
addr: &str,
) -> String {
let name = if display_name == addr {
let name = if display_name.is_empty() {
addr.to_owned()
} else {
format!("{display_name} ({addr})")
display_name.to_owned()
};
translated(context, StockMessage::SetupContactQRDescription)
.await

View File

@@ -129,6 +129,8 @@ impl Summary {
{
msg.get_file(context)
.and_then(|path| path.to_str().map(|p| p.to_owned()))
} else if msg.viewtype == Viewtype::Webxdc {
Some("webxdc-icon://last-msg-id".to_string())
} else {
None
};
@@ -356,6 +358,18 @@ mod tests {
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio
let mut msg = Message::new(Viewtype::File);
let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
.await
.unwrap();
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_eq!(msg.viewtype, Viewtype::Webxdc);
assert_summary_texts(&msg, ctx, "nice app!").await;
msg.set_text(some_text.clone());
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
let mut msg = Message::new(Viewtype::File);
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files

View File

@@ -120,6 +120,7 @@ impl Context {
/// Adds most recent qr-code tokens for a given chat to the list of items to be synced.
/// If device synchronization is disabled,
/// no tokens exist or the chat is unpromoted, the function does nothing.
/// The caller should perform `SchedulerState::interrupt_smtp()` on its own to trigger sending.
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
if !self.should_send_sync_msgs().await? {
return Ok(());
@@ -154,6 +155,7 @@ impl Context {
/// Adds deleted qr-code token to the list of items to be synced
/// so that the token also gets deleted on the other devices.
/// This interrupts SMTP on its own.
pub(crate) async fn sync_qr_code_token_deletion(
&self,
invitenumber: String,
@@ -164,10 +166,16 @@ impl Context {
auth,
grpid: None,
}))
.await
.await?;
self.scheduler.interrupt_smtp().await;
Ok(())
}
/// Sends out a self-sent message with items to be synchronized, if any.
///
/// Mustn't be called from multiple tasks in parallel to avoid sending the same sync items twice
/// because sync items are removed from the db only after successful sending. We guarantee this
/// by calling `send_sync_msg()` only from the SMTP loop.
pub async fn send_sync_msg(&self) -> Result<Option<MsgId>> {
if let Some((json, ids)) = self.build_sync_json().await? {
let chat_id =
@@ -608,7 +616,8 @@ mod tests {
alice
.set_config(Config::Displayname, Some("Alice Human"))
.await?;
alice.pop_sent_msg().await; // Sync message
alice.send_sync_msg().await?;
alice.pop_sent_msg().await;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.text, "hi");

View File

@@ -2,7 +2,7 @@
//!
//! This private module is only compiled for test runs.
#![allow(clippy::indexing_slicing)]
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
use std::ops::{Deref, DerefMut};
use std::panic;
@@ -175,7 +175,12 @@ impl TestContextManager {
));
let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap();
join_securejoin(&scanner.ctx, &qr).await.unwrap();
self.exec_securejoin_qr(scanner, scanned, &qr).await;
}
/// Executes SecureJoin initiated by `scanner` scanning `qr` generated by `scanned`.
pub async fn exec_securejoin_qr(&self, scanner: &TestContext, scanned: &TestContext, qr: &str) {
join_securejoin(&scanner.ctx, qr).await.unwrap();
loop {
if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await {
@@ -472,6 +477,36 @@ impl TestContext {
update_msg_state(&self.ctx, msg_id, MessageState::OutDelivered)
.await
.expect("failed to update message state");
let payload_headers = payload.split("\r\n\r\n").next().unwrap().lines();
let payload_header_names: Vec<_> = payload_headers
.map(|h| h.split(':').next().unwrap())
.collect();
// Check that we are sending exactly one From, Subject, Date, To, Message-ID, and MIME-Version header:
for header in &[
"From",
"Subject",
"Date",
"To",
"Message-ID",
"MIME-Version",
] {
assert_eq!(
payload_header_names.iter().filter(|h| *h == header).count(),
1,
"This sent email should contain the header {header} exactly 1 time:\n{payload}"
);
}
// Check that we aren't sending any header twice:
let mut hash_set = HashSet::new();
for header_name in payload_header_names {
assert!(
hash_set.insert(header_name),
"This sent email shouldn't contain the header {header_name} multiple times:\n{payload}"
);
}
Some(SentMessage {
payload,
sender_msg_id: msg_id,
@@ -1104,6 +1139,7 @@ pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) {
/// Pops a sync message from alice0 and receives it on alice1. Should be used after an action on
/// alice0's side that implies sending a sync message.
pub(crate) async fn sync(alice0: &TestContext, alice1: &TestContext) {
alice0.send_sync_msg().await.unwrap();
let sync_msg = alice0.pop_sent_msg().await;
let no_msg = alice1.recv_msg_opt(&sync_msg).await;
assert!(no_msg.is_none());

View File

@@ -881,7 +881,7 @@ async fn test_verified_member_added_reordering() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_no_unencrypted_name_if_verified() -> Result<()> {
async fn test_no_unencrypted_name_if_encrypted() -> Result<()> {
let mut tcm = TestContextManager::new();
for verified in [false, true] {
let alice = tcm.alice().await;
@@ -898,7 +898,7 @@ async fn test_no_unencrypted_name_if_verified() -> Result<()> {
let chat_id = bob.create_chat(&alice).await.id;
let msg = &bob.send_text(chat_id, "hi").await;
assert_eq!(msg.payload.contains("Bob Smith"), !verified);
assert_eq!(msg.payload.contains("Bob Smith"), false);
assert!(msg.payload.contains("BEGIN PGP MESSAGE"));
let msg = alice.recv_msg(msg).await;

View File

@@ -6,6 +6,7 @@
use std::borrow::Cow;
use std::io::{Cursor, Write};
use std::mem;
use std::ops::{AddAssign, Deref};
use std::path::{Path, PathBuf};
use std::str::from_utf8;
// If a time value doesn't need to be sent to another host, saved to the db or otherwise used across
@@ -19,7 +20,7 @@ pub use std::time::SystemTime as Time;
#[cfg(not(test))]
pub use std::time::SystemTime;
use anyhow::{bail, Context as _, Result};
use anyhow::{bail, ensure, Context as _, Result};
use base64::Engine as _;
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
use deltachat_contact_tools::EmailAddress;
@@ -29,6 +30,7 @@ use futures::{StreamExt, TryStreamExt};
use mailparse::dateparse;
use mailparse::headers::Headers;
use mailparse::MailHeaderMap;
use num_traits::PrimInt;
use rand::{thread_rng, Rng};
use tokio::{fs, io};
use url::Url;
@@ -256,6 +258,7 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time
/// Generate an ID. The generated ID should be as short and as unique as possible:
/// - short, because it may also used as part of Message-ID headers or in QR codes
/// - unique as two IDs generated on two devices should not be the same. However, collisions are not world-wide but only by the few contacts.
///
/// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters.
///
/// Additional information when used as a message-id or group-id:
@@ -303,7 +306,7 @@ pub fn get_filesuffix_lc(path_filename: &str) -> Option<String> {
/// Returns the `(width, height)` of the given image buffer.
pub fn get_filemeta(buf: &[u8]) -> Result<(u32, u32)> {
let image = image::io::Reader::new(Cursor::new(buf)).with_guessed_format()?;
let image = image::ImageReader::new(Cursor::new(buf)).with_guessed_format()?;
let dimensions = image.into_dimensions()?;
Ok(dimensions)
}
@@ -361,6 +364,41 @@ pub async fn delete_files_in_dir(context: &Context, path: impl AsRef<Path>) -> R
Ok(())
}
/// A guard which will remove the path when dropped.
///
/// It implements [`Deref`] so it can be used as a `&Path`.
#[derive(Debug)]
pub(crate) struct TempPathGuard {
path: PathBuf,
}
impl TempPathGuard {
pub(crate) fn new(path: PathBuf) -> Self {
Self { path }
}
}
impl Drop for TempPathGuard {
fn drop(&mut self) {
let path = self.path.clone();
std::fs::remove_file(path).ok();
}
}
impl Deref for TempPathGuard {
type Target = Path;
fn deref(&self) -> &Self::Target {
&self.path
}
}
impl AsRef<Path> for TempPathGuard {
fn as_ref(&self) -> &Path {
self
}
}
pub(crate) async fn create_folder(
context: &Context,
path: impl AsRef<Path>,
@@ -647,6 +685,16 @@ pub(crate) fn buf_decompress(buf: &[u8]) -> Result<Vec<u8>> {
Ok(mem::take(decompressor.get_mut()))
}
/// Increments `*t` and checks that it equals to `expected` after that.
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,
expected: T,
) -> Result<()> {
*t += T::one();
ensure!(*t == expected, "Incremented value != {expected:?}");
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::indexing_slicing)]

View File

@@ -18,6 +18,7 @@
mod integration;
mod maps_integration;
use std::cmp::max;
use std::path::Path;
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
@@ -122,6 +123,11 @@ impl StatusUpdateSerial {
StatusUpdateSerial(id)
}
/// Minimum value.
pub const MIN: Self = Self(1);
/// Maximum value.
pub const MAX: Self = Self(u32::MAX - 1);
/// Gets StatusUpdateSerial as untyped integer.
/// Avoid using this outside ffi.
pub fn to_u32(self) -> u32 {
@@ -196,6 +202,9 @@ fn find_zip_entry<'a>(
None
}
/// Status update JSON size soft limit.
const STATUS_UPDATE_SIZE_MAX: usize = 100 << 10;
impl Context {
/// check if a file is an acceptable webxdc for sending or receiving.
pub(crate) async fn is_webxdc_file(&self, filename: &str, file: &[u8]) -> Result<bool> {
@@ -505,22 +514,19 @@ impl Context {
Ok(())
}
/// Pops one record of queued webxdc status updates.
/// This function exists to make the sqlite statement testable.
async fn pop_smtp_status_update(
/// Returns one record of the queued webxdc status updates.
async fn smtp_status_update_get(
&self,
) -> Result<Option<(MsgId, StatusUpdateSerial, StatusUpdateSerial, String)>> {
let _lock = self.sql.write_lock().await;
) -> Result<Option<(MsgId, i64, StatusUpdateSerial, String)>> {
let res = self
.sql
.query_row_optional(
"DELETE FROM smtp_status_updates
WHERE msg_id IN (SELECT msg_id FROM smtp_status_updates LIMIT 1)
RETURNING msg_id, first_serial, last_serial, descr",
"SELECT msg_id, first_serial, last_serial, descr \
FROM smtp_status_updates LIMIT 1",
(),
|row| {
let instance_id: MsgId = row.get(0)?;
let first_serial: StatusUpdateSerial = row.get(1)?;
let first_serial: i64 = row.get(1)?;
let last_serial: StatusUpdateSerial = row.get(2)?;
let descr: String = row.get(3)?;
Ok((instance_id, first_serial, last_serial, descr))
@@ -530,19 +536,50 @@ impl Context {
Ok(res)
}
async fn smtp_status_update_pop_serials(
&self,
msg_id: MsgId,
first: i64,
first_new: StatusUpdateSerial,
) -> Result<()> {
if self
.sql
.execute(
"DELETE FROM smtp_status_updates \
WHERE msg_id=? AND first_serial=? AND last_serial<?",
(msg_id, first, first_new),
)
.await?
> 0
{
return Ok(());
}
self.sql
.execute(
"UPDATE smtp_status_updates SET first_serial=? \
WHERE msg_id=? AND first_serial=?",
(first_new, msg_id, first),
)
.await?;
Ok(())
}
/// Attempts to send queued webxdc status updates.
pub(crate) async fn flush_status_updates(&self) -> Result<()> {
loop {
let (instance_id, first_serial, last_serial, descr) =
match self.pop_smtp_status_update().await? {
Some(res) => res,
None => return Ok(()),
};
if let Some(json) = self
.render_webxdc_status_update_object(instance_id, Some((first_serial, last_serial)))
.await?
{
let (instance_id, first, last, descr) = match self.smtp_status_update_get().await? {
Some(res) => res,
None => return Ok(()),
};
let (json, first_new) = self
.render_webxdc_status_update_object(
instance_id,
StatusUpdateSerial(max(first, 1).try_into()?),
last,
Some(STATUS_UPDATE_SIZE_MAX),
)
.await?;
if let Some(json) = json {
let instance = Message::load_from_db(self, instance_id).await?;
let mut status_update = Message {
chat_id: instance.chat_id,
@@ -559,6 +596,8 @@ impl Context {
status_update.param.remove(Param::GuaranteeE2ee); // may be set by set_quote(), if #2985 is done, this line can be removed
chat::send_msg(self, instance.chat_id, &mut status_update).await?;
}
self.smtp_status_update_pop_serials(instance_id, first, first_new)
.await?;
}
}
@@ -690,45 +729,59 @@ impl Context {
/// Renders JSON-object for status updates as used on the wire.
///
/// Example: `{"updates": [{"payload":"any update data"},
/// {"payload":"another update data"}]}`
/// Returns optional JSON and the first serial of updates not included due to a JSON size
/// limit. If all requested updates are included, returns the first not requested serial.
///
/// `range` is an optional range of status update serials to send.
/// If it is `None`, all updates are sent.
/// This is used when a message is resent using [`crate::chat::resend_msgs`].
/// Example JSON: `{"updates": [{"payload":"any update data"},
/// {"payload":"another update data"}]}`
///
/// * `(first, last)`: range of status update serials to send.
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,
range: Option<(StatusUpdateSerial, StatusUpdateSerial)>,
) -> Result<Option<String>> {
let json = self
first: StatusUpdateSerial,
last: StatusUpdateSerial,
size_max: Option<usize>,
) -> Result<(Option<String>, StatusUpdateSerial)> {
let (json, first_new) = self
.sql
.query_map(
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND id>=? AND id<=? ORDER BY id",
(
instance_msg_id,
range.map(|r|r.0).unwrap_or(StatusUpdateSerial(0)),
range.map(|r|r.1).unwrap_or(StatusUpdateSerial(u32::MAX)),
),
|row| row.get::<_, String>(0),
"SELECT id, update_item FROM msgs_status_updates \
WHERE msg_id=? AND id>=? AND id<=? ORDER BY id",
(instance_msg_id, first, last),
|row| {
let id: StatusUpdateSerial = row.get(0)?;
let update_item: String = row.get(1)?;
Ok((id, update_item))
},
|rows| {
let mut json = String::default();
for row in rows {
let update_item = row?;
let (id, update_item) = row?;
if !json.is_empty()
&& json.len() + update_item.len() >= size_max.unwrap_or(usize::MAX)
{
return Ok((json, id));
}
if !json.is_empty() {
json.push_str(",\n");
}
json.push_str(&update_item);
}
Ok(json)
Ok((
json,
// Too late to fail here if an overflow happens. It's still better to send
// the updates.
StatusUpdateSerial::new(last.to_u32().saturating_add(1)),
))
},
)
.await?;
if json.is_empty() {
Ok(None)
} else {
Ok(Some(format!(r#"{{"updates":[{json}]}}"#)))
}
let json = match json.is_empty() {
true => None,
false => Some(format!(r#"{{"updates":[{json}]}}"#)),
};
Ok((json, first_new))
}
}
@@ -1089,10 +1142,13 @@ mod tests {
assert_eq!(alice_grp.get_msg_cnt(&alice).await?, 3);
resend_msgs(&alice, &[alice_instance.id]).await?;
let sent1 = alice.pop_sent_msg().await;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
// Bob received webxdc, legacy info-messages updates are received but not added to the chat
// Bob receives webxdc, legacy info-messages updates are received and added to the chat.
let bob = tcm.bob().await;
let bob_instance = bob.recv_msg(&sent1).await;
bob.recv_msg_trash(&sent2).await;
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
assert!(!bob_instance.is_info());
assert_eq!(
@@ -1684,6 +1740,79 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_send_big_webxdc_status_update() -> Result<()> {
let alice = TestContext::new_alice().await;
alice.set_config_bool(Config::BccSelf, true).await?;
let bob = TestContext::new_bob().await;
let alice_chat = alice.create_chat(&bob).await;
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
let sent1 = &alice.pop_sent_msg().await;
assert_eq!(alice_instance.viewtype, Viewtype::Webxdc);
assert!(!sent1.payload().contains("report-type=status-update"));
let update1_str = r#"{"payload":{"foo":""#.to_string()
+ &String::from_utf8(vec![b'a'; STATUS_UPDATE_SIZE_MAX])?
+ r#""}"#;
alice
.send_webxdc_status_update(alice_instance.id, &(update1_str.clone() + "}"), "descr1")
.await?;
alice
.send_webxdc_status_update(
alice_instance.id,
r#"{"payload" : {"foo":"bar2"}}"#,
"descr2",
)
.await?;
alice
.send_webxdc_status_update(
alice_instance.id,
r#"{"payload" : {"foo":"bar3"}}"#,
"descr3",
)
.await?;
alice.flush_status_updates().await?;
// There's the message stack, so we pop messages in the reverse order.
let sent3 = &alice.pop_sent_msg().await;
let alice_update = sent3.load_from_db().await;
assert_eq!(alice_update.text, "descr3".to_string());
let sent2 = &alice.pop_sent_msg().await;
let alice_update = sent2.load_from_db().await;
assert_eq!(alice_update.text, "descr3".to_string());
assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1);
// Bob receives the instance.
let bob_instance = bob.recv_msg(sent1).await;
let bob_chat_id = bob_instance.chat_id;
assert_eq!(bob_instance.rfc724_mid, alice_instance.rfc724_mid);
assert_eq!(bob_instance.viewtype, Viewtype::Webxdc);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
// Bob receives the status updates.
bob.recv_msg_trash(sent2).await;
expect_status_update_event(&bob, bob_instance.id).await?;
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
"[".to_string() + &update1_str + r#","serial":1,"max_serial":1}]"#
);
bob.recv_msg_trash(sent3).await;
for _ in 0..2 {
expect_status_update_event(&bob, bob_instance.id).await?;
}
assert_eq!(
bob.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(1))
.await?,
r#"[{"payload":{"foo":"bar2"},"serial":2,"max_serial":3},
{"payload":{"foo":"bar3"},"serial":3,"max_serial":3}]"#
);
assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_render_webxdc_status_update_object() -> Result<()> {
let t = TestContext::new_alice().await;
@@ -1695,17 +1824,20 @@ mod tests {
)
.await?;
chat_id.set_draft(&t, Some(&mut instance)).await?;
assert!(t
.render_webxdc_status_update_object(instance.id, None)
.await?
.is_none());
let (first, last) = (StatusUpdateSerial(1), StatusUpdateSerial::MAX);
assert_eq!(
t.render_webxdc_status_update_object(instance.id, first, last, None)
.await?,
(None, StatusUpdateSerial(u32::MAX))
);
t.send_webxdc_status_update(instance.id, r#"{"payload": 1}"#, "bla")
.await?;
assert!(t
.render_webxdc_status_update_object(instance.id, None)
.await?
.is_some());
let (object, first_new) = t
.render_webxdc_status_update_object(instance.id, first, last, None)
.await?;
assert!(object.is_some());
assert_eq!(first_new, StatusUpdateSerial(u32::MAX));
Ok(())
}
@@ -1723,13 +1855,16 @@ mod tests {
.await?;
t.send_webxdc_status_update(instance.id, r#"{"payload": 4}"#, "d")
.await?;
let json = t
let (json, first_new) = t
.render_webxdc_status_update_object(
instance.id,
Some((StatusUpdateSerial(2), StatusUpdateSerial(3))),
StatusUpdateSerial(2),
StatusUpdateSerial(3),
None,
)
.await?
.unwrap();
.await?;
let json = json.unwrap();
assert_eq!(first_new, StatusUpdateSerial(4));
let json = Regex::new(r#""uid":"[^"]*""#)
.unwrap()
.replace_all(&json, "XXX");
@@ -1761,7 +1896,7 @@ mod tests {
let instance1 = send_webxdc_instance(&t, chat_id).await?;
let instance2 = send_webxdc_instance(&t, chat_id).await?;
let instance3 = send_webxdc_instance(&t, chat_id).await?;
assert!(t.pop_smtp_status_update().await?.is_none());
assert!(t.smtp_status_update_get().await?.is_none());
t.send_webxdc_status_update(instance1.id, r#"{"payload": "1a"}"#, "descr1a")
.await?;
@@ -1782,20 +1917,27 @@ mod tests {
3
);
// order of pop_status_update() is not defined, therefore the more complicated test
// order of smtp_status_update_get() is not defined, therefore the more complicated test
let mut instances_checked = 0;
for i in 0..3 {
let (instance, min_ser, max_ser, descr) = t.pop_smtp_status_update().await?.unwrap();
let (instance, min_ser, max_ser, descr) = t.smtp_status_update_get().await?.unwrap();
t.smtp_status_update_pop_serials(
instance,
min_ser,
StatusUpdateSerial::new(max_ser.to_u32().checked_add(1).unwrap()),
)
.await?;
let min_ser: u32 = min_ser.try_into()?;
if instance == instance1.id {
assert_eq!(min_ser, max_ser);
assert_eq!(min_ser, max_ser.to_u32());
assert_eq!(descr, "descr1a");
instances_checked += 1;
} else if instance == instance2.id {
assert_eq!(min_ser.to_u32(), max_ser.to_u32() - 1);
assert_eq!(min_ser, max_ser.to_u32() - 1);
assert_eq!(descr, "descr2b");
instances_checked += 1;
} else if instance == instance3.id {
assert_eq!(min_ser.to_u32(), max_ser.to_u32() - 2);
assert_eq!(min_ser, max_ser.to_u32() - 2);
assert_eq!(descr, "descr3c");
instances_checked += 1;
} else {
@@ -1809,7 +1951,7 @@ mod tests {
);
}
assert_eq!(instances_checked, 3);
assert!(t.pop_smtp_status_update().await?.is_none());
assert!(t.smtp_status_update_get().await?.is_none());
Ok(())
}
@@ -1836,12 +1978,11 @@ mod tests {
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload": {"foo":"bar"}}"#, "descr")
.await?;
alice.flush_status_updates().await?;
expect_status_update_event(&alice, alice_instance.id).await?;
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload":42, "info":"i"}"#, "descr")
.await?;
alice.flush_status_updates().await?;
expect_status_update_event(&alice, alice_instance.id).await?;
assert_eq!(
alice
.sql
@@ -2509,6 +2650,7 @@ sth_for_the = "future""#
assert_eq!(chatlist.len(), 1);
let summary = chatlist.get_summary(&t, 0, None).await?;
assert_eq!(summary.text, "nice app!".to_string());
assert_eq!(summary.thumbnail_path.unwrap(), "webxdc-icon://last-msg-id");
Ok(())
}

View File

@@ -19,7 +19,7 @@ Authorization | OAuth2 ([RFC 6749][])
End-to-end encryption | [Autocrypt Level 1][], OpenPGP ([RFC 4880][]), Security Multiparts for MIME ([RFC 1847][]) and [“Mixed Up” Encryption repairing](https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html)
Detect/prevent active attacks | [securejoin][] protocols
Compare public keys | [openpgp4fpr][] URI Scheme
Header encryption | [Protected Headers for Cryptographic E-mail](https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/)
Header encryption | [Header Protection for Cryptographically Protected E-mail](https://datatracker.ietf.org/doc/draft-ietf-lamps-header-protection/)
Configuration assistance | [Autoconfigure](https://web.archive.org/web/20210402044801/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) and [Autodiscover][]
Messenger functions | [Chat-over-Email](https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#chat-mail-specification)
Detect mailing list | List-Id ([RFC 2919][]) and Precedence ([RFC 3834][])