mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
Compare commits
237 Commits
v1.141.0
...
link2xt/na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
365afd9ac5 | ||
|
|
418dfbf994 | ||
|
|
533a872118 | ||
|
|
2ae854e8ea | ||
|
|
3969383857 | ||
|
|
e4ebb91712 | ||
|
|
eb3c1b3c25 | ||
|
|
c257482838 | ||
|
|
0a46e64971 | ||
|
|
845420cf17 | ||
|
|
96ea0db88e | ||
|
|
d99c735e12 | ||
|
|
d48f4100e9 | ||
|
|
7e73d5fdac | ||
|
|
152cdfe9bc | ||
|
|
a9eedafbcb | ||
|
|
5baf191483 | ||
|
|
2d2e703884 | ||
|
|
026450ddf3 | ||
|
|
5646782d23 | ||
|
|
dd1c2e836b | ||
|
|
be73076e9e | ||
|
|
9d47be0d8a | ||
|
|
fcf3dbbad4 | ||
|
|
d344cc3bdd | ||
|
|
93e181b2da | ||
|
|
3867808927 | ||
|
|
c7c3b9ca90 | ||
|
|
54cfc21e28 | ||
|
|
f01514dba4 | ||
|
|
ee5723416e | ||
|
|
aab8ef2726 | ||
|
|
84c1ffd7cc | ||
|
|
273158a337 | ||
|
|
099f0e2d18 | ||
|
|
2dd85afdc2 | ||
|
|
cdeca9ed9d | ||
|
|
af77c0c987 | ||
|
|
f912bc78e6 | ||
|
|
137ee9334c | ||
|
|
36e5e964e5 | ||
|
|
495337743a | ||
|
|
775edab7b1 | ||
|
|
fe9fa17005 | ||
|
|
ef12a76a9e | ||
|
|
6b3de9d7da | ||
|
|
3599e4be16 | ||
|
|
8dc844e194 | ||
|
|
104c60840a | ||
|
|
f2cb098148 | ||
|
|
30b998eca3 | ||
|
|
b5133fe8c8 | ||
|
|
0d0f556f21 | ||
|
|
0e365395bf | ||
|
|
08ec133aac | ||
|
|
7d7391887a | ||
|
|
e7d4ccffe2 | ||
|
|
8538a3c148 | ||
|
|
cb4b992204 | ||
|
|
af4d54ab50 | ||
|
|
1faff84905 | ||
|
|
62fde21d9a | ||
|
|
6f3729a00f | ||
|
|
fbf66ba02b | ||
|
|
ed74f4d1d9 | ||
|
|
a268946f8d | ||
|
|
7432c6de84 | ||
|
|
7fe9342d0d | ||
|
|
a0e89e4d4e | ||
|
|
0c3a476449 | ||
|
|
de517c15ff | ||
|
|
b83d5b0dbf | ||
|
|
27924a259f | ||
|
|
530256b1bf | ||
|
|
23d15d7485 | ||
|
|
3c38d2e105 | ||
|
|
a53ffcf5e3 | ||
|
|
22366cf246 | ||
|
|
ddc2b86875 | ||
|
|
9e966615f2 | ||
|
|
3335fc727d | ||
|
|
00d7b38e02 | ||
|
|
2a8a98c432 | ||
|
|
13841491d4 | ||
|
|
2137c05cd6 | ||
|
|
6519630d46 | ||
|
|
7c6d6a4b12 | ||
|
|
745b33f174 | ||
|
|
153188db20 | ||
|
|
4a2ebd0c81 | ||
|
|
e701709645 | ||
|
|
1ca835f34d | ||
|
|
1c021ae5ca | ||
|
|
479a4c2880 | ||
|
|
5ce44ade17 | ||
|
|
f03ffa7641 | ||
|
|
b44185948d | ||
|
|
6b4532a08e | ||
|
|
86ad5506e3 | ||
|
|
6513349c09 | ||
|
|
92685189aa | ||
|
|
3b76622cf1 | ||
|
|
c5a524d3c6 | ||
|
|
17eb85b9cd | ||
|
|
3c688360fb | ||
|
|
9f220768c2 | ||
|
|
fd183c6ee5 | ||
|
|
9788fb16e8 | ||
|
|
39ed587959 | ||
|
|
c4327a0558 | ||
|
|
1b92d18777 | ||
|
|
a67503ae4a | ||
|
|
c54f39bea0 | ||
|
|
ff3138fa43 | ||
|
|
09d46942ca | ||
|
|
84e365d263 | ||
|
|
b31bcf5561 | ||
|
|
da50d682e1 | ||
|
|
094d310f5c | ||
|
|
642eaf92d7 | ||
|
|
76c032a2c4 | ||
|
|
a74b04d175 | ||
|
|
c9448feafc | ||
|
|
8314f3e30c | ||
|
|
935da2db49 | ||
|
|
b5e95fa1ef | ||
|
|
b60d8356cb | ||
|
|
ee7a7a2f9d | ||
|
|
b5eb824346 | ||
|
|
41867b89a0 | ||
|
|
7e7aa7aba0 | ||
|
|
fd1dab7c7b | ||
|
|
a69f9f01b3 | ||
|
|
c808ed1368 | ||
|
|
21be85071a | ||
|
|
a30c6ae1f7 | ||
|
|
0324884124 | ||
|
|
ad225b12c2 | ||
|
|
0dd5e5ab7d | ||
|
|
490f41cda8 | ||
|
|
c163438eaf | ||
|
|
ef925b0948 | ||
|
|
0fceb270ca | ||
|
|
4ec5d12213 | ||
|
|
d9c0e47581 | ||
|
|
8ec4a8ad46 | ||
|
|
40d355209b | ||
|
|
354702fcab | ||
|
|
bfc7ae1eff | ||
|
|
cccefe15b3 | ||
|
|
bb4236ffed | ||
|
|
14d57e780b | ||
|
|
76a43c8de6 | ||
|
|
b807435c42 | ||
|
|
3b040fd4b5 | ||
|
|
b9b9ed197e | ||
|
|
03523ab589 | ||
|
|
c4efe59a12 | ||
|
|
d46f53a004 | ||
|
|
5fb5fd4318 | ||
|
|
a3cb58484f | ||
|
|
04fd2cdcab | ||
|
|
a710c034e4 | ||
|
|
bd651d9ef3 | ||
|
|
7f3e8f9796 | ||
|
|
837311abce | ||
|
|
c596ee0256 | ||
|
|
5815d8f1dd | ||
|
|
2675e7b2e1 | ||
|
|
8f400dda85 | ||
|
|
2a605b93cd | ||
|
|
e4d65b2f3b | ||
|
|
87a45e88dc | ||
|
|
d6d90db957 | ||
|
|
eb669afb8f | ||
|
|
d1cf80001e | ||
|
|
307d11f503 | ||
|
|
73f527e772 | ||
|
|
5143ebece1 | ||
|
|
9996c2db80 | ||
|
|
0f26da4028 | ||
|
|
a3dd37b011 | ||
|
|
6b11b0ea8d | ||
|
|
faad7d5843 | ||
|
|
ef0d6d0c90 | ||
|
|
bd83fb3d38 | ||
|
|
f84e603318 | ||
|
|
d77459e4fc | ||
|
|
2c14bd353f | ||
|
|
0860508a1d | ||
|
|
f81daa16b3 | ||
|
|
436b00e3cb | ||
|
|
4d52aa8b7f | ||
|
|
c2d5488663 | ||
|
|
cc51d51a78 | ||
|
|
7f1068e37e | ||
|
|
81777fac47 | ||
|
|
9a6147b643 | ||
|
|
a2dacc333c | ||
|
|
088008a030 | ||
|
|
a198e9fce8 | ||
|
|
3f087e5fb1 | ||
|
|
5beb4a5f27 | ||
|
|
ba7eaca762 | ||
|
|
d31f897f9e | ||
|
|
e60598bafd | ||
|
|
df29767fc7 | ||
|
|
e58a1a2aad | ||
|
|
74f98e2b79 | ||
|
|
c4cfde3c4c | ||
|
|
5792d7b18d | ||
|
|
5fa7cff468 | ||
|
|
a76a2715ad | ||
|
|
2d2a61f7df | ||
|
|
9f963c0b61 | ||
|
|
69595a6bb4 | ||
|
|
bbac5a499a | ||
|
|
1b241b62f3 | ||
|
|
1f36595d19 | ||
|
|
e8c0f85016 | ||
|
|
2dbddef5e9 | ||
|
|
4a34ae5cdc | ||
|
|
b2ad958340 | ||
|
|
53217d5eb8 | ||
|
|
7a5dca2645 | ||
|
|
170cbb6635 | ||
|
|
ee2fffb52b | ||
|
|
68b62392bf | ||
|
|
222e1ce4a6 | ||
|
|
ac198b17bf | ||
|
|
4ed9c04e9b | ||
|
|
ce44312ac0 | ||
|
|
71104e9312 | ||
|
|
ced5f51482 | ||
|
|
c400491c07 | ||
|
|
72a1406b86 | ||
|
|
11e13d1873 |
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -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"
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/node-docs.yml
vendored
2
.github/workflows/node-docs.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/upload-docs.yml
vendored
2
.github/workflows/upload-docs.yml
vendored
@@ -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
|
||||
|
||||
326
CHANGELOG.md
326
CHANGELOG.md
@@ -1,5 +1,316 @@
|
||||
# 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
|
||||
|
||||
- Add `is_muted` config option.
|
||||
- Parse vcards exported by protonmail ([#5723](https://github.com/deltachat/deltachat-core-rust/pull/5723)).
|
||||
- Disable sending sync messages for bots ([#5705](https://github.com/deltachat/deltachat-core-rust/pull/5705)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Don't fail if going to send plaintext, but some peerstate is missing.
|
||||
- Correctly sanitize input everywhere ([#5697](https://github.com/deltachat/deltachat-core-rust/pull/5697)).
|
||||
- Do not try to register non-iOS tokens for heartbeats.
|
||||
- imap: Reset new_mail if folder is ignored.
|
||||
- Use and prefer Date from signed message part ([#5716](https://github.com/deltachat/deltachat-core-rust/pull/5716)).
|
||||
- Distinguish between database errors and no gossip topic.
|
||||
- MimeFactory::verified: Return true for self-chat.
|
||||
|
||||
### Refactor
|
||||
|
||||
- `MimeFactory::is_e2ee_guaranteed()`: always respect `Param::ForcePlaintext`.
|
||||
- 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)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document vCards in the specification ([#5724](https://github.com/deltachat/deltachat-core-rust/pull/5724))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump toml from 0.8.13 to 0.8.14.
|
||||
- cargo: Bump serde_json from 1.0.117 to 1.0.120.
|
||||
- cargo: Bump syn from 2.0.66 to 2.0.68.
|
||||
- cargo: Bump async-broadcast from 0.7.0 to 0.7.1.
|
||||
- cargo: Bump url from 2.5.0 to 2.5.2.
|
||||
- cargo: Bump log from 0.4.21 to 0.4.22.
|
||||
- cargo: Bump regex from 1.10.4 to 1.10.5.
|
||||
- cargo: Bump proptest from 1.4.0 to 1.5.0.
|
||||
- cargo: Bump uuid from 1.8.0 to 1.9.1.
|
||||
- cargo: Bump backtrace from 0.3.72 to 0.3.73.
|
||||
- cargo: Bump quick-xml from 0.31.0 to 0.35.0.
|
||||
- cargo: Update yerpc to 0.6.2.
|
||||
- cargo: Update rPGP from 0.11 to 0.13.
|
||||
|
||||
## [1.141.1] - 2024-06-27
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update quota if it's stale, not fresh ([#5683](https://github.com/deltachat/deltachat-core-rust/pull/5683)).
|
||||
- sql: Assign migration adding msgs.deleted a new number.
|
||||
|
||||
### Refactor
|
||||
|
||||
- mimefactory: Factor out header confidentiality policy ([#5715](https://github.com/deltachat/deltachat-core-rust/pull/5715)).
|
||||
- Improve logging during SMTP/IMAP configuration.
|
||||
|
||||
## [1.141.0] - 2024-06-24
|
||||
|
||||
### API-Changes
|
||||
@@ -4480,3 +4791,18 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.140.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.0...v1.140.1
|
||||
[1.140.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.140.1...v1.140.2
|
||||
[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
|
||||
|
||||
1912
Cargo.lock
generated
1912
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
52
Cargo.toml
52
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.141.0"
|
||||
version = "1.142.12"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -34,20 +34,20 @@ 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" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.0"
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.10.0", default-features = false, features = ["runtime-tokio"] }
|
||||
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"] }
|
||||
bytes = "1"
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
@@ -58,12 +58,13 @@ futures = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
http-body-util = "0.1.2"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.7"
|
||||
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.23.0", default-features = false }
|
||||
iroh-gossip = { version = "0.23.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 +76,19 @@ num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pgp = { version = "0.13.2", default-features = false }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quick-xml = "0.36"
|
||||
quoted_printable = "0.5"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.11.27", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
@@ -105,11 +105,11 @@ url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = { workspace = true }
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
nu-ansi-term = { workspace = true }
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = { workspace = true }
|
||||
testdir = "0.9.0"
|
||||
@@ -159,34 +159,46 @@ harness = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
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"
|
||||
log = "0.4"
|
||||
nu-ansi-term = "0.46"
|
||||
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.5.2"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"async-native-tls/vendored",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl"
|
||||
]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
BIN
assets/certificates/imap.nauta.cu.der
Normal file
BIN
assets/certificates/imap.nauta.cu.der
Normal file
Binary file not shown.
BIN
assets/certificates/smtp.nauta.cu.der
Normal file
BIN
assets/certificates/smtp.nauta.cu.der
Normal file
Binary file not shown.
@@ -22,7 +22,8 @@
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string,
|
||||
clippy::bool_to_int_with_if
|
||||
clippy::bool_to_int_with_if,
|
||||
clippy::manual_range_contains
|
||||
)]
|
||||
|
||||
use std::fmt;
|
||||
@@ -35,10 +36,6 @@ use chrono::{DateTime, NaiveDateTime};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
// TODOs to clean up:
|
||||
// - Check if sanitizing is done correctly everywhere
|
||||
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A Contact, as represented in a VCard.
|
||||
pub struct VcardContact {
|
||||
@@ -115,7 +112,9 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
|
||||
// TODO this doesn't handle the case where there are quotes around a colon
|
||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
||||
// This could be improved in the future, but for now, the parsing is good enough.
|
||||
let (params, value) = remainder.split_once(':')?;
|
||||
// In the example from above, `params` is now `;TYPE=work`
|
||||
// and `value` is now `alice@example.com`
|
||||
@@ -175,7 +174,15 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
let mut photo = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for line in lines.by_ref() {
|
||||
for mut line in lines.by_ref() {
|
||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
||||
// Remove the group name, if the group is called "item1".
|
||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
||||
//
|
||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
||||
line = remainder;
|
||||
}
|
||||
|
||||
if let Some(email) = vcard_property(line, "email") {
|
||||
addr.get_or_insert(email);
|
||||
} else if let Some(name) = vcard_property(line, "fn") {
|
||||
@@ -183,6 +190,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
|
||||
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
|
||||
{
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
||||
@@ -263,27 +271,27 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the name and address
|
||||
/// Takes a name and an address and sanitizes them:
|
||||
/// - Extracts a name from the addr if the addr is in form "Alice <alice@example.org>"
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
|
||||
captures.get(1).map_or("", |m| m.as_str())
|
||||
} else {
|
||||
strip_rtlo_characters(name)
|
||||
name
|
||||
},
|
||||
captures
|
||||
.get(2)
|
||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
strip_rtlo_characters(&normalize_name(name)),
|
||||
addr.to_string(),
|
||||
)
|
||||
(name, addr.to_string())
|
||||
};
|
||||
let mut name = normalize_name(&name);
|
||||
let mut name = sanitize_name(name);
|
||||
|
||||
// If the 'display name' is just the address, remove it:
|
||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
||||
@@ -295,31 +303,77 @@ pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
(name, addr)
|
||||
}
|
||||
|
||||
/// Normalize a name.
|
||||
/// Sanitizes a name.
|
||||
///
|
||||
/// - Remove quotes (come from some bad MUA implementations)
|
||||
/// - Trims the resulting string
|
||||
///
|
||||
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
|
||||
pub fn normalize_name(full_name: &str) -> String {
|
||||
let full_name = full_name.trim();
|
||||
if full_name.is_empty() {
|
||||
return full_name.into();
|
||||
}
|
||||
/// - Removes newlines and trims the string
|
||||
/// - Removes quotes (come from some bad MUA implementations)
|
||||
/// - Removes potentially-malicious bidi characters
|
||||
pub fn sanitize_name(name: &str) -> String {
|
||||
let name = sanitize_single_line(name);
|
||||
|
||||
match full_name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
|
||||
.get(1..full_name.len() - 1)
|
||||
match name.as_bytes() {
|
||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => name
|
||||
.get(1..name.len() - 1)
|
||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
||||
_ => full_name.to_string(),
|
||||
_ => name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitizes user input
|
||||
///
|
||||
/// - Removes newlines and trims the string
|
||||
/// - Removes potentially-malicious bidi characters
|
||||
pub fn sanitize_single_line(input: &str) -> String {
|
||||
sanitize_bidi_characters(input.replace(['\n', '\r'], " ").trim())
|
||||
}
|
||||
|
||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
||||
/// This method strips all occurrences of the RTLO Unicode character.
|
||||
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
|
||||
pub fn strip_rtlo_characters(input_str: &str) -> String {
|
||||
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
|
||||
const ISOLATE_CHARACTERS: [char; 3] = ['\u{2066}', '\u{2067}', '\u{2068}'];
|
||||
const POP_ISOLATE_CHARACTER: char = '\u{2069}';
|
||||
/// Some control unicode characters can influence whether adjacent text is shown from
|
||||
/// left to right or from right to left.
|
||||
///
|
||||
/// Since user input is not supposed to influence how adjacent text looks,
|
||||
/// this function removes some of these characters.
|
||||
///
|
||||
/// Also see https://github.com/deltachat/deltachat-core-rust/issues/3479.
|
||||
pub fn sanitize_bidi_characters(input_str: &str) -> String {
|
||||
// RTLO_CHARACTERS are apparently rarely used in practice.
|
||||
// They can impact all following text, so, better remove them all:
|
||||
let input_str = input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "");
|
||||
|
||||
// If the ISOLATE characters are not ended with a POP DIRECTIONAL ISOLATE character,
|
||||
// we regard the input as potentially malicious and simply remove all ISOLATE characters.
|
||||
// See https://en.wikipedia.org/wiki/Bidirectional_text#Unicode_bidi_support
|
||||
// and https://www.w3.org/International/questions/qa-bidi-unicode-controls.en
|
||||
// for an explanation about ISOLATE characters.
|
||||
fn isolate_characters_are_valid(input_str: &str) -> bool {
|
||||
let mut isolate_character_nesting: i32 = 0;
|
||||
for char in input_str.chars() {
|
||||
if ISOLATE_CHARACTERS.contains(&char) {
|
||||
isolate_character_nesting += 1;
|
||||
} else if char == POP_ISOLATE_CHARACTER {
|
||||
isolate_character_nesting -= 1;
|
||||
}
|
||||
|
||||
// According to Wikipedia, 125 levels are allowed:
|
||||
// https://en.wikipedia.org/wiki/Unicode_control_characters
|
||||
// (although, in practice, we could also significantly lower this number)
|
||||
if isolate_character_nesting < 0 || isolate_character_nesting > 125 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
isolate_character_nesting == 0
|
||||
}
|
||||
|
||||
if isolate_characters_are_valid(&input_str) {
|
||||
input_str
|
||||
} else {
|
||||
input_str.replace(
|
||||
|char| ISOLATE_CHARACTERS.contains(&char) || POP_ISOLATE_CHARACTER == char,
|
||||
"",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
@@ -668,4 +722,89 @@ END:VCARD
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protonmail_vcard() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN;PREF=1:Alice Wonderland
|
||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
ITEM1.X-PM-ENCRYPT:true
|
||||
ITEM1.X-PM-SIGN:true
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_name() {
|
||||
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
||||
assert_eq!(&sanitize_name("<"), "<");
|
||||
assert_eq!(&sanitize_name(">"), ">");
|
||||
assert_eq!(&sanitize_name("'"), "'");
|
||||
assert_eq!(&sanitize_name("\""), "\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_single_line() {
|
||||
assert_eq!(sanitize_single_line("Hi\naiae "), "Hi aiae");
|
||||
assert_eq!(sanitize_single_line("\r\nahte\n\r"), "ahte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_bidi_characters() {
|
||||
// Legit inputs:
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat\u{2069}"),
|
||||
"Tes\u{2067}ting Delta Chat\u{2069}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"),
|
||||
"Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"),
|
||||
"Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"
|
||||
);
|
||||
|
||||
// Potentially-malicious inputs:
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{202C}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Testing Delta Chat\u{2069}"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2069}ting Delta Chat\u{2067}"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&sanitize_bidi_characters("Tes\u{2068}ting Delta Chat"),
|
||||
"Testing Delta Chat"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.141.0"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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,9 +420,10 @@ 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)
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* 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,
|
||||
* 1=send a copy of outgoing messages to self (default).
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
* however, on the other hand, may lead to unwanted notifications in non-delta clients.
|
||||
* - `sentbox_watch`= 1=watch `Sent`-folder for changes,
|
||||
@@ -519,6 +520,11 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||
* until `dc_accept_chat()` is called.
|
||||
* - `is_chatmail` = 1 if the the server is a chatmail server, 0 otherwise.
|
||||
* - `is_muted` = Whether a context is muted by the user.
|
||||
* Muted contexts should not sound, vibrate or show notifications.
|
||||
* In contrast to `dc_set_chat_mute_duration()`,
|
||||
* fresh message and badge counters are not changed by this setting,
|
||||
* but should be tuned down where appropriate.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
@@ -2499,7 +2505,9 @@ 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_SOCKS5_PROXY 270 // text1=host, text2=port
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
#define DC_QR_URL 332 // text1=URL
|
||||
@@ -2545,6 +2553,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().
|
||||
*
|
||||
@@ -2552,6 +2561,10 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to use the given service for video chats;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_SOCKS5_PROXY with dc_lot_t::text1=host, dc_lot_t::text2=port:
|
||||
* ask the user if they want to use the given proxy and overwrite the previous one, if any.
|
||||
* if so, call dc_set_config_from_qr() and restart I/O.
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
* e-mail address scanned, optionally, a draft message could be set in
|
||||
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
|
||||
@@ -6643,6 +6656,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."
|
||||
@@ -6650,7 +6665,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.
|
||||
@@ -7369,11 +7384,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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,9 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => None,
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Socks5Proxy { host, .. } => Some(host),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
@@ -67,7 +68,10 @@ impl Lot {
|
||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||
Self::Qr(_) => None,
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::Socks5Proxy { port, .. } => Some(Cow::Owned(format!("{port}"))),
|
||||
_ => None,
|
||||
},
|
||||
Self::Error(_) => None,
|
||||
}
|
||||
}
|
||||
@@ -101,8 +105,9 @@ impl Lot {
|
||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Socks5Proxy { .. } => LotState::QrSocks5Proxy,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
Qr::Text { .. } => LotState::QrText,
|
||||
@@ -126,8 +131,9 @@ impl Lot {
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Socks5Proxy { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
@@ -177,9 +183,14 @@ pub enum LotState {
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackup2 = 252,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
/// text1=host, text2=port
|
||||
QrSocks5Proxy = 270,
|
||||
|
||||
/// id=contact
|
||||
QrAddr = 320,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.141.0"
|
||||
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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -32,13 +32,21 @@ pub enum QrObject {
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
Backup {
|
||||
ticket: String,
|
||||
Backup2 {
|
||||
auth_token: String,
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Socks5Proxy {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: Option<String>,
|
||||
pass: Option<String>,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
draft: Option<String>,
|
||||
@@ -129,8 +137,13 @@ impl From<Qr> for QrObject {
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
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,
|
||||
@@ -139,6 +152,17 @@ impl From<Qr> for QrObject {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
} => QrObject::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
},
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"dependencies": {
|
||||
"@deltachat/tiny-emitter": "3.0.0",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"yerpc": "^0.4.3"
|
||||
"yerpc": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.21",
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.141.0"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.141.0"
|
||||
version = "1.142.12"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
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 }
|
||||
nu-ansi-term = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::borrow::Cow::{self, Borrowed, Owned};
|
||||
use std::io::{self, Write};
|
||||
use std::process::Command;
|
||||
|
||||
use ansi_term::Color;
|
||||
use anyhow::{bail, Error};
|
||||
use deltachat::chat::ChatId;
|
||||
use deltachat::config;
|
||||
@@ -22,6 +21,7 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::EventType;
|
||||
use log::{error, info, warn};
|
||||
use nu_ansi_term::Color;
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
@@ -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",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.141.0"
|
||||
version = "1.142.12"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -9,18 +9,19 @@ import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from . import Account, const
|
||||
if TYPE_CHECKING:
|
||||
from . import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -35,28 +36,15 @@ class DirectImap:
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
# Assume the testing server supports TLS on port 993.
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
port = 993
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.SocketSecurity.PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.SocketSecurity.STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
@@ -30,16 +31,45 @@ 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):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
alice, bob, fiona = acfactory.get_online_accounts(3)
|
||||
|
||||
# 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)
|
||||
@@ -74,6 +104,21 @@ 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
|
||||
|
||||
# The QR code token is synced, so alice2 must be able to handle join requests.
|
||||
logging.info("Fiona joins verified group via alice2")
|
||||
alice.stop_io()
|
||||
fiona.secure_join(qr_code)
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
fiona.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
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."""
|
||||
@@ -612,7 +657,8 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
logging.info("Bob scanned withdrawn QR code")
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
|
||||
if (
|
||||
event.kind == EventType.WARNING
|
||||
and "Ignoring vg-request-with-auth message because of invalid auth code." in event.msg
|
||||
):
|
||||
break
|
||||
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))
|
||||
|
||||
@@ -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,38 @@ 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_configure_alternative_port(acfactory) -> None:
|
||||
"""Test that configuration with alternative port 443 works."""
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
account.set_config("mail_port", "443")
|
||||
account.set_config("send_port", "443")
|
||||
|
||||
account.configure()
|
||||
|
||||
|
||||
def test_configure_username(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
addr = account.get_config("addr")
|
||||
account.set_config("mail_user", addr)
|
||||
account.configure()
|
||||
|
||||
assert account.get_config("configured_mail_user") == addr
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -103,12 +138,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 +648,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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.141.0"
|
||||
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 }
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.141.0"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
42
deny.toml
42
deny.toml
@@ -1,7 +1,6 @@
|
||||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2022-0093",
|
||||
|
||||
# Timing attack on RSA.
|
||||
# Delta Chat does not use RSA for new keys
|
||||
@@ -10,15 +9,8 @@ ignore = [
|
||||
# <https://rustsec.org/advisories/RUSTSEC-2023-0071>
|
||||
"RUSTSEC-2023-0071",
|
||||
|
||||
# Unmaintained ansi_term
|
||||
"RUSTSEC-2021-0139",
|
||||
|
||||
# Unmaintained encoding
|
||||
"RUSTSEC-2021-0153",
|
||||
|
||||
# Problem in curve25519-dalek 3.2.0 used by iroh 0.4.
|
||||
# curve25519-dalek 4.1.3 has the problem fixed.
|
||||
"RUSTSEC-2024-0344",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -31,70 +23,42 @@ skip = [
|
||||
{ name = "asn1-rs-impl", version = "0.1.0" },
|
||||
{ name = "asn1-rs", version = "0.5.2" },
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base16ct", version = "0.1.1" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
{ name = "darling_macro", version = "<0.14" },
|
||||
{ name = "darling", version = "<0.14" },
|
||||
{ name = "der_derive", version = "0.6.1" },
|
||||
{ name = "derive_more", version = "0.99.17" },
|
||||
{ name = "der-parser", version = "8.2.0" },
|
||||
{ name = "der", version = "0.6.1" },
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "dlopen2", version = "0.4.1" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
{ name = "event-listener", version = "4.0.3" },
|
||||
{ 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" },
|
||||
{ name = "netlink-packet-route", version = "0.15.0" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "oid-registry", version = "0.6.1" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pem", version = "1.1.1" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "rcgen", version = "<0.12.1" },
|
||||
{ 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" },
|
||||
{ name = "rustls-webpki", version = "0.101.7" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "ssh-encoding", version = "0.1.0" },
|
||||
{ name = "ssh-key", version = "0.5.1" },
|
||||
{ name = "strsim", version = "0.10.0" },
|
||||
{ name = "sync_wrapper", version = "0.1.2" },
|
||||
{ name = "synstructure", version = "0.12.6" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "system-configuration-sys", version = "0.5.0" },
|
||||
{ name = "system-configuration", version = "0.5.1" },
|
||||
{ name = "time", version = "<0.3" },
|
||||
{ name = "tokio-rustls", version = "0.24.1" },
|
||||
{ name = "toml_edit", version = "0.21.1" },
|
||||
{ name = "untrusted", version = "0.7.1" },
|
||||
{ name = "wasi", version = "<0.11" },
|
||||
{ name = "webpki-roots", version ="0.25.4" },
|
||||
{ name = "windows_aarch64_gnullvm", version = "<0.52" },
|
||||
@@ -104,12 +68,10 @@ skip = [
|
||||
{ name = "windows_i686_msvc", version = "<0.52" },
|
||||
{ name = "windows-sys", version = "<0.52" },
|
||||
{ name = "windows-targets", version = "<0.52" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows", version = "<0.54.0" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.52" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.52" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.52" },
|
||||
{ name = "winnow", version = "0.5.40" },
|
||||
{ name = "winreg", version = "0.50.0" },
|
||||
{ name = "x509-parser", version = "<0.16.0" },
|
||||
]
|
||||
|
||||
2675
fuzz/Cargo.lock
generated
2675
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
@@ -135,6 +136,7 @@ module.exports = {
|
||||
DC_QR_LOGIN: 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_SOCKS5_PROXY: 270,
|
||||
DC_QR_TEXT: 330,
|
||||
DC_QR_URL: 332,
|
||||
DC_QR_WEBRTC_INSTANCE: 260,
|
||||
@@ -266,7 +268,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,
|
||||
|
||||
@@ -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,
|
||||
@@ -135,6 +136,7 @@ export enum C {
|
||||
DC_QR_LOGIN = 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_SOCKS5_PROXY = 270,
|
||||
DC_QR_TEXT = 330,
|
||||
DC_QR_URL = 332,
|
||||
DC_QR_WEBRTC_INSTANCE = 260,
|
||||
@@ -266,7 +268,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,
|
||||
|
||||
@@ -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.0"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.141.0"
|
||||
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"
|
||||
|
||||
@@ -8,19 +8,19 @@ import io
|
||||
import pathlib
|
||||
import ssl
|
||||
from contextlib import contextmanager
|
||||
from typing import List
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from imap_tools import (
|
||||
AND,
|
||||
Header,
|
||||
MailBox,
|
||||
MailBoxTls,
|
||||
MailMessage,
|
||||
MailMessageFlags,
|
||||
errors,
|
||||
)
|
||||
|
||||
from deltachat import Account, const
|
||||
if TYPE_CHECKING:
|
||||
from deltachat import Account
|
||||
|
||||
FLAGS = b"FLAGS"
|
||||
FETCH = b"FETCH"
|
||||
@@ -28,7 +28,7 @@ ALL = "1:*"
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
def __init__(self, account: "Account") -> None:
|
||||
self.account = account
|
||||
self.logid = account.get_config("displayname") or id(account)
|
||||
self._idling = False
|
||||
@@ -36,27 +36,13 @@ class DirectImap:
|
||||
|
||||
def connect(self):
|
||||
host = self.account.get_config("configured_mail_server")
|
||||
port = int(self.account.get_config("configured_mail_port"))
|
||||
security = int(self.account.get_config("configured_mail_security"))
|
||||
port = 993
|
||||
|
||||
user = self.account.get_config("addr")
|
||||
host = user.rsplit("@")[-1]
|
||||
pw = self.account.get_config("mail_pw")
|
||||
|
||||
if security == const.DC_SOCKET_PLAIN:
|
||||
ssl_context = None
|
||||
else:
|
||||
ssl_context = ssl.create_default_context()
|
||||
|
||||
# don't check if certificate hostname doesn't match target hostname
|
||||
ssl_context.check_hostname = False
|
||||
|
||||
# don't check if the certificate is trusted by a certificate authority
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.DC_SOCKET_STARTTLS:
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBox(host, port, ssl_context=ssl.create_default_context())
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-06-24
|
||||
2024-09-02
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=828e5ddc7e6609b582fbd7f063cc3f60b580ce96
|
||||
REV=ab970e40d3979893c3bb6a93030e1a52223d7db6
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
77
spec.md
77
spec.md
@@ -1,6 +1,6 @@
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.34.0
|
||||
Version: 0.35.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
@@ -22,7 +22,13 @@ to implement typical messenger functions.
|
||||
- [Locations](#locations)
|
||||
- [User locations](#user-locations)
|
||||
- [Points of interest](#points-of-interest)
|
||||
- [Stickers](#stickers)
|
||||
- [Voice messages](#voice-messages)
|
||||
- [Reactions](#reactions)
|
||||
- [Attaching a contact to a message](#attaching-a-contact-to-a-message)
|
||||
- [Transitioning to a new e-mail address (AEAP)](#transitioning-to-a-new-e-mail-address-aeap)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
- [Sync messages](#sync-messages)
|
||||
|
||||
|
||||
# Encryption
|
||||
@@ -461,6 +467,58 @@ As an extension to RFC 9078, it is allowed to send empty reaction message,
|
||||
in which case all previously sent reactions are retracted.
|
||||
|
||||
|
||||
# Attaching a contact to a message
|
||||
|
||||
Messengers MAY allow the user to attach a contact to a message
|
||||
in order to share it with the chat partner.
|
||||
The contact MUST be sent as a [vCard](https://datatracker.ietf.org/doc/html/rfc6350).
|
||||
|
||||
The vCard MUST contain `EMAIL`,
|
||||
`FN` (display name),
|
||||
and `VERSION` (which version of the vCard standard you're using).
|
||||
If available, it SHOULD contain
|
||||
`REV` (current timestamp),
|
||||
`PHOTO` (avatar), and
|
||||
`KEY` (OpenPGP public key,
|
||||
in binary format,
|
||||
encoded with vanilla base64;
|
||||
note that this is different from the OpenPGP 'ASCII Armor' format).
|
||||
|
||||
Example vCard:
|
||||
|
||||
```
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
EMAIL:alice@example.org
|
||||
FN:Alice Wonderland
|
||||
KEY:data:application/pgp-keys;base64,[Base64-data]
|
||||
PHOTO:data:image/jpeg;base64,[image in Base64]
|
||||
REV:20240418T184242Z
|
||||
END:VCARD
|
||||
```
|
||||
|
||||
It is fine if messengers do include a full vCard parser
|
||||
and e.g. simply search for the line starting with `EMAIL`
|
||||
in order to get the email address.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
@@ -484,21 +542,4 @@ We define the effective date of a message
|
||||
as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
|
||||
# Transitioning to a new e-mail address (AEAP)
|
||||
|
||||
When receiving a message:
|
||||
- If the key exists, but belongs to another address
|
||||
- AND there is a `Chat-Version` header
|
||||
- AND the message is signed correctly
|
||||
- AND the From address is (also) in the encrypted (and therefore signed) headers
|
||||
- AND the message timestamp is newer than the contact's `lastseen`
|
||||
(to prevent changing the address back when messages arrive out of order)
|
||||
(this condition is not that important
|
||||
since we will have eventual consistency even without it):
|
||||
|
||||
Replace the contact in _all_ groups,
|
||||
possibly deduplicate the members list,
|
||||
and add a system message to all of these chats.
|
||||
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
145
src/chat.rs
145
src/chat.rs
@@ -8,7 +8,7 @@ use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::{strip_rtlo_characters, ContactAddress};
|
||||
use deltachat_contact_tools::{sanitize_bidi_characters, sanitize_single_line, ContactAddress};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
@@ -46,9 +46,10 @@ use crate::stock_str;
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{
|
||||
buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp,
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
smeared_time, time, IsNoneOrEmpty, SystemTime,
|
||||
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)]
|
||||
@@ -321,7 +322,7 @@ impl ChatId {
|
||||
param: Option<String>,
|
||||
timestamp: i64,
|
||||
) -> Result<Self> {
|
||||
let grpname = strip_rtlo_characters(grpname);
|
||||
let grpname = sanitize_single_line(grpname);
|
||||
let timestamp = cmp::min(timestamp, smeared_time(context));
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
@@ -1933,8 +1934,8 @@ impl Chat {
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
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 -
|
||||
// send_sync_msg() is called a moment later at `smtp::send_smtp_messages()`
|
||||
// when the group creation message is already in the `smtp` table --
|
||||
// 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(())
|
||||
}
|
||||
|
||||
@@ -2858,7 +2859,7 @@ pub async fn send_msg_sync(context: &Context, chat_id: ChatId, msg: &mut Message
|
||||
async fn send_msg_inner(context: &Context, chat_id: ChatId, msg: &mut Message) -> Result<MsgId> {
|
||||
// protect all system messages against RTLO attacks
|
||||
if msg.is_system_message() {
|
||||
msg.text = strip_rtlo_characters(&msg.text);
|
||||
msg.text = sanitize_bidi_characters(&msg.text);
|
||||
}
|
||||
|
||||
if !prepare_send_msg(context, chat_id, msg).await?.is_empty() {
|
||||
@@ -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 {
|
||||
@@ -3503,7 +3495,7 @@ pub async fn create_group_chat(
|
||||
protect: ProtectionStatus,
|
||||
chat_name: &str,
|
||||
) -> Result<ChatId> {
|
||||
let chat_name = improve_single_line_input(chat_name);
|
||||
let chat_name = sanitize_single_line(chat_name);
|
||||
ensure!(!chat_name.is_empty(), "Invalid chat name");
|
||||
|
||||
let grpid = create_id();
|
||||
@@ -3734,15 +3726,13 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
bail!("can not add contact because the account is not part of the group/broadcast");
|
||||
}
|
||||
|
||||
let sync_qr_code_tokens;
|
||||
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
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
&& context.send_sync_msg().await.log_err(context).is_ok();
|
||||
sync_qr_code_tokens = true;
|
||||
} else {
|
||||
sync_qr_code_tokens = false;
|
||||
}
|
||||
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -3786,6 +3776,15 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
return Err(e);
|
||||
}
|
||||
sync = Nosync;
|
||||
if sync_qr_code_tokens
|
||||
&& context
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
{
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
if sync.into() {
|
||||
@@ -4017,7 +4016,7 @@ async fn rename_ex(
|
||||
chat_id: ChatId,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let new_name = improve_single_line_input(new_name);
|
||||
let new_name = sanitize_single_line(new_name);
|
||||
/* the function only sets the names of group chats; normal chats get their names from the contacts */
|
||||
let mut success = false;
|
||||
|
||||
@@ -4048,7 +4047,7 @@ async fn rename_ex(
|
||||
if chat.is_promoted()
|
||||
&& !chat.is_mailing_list()
|
||||
&& chat.typ != Chattype::Broadcast
|
||||
&& improve_single_line_input(&chat.name) != new_name
|
||||
&& sanitize_single_line(&chat.name) != new_name
|
||||
{
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text =
|
||||
@@ -4272,9 +4271,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 +4693,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 +5872,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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
141
src/config.rs
141
src/config.rs
@@ -6,21 +6,21 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::addr_cmp;
|
||||
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
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;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input};
|
||||
use crate::tools::get_abs_path;
|
||||
|
||||
/// The available configuration keys.
|
||||
#[derive(
|
||||
@@ -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,
|
||||
|
||||
@@ -194,45 +200,74 @@ pub enum Config {
|
||||
/// The primary email address. Also see `SecondaryAddrs`.
|
||||
ConfiguredAddr,
|
||||
|
||||
/// List of configured IMAP servers as a JSON array.
|
||||
ConfiguredImapServers,
|
||||
|
||||
/// Configured IMAP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailServer,
|
||||
|
||||
/// Configured IMAP server port.
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailPort,
|
||||
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_imap_servers` for new configurations.
|
||||
ConfiguredMailSecurity,
|
||||
|
||||
/// Configured IMAP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
ConfiguredMailUser,
|
||||
|
||||
/// Configured IMAP server password.
|
||||
ConfiguredMailPw,
|
||||
|
||||
/// Configured IMAP server port.
|
||||
ConfiguredMailPort,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// List of configured SMTP servers as a JSON array.
|
||||
ConfiguredSmtpServers,
|
||||
|
||||
/// Configured SMTP server hostname.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendServer,
|
||||
|
||||
/// Configured SMTP server port.
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendPort,
|
||||
|
||||
/// Configured SMTP server security (e.g. TLS, STARTTLS).
|
||||
///
|
||||
/// This is replaced by `configured_smtp_servers` for new configurations.
|
||||
ConfiguredSendSecurity,
|
||||
|
||||
/// Configured SMTP server username.
|
||||
///
|
||||
/// This is set if user has configured username manually.
|
||||
ConfiguredSendUser,
|
||||
|
||||
/// Configured SMTP server password.
|
||||
ConfiguredSendPw,
|
||||
|
||||
/// 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.
|
||||
ConfiguredServerFlags,
|
||||
|
||||
/// Configured SMTP server security (e.g. TLS, STARTTLS).
|
||||
ConfiguredSendSecurity,
|
||||
|
||||
/// Configured folder for incoming messages.
|
||||
ConfiguredInboxFolder,
|
||||
|
||||
@@ -257,6 +292,12 @@ 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,
|
||||
|
||||
/// All secondary self addresses separated by spaces
|
||||
/// (`addr1@example.org addr2@example.org addr3@example.org`)
|
||||
SecondaryAddrs,
|
||||
@@ -314,7 +355,8 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))]
|
||||
DownloadLimit,
|
||||
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set.
|
||||
/// Enable sending and executing (applying) sync messages. Sending requires `BccSelf` to be set
|
||||
/// and `Bot` unset.
|
||||
#[strum(props(default = "1"))]
|
||||
SyncMsgs,
|
||||
|
||||
@@ -378,9 +420,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
|
||||
@@ -394,10 +433,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +459,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?,
|
||||
@@ -481,7 +517,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.
|
||||
@@ -494,11 +531,23 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
///
|
||||
/// This requires that both `SyncMsgs` and `BccSelf` settings are enabled.
|
||||
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SyncMsgs).await?
|
||||
&& self.get_config_bool(Config::BccSelf).await?)
|
||||
&& self.get_config_bool(Config::BccSelf).await?
|
||||
&& !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.
|
||||
@@ -647,7 +696,7 @@ impl Context {
|
||||
}
|
||||
Config::Displayname => {
|
||||
if let Some(v) = value {
|
||||
better_value = improve_single_line_input(v);
|
||||
better_value = sanitize_single_line(v);
|
||||
value = Some(&better_value);
|
||||
}
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
@@ -685,7 +734,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Box::pin(self.send_sync_msg()).await.log_err(self).ok();
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -950,6 +999,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;
|
||||
@@ -976,7 +1040,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);
|
||||
@@ -1051,7 +1114,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;
|
||||
@@ -1074,7 +1138,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?;
|
||||
|
||||
550
src/configure.rs
550
src/configure.rs
@@ -11,7 +11,7 @@
|
||||
|
||||
mod auto_mozilla;
|
||||
mod auto_outlook;
|
||||
mod server_params;
|
||||
pub(crate) mod server_params;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
@@ -25,14 +25,16 @@ use tokio::task;
|
||||
|
||||
use crate::config::{self, Config};
|
||||
use crate::context::Context;
|
||||
use crate::imap::{session::Session as ImapSession, Imap};
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
|
||||
};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::smtp::Smtp;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
@@ -110,20 +112,15 @@ impl Context {
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::load_candidate_params(self).await?;
|
||||
let param = EnteredLoginParam::load(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;
|
||||
let configured_param_res = configure(self, ¶m).await;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await?;
|
||||
|
||||
on_configure_completed(self, param, old_addr).await?;
|
||||
on_configure_completed(self, configured_param_res?, old_addr).await?;
|
||||
|
||||
success?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -132,7 +129,7 @@ impl Context {
|
||||
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
param: LoginParam,
|
||||
param: ConfiguredLoginParam,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = param.provider {
|
||||
@@ -182,21 +179,28 @@ async fn on_configure_completed(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
/// Retrieves data from autoconfig and provider database
|
||||
/// to transform user-entered login parameters into complete configuration.
|
||||
async fn get_configured_param(
|
||||
ctx: &Context,
|
||||
param: &EnteredLoginParam,
|
||||
) -> Result<ConfiguredLoginParam> {
|
||||
ensure!(!param.addr.is_empty(), "Missing email address.");
|
||||
|
||||
ensure!(!param.imap.password.is_empty(), "Missing (IMAP) password.");
|
||||
|
||||
// SMTP password is an "advanced" setting. If unset, use the same password as for IMAP.
|
||||
let smtp_password = if param.smtp.password.is_empty() {
|
||||
param.imap.password.clone()
|
||||
} else {
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let socks5_config = param.socks5_config.clone();
|
||||
let socks5_enabled = socks5_config.is_some();
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
// 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 {
|
||||
let mut addr = param.addr.clone();
|
||||
if param.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);
|
||||
@@ -205,7 +209,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
.and_then(|e| e.parse().ok())
|
||||
{
|
||||
info!(ctx, "Authorized address is {}", oauth2_addr);
|
||||
param.addr = oauth2_addr;
|
||||
addr = oauth2_addr;
|
||||
ctx.sql
|
||||
.set_raw_config("addr", Some(param.addr.as_str()))
|
||||
.await?;
|
||||
@@ -216,11 +220,10 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
// Step 2: Autoconfig
|
||||
progress!(ctx, 200);
|
||||
|
||||
let provider;
|
||||
let param_autoconfig;
|
||||
if param.imap.server.is_empty()
|
||||
&& param.imap.port == 0
|
||||
@@ -232,66 +235,48 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
&& param.smtp.user.is_empty()
|
||||
{
|
||||
// no advanced parameters entered by the user: query provider-database or do Autoconfig
|
||||
|
||||
info!(
|
||||
ctx,
|
||||
"checking internal provider-info for offline autoconfig"
|
||||
);
|
||||
|
||||
if let Some(provider) =
|
||||
provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await
|
||||
{
|
||||
param.provider = Some(provider);
|
||||
match provider.status {
|
||||
provider::Status::Ok | provider::Status::Preparation => {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "offline autoconfig found, but no servers defined");
|
||||
param_autoconfig = None;
|
||||
} else {
|
||||
info!(ctx, "offline autoconfig found");
|
||||
let servers = provider
|
||||
.server
|
||||
.iter()
|
||||
.map(|s| ServerParams {
|
||||
protocol: s.protocol,
|
||||
socket: s.socket,
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::Email => param.addr.to_string(),
|
||||
UsernamePattern::Emaillocalpart => {
|
||||
if let Some(at) = param.addr.find('@') {
|
||||
param.addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
param.addr.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
strict_tls: Some(provider.opt.strict_tls),
|
||||
})
|
||||
.collect();
|
||||
provider = provider::get_provider_info(ctx, ¶m_domain, socks5_enabled).await;
|
||||
if let Some(provider) = provider {
|
||||
if provider.server.is_empty() {
|
||||
info!(ctx, "Offline autoconfig found, but no servers defined.");
|
||||
param_autoconfig = None;
|
||||
} else {
|
||||
info!(ctx, "Offline autoconfig found.");
|
||||
let servers = provider
|
||||
.server
|
||||
.iter()
|
||||
.map(|s| ServerParams {
|
||||
protocol: s.protocol,
|
||||
socket: s.socket,
|
||||
hostname: s.hostname.to_string(),
|
||||
port: s.port,
|
||||
username: match s.username_pattern {
|
||||
UsernamePattern::Email => param.addr.to_string(),
|
||||
UsernamePattern::Emaillocalpart => {
|
||||
if let Some(at) = param.addr.find('@') {
|
||||
param.addr.split_at(at).0.to_string()
|
||||
} else {
|
||||
param.addr.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
param_autoconfig = Some(servers)
|
||||
}
|
||||
}
|
||||
provider::Status::Broken => {
|
||||
info!(ctx, "offline autoconfig found, provider is broken");
|
||||
param_autoconfig = None;
|
||||
}
|
||||
param_autoconfig = Some(servers)
|
||||
}
|
||||
} 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, ¶m_domain, ¶m_addr_urlencoded).await
|
||||
}
|
||||
info!(ctx, "No offline autoconfig found.");
|
||||
param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
|
||||
}
|
||||
} else {
|
||||
provider = None;
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
@@ -308,7 +293,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,145 +305,149 @@ 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, ¶m.addr, ¶m_domain);
|
||||
|
||||
let configured_login_param = ConfiguredLoginParam {
|
||||
addr,
|
||||
imap: servers
|
||||
.iter()
|
||||
.filter_map(|params| {
|
||||
let Ok(security) = params.socket.try_into() else {
|
||||
return None;
|
||||
};
|
||||
if params.protocol == Protocol::Imap {
|
||||
Some(ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: params.hostname.clone(),
|
||||
port: params.port,
|
||||
security,
|
||||
},
|
||||
user: params.username.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
imap_user: param.imap.user.clone(),
|
||||
imap_password: param.imap.password.clone(),
|
||||
smtp: servers
|
||||
.iter()
|
||||
.filter_map(|params| {
|
||||
let Ok(security) = params.socket.try_into() else {
|
||||
return None;
|
||||
};
|
||||
if params.protocol == Protocol::Smtp {
|
||||
Some(ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: params.hostname.clone(),
|
||||
port: params.port,
|
||||
security,
|
||||
},
|
||||
user: params.username.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
smtp_user: param.smtp.user.clone(),
|
||||
smtp_password,
|
||||
socks5_config: param.socks5_config.clone(),
|
||||
provider,
|
||||
certificate_checks: match param.certificate_checks {
|
||||
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
|
||||
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
|
||||
EnteredCertificateChecks::AcceptInvalidCertificates
|
||||
| EnteredCertificateChecks::AcceptInvalidCertificates2 => {
|
||||
ConfiguredCertificateChecks::AcceptInvalidCertificates
|
||||
}
|
||||
},
|
||||
oauth2: param.oauth2,
|
||||
};
|
||||
Ok(configured_login_param)
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let strict_tls = configured_param.strict_tls();
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
// Spawn SMTP configuration task
|
||||
let mut smtp = Smtp::new();
|
||||
|
||||
// to try SMTP while connecting to IMAP.
|
||||
let context_smtp = ctx.clone();
|
||||
let mut smtp_param = param.smtp.clone();
|
||||
let smtp_addr = param.addr.clone();
|
||||
let smtp_servers: Vec<ServerParams> = servers
|
||||
.iter()
|
||||
.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_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
let smtp_socks5 = configured_param.socks5_config.clone();
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
let mut errors = Vec::new();
|
||||
for smtp_server in smtp_servers {
|
||||
smtp_param.user.clone_from(&smtp_server.username);
|
||||
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,
|
||||
};
|
||||
let mut smtp = Smtp::new();
|
||||
smtp.connect(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&smtp_socks5,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
)
|
||||
.await?;
|
||||
|
||||
match try_smtp_one_param(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&socks5_config,
|
||||
&smtp_addr,
|
||||
provider_strict_tls,
|
||||
&mut smtp,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
smtp_configured = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
}
|
||||
|
||||
if smtp_configured {
|
||||
Ok(smtp_param)
|
||||
} else {
|
||||
Err(errors)
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
|
||||
progress!(ctx, 600);
|
||||
|
||||
// Configure IMAP
|
||||
|
||||
let mut imap: Option<(Imap, ImapSession)> = None;
|
||||
let imap_servers: Vec<&ServerParams> = servers
|
||||
.iter()
|
||||
.filter(|params| params.protocol == Protocol::Imap)
|
||||
.collect();
|
||||
let imap_servers_count = imap_servers.len();
|
||||
let mut errors = Vec::new();
|
||||
for (imap_server_index, imap_server) in imap_servers.into_iter().enumerate() {
|
||||
param.imap.user.clone_from(&imap_server.username);
|
||||
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,
|
||||
¶m.imap,
|
||||
¶m.socks5_config,
|
||||
¶m.addr,
|
||||
provider_strict_tls,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(configured_imap) => {
|
||||
imap = Some(configured_imap);
|
||||
break;
|
||||
}
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
progress!(
|
||||
ctx,
|
||||
600 + (800 - 600) * (1 + imap_server_index) / imap_servers_count
|
||||
);
|
||||
}
|
||||
let (mut imap, mut imap_session) = match imap {
|
||||
Some(imap) => imap,
|
||||
None => bail!(nicer_configuration_error(ctx, errors).await),
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
configured_param.socks5_config.clone(),
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
r,
|
||||
);
|
||||
let mut imap_session = match imap.connect(ctx).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
// Wait for SMTP configuration
|
||||
match smtp_config_task.await.unwrap() {
|
||||
Ok(smtp_param) => {
|
||||
param.smtp = smtp_param;
|
||||
}
|
||||
Err(errors) => {
|
||||
bail!(nicer_configuration_error(ctx, errors).await);
|
||||
}
|
||||
}
|
||||
smtp_config_task.await.unwrap()?;
|
||||
|
||||
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 +455,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?;
|
||||
|
||||
@@ -489,8 +476,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// the trailing underscore is correct
|
||||
param.save_as_configured_params(ctx).await?;
|
||||
configured_param.save_as_configured_params(ctx).await?;
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
@@ -508,7 +494,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(configured_param)
|
||||
}
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
@@ -517,16 +503,17 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
/// B. If we have no configuration yet, search configuration in Thunderbird's central database
|
||||
async fn get_autoconfig(
|
||||
ctx: &Context,
|
||||
param: &LoginParam,
|
||||
param: &EnteredLoginParam,
|
||||
param_domain: &str,
|
||||
param_addr_urlencoded: &str,
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.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,
|
||||
¶m.addr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -541,7 +528,7 @@ async fn get_autoconfig(
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
),
|
||||
param,
|
||||
¶m.addr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -577,7 +564,7 @@ async fn get_autoconfig(
|
||||
if let Ok(res) = moz_autoconfigure(
|
||||
ctx,
|
||||
&format!("https://autoconfig.thunderbird.net/v1.1/{}", ¶m_domain),
|
||||
param,
|
||||
¶m.addr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -587,140 +574,19 @@ async fn get_autoconfig(
|
||||
None
|
||||
}
|
||||
|
||||
async fn try_imap_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
) -> Result<(Imap, ImapSession), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
} else {
|
||||
"None".to_string()
|
||||
}
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {:#}", err);
|
||||
return Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{err:#}"),
|
||||
});
|
||||
}
|
||||
Ok(imap) => imap,
|
||||
};
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {:#}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{err:#}"),
|
||||
})
|
||||
}
|
||||
Ok(session) => {
|
||||
info!(context, "success: {}", inf);
|
||||
Ok((imap, session))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_smtp_one_param(
|
||||
context: &Context,
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
} else {
|
||||
"None".to_string()
|
||||
}
|
||||
);
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect(context, param, socks5_config, addr, provider_strict_tls)
|
||||
.await
|
||||
async fn nicer_configuration_error(context: &Context, e: String) -> String {
|
||||
if e.to_lowercase().contains("could not resolve")
|
||||
|| e.to_lowercase().contains("connection attempts")
|
||||
|| e.to_lowercase()
|
||||
.contains("temporary failure in name resolution")
|
||||
|| e.to_lowercase().contains("name or service not known")
|
||||
|| e.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{err:#}"),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
smtp.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure to connect and login with email client configuration.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Trying {config}…\nError: {msg}")]
|
||||
pub struct ConfigurationError {
|
||||
/// Tried configuration description.
|
||||
config: String,
|
||||
|
||||
/// Error message.
|
||||
msg: String,
|
||||
}
|
||||
|
||||
async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationError>) -> String {
|
||||
let first_err = if let Some(f) = errors.first() {
|
||||
f
|
||||
} else {
|
||||
// This means configuration failed but no errors have been captured. This should never
|
||||
// happen, but if it does, the user will see classic "Error: no error".
|
||||
return "no error".to_string();
|
||||
};
|
||||
|
||||
if errors.iter().all(|e| {
|
||||
e.msg.to_lowercase().contains("could not resolve")
|
||||
|| e.msg.to_lowercase().contains("no dns resolution results")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("temporary failure in name resolution")
|
||||
|| e.msg.to_lowercase().contains("name or service not known")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
}) {
|
||||
return stock_str::error_no_network(context).await;
|
||||
}
|
||||
|
||||
if errors.iter().all(|e| e.msg == first_err.msg) {
|
||||
return first_err.msg.to_string();
|
||||
}
|
||||
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
e
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -730,7 +596,7 @@ pub enum Error {
|
||||
|
||||
#[error("XML error at position {position}: {error}")]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
position: u64,
|
||||
#[source]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
@@ -746,7 +612,9 @@ pub enum Error {
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::login_param::EnteredServerLoginParam;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -758,4 +626,24 @@ mod tests {
|
||||
t.set_config(Config::MailPw, Some("123456")).await.unwrap();
|
||||
assert!(t.configure().await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_configured_param() -> Result<()> {
|
||||
let t = &TestContext::new().await;
|
||||
let entered_param = EnteredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
|
||||
imap: EnteredServerLoginParam {
|
||||
user: "alice@example.net".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
let configured_param = get_configured_param(t, &entered_param).await?;
|
||||
assert_eq!(configured_param.imap_user, "alice@example.net");
|
||||
assert_eq!(configured_param.smtp_user, "");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -80,7 +79,7 @@ fn parse_server<B: BufRead>(
|
||||
})
|
||||
.map(|typ| {
|
||||
typ.unwrap()
|
||||
.decode_and_unescape_value(reader)
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_lowercase()
|
||||
})
|
||||
@@ -191,7 +190,7 @@ fn parse_xml_with_address(in_emailaddr: &str, xml_raw: &str) -> Result<MozAutoco
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let moz_ac = parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
@@ -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(¶m_in.addr, &xml_raw);
|
||||
let res = parse_serverparams(addr, &xml_raw);
|
||||
if let Err(err) = &res {
|
||||
warn!(
|
||||
context,
|
||||
|
||||
@@ -162,7 +162,7 @@ fn parse_xml_reader<B: BufRead>(
|
||||
|
||||
fn parse_xml(xml_raw: &str) -> Result<ParsingResult, Error> {
|
||||
let mut reader = quick_xml::Reader::from_str(xml_raw);
|
||||
reader.trim_text(true);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
parse_xml_reader(&mut reader).map_err(|error| Error::InvalidXml {
|
||||
position: reader.buffer_position(),
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ use async_channel::{self as channel, Receiver, Sender};
|
||||
use base64::Engine as _;
|
||||
pub use deltachat_contact_tools::may_be_valid_addr;
|
||||
use deltachat_contact_tools::{
|
||||
self as contact_tools, addr_cmp, addr_normalize, sanitize_name_and_addr, strip_rtlo_characters,
|
||||
self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
|
||||
ContactAddress, VcardContact,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
@@ -30,16 +30,13 @@ use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{
|
||||
duration_to_str, get_abs_path, improve_single_line_input, smeared_time, time, SystemTime,
|
||||
};
|
||||
use crate::tools::{duration_to_str, get_abs_path, smeared_time, time, SystemTime};
|
||||
use crate::{chat, chatlist_events, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
@@ -626,9 +623,7 @@ impl Contact {
|
||||
name: &str,
|
||||
addr: &str,
|
||||
) -> Result<ContactId> {
|
||||
let name = improve_single_line_input(name);
|
||||
|
||||
let (name, addr) = sanitize_name_and_addr(&name, addr);
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let addr = ContactAddress::new(&addr)?;
|
||||
|
||||
let (contact_id, sth_modified) =
|
||||
@@ -646,7 +641,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()),
|
||||
@@ -751,7 +746,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(
|
||||
@@ -769,7 +764,7 @@ impl Contact {
|
||||
return Ok((ContactId::SELF, sth_modified));
|
||||
}
|
||||
|
||||
let mut name = strip_rtlo_characters(name);
|
||||
let mut name = sanitize_name(name);
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if origin <= Origin::OutgoingTo {
|
||||
// The user may accidentally have written to a "noreply" address with another MUA:
|
||||
@@ -1001,7 +996,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,
|
||||
@@ -1195,7 +1190,10 @@ impl Contact {
|
||||
);
|
||||
|
||||
let contact = Contact::get_by_id(context, contact_id).await?;
|
||||
let loginparam = LoginParam::load_configured_params(context).await?;
|
||||
let addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
|
||||
|
||||
let Some(peerstate) = peerstate.filter(|peerstate| peerstate.peek_key(false).is_some())
|
||||
@@ -1224,8 +1222,8 @@ impl Contact {
|
||||
.peek_key(false)
|
||||
.map(|k| k.fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if loginparam.addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
if addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&peerstate.addr,
|
||||
@@ -1239,7 +1237,7 @@ impl Contact {
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, "");
|
||||
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
@@ -1406,6 +1404,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
|
||||
@@ -1917,14 +1926,19 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deltachat_contact_tools::{may_be_valid_addr, normalize_name};
|
||||
use deltachat_contact_tools::may_be_valid_addr;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::{get_chat_contacts, send_text_msg, Chat};
|
||||
@@ -1963,15 +1977,6 @@ mod tests {
|
||||
assert_eq!(may_be_valid_addr("user@domain.tld."), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_name() {
|
||||
assert_eq!(&normalize_name(" hello world "), "hello world");
|
||||
assert_eq!(&normalize_name("<"), "<");
|
||||
assert_eq!(&normalize_name(">"), ">");
|
||||
assert_eq!(&normalize_name("'"), "'");
|
||||
assert_eq!(&normalize_name("\""), "\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_addr() {
|
||||
assert_eq!(addr_normalize("mailto:john@doe.com"), "john@doe.com");
|
||||
@@ -2681,6 +2686,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
|
||||
@@ -2704,6 +2711,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(())
|
||||
}
|
||||
|
||||
@@ -2881,7 +2890,7 @@ Hi."#;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
|
||||
let green = ansi_term::Color::Green.normal();
|
||||
let green = nu_ansi_term::Color::Green.normal();
|
||||
assert!(
|
||||
contact.was_seen_recently(),
|
||||
"{}",
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::download::DownloadState;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::imap::{FolderMeaning, Imap, ServerMetadata};
|
||||
use crate::key::{load_self_public_key, load_self_secret_key, DcKey as _};
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::login_param::{ConfiguredLoginParam, EnteredLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peer_channels::Iroh;
|
||||
@@ -541,18 +541,10 @@ impl Context {
|
||||
}
|
||||
|
||||
// update quota (to send warning if full) - but only check it once in a while
|
||||
let quota_needs_update = {
|
||||
let quota = self.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.filter(|quota| {
|
||||
time_elapsed("a.modified)
|
||||
> Duration::from_secs(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
})
|
||||
.is_none()
|
||||
};
|
||||
|
||||
if quota_needs_update {
|
||||
if self
|
||||
.quota_needs_update(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
.await
|
||||
{
|
||||
if let Err(err) = self.update_recent_quota(&mut session).await {
|
||||
warn!(self, "Failed to update quota: {err:#}.");
|
||||
}
|
||||
@@ -723,8 +715,10 @@ impl Context {
|
||||
/// Returns information about the context as key-value pairs.
|
||||
pub async fn get_info(&self) -> Result<BTreeMap<&'static str, String>> {
|
||||
let unset = "0";
|
||||
let l = LoginParam::load_candidate_params_unchecked(self).await?;
|
||||
let l2 = LoginParam::load_configured_params(self).await?;
|
||||
let l = EnteredLoginParam::load(self).await?;
|
||||
let l2 = ConfiguredLoginParam::load(self)
|
||||
.await?
|
||||
.map_or_else(|| "Not configured".to_string(), |param| param.to_string());
|
||||
let secondary_addrs = self.get_secondary_self_addrs().await?.join(", ");
|
||||
let displayname = self.get_config(Config::Displayname).await?;
|
||||
let chats = get_chat_cnt(self).await?;
|
||||
@@ -815,13 +809,23 @@ impl Context {
|
||||
res.insert("is_configured", is_configured.to_string());
|
||||
res.insert("socks5_enabled", socks5_enabled.to_string());
|
||||
res.insert("entered_account_settings", l.to_string());
|
||||
res.insert("used_account_settings", l2.to_string());
|
||||
res.insert("used_account_settings", l2);
|
||||
|
||||
if let Some(server_id) = &*self.server_id.read().await {
|
||||
res.insert("imap_server_id", format!("{server_id:?}"));
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
|
||||
if let Some(metadata) = &*self.metadata.read().await {
|
||||
if let Some(comment) = &metadata.comment {
|
||||
@@ -1558,6 +1562,22 @@ mod tests {
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_muted_context() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
t.set_config(Config::IsMuted, Some("1")).await?;
|
||||
let chat = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &chat).await;
|
||||
|
||||
// muted contexts should still show dimmed badge counters eg. in the sidebars,
|
||||
// (same as muted chats show dimmed badge counters in the chatlist)
|
||||
// therefore the fresh messages count should not be affected.
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -129,7 +129,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
};
|
||||
|
||||
let mut reader = quick_xml::Reader::from_str(buf);
|
||||
reader.check_end_names(false);
|
||||
reader.config_mut().check_end_names = false;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
@@ -299,7 +299,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
|
||||
})
|
||||
{
|
||||
let href = href
|
||||
.decode_and_unescape_value(reader)
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
@@ -348,7 +348,7 @@ fn maybe_push_tag(
|
||||
fn tag_contains_attr(event: &BytesStart, reader: &Reader<impl BufRead>, name: &str) -> bool {
|
||||
event.attributes().any(|r| {
|
||||
r.map(|a| {
|
||||
a.decode_and_unescape_value(reader)
|
||||
a.decode_and_unescape_value(reader.decoder())
|
||||
.map(|v| v == name)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
@@ -457,7 +457,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dehtml_parse_href() {
|
||||
let html = "<a href=url>text</a";
|
||||
let html = "<a href=url>text</a>";
|
||||
let plain = dehtml(html).unwrap().text;
|
||||
|
||||
assert_eq!(plain, "[text](url)");
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum HeaderDef {
|
||||
Date,
|
||||
From_,
|
||||
To,
|
||||
AutoSubmitted,
|
||||
|
||||
/// Carbon copy.
|
||||
Cc,
|
||||
|
||||
342
src/imap.rs
342
src/imap.rs
@@ -16,7 +16,7 @@ use std::{
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use async_imap::types::{Fetch, Flag, Name, NameAttribute, UnsolicitedResponse};
|
||||
use deltachat_contact_tools::{normalize_name, ContactAddress};
|
||||
use deltachat_contact_tools::ContactAddress;
|
||||
use futures::{FutureExt as _, StreamExt, TryStreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -32,11 +32,12 @@ 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::{
|
||||
prioritize_server_login_params, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
};
|
||||
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,
|
||||
};
|
||||
@@ -74,12 +75,17 @@ pub(crate) struct Imap {
|
||||
addr: String,
|
||||
|
||||
/// Login parameters.
|
||||
lp: ServerLoginParam,
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
|
||||
/// Password.
|
||||
password: String,
|
||||
|
||||
/// SOCKS 5 configuration.
|
||||
socks5_config: Option<Socks5Config>,
|
||||
strict_tls: bool,
|
||||
|
||||
oauth2: bool,
|
||||
|
||||
login_failed_once: bool,
|
||||
|
||||
pub(crate) connectivity: ConnectivityStore,
|
||||
@@ -229,38 +235,29 @@ impl Imap {
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub fn new(
|
||||
lp: &ServerLoginParam,
|
||||
lp: Vec<ConfiguredServerLoginParam>,
|
||||
password: String,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
strict_tls: bool,
|
||||
oauth2: 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 {
|
||||
) -> Self {
|
||||
Imap {
|
||||
idle_interrupt_receiver,
|
||||
addr: addr.to_string(),
|
||||
lp: lp.clone(),
|
||||
lp,
|
||||
password,
|
||||
socks5_config,
|
||||
strict_tls,
|
||||
oauth2,
|
||||
login_failed_once: false,
|
||||
connectivity: Default::default(),
|
||||
conn_last_try: UNIX_EPOCH,
|
||||
conn_backoff_ms: 0,
|
||||
// 1 connection per minute + a burst of 2.
|
||||
ratelimit: Ratelimit::new(Duration::new(120, 0), 2.0),
|
||||
};
|
||||
|
||||
Ok(imap)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new disconnected IMAP client using configured parameters.
|
||||
@@ -268,24 +265,18 @@ impl Imap {
|
||||
context: &Context,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Result<Self> {
|
||||
if !context.is_configured().await? {
|
||||
bail!("IMAP Connect without configured params");
|
||||
}
|
||||
|
||||
let param = LoginParam::load_configured_params(context).await?;
|
||||
// the trailing underscore is correct
|
||||
|
||||
let param = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let imap = Self::new(
|
||||
¶m.imap,
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
param.socks5_config.clone(),
|
||||
¶m.addr,
|
||||
param
|
||||
.provider
|
||||
.map_or(param.socks5_config.is_some(), |provider| {
|
||||
provider.opt.strict_tls
|
||||
}),
|
||||
param.strict_tls(),
|
||||
param.oauth2,
|
||||
idle_interrupt_receiver,
|
||||
)?;
|
||||
);
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
@@ -297,10 +288,6 @@ impl Imap {
|
||||
/// instead if you are going to actually use connection rather than trying connection
|
||||
/// parameters.
|
||||
pub(crate) async fn connect(&mut self, context: &Context) -> Result<Session> {
|
||||
if self.lp.server.is_empty() {
|
||||
bail!("IMAP operation attempted while it is torn down");
|
||||
}
|
||||
|
||||
let now = tools::Time::now();
|
||||
let until_can_send = max(
|
||||
min(self.conn_last_try, now)
|
||||
@@ -342,127 +329,107 @@ 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;
|
||||
|
||||
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
|
||||
let login_params = prioritize_server_login_params(&context.sql, &self.lp, "imap").await?;
|
||||
let mut first_error = None;
|
||||
for lp in login_params {
|
||||
info!(context, "IMAP trying to connect to {}.", &lp.connection);
|
||||
let connection_candidate = lp.connection.clone();
|
||||
let client = match Client::connect(
|
||||
context,
|
||||
self.socks5_config.clone(),
|
||||
self.strict_tls,
|
||||
connection_candidate,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(context, "IMAP failed to connect: {err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
self.conn_backoff_ms = BACKOFF_MIN_MS;
|
||||
self.ratelimit.send();
|
||||
|
||||
let imap_user: &str = lp.user.as_ref();
|
||||
let imap_pw: &str = &self.password;
|
||||
|
||||
let login_res = if self.oauth2 {
|
||||
info!(context, "Logging into IMAP server with OAuth 2.");
|
||||
let addr: &str = self.addr.as_ref();
|
||||
|
||||
let token = get_oauth2_access_token(context, addr, imap_pw, true)
|
||||
.await?
|
||||
.context("IMAP could not get OAUTH token")?;
|
||||
let auth = OAuth2 {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", auth).await
|
||||
} else {
|
||||
let imap_server: &str = self.lp.server.as_ref();
|
||||
let imap_port = self.lp.port;
|
||||
info!(context, "Logging into IMAP server with LOGIN.");
|
||||
client.login(imap_user, imap_pw).await
|
||||
};
|
||||
|
||||
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
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
// Store server ID in the context to display in account info.
|
||||
let mut lock = context.server_id.write().await;
|
||||
lock.clone_from(&session.capabilities.server_id);
|
||||
|
||||
self.login_failed_once = false;
|
||||
context.emit_event(EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}",
|
||||
lp.user
|
||||
)));
|
||||
self.connectivity.set_connected(context).await;
|
||||
info!(context, "Successfully logged into IMAP server");
|
||||
return Ok(session);
|
||||
}
|
||||
};
|
||||
let client = connection_res?;
|
||||
self.conn_backoff_ms = BACKOFF_MIN_MS;
|
||||
self.ratelimit.send();
|
||||
|
||||
let imap_user: &str = self.lp.user.as_ref();
|
||||
let imap_pw: &str = self.lp.password.as_ref();
|
||||
let oauth2 = self.lp.oauth2;
|
||||
Err(err) => {
|
||||
let imap_user = lp.user.to_owned();
|
||||
let message = stock_str::cannot_login(context, &imap_user).await;
|
||||
|
||||
let login_res = if oauth2 {
|
||||
info!(context, "Logging into IMAP server with OAuth 2");
|
||||
let addr: &str = self.addr.as_ref();
|
||||
let err_str = err.to_string();
|
||||
warn!(context, "IMAP failed to login: {err:#}.");
|
||||
first_error.get_or_insert(format_err!("{message} ({err:#})"));
|
||||
|
||||
let token = get_oauth2_access_token(context, addr, imap_pw, true)
|
||||
.await?
|
||||
.context("IMAP could not get OAUTH token")?;
|
||||
let auth = OAuth2 {
|
||||
user: imap_user.into(),
|
||||
access_token: token,
|
||||
};
|
||||
client.authenticate("XOAUTH2", auth).await
|
||||
} else {
|
||||
info!(context, "Logging into IMAP server with LOGIN");
|
||||
client.login(imap_user, imap_pw).await
|
||||
};
|
||||
|
||||
match login_res {
|
||||
Ok(session) => {
|
||||
// Store server ID in the context to display in account info.
|
||||
let mut lock = context.server_id.write().await;
|
||||
lock.clone_from(&session.capabilities.server_id);
|
||||
|
||||
self.login_failed_once = false;
|
||||
context.emit_event(EventType::ImapConnected(format!(
|
||||
"IMAP-LOGIN as {}",
|
||||
self.lp.user
|
||||
)));
|
||||
self.connectivity.set_connected(context).await;
|
||||
info!(context, "Successfully logged into IMAP server");
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
Err(err) => {
|
||||
let imap_user = self.lp.user.to_owned();
|
||||
let message = stock_str::cannot_login(context, &imap_user).await;
|
||||
|
||||
warn!(context, "{} ({:#})", message, err);
|
||||
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& err.to_string().to_lowercase().contains("authentication")
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
if let Err(e) = context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
let lock = context.wrong_pw_warning_mutex.lock().await;
|
||||
if self.login_failed_once
|
||||
&& err_str.to_lowercase().contains("authentication")
|
||||
&& context.get_config_bool(Config::NotifyAboutWrongPw).await?
|
||||
{
|
||||
warn!(context, "{:#}", e);
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text.clone_from(&message);
|
||||
if let Err(e) =
|
||||
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
|
||||
if let Err(e) = context
|
||||
.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await
|
||||
{
|
||||
warn!(context, "{:#}", e);
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
{
|
||||
warn!(context, "{e:#}.");
|
||||
}
|
||||
drop(lock);
|
||||
|
||||
Err(format_err!("{}\n\n{:#}", message, err))
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text.clone_from(&message);
|
||||
if let Err(e) = chat::add_device_msg_with_importance(
|
||||
context,
|
||||
None,
|
||||
Some(&mut msg),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(context, "Failed to add device message: {e:#}.");
|
||||
}
|
||||
} else {
|
||||
self.login_failed_once = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(first_error.unwrap_or_else(|| format_err!("No IMAP connection candidates provided")))
|
||||
}
|
||||
|
||||
/// Prepare for IMAP operation.
|
||||
@@ -483,7 +450,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?;
|
||||
}
|
||||
@@ -540,6 +511,7 @@ impl Imap {
|
||||
) -> Result<bool> {
|
||||
if should_ignore_folder(context, folder, folder_meaning).await? {
|
||||
info!(context, "Not fetching from {folder:?}.");
|
||||
session.new_mail = false;
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -1090,18 +1062,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,
|
||||
@@ -1200,6 +1166,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);
|
||||
@@ -1522,7 +1491,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(())
|
||||
@@ -1552,8 +1521,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>).
|
||||
///
|
||||
@@ -1587,16 +1556,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)
|
||||
@@ -1844,6 +1814,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);
|
||||
}
|
||||
@@ -1857,7 +1841,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 {
|
||||
@@ -2424,12 +2408,6 @@ async fn add_all_recipients_as_contacts(
|
||||
|
||||
let mut any_modified = false;
|
||||
for recipient in recipients {
|
||||
let display_name_normalized = recipient
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(|s| normalize_name(s))
|
||||
.unwrap_or_default();
|
||||
|
||||
let recipient_addr = match ContactAddress::new(&recipient.addr) {
|
||||
Err(err) => {
|
||||
warn!(
|
||||
@@ -2445,7 +2423,7 @@ async fn add_all_recipients_as_contacts(
|
||||
|
||||
let (_, modified) = Contact::add_or_lookup(
|
||||
context,
|
||||
&display_name_normalized,
|
||||
&recipient.display_name.unwrap_or_default(),
|
||||
&recipient_addr,
|
||||
Origin::OutgoingTo,
|
||||
)
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use anyhow::{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::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
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::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
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 +39,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 +108,98 @@ impl Client {
|
||||
Ok(Session::new(session, capabilities))
|
||||
}
|
||||
|
||||
pub async fn connect_secure(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
async fn connection_attempt(
|
||||
context: Context,
|
||||
host: String,
|
||||
security: ConnectionSecurity,
|
||||
resolved_addr: SocketAddr,
|
||||
strict_tls: bool,
|
||||
) -> 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?;
|
||||
let context = &context;
|
||||
let host = &host;
|
||||
info!(
|
||||
context,
|
||||
"Attempting IMAP connection to {host} ({resolved_addr})."
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure(resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
||||
}
|
||||
ConnectionSecurity::Plain => Client::connect_insecure(resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(client) => {
|
||||
let ip_addr = resolved_addr.ip().to_string();
|
||||
let port = resolved_addr.port();
|
||||
|
||||
let save_cache = match security {
|
||||
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
|
||||
ConnectionSecurity::Plain => false,
|
||||
};
|
||||
if save_cache {
|
||||
update_connect_timestamp(context, host, &ip_addr).await?;
|
||||
}
|
||||
update_connection_history(context, "imap", host, port, &ip_addr, time()).await?;
|
||||
Ok(client)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to connect to {host} ({resolved_addr}): {err:#}."
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
context: &Context,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
) -> Result<Self> {
|
||||
let host = &candidate.host;
|
||||
let port = candidate.port;
|
||||
let security = candidate.security;
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
let client = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Plain => {
|
||||
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
|
||||
}
|
||||
};
|
||||
Ok(client)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
|
||||
ConnectionSecurity::Plain => false,
|
||||
};
|
||||
|
||||
let connection_futures =
|
||||
lookup_host_with_cache(context, host, port, "imap", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resolved_addr| {
|
||||
let context = context.clone();
|
||||
let host = host.to_string();
|
||||
Self::connection_attempt(context, host, security, resolved_addr, strict_tls)
|
||||
});
|
||||
run_connection_attempts(connection_futures).await
|
||||
}
|
||||
}
|
||||
|
||||
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 +210,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 +222,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 +239,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 +249,7 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_secure_socks5(
|
||||
async fn connect_secure_socks5(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
@@ -168,9 +257,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 +270,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 +287,7 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_starttls_socks5(
|
||||
async fn connect_starttls_socks5(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
@@ -208,7 +295,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 +312,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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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\
|
||||
)])";
|
||||
|
||||
|
||||
661
src/imex.rs
661
src/imex.rs
@@ -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
372
src/imex/key_transfer.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
@@ -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,44 +16,41 @@
|
||||
//! 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 tokio::fs::{self, File};
|
||||
use tokio::io::{self, AsyncWriteExt, BufWriter};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
use tokio::task::{JoinHandle, JoinSet};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use iroh_net::relay::RelayMode;
|
||||
use iroh_net::Endpoint;
|
||||
use tokio::fs;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::blob::BlobDirContents;
|
||||
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
|
||||
use crate::chat::add_device_msg;
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::qr::{self, Qr};
|
||||
use crate::qr::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.
|
||||
///
|
||||
@@ -70,15 +62,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 +93,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 +107,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,78 +261,45 @@ 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,
|
||||
}
|
||||
pub async fn get_backup2(
|
||||
context: &Context,
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
auth_token: String,
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
|
||||
impl TempPathGuard {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind(0).await?;
|
||||
|
||||
impl Drop for TempPathGuard {
|
||||
fn drop(&mut self) {
|
||||
let path = self.path.clone();
|
||||
tokio::spawn(async move {
|
||||
fs::remove_file(&path).await.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
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?;
|
||||
|
||||
impl Deref for TempPathGuard {
|
||||
type Target = Path;
|
||||
let passphrase = String::new();
|
||||
info!(context, "Starting to read backup from the stream.");
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
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));
|
||||
|
||||
/// 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,
|
||||
}
|
||||
// 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.");
|
||||
|
||||
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())
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Contacts a backup provider and receives the backup from it.
|
||||
@@ -381,218 +308,22 @@ impl From<SendProgress> for EventType {
|
||||
/// 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.
|
||||
/// This is a long running operation which will return 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
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variant of it. It
|
||||
/// does avoid having [`iroh_net::NodeAddr`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
ensure!(
|
||||
matches!(qr, Qr::Backup { .. }),
|
||||
"QR code for backup must be of type DCBACKUP"
|
||||
);
|
||||
ensure!(
|
||||
!context.is_configured().await?,
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
// Acquire global "ongoing" mutex.
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
let _guard = context.scheduler.pause(context.clone()).await;
|
||||
info!(
|
||||
context,
|
||||
"Running get_backup for {}",
|
||||
qr::format_backup(&qr)?
|
||||
);
|
||||
let res = tokio::select! {
|
||||
biased;
|
||||
res = get_backup_inner(context, qr) => res,
|
||||
_ = cancel_token.recv() => Err(format_err!("cancelled")),
|
||||
};
|
||||
context.free_ongoing().await;
|
||||
res
|
||||
}
|
||||
|
||||
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
|
||||
let ticket = match qr {
|
||||
Qr::Backup { ticket } => ticket,
|
||||
_ => bail!("QR code for backup must be of type DCBACKUP"),
|
||||
};
|
||||
|
||||
match transfer_from_provider(context, &ticket).await {
|
||||
Ok(()) => {
|
||||
context.sql.run_migrations(context).await?;
|
||||
delete_and_reset_all_device_msgs(context).await?;
|
||||
context.emit_event(ReceiveProgress::Completed.into());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
// Clean up any blobs we already wrote.
|
||||
let readdir = fs::read_dir(context.get_blobdir()).await?;
|
||||
let mut readdir = ReadDirStream::new(readdir);
|
||||
while let Some(dirent) = readdir.next().await {
|
||||
if let Ok(dirent) = dirent {
|
||||
fs::remove_file(dirent.path()).await.ok();
|
||||
}
|
||||
}
|
||||
context.emit_event(ReceiveProgress::Failed.into());
|
||||
Err(err)
|
||||
}
|
||||
match qr {
|
||||
Qr::Backup2 {
|
||||
node_addr,
|
||||
auth_token,
|
||||
} => get_backup2(context, node_addr, auth_token).await?,
|
||||
_ => bail!("QR code for backup must be of type DCBACKUP2"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()> {
|
||||
let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress());
|
||||
spawn_progress_proxy(context.clone(), progress.subscribe());
|
||||
let on_connected = || {
|
||||
context.emit_event(ReceiveProgress::Connected.into());
|
||||
async { Ok(()) }
|
||||
};
|
||||
let on_collection = |collection: &Collection| {
|
||||
context.emit_event(ReceiveProgress::CollectionReceived.into());
|
||||
progress.set_total(collection.total_blobs_size());
|
||||
async { Ok(()) }
|
||||
};
|
||||
let jobs = Mutex::new(JoinSet::default());
|
||||
let on_blob =
|
||||
|hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name);
|
||||
|
||||
// Perform the transfer.
|
||||
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
|
||||
let stats = iroh::get::run_ticket(
|
||||
ticket,
|
||||
keylog,
|
||||
MAX_CONCURRENT_DIALS,
|
||||
on_connected,
|
||||
on_collection,
|
||||
on_blob,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut jobs = jobs.lock().await;
|
||||
while let Some(job) = jobs.join_next().await {
|
||||
job.context("job failed")?;
|
||||
}
|
||||
drop(progress);
|
||||
info!(
|
||||
context,
|
||||
"Backup transfer finished, transfer rate was {} Mbps.",
|
||||
stats.mbits()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get callback when a blob is received from the provider.
|
||||
///
|
||||
/// This writes the blobs to the blobdir. If the blob is the database it will import it to
|
||||
/// the database of the current [`Context`].
|
||||
async fn on_blob(
|
||||
context: &Context,
|
||||
progress: &ProgressEmitter,
|
||||
jobs: &Mutex<JoinSet<()>>,
|
||||
ticket: &Ticket,
|
||||
_hash: Hash,
|
||||
mut reader: DataStream,
|
||||
name: String,
|
||||
) -> Result<DataStream> {
|
||||
ensure!(!name.is_empty(), "Received a nameless blob");
|
||||
let path = if name.starts_with("db/") {
|
||||
let context_dir = context
|
||||
.get_blobdir()
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("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");
|
||||
}
|
||||
dbfile
|
||||
} else {
|
||||
ensure!(name.starts_with("blob/"), "malformatted blob name");
|
||||
let blobname = name.rsplit('/').next().context("malformatted blob name")?;
|
||||
context.get_blobdir().join(blobname)
|
||||
};
|
||||
|
||||
let mut wrapped_reader = progress.wrap_async_read(&mut reader);
|
||||
let file = File::create(&path).await?;
|
||||
let mut file = BufWriter::with_capacity(128 * 1024, file);
|
||||
io::copy(&mut wrapped_reader, &mut file).await?;
|
||||
file.flush().await?;
|
||||
|
||||
if name.starts_with("db/") {
|
||||
let context = context.clone();
|
||||
let token = ticket.token().to_string();
|
||||
jobs.lock().await.spawn(async move {
|
||||
if let Err(err) = context.sql.import(&path, token).await {
|
||||
error!(context, "cannot import database: {:#?}", err);
|
||||
}
|
||||
if let Err(err) = fs::remove_file(&path).await {
|
||||
error!(
|
||||
context,
|
||||
"failed to delete database import file '{}': {:#?}",
|
||||
path.display(),
|
||||
err,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(reader)
|
||||
}
|
||||
|
||||
/// Spawns a task proxying progress events.
|
||||
///
|
||||
/// This spawns a tokio task which receives events from the [`ProgressEmitter`] and sends
|
||||
/// them to the context. The task finishes when the emitter is dropped.
|
||||
///
|
||||
/// This could be done directly in the emitter by making it less generic.
|
||||
fn spawn_progress_proxy(context: Context, mut rx: broadcast::Receiver<u16>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(step) => context.emit_event(ReceiveProgress::BlobProgress(step).into()),
|
||||
Err(RecvError::Closed) => break,
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Create [`EventType::ImexProgress`] events using readable names.
|
||||
///
|
||||
/// Plus you get warnings if you don't use all variants.
|
||||
#[derive(Debug)]
|
||||
enum ReceiveProgress {
|
||||
Connected,
|
||||
CollectionReceived,
|
||||
/// A value between 0 and 85 interpreted as a percentage.
|
||||
///
|
||||
/// Other values are already used by the other variants of this enum.
|
||||
BlobProgress(u16),
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl ReceiveProgress {
|
||||
/// The maximum value for [`ReceiveProgress::BlobProgress`].
|
||||
///
|
||||
/// This only exists to keep this magic value local in this type.
|
||||
fn max_blob_progress() -> u16 {
|
||||
85
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReceiveProgress> for EventType {
|
||||
fn from(source: ReceiveProgress) -> Self {
|
||||
let val = match source {
|
||||
ReceiveProgress::Connected => 50,
|
||||
ReceiveProgress::CollectionReceived => 100,
|
||||
ReceiveProgress::BlobProgress(val) => 100 + 10 * val,
|
||||
ReceiveProgress::Completed => 1000,
|
||||
ReceiveProgress::Failed => 0,
|
||||
};
|
||||
EventType::ImexProgress(val.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
@@ -672,24 +403,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();
|
||||
|
||||
51
src/key.rs
51
src/key.rs
@@ -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,26 @@ 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();
|
||||
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)| {
|
||||
(
|
||||
key.trim().to_lowercase(),
|
||||
values
|
||||
.last()
|
||||
.map_or_else(String::new, |s| s.trim().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Ok((key, headers))
|
||||
}
|
||||
|
||||
/// Serialise the key as bytes.
|
||||
@@ -77,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> {
|
||||
@@ -168,16 +189,17 @@ impl DcKey for SignedPublicKey {
|
||||
// safe to ignore this error.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error.
|
||||
let headers = header.map(|(key, value)| {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert(key.to_string(), value.to_string());
|
||||
m
|
||||
});
|
||||
let headers =
|
||||
header.map(|(key, value)| BTreeMap::from([(key.to_string(), vec![value.to_string()])]));
|
||||
let mut buf = Vec::new();
|
||||
self.to_armored_writer(&mut buf, headers.as_ref())
|
||||
self.to_armored_writer(&mut buf, headers.as_ref().into())
|
||||
.unwrap_or_default();
|
||||
std::string::String::from_utf8(buf).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_private() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl DcKey for SignedSecretKey {
|
||||
@@ -186,16 +208,17 @@ impl DcKey for SignedSecretKey {
|
||||
// safe to do these unwraps.
|
||||
// Because we write to a Vec<u8> the io::Write impls never
|
||||
// fail and we can hide this error. The string is always ASCII.
|
||||
let headers = header.map(|(key, value)| {
|
||||
let mut m = BTreeMap::new();
|
||||
m.insert(key.to_string(), value.to_string());
|
||||
m
|
||||
});
|
||||
let headers =
|
||||
header.map(|(key, value)| BTreeMap::from([(key.to_string(), vec![value.to_string()])]));
|
||||
let mut buf = Vec::new();
|
||||
self.to_armored_writer(&mut buf, headers.as_ref())
|
||||
self.to_armored_writer(&mut buf, headers.as_ref().into())
|
||||
.unwrap_or_default();
|
||||
std::string::String::from_utf8(buf).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn is_private() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Deltachat extension trait for secret keys.
|
||||
|
||||
@@ -109,7 +109,7 @@ impl Kml {
|
||||
ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large");
|
||||
|
||||
let mut reader = quick_xml::Reader::from_reader(to_parse);
|
||||
reader.trim_text(true);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut kml = Kml::new();
|
||||
kml.locations = Vec::with_capacity(100);
|
||||
@@ -226,7 +226,7 @@ impl Kml {
|
||||
== "addr"
|
||||
}) {
|
||||
self.addr = addr
|
||||
.decode_and_unescape_value(reader)
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.ok()
|
||||
.map(|a| a.into_owned());
|
||||
}
|
||||
@@ -256,7 +256,7 @@ impl Kml {
|
||||
}) {
|
||||
let v = acc
|
||||
.unwrap()
|
||||
.decode_and_unescape_value(reader)
|
||||
.decode_and_unescape_value(reader.decoder())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.curr.accuracy = v.trim().parse().unwrap_or_default();
|
||||
|
||||
1123
src/login_param.rs
1123
src/login_param.rs
File diff suppressed because it is too large
Load Diff
210
src/message.rs
210
src/message.rs
@@ -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=''",
|
||||
@@ -2298,6 +2357,25 @@ mod tests {
|
||||
assert_eq!(quoted_msg.get_text(), msg2.quoted_text().unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_quote() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.send_recv_accept(alice, bob, "Hi!").await;
|
||||
let msg = tcm
|
||||
.send_recv(
|
||||
alice,
|
||||
bob,
|
||||
"On 2024-08-28, Alice wrote:\n> A quote.\nNot really.",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(msg.quoted_text().is_none());
|
||||
assert!(msg.quoted_message(bob).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_unencrypted_quote_encrypted_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -2325,12 +2403,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 +2613,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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -116,34 +117,6 @@ pub struct RenderedEmail {
|
||||
pub subject: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct MessageHeaders {
|
||||
/// Opportunistically protected headers.
|
||||
///
|
||||
/// These headers are placed into encrypted part *if* the message is encrypted. Place headers
|
||||
/// which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the
|
||||
/// message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here.
|
||||
///
|
||||
/// If the message is not encrypted, these headers are placed into IMF header section, so make
|
||||
/// sure that the message will be encrypted if you place any sensitive information here.
|
||||
pub protected: Vec<Header>,
|
||||
|
||||
/// Headers that must go into IMF header section.
|
||||
///
|
||||
/// These are standard headers such as Date, In-Reply-To, References, which cannot be placed
|
||||
/// anywhere else according to the standard. Placing headers here also allows them to be fetched
|
||||
/// individually over IMAP without downloading the message body. This is why Chat-Version is
|
||||
/// placed here.
|
||||
pub unprotected: Vec<Header>,
|
||||
|
||||
/// Headers that MUST NOT go into IMF header section.
|
||||
///
|
||||
/// These are large headers which may hit the header section size limit on the server, such as
|
||||
/// Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here
|
||||
/// that servers mess up with in the IMF header section, like Message-ID.
|
||||
pub hidden: Vec<Header>,
|
||||
}
|
||||
|
||||
impl MimeFactory {
|
||||
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
|
||||
let chat = Chat::load_from_db(context, msg.chat_id).await?;
|
||||
@@ -213,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;
|
||||
}
|
||||
@@ -314,14 +287,11 @@ impl MimeFactory {
|
||||
fn is_e2ee_guaranteed(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if chat.is_protected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
!msg.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
&& msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
|
||||
&& (chat.is_protected()
|
||||
|| msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default())
|
||||
}
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
@@ -330,19 +300,12 @@ impl MimeFactory {
|
||||
fn verified(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if chat.is_protected() {
|
||||
if msg.get_info_type() == SystemMessage::SecurejoinMessage {
|
||||
// Securejoin messages are supposed to verify a key.
|
||||
// In order to do this, it is necessary that they can be sent
|
||||
// to a key that is not yet verified.
|
||||
// This has to work independently of whether the chat is protected right now.
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
chat.is_self_talk() ||
|
||||
// Securejoin messages are supposed to verify a key.
|
||||
// In order to do this, it is necessary that they can be sent
|
||||
// to a key that is not yet verified.
|
||||
// This has to work independently of whether the chat is protected right now.
|
||||
chat.is_protected() && msg.get_info_type() != SystemMessage::SecurejoinMessage
|
||||
}
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
@@ -491,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)
|
||||
@@ -507,7 +470,7 @@ impl MimeFactory {
|
||||
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
|
||||
/// `smtp`-table to be used by the SMTP loop
|
||||
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
|
||||
let mut headers: MessageHeaders = Default::default();
|
||||
let mut headers = Vec::<Header>::new();
|
||||
|
||||
let from = Address::new_mailbox_with_name(
|
||||
self.from_displayname.to_string(),
|
||||
@@ -562,18 +525,14 @@ impl MimeFactory {
|
||||
// Start with Internet Message Format headers in the order of the standard example
|
||||
// <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
|
||||
let from_header = Header::new_with_value("From".into(), vec![from]).unwrap();
|
||||
headers.protected.push(from_header.clone());
|
||||
headers.push(from_header.clone());
|
||||
|
||||
if let Some(sender_displayname) = &self.sender_displayname {
|
||||
let sender =
|
||||
Address::new_mailbox_with_name(sender_displayname.clone(), self.from_addr.clone());
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
headers.push(Header::new_with_value("Sender".into(), vec![sender]).unwrap());
|
||||
}
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new_with_value("To".into(), to.clone()).unwrap());
|
||||
headers.push(Header::new_with_value("To".into(), to.clone()).unwrap());
|
||||
|
||||
let subject_str = self.subject_str(context).await?;
|
||||
let encoded_subject = if subject_str
|
||||
@@ -586,14 +545,12 @@ impl MimeFactory {
|
||||
} else {
|
||||
encode_words(&subject_str)
|
||||
};
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Subject".into(), encoded_subject));
|
||||
headers.push(Header::new("Subject".into(), encoded_subject));
|
||||
|
||||
let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
|
||||
.unwrap()
|
||||
.to_rfc2822();
|
||||
headers.unprotected.push(Header::new("Date".into(), date));
|
||||
headers.push(Header::new("Date".into(), date));
|
||||
|
||||
let rfc724_mid = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg.rfc724_mid.clone(),
|
||||
@@ -601,38 +558,43 @@ impl MimeFactory {
|
||||
};
|
||||
let rfc724_mid_headervalue = render_rfc724_mid(&rfc724_mid);
|
||||
let rfc724_mid_header = Header::new("Message-ID".into(), rfc724_mid_headervalue);
|
||||
headers.unprotected.push(rfc724_mid_header.clone());
|
||||
headers.hidden.push(rfc724_mid_header);
|
||||
headers.push(rfc724_mid_header);
|
||||
|
||||
// Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
|
||||
if !self.in_reply_to.is_empty() {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
||||
headers.push(Header::new("In-Reply-To".into(), self.in_reply_to.clone()));
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("References".into(), self.references.clone()));
|
||||
headers.push(Header::new("References".into(), self.references.clone()));
|
||||
}
|
||||
|
||||
// Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
|
||||
if let Loaded::Mdn { .. } = self.loaded {
|
||||
headers.unprotected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-replied".to_string(),
|
||||
));
|
||||
} else if context.get_config_bool(Config::Bot).await? {
|
||||
headers.unprotected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-generated".to_string(),
|
||||
));
|
||||
} else if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
if msg.param.get_cmd() == SystemMessage::SecurejoinMessage {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if step != "vg-request" && step != "vc-request" {
|
||||
headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-replied".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Loaded::Message { chat, .. } = &self.loaded {
|
||||
if chat.typ == Chattype::Broadcast {
|
||||
let encoded_chat_name = encode_words(&chat.name);
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"List-ID".into(),
|
||||
format!("{encoded_chat_name} <{}>", chat.grpid),
|
||||
));
|
||||
@@ -640,15 +602,13 @@ impl MimeFactory {
|
||||
}
|
||||
|
||||
// Non-standard headers.
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||
headers.push(Header::new("Chat-Version".to_string(), "1.0".to_string()));
|
||||
|
||||
if self.req_mdn {
|
||||
// we use "Chat-Disposition-Notification-To"
|
||||
// because replies to "Disposition-Notification-To" are weird in many cases
|
||||
// eg. are just freetext and/or do not follow any standard.
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Disposition-Notification-To".into(),
|
||||
self.from_addr.clone(),
|
||||
));
|
||||
@@ -656,7 +616,6 @@ impl MimeFactory {
|
||||
|
||||
let verified = self.verified();
|
||||
let grpimage = self.grpimage();
|
||||
let force_plaintext = self.should_force_plaintext();
|
||||
let skip_autocrypt = self.should_skip_autocrypt();
|
||||
let e2ee_guaranteed = self.is_e2ee_guaranteed();
|
||||
let encrypt_helper = EncryptHelper::new(context).await?;
|
||||
@@ -664,9 +623,7 @@ impl MimeFactory {
|
||||
if !skip_autocrypt {
|
||||
// unless determined otherwise we add the Autocrypt header
|
||||
let aheader = encrypt_helper.get_aheader().to_string();
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Autocrypt".into(), aheader));
|
||||
headers.push(Header::new("Autocrypt".into(), aheader));
|
||||
}
|
||||
|
||||
// Add ephemeral timer for non-MDN messages.
|
||||
@@ -675,71 +632,23 @@ impl MimeFactory {
|
||||
if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?;
|
||||
if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Ephemeral-Timer".to_string(),
|
||||
duration.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
|
||||
// Content-Type
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||
|
||||
let mut is_gossiped = false;
|
||||
|
||||
let peerstates = self.peerstates_for_recipients(context).await?;
|
||||
let should_encrypt =
|
||||
encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_encrypted = should_encrypt && !force_plaintext;
|
||||
|
||||
if is_encrypted {
|
||||
headers.unprotected.insert(
|
||||
0,
|
||||
Header::new_with_value(
|
||||
"To".into(),
|
||||
to.into_iter()
|
||||
.map(|header| match header {
|
||||
Address::Mailbox(mb) => Address::Mailbox(Mailbox {
|
||||
address: mb.address,
|
||||
name: None,
|
||||
}),
|
||||
Address::Group(name, participants) => Address::new_group(
|
||||
name,
|
||||
participants
|
||||
.into_iter()
|
||||
.map(|mb| Mailbox {
|
||||
address: mb.address,
|
||||
name: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let is_encrypted = !self.should_force_plaintext()
|
||||
&& encrypt_helper.should_encrypt(context, e2ee_guaranteed, &peerstates)?;
|
||||
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if is_encrypted && verified || is_securejoin_message {
|
||||
headers.unprotected.insert(
|
||||
0,
|
||||
Header::new_with_value(
|
||||
"From".into(),
|
||||
vec![Address::new_mailbox(self.from_addr.clone())],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
headers.unprotected.insert(0, from_header);
|
||||
}
|
||||
|
||||
let message = match &self.loaded {
|
||||
Loaded::Message { msg, .. } => {
|
||||
@@ -773,7 +682,7 @@ impl MimeFactory {
|
||||
})
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => self.render_mdn(context).await?,
|
||||
Loaded::Mdn { .. } => self.render_mdn()?,
|
||||
};
|
||||
|
||||
let get_content_type_directives_header = || {
|
||||
@@ -782,16 +691,130 @@ impl MimeFactory {
|
||||
"protected-headers=\"v1\"".to_string(),
|
||||
)
|
||||
};
|
||||
|
||||
// Split headers based on header confidentiality policy.
|
||||
|
||||
// Headers that must go into IMF header section.
|
||||
//
|
||||
// These are standard headers such as Date, In-Reply-To, References, which cannot be placed
|
||||
// anywhere else according to the standard. Placing headers here also allows them to be fetched
|
||||
// individually over IMAP without downloading the message body. This is why Chat-Version is
|
||||
// placed here.
|
||||
let mut unprotected_headers: Vec<Header> = Vec::new();
|
||||
|
||||
// Headers that MUST NOT go into IMF header section.
|
||||
//
|
||||
// These are large headers which may hit the header section size limit on the server, such as
|
||||
// Chat-User-Avatar with a base64-encoded image inside. Also there are headers duplicated here
|
||||
// that servers mess up with in the IMF header section, like Message-ID.
|
||||
//
|
||||
// The header should be hidden from MTA
|
||||
// by moving it either into protected part
|
||||
// in case of encrypted mails
|
||||
// or unprotected MIME preamble in case of unencrypted mails.
|
||||
let mut hidden_headers: Vec<Header> = Vec::new();
|
||||
|
||||
// Opportunistically protected headers.
|
||||
//
|
||||
// These headers are placed into encrypted part *if* the message is encrypted. Place headers
|
||||
// which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the
|
||||
// message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here.
|
||||
//
|
||||
// If the message is not encrypted, these headers are placed into IMF header section, so make
|
||||
// sure that the message will be encrypted if you place any sensitive information here.
|
||||
let mut protected_headers: Vec<Header> = Vec::new();
|
||||
|
||||
// MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
|
||||
unprotected_headers.push(Header::new("MIME-Version".into(), "1.0".into()));
|
||||
for header in headers {
|
||||
let header_name = header.name.to_lowercase();
|
||||
if header_name == "message-id" {
|
||||
unprotected_headers.push(header.clone());
|
||||
hidden_headers.push(header);
|
||||
} else if header_name == "chat-user-avatar" {
|
||||
hidden_headers.push(header);
|
||||
} else if header_name == "autocrypt" {
|
||||
unprotected_headers.push(header.clone());
|
||||
} else if header_name == "from" {
|
||||
// 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 {
|
||||
unprotected_headers.push(
|
||||
Header::new_with_value(
|
||||
header.name,
|
||||
to.clone()
|
||||
.into_iter()
|
||||
.map(|header| match header {
|
||||
Address::Mailbox(mb) => Address::Mailbox(Mailbox {
|
||||
address: mb.address,
|
||||
name: None,
|
||||
}),
|
||||
Address::Group(name, participants) => Address::new_group(
|
||||
name,
|
||||
participants
|
||||
.into_iter()
|
||||
.map(|mb| Mailbox {
|
||||
address: mb.address,
|
||||
name: None,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
} else {
|
||||
unprotected_headers.push(header);
|
||||
}
|
||||
} else if is_encrypted {
|
||||
protected_headers.push(header.clone());
|
||||
|
||||
match header_name.as_str() {
|
||||
"subject" => {
|
||||
unprotected_headers.push(Header::new(header.name, "[...]".to_string()));
|
||||
}
|
||||
"date"
|
||||
| "in-reply-to"
|
||||
| "references"
|
||||
| "auto-submitted"
|
||||
| "chat-version"
|
||||
| "autocrypt-setup-message" => {
|
||||
unprotected_headers.push(header);
|
||||
}
|
||||
_ => {
|
||||
// Other headers are removed from unprotected part.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Copy the header to the protected headers
|
||||
// in case of signed-only message.
|
||||
// If the message is not signed, this value will not be used.
|
||||
protected_headers.push(header.clone());
|
||||
unprotected_headers.push(header)
|
||||
}
|
||||
}
|
||||
|
||||
let outer_message = if is_encrypted {
|
||||
// Store protected headers in the inner message.
|
||||
let message = headers
|
||||
.protected
|
||||
let message = protected_headers
|
||||
.into_iter()
|
||||
.fold(message, |message, header| message.header(header));
|
||||
|
||||
// Add hidden headers to encrypted payload.
|
||||
let mut message = headers
|
||||
.hidden
|
||||
let mut message = hidden_headers
|
||||
.into_iter()
|
||||
.fold(message, |message, header| message.header(header));
|
||||
|
||||
@@ -867,7 +890,6 @@ impl MimeFactory {
|
||||
.body(encrypted)
|
||||
.build(),
|
||||
)
|
||||
.header(("Subject".to_string(), "...".to_string()))
|
||||
} else if matches!(self.loaded, Loaded::Mdn { .. }) {
|
||||
// Never add outer multipart/mixed wrapper to MDN
|
||||
// as multipart/report Content-Type is used to recognize MDNs
|
||||
@@ -877,41 +899,24 @@ impl MimeFactory {
|
||||
// that normally only allows encrypted mails.
|
||||
|
||||
// Hidden headers are dropped.
|
||||
|
||||
// Store protected headers in the outer message.
|
||||
let message = headers
|
||||
.protected
|
||||
.iter()
|
||||
.fold(message, |message, header| message.header(header.clone()));
|
||||
|
||||
let protected: HashSet<Header> = HashSet::from_iter(headers.protected.into_iter());
|
||||
for h in headers.unprotected.split_off(0) {
|
||||
if !protected.contains(&h) {
|
||||
headers.unprotected.push(h);
|
||||
}
|
||||
}
|
||||
|
||||
message
|
||||
} else {
|
||||
let message = headers
|
||||
.hidden
|
||||
let message = hidden_headers
|
||||
.into_iter()
|
||||
.fold(message, |message, header| message.header(header));
|
||||
let message = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Mixed)
|
||||
.child(message.build());
|
||||
let message = headers
|
||||
.protected
|
||||
let message = protected_headers
|
||||
.iter()
|
||||
.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(headers.protected.into_iter());
|
||||
for h in headers.unprotected.split_off(0) {
|
||||
if !protected.contains(&h) {
|
||||
headers.unprotected.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());
|
||||
@@ -938,8 +943,7 @@ impl MimeFactory {
|
||||
};
|
||||
|
||||
// Store the unprotected headers on the outer message.
|
||||
let outer_message = headers
|
||||
.unprotected
|
||||
let outer_message = unprotected_headers
|
||||
.into_iter()
|
||||
.fold(outer_message, |message, header| message.header(header));
|
||||
|
||||
@@ -1036,7 +1040,7 @@ impl MimeFactory {
|
||||
async fn render_message(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
headers: &mut MessageHeaders,
|
||||
headers: &mut Vec<Header>,
|
||||
grpimage: &Option<String>,
|
||||
is_encrypted: bool,
|
||||
) -> Result<(PartBuilder, Vec<PartBuilder>)> {
|
||||
@@ -1056,23 +1060,17 @@ impl MimeFactory {
|
||||
Chattype::Broadcast => false,
|
||||
};
|
||||
if chat.is_protected() && send_verified_headers {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||
headers.push(Header::new("Chat-Verified".to_string(), "1".to_string()));
|
||||
}
|
||||
|
||||
if chat.typ == Chattype::Group {
|
||||
// Send group ID unless it is an ad hoc group that has no ID.
|
||||
if !chat.grpid.is_empty() {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
headers.push(Header::new("Chat-Group-ID".into(), chat.grpid.clone()));
|
||||
}
|
||||
|
||||
let encoded = encode_words(&chat.name);
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Group-Name".into(), encoded));
|
||||
headers.push(Header::new("Chat-Group-Name".into(), encoded));
|
||||
|
||||
match command {
|
||||
SystemMessage::MemberRemovedFromGroup => {
|
||||
@@ -1091,7 +1089,7 @@ impl MimeFactory {
|
||||
};
|
||||
|
||||
if !email_to_remove.is_empty() {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Group-Member-Removed".into(),
|
||||
email_to_remove.into(),
|
||||
));
|
||||
@@ -1103,7 +1101,7 @@ impl MimeFactory {
|
||||
Some(stock_str::msg_add_member_remote(context, email_to_add).await);
|
||||
|
||||
if !email_to_add.is_empty() {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Group-Member-Added".into(),
|
||||
email_to_add.into(),
|
||||
));
|
||||
@@ -1113,7 +1111,7 @@ impl MimeFactory {
|
||||
context,
|
||||
"Sending secure-join message {:?}.", "vg-member-added",
|
||||
);
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Secure-Join".to_string(),
|
||||
"vg-member-added".to_string(),
|
||||
));
|
||||
@@ -1121,18 +1119,18 @@ impl MimeFactory {
|
||||
}
|
||||
SystemMessage::GroupNameChanged => {
|
||||
let old_name = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Group-Name-Changed".into(),
|
||||
maybe_encode_words(old_name),
|
||||
));
|
||||
}
|
||||
SystemMessage::GroupImageChanged => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"group-avatar-changed".to_string(),
|
||||
));
|
||||
if grpimage.is_none() {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Group-Avatar".to_string(),
|
||||
"0".to_string(),
|
||||
));
|
||||
@@ -1144,13 +1142,13 @@ impl MimeFactory {
|
||||
|
||||
match command {
|
||||
SystemMessage::LocationStreamingEnabled => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".into(),
|
||||
"location-streaming-enabled".into(),
|
||||
));
|
||||
}
|
||||
SystemMessage::EphemeralTimerChanged => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"ephemeral-timer-changed".to_string(),
|
||||
));
|
||||
@@ -1166,15 +1164,13 @@ impl MimeFactory {
|
||||
// Adding this header without encryption leaks some
|
||||
// information about the message contents, but it can
|
||||
// already be easily guessed from message timing and size.
|
||||
headers.unprotected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Auto-Submitted".to_string(),
|
||||
"auto-generated".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::AutocryptSetupMessage => {
|
||||
headers
|
||||
.unprotected
|
||||
.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||
headers.push(Header::new("Autocrypt-Setup-Message".into(), "v1".into()));
|
||||
|
||||
placeholdertext = Some(stock_str::ac_setup_msg_body(context).await);
|
||||
}
|
||||
@@ -1182,13 +1178,11 @@ impl MimeFactory {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
if !step.is_empty() {
|
||||
info!(context, "Sending secure-join message {step:?}.");
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Secure-Join".into(), step.into()));
|
||||
headers.push(Header::new("Secure-Join".into(), step.into()));
|
||||
|
||||
let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
if !param2.is_empty() {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
|
||||
"Secure-Join-Auth".into()
|
||||
} else {
|
||||
@@ -1200,32 +1194,30 @@ impl MimeFactory {
|
||||
|
||||
let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default();
|
||||
if !fingerprint.is_empty() {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Secure-Join-Fingerprint".into(),
|
||||
fingerprint.into(),
|
||||
));
|
||||
}
|
||||
if let Some(id) = msg.param.get(Param::Arg4) {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Secure-Join-Group".into(), id.into()));
|
||||
headers.push(Header::new("Secure-Join-Group".into(), id.into()));
|
||||
};
|
||||
}
|
||||
}
|
||||
SystemMessage::ChatProtectionEnabled => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"protection-enabled".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::ChatProtectionDisabled => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".to_string(),
|
||||
"protection-disabled".to_string(),
|
||||
));
|
||||
}
|
||||
SystemMessage::IrohNodeAddr => {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
HeaderDef::IrohNodeAddr.get_headername().to_string(),
|
||||
serde_json::to_string(
|
||||
&context
|
||||
@@ -1244,22 +1236,20 @@ impl MimeFactory {
|
||||
let avatar = build_avatar_file(context, grpimage)
|
||||
.await
|
||||
.context("Cannot attach group image")?;
|
||||
headers.hidden.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Group-Avatar".into(),
|
||||
format!("base64:{avatar}"),
|
||||
));
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::Sticker {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Content".into(), "sticker".into()));
|
||||
headers.push(Header::new("Chat-Content".into(), "sticker".into()));
|
||||
} else if msg.viewtype == Viewtype::VideochatInvitation {
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Content".into(),
|
||||
"videochat-invitation".into(),
|
||||
));
|
||||
headers.protected.push(Header::new(
|
||||
headers.push(Header::new(
|
||||
"Chat-Webrtc-Room".into(),
|
||||
msg.param.get(Param::WebrtcRoom).unwrap_or_default().into(),
|
||||
));
|
||||
@@ -1270,16 +1260,12 @@ impl MimeFactory {
|
||||
|| msg.viewtype == Viewtype::Video
|
||||
{
|
||||
if msg.viewtype == Viewtype::Voice {
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Voice-Message".into(), "1".into()));
|
||||
headers.push(Header::new("Chat-Voice-Message".into(), "1".into()));
|
||||
}
|
||||
let duration_ms = msg.param.get_int(Param::Duration).unwrap_or_default();
|
||||
if duration_ms > 0 {
|
||||
let dur = duration_ms.to_string();
|
||||
headers
|
||||
.protected
|
||||
.push(Header::new("Chat-Duration".into(), dur));
|
||||
headers.push(Header::new("Chat-Duration".into(), dur));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1392,11 +1378,14 @@ impl MimeFactory {
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if msg.viewtype == Viewtype::Webxdc {
|
||||
let topic = peer_channels::create_random_topic();
|
||||
headers
|
||||
.protected
|
||||
.push(create_iroh_header(context, topic, msg.id).await?);
|
||||
if let Some(json) = context
|
||||
.render_webxdc_status_update_object(msg.id, None)
|
||||
headers.push(create_iroh_header(context, topic, msg.id).await?);
|
||||
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));
|
||||
@@ -1406,15 +1395,13 @@ impl MimeFactory {
|
||||
if self.attach_selfavatar {
|
||||
match context.get_config(Config::Selfavatar).await? {
|
||||
Some(path) => match build_avatar_file(context, &path).await {
|
||||
Ok(avatar) => headers.hidden.push(Header::new(
|
||||
Ok(avatar) => headers.push(Header::new(
|
||||
"Chat-User-Avatar".into(),
|
||||
format!("base64:{avatar}"),
|
||||
)),
|
||||
Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
|
||||
},
|
||||
None => headers
|
||||
.protected
|
||||
.push(Header::new("Chat-User-Avatar".into(), "0".into())),
|
||||
None => headers.push(Header::new("Chat-User-Avatar".into(), "0".into())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1422,7 +1409,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
|
||||
//
|
||||
@@ -1448,16 +1435,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
|
||||
@@ -2363,7 +2349,8 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// send message to bob: that should get multipart/mixed because of the avatar moved to inner header;
|
||||
// send message to bob: that should get multipart/signed.
|
||||
// `Subject:` is protected by copying it.
|
||||
// make sure, `Subject:` stays in the outer header (imf header)
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text("this is the text!".to_string());
|
||||
@@ -2375,7 +2362,7 @@ mod tests {
|
||||
assert_eq!(part.match_indices("multipart/signed").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
@@ -2423,7 +2410,7 @@ mod tests {
|
||||
assert_eq!(part.match_indices("multipart/signed").count(), 1);
|
||||
assert_eq!(part.match_indices("From:").count(), 1);
|
||||
assert_eq!(part.match_indices("Message-ID:").count(), 1);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 0);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 1);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
@@ -2532,6 +2519,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");
|
||||
|
||||
@@ -6,11 +6,12 @@ use std::path::Path;
|
||||
use std::str;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, addr_normalize, strip_rtlo_characters};
|
||||
use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
|
||||
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};
|
||||
@@ -220,13 +218,8 @@ impl MimeMessage {
|
||||
let mail = mailparse::parse_mail(body)?;
|
||||
|
||||
let timestamp_rcvd = smeared_time(context);
|
||||
let timestamp_sent = mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.map_or(timestamp_rcvd, |value| {
|
||||
min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
|
||||
});
|
||||
let mut timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
|
||||
let mut hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
@@ -254,6 +247,8 @@ impl MimeMessage {
|
||||
// We don't remove "subject" from `headers` because currently just signed
|
||||
// messages are shown as unencrypted anyway.
|
||||
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
|
||||
MimeMessage::merge_headers(
|
||||
context,
|
||||
&mut headers,
|
||||
@@ -349,6 +344,8 @@ impl MimeMessage {
|
||||
content
|
||||
});
|
||||
if let (Ok(mail), true) = (mail, encrypted) {
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
|
||||
if !signatures.is_empty() {
|
||||
// Handle any gossip headers if the mail was encrypted. See section
|
||||
// "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
|
||||
@@ -396,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.
|
||||
@@ -411,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.
|
||||
@@ -420,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() {
|
||||
@@ -525,6 +521,18 @@ impl MimeMessage {
|
||||
Ok(parser)
|
||||
}
|
||||
|
||||
fn get_timestamp_sent(
|
||||
hdrs: &[mailparse::MailHeader<'_>],
|
||||
default: i64,
|
||||
timestamp_rcvd: i64,
|
||||
) -> i64 {
|
||||
hdrs.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.map_or(default, |value| {
|
||||
min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses system messages.
|
||||
fn parse_system_message_headers(&mut self, context: &Context) {
|
||||
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
|
||||
@@ -774,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!(
|
||||
@@ -2048,7 +2064,7 @@ fn get_attachment_filename(
|
||||
};
|
||||
}
|
||||
|
||||
let desired_filename = desired_filename.map(|filename| strip_rtlo_characters(&filename));
|
||||
let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
|
||||
|
||||
Ok(desired_filename)
|
||||
}
|
||||
@@ -2138,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?
|
||||
@@ -2167,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);
|
||||
@@ -2296,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,
|
||||
|
||||
426
src/net.rs
426
src/net.rs
@@ -1,206 +1,92 @@
|
||||
//! # Common network utilities.
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::future::Future;
|
||||
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::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::sql::Sql;
|
||||
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)
|
||||
/// Returns timestamp of the most recent successful connection
|
||||
/// to the host and port for given protocol.
|
||||
pub(crate) async fn load_connection_timestamp(
|
||||
sql: &Sql,
|
||||
alpn: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
addr: Option<&str>,
|
||||
) -> Result<Option<i64>> {
|
||||
let timestamp = sql
|
||||
.query_get_value(
|
||||
"SELECT timestamp FROM connection_history
|
||||
WHERE host = ?
|
||||
AND port = ?
|
||||
AND alpn = ?
|
||||
AND addr = IFNULL(?, addr)",
|
||||
(host, port, alpn, addr),
|
||||
)
|
||||
.await?;
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
/// Returns a TCP connection stream with read/write timeouts set
|
||||
@@ -208,7 +94,127 @@ 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)
|
||||
}
|
||||
|
||||
/// Runs connection attempt futures.
|
||||
///
|
||||
/// Accepts iterator of connection attempt futures
|
||||
/// and runs them until one of them succeeds
|
||||
/// or all of them fail.
|
||||
///
|
||||
/// If all connection attempts fail, returns the first error.
|
||||
///
|
||||
/// This functions starts with one connection attempt and maintains
|
||||
/// up to five parallel connection attempts if connecting takes time.
|
||||
pub(crate) async fn run_connection_attempts<O, I, F>(mut futures: I) -> Result<O>
|
||||
where
|
||||
I: Iterator<Item = F>,
|
||||
F: Future<Output = Result<O>> + Send + 'static,
|
||||
O: Send + 'static,
|
||||
{
|
||||
let mut connection_attempt_set = JoinSet::new();
|
||||
|
||||
// Start additional connection attempts after 300 ms, 1 s, 5 s and 10 s.
|
||||
// This way we can have up to 5 parallel connection attempts at the same time.
|
||||
let mut delay_set = JoinSet::new();
|
||||
for delay in [
|
||||
Duration::from_millis(300),
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(10),
|
||||
] {
|
||||
delay_set.spawn(tokio::time::sleep(delay));
|
||||
}
|
||||
|
||||
let mut first_error = None;
|
||||
|
||||
let res = loop {
|
||||
if let Some(fut) = futures.next() {
|
||||
connection_attempt_set.spawn(fut);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
|
||||
res = connection_attempt_set.join_next() => {
|
||||
match res {
|
||||
Some(res) => {
|
||||
match res.context("Failed to join task") {
|
||||
Ok(Ok(conn)) => {
|
||||
// Successfully connected.
|
||||
break Ok(conn);
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
// Some connection attempt failed.
|
||||
first_error.get_or_insert(err);
|
||||
}
|
||||
Err(err) => {
|
||||
break Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Out of connection attempts.
|
||||
//
|
||||
// Break out of the loop and return error.
|
||||
break Err(
|
||||
first_error.unwrap_or_else(|| format_err!("No connection attempts were made"))
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_ = delay_set.join_next(), if !delay_set.is_empty() => {
|
||||
// Delay expired.
|
||||
//
|
||||
// Don't do anything other than pushing
|
||||
// another connection attempt into `connection_attempt_set`.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Abort remaining connection attempts and free resources
|
||||
// such as OS sockets and `Context` references
|
||||
// held by connection attempt tasks.
|
||||
//
|
||||
// `delay_set` contains just `sleep` tasks
|
||||
// so no need to await futures there,
|
||||
// it is enough that futures are aborted
|
||||
// when the set is dropped.
|
||||
connection_attempt_set.shutdown().await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// 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 +225,11 @@ 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;
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to connect to {}: {:#}.", resolved_addr, err
|
||||
);
|
||||
last_error = Some(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)
|
||||
let connection_futures = lookup_host_with_cache(context, host, port, "", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(connect_tcp_inner);
|
||||
run_connection_attempts(connection_futures).await
|
||||
}
|
||||
|
||||
869
src/net/dns.rs
Normal file
869
src/net/dns.rs
Normal file
@@ -0,0 +1,869 @@
|
||||
//! 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(())
|
||||
}
|
||||
|
||||
/// Preloaded DNS results that can be used in case of DNS server failures.
|
||||
///
|
||||
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
|
||||
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
|
||||
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))],
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
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::with_capacity(input.len());
|
||||
for addr in input {
|
||||
let timestamp = load_connection_timestamp(
|
||||
&context.sql,
|
||||
alpn,
|
||||
host,
|
||||
addr.port(),
|
||||
Some(&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 let Some(ips) = DNS_PRELOAD.get(hostname) {
|
||||
for ip in ips {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
if !resolved_addrs.contains(&addr) {
|
||||
resolved_addrs.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
253
src/net/http.rs
253
src/net/http.rs
@@ -1,23 +1,17 @@
|
||||
//! # HTTP module.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use bytes::Bytes;
|
||||
use http_body_util::BodyExt;
|
||||
use hyper_util::rt::TokioIo;
|
||||
use mime::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
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"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
@@ -33,43 +27,95 @@ pub struct Response {
|
||||
|
||||
/// Retrieves the text contents of URL using HTTP GET request.
|
||||
pub async fn read_url(context: &Context, url: &str) -> Result<String> {
|
||||
Ok(read_url_inner(context, url).await?.text().await?)
|
||||
let response = read_url_blob(context, url).await?;
|
||||
let text = String::from_utf8_lossy(&response.blob);
|
||||
Ok(text.to_string())
|
||||
}
|
||||
|
||||
async fn get_http_sender<B>(
|
||||
context: &Context,
|
||||
parsed_url: hyper::Uri,
|
||||
) -> Result<hyper::client::conn::http1::SendRequest<B>>
|
||||
where
|
||||
B: hyper::body::Body + 'static + Send,
|
||||
B::Data: Send,
|
||||
B::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
{
|
||||
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
|
||||
let host = parsed_url.host().context("URL has no host")?;
|
||||
let socks5_config_opt = Socks5Config::from_database(&context.sql).await?;
|
||||
|
||||
let stream: Box<dyn SessionStream> = match scheme {
|
||||
"http" => {
|
||||
let port = parsed_url.port_u16().unwrap_or(80);
|
||||
|
||||
// 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 = false;
|
||||
if let Some(socks5_config) = socks5_config_opt {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, host, port, load_cache)
|
||||
.await?;
|
||||
Box::new(socks5_stream)
|
||||
} else {
|
||||
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
|
||||
Box::new(tcp_stream)
|
||||
}
|
||||
}
|
||||
"https" => {
|
||||
let port = parsed_url.port_u16().unwrap_or(443);
|
||||
let load_cache = true;
|
||||
let strict_tls = true;
|
||||
|
||||
if let Some(socks5_config) = socks5_config_opt {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, host, port, load_cache)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], socks5_stream).await?;
|
||||
Box::new(tls_stream)
|
||||
} else {
|
||||
let tcp_stream = crate::net::connect_tcp(context, host, port, load_cache).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream).await?;
|
||||
Box::new(tls_stream)
|
||||
}
|
||||
}
|
||||
_ => bail!("Unknown URL scheme"),
|
||||
};
|
||||
|
||||
let io = TokioIo::new(stream);
|
||||
let (sender, conn) = hyper::client::conn::http1::handshake(io).await?;
|
||||
tokio::task::spawn(conn);
|
||||
|
||||
Ok(sender)
|
||||
}
|
||||
|
||||
/// Retrieves the binary contents of URL using HTTP GET request.
|
||||
pub async fn read_url_blob(context: &Context, url: &str) -> Result<Response> {
|
||||
let response = read_url_inner(context, url).await?;
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok());
|
||||
let mimetype = content_type
|
||||
.as_ref()
|
||||
.map(|mime| mime.essence_str().to_string());
|
||||
let encoding = content_type.as_ref().and_then(|mime| {
|
||||
mime.get_param(mime::CHARSET)
|
||||
.map(|charset| charset.as_str().to_string())
|
||||
});
|
||||
let blob: Vec<u8> = response.bytes().await?.into();
|
||||
Ok(Response {
|
||||
blob,
|
||||
mimetype,
|
||||
encoding,
|
||||
})
|
||||
}
|
||||
|
||||
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)?;
|
||||
let mut url = url.to_string();
|
||||
|
||||
// Follow up to 10 http-redirects
|
||||
for _i in 0..10 {
|
||||
let response = client.get(&url).send().await?;
|
||||
let parsed_url = url
|
||||
.parse::<hyper::Uri>()
|
||||
.with_context(|| format!("Failed to parse URL {url:?}"))?;
|
||||
|
||||
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
|
||||
let authority = parsed_url
|
||||
.authority()
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
|
||||
let req = hyper::Request::builder()
|
||||
.uri(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(http_body_util::Empty::<Bytes>::new())?;
|
||||
let response = sender.send_request(req).await?;
|
||||
|
||||
if response.status().is_redirection() {
|
||||
let headers = response.headers();
|
||||
let header = headers
|
||||
let header = response
|
||||
.headers()
|
||||
.get_all("location")
|
||||
.iter()
|
||||
.last()
|
||||
@@ -80,26 +126,119 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(response);
|
||||
let content_type = response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.parse::<Mime>().ok());
|
||||
let mimetype = content_type
|
||||
.as_ref()
|
||||
.map(|mime| mime.essence_str().to_string());
|
||||
let encoding = content_type.as_ref().and_then(|mime| {
|
||||
mime.get_param(mime::CHARSET)
|
||||
.map(|charset| charset.as_str().to_string())
|
||||
});
|
||||
let body = response.collect().await?.to_bytes();
|
||||
let blob: Vec<u8> = body.to_vec();
|
||||
return Ok(Response {
|
||||
blob,
|
||||
mimetype,
|
||||
encoding,
|
||||
});
|
||||
}
|
||||
|
||||
Err(anyhow!("Followed 10 redirections"))
|
||||
}
|
||||
|
||||
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
|
||||
let builder = reqwest::ClientBuilder::new()
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
/// Sends an empty POST request to the URL.
|
||||
///
|
||||
/// Returns response text and whether request was successful or not.
|
||||
///
|
||||
/// Does not follow redirects.
|
||||
pub(crate) async fn post_empty(context: &Context, url: &str) -> Result<(String, bool)> {
|
||||
let parsed_url = url
|
||||
.parse::<hyper::Uri>()
|
||||
.with_context(|| format!("Failed to parse URL {url:?}"))?;
|
||||
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
|
||||
if scheme != "https" {
|
||||
bail!("POST requests to non-HTTPS URLs are not allowed");
|
||||
}
|
||||
|
||||
let builder = if let Some(socks5_config) = socks5_config {
|
||||
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
|
||||
builder.proxy(proxy)
|
||||
} else {
|
||||
// Disable usage of "system" proxy configured via environment variables.
|
||||
// It is enabled by default in `reqwest`, see
|
||||
// <https://docs.rs/reqwest/0.11.14/reqwest/struct.ClientBuilder.html#method.no_proxy>
|
||||
// for documentation.
|
||||
builder.no_proxy()
|
||||
};
|
||||
Ok(builder.build()?)
|
||||
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
|
||||
let authority = parsed_url
|
||||
.authority()
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
let req = hyper::Request::post(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(http_body_util::Empty::<Bytes>::new())?;
|
||||
|
||||
let response = sender.send_request(req).await?;
|
||||
|
||||
let response_status = response.status();
|
||||
let body = response.collect().await?.to_bytes();
|
||||
let text = String::from_utf8_lossy(&body);
|
||||
let response_text = text.to_string();
|
||||
|
||||
Ok((response_text, response_status.is_success()))
|
||||
}
|
||||
|
||||
/// Posts string to the given URL.
|
||||
///
|
||||
/// Returns true if successful HTTP response code was returned.
|
||||
///
|
||||
/// Does not follow redirects.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn post_string(context: &Context, url: &str, body: String) -> Result<bool> {
|
||||
let parsed_url = url
|
||||
.parse::<hyper::Uri>()
|
||||
.with_context(|| format!("Failed to parse URL {url:?}"))?;
|
||||
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
|
||||
if scheme != "https" {
|
||||
bail!("POST requests to non-HTTPS URLs are not allowed");
|
||||
}
|
||||
|
||||
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
|
||||
let authority = parsed_url
|
||||
.authority()
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
|
||||
let request = hyper::Request::post(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.body(body)?;
|
||||
let response = sender.send_request(request).await?;
|
||||
|
||||
Ok(response.status().is_success())
|
||||
}
|
||||
|
||||
/// Sends a POST request with x-www-form-urlencoded data.
|
||||
///
|
||||
/// Does not follow redirects.
|
||||
pub(crate) async fn post_form<T: Serialize + ?Sized>(
|
||||
context: &Context,
|
||||
url: &str,
|
||||
form: &T,
|
||||
) -> Result<Bytes> {
|
||||
let parsed_url = url
|
||||
.parse::<hyper::Uri>()
|
||||
.with_context(|| format!("Failed to parse URL {url:?}"))?;
|
||||
let scheme = parsed_url.scheme_str().context("URL has no scheme")?;
|
||||
if scheme != "https" {
|
||||
bail!("POST requests to non-HTTPS URLs are not allowed");
|
||||
}
|
||||
|
||||
let encoded_body = serde_urlencoded::to_string(form).context("Failed to encode data")?;
|
||||
let mut sender = get_http_sender(context, parsed_url.clone()).await?;
|
||||
let authority = parsed_url
|
||||
.authority()
|
||||
.context("URL has no authority")?
|
||||
.clone();
|
||||
let request = hyper::Request::post(parsed_url.path())
|
||||
.header(hyper::header::HOST, authority.as_str())
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.body(encoded_body)?;
|
||||
let response = sender.send_request(request).await?;
|
||||
let bytes = response.collect().await?.to_bytes();
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
@@ -14,11 +14,33 @@ static LETSENCRYPT_ROOT: Lazy<Certificate> = Lazy::new(|| {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub fn build_tls(strict_tls: bool) -> TlsConnector {
|
||||
static IMAP_NAUTA_CU: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../../assets/certificates/imap.nauta.cu.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static SMTP_NAUTA_CU: Lazy<Certificate> = Lazy::new(|| {
|
||||
Certificate::from_der(include_bytes!(
|
||||
"../../assets/certificates/smtp.nauta.cu.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
fn build_tls(strict_tls: bool, hostname: &str, alpns: &[&str]) -> TlsConnector {
|
||||
let tls_builder = TlsConnector::new()
|
||||
.min_protocol_version(Some(Protocol::Tlsv12))
|
||||
.request_alpns(alpns)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
|
||||
// Add self-signed certificates for known hostnames.
|
||||
let tls_builder = match hostname {
|
||||
"imap.nauta.cu" => tls_builder.add_root_certificate(IMAP_NAUTA_CU.clone()),
|
||||
"smtp.nauta.cu" => tls_builder.add_root_certificate(SMTP_NAUTA_CU.clone()),
|
||||
_ => tls_builder,
|
||||
};
|
||||
|
||||
if strict_tls {
|
||||
tls_builder
|
||||
} else {
|
||||
@@ -31,9 +53,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, hostname, alpn);
|
||||
let tls_stream = tls.connect(hostname, stream).await?;
|
||||
Ok(tls_stream)
|
||||
}
|
||||
@@ -46,7 +69,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, "example.org", &[]);
|
||||
let _ = build_tls(false, "example.org", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::net::http::post_form;
|
||||
use crate::net::read_url_blob;
|
||||
use crate::provider;
|
||||
use crate::provider::Oauth2Authorizer;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::tools::time;
|
||||
|
||||
const OAUTH2_GMAIL: Oauth2 = Oauth2 {
|
||||
@@ -159,22 +160,20 @@ 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)?;
|
||||
|
||||
let response: Response = match client.post(post_url).form(&post_param).send().await {
|
||||
Ok(resp) => match resp.json().await {
|
||||
let response: Response = match post_form(context, post_url, &post_param).await {
|
||||
Ok(resp) => match serde_json::from_slice(&resp) {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to parse OAuth2 JSON response from {}: error: {}", token_url, err
|
||||
"Failed to parse OAuth2 JSON response from {token_url}: {err:#}."
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(context, "Error calling OAuth2 at {}: {:?}", token_url, err);
|
||||
warn!(context, "Error calling OAuth2 at {token_url}: {err:#}.");
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
@@ -243,11 +242,20 @@ pub(crate) async fn get_oauth2_addr(
|
||||
}
|
||||
|
||||
if let Some(access_token) = get_oauth2_access_token(context, addr, code, false).await? {
|
||||
let addr_out = oauth2.get_addr(context, &access_token).await;
|
||||
let addr_out = match oauth2.get_addr(context, &access_token).await {
|
||||
Ok(addr) => addr,
|
||||
Err(err) => {
|
||||
warn!(context, "Error getting addr: {err:#}.");
|
||||
None
|
||||
}
|
||||
};
|
||||
if addr_out.is_none() {
|
||||
// regenerate
|
||||
if let Some(access_token) = get_oauth2_access_token(context, addr, code, true).await? {
|
||||
Ok(oauth2.get_addr(context, &access_token).await)
|
||||
Ok(oauth2
|
||||
.get_addr(context, &access_token)
|
||||
.await
|
||||
.unwrap_or_default())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -279,7 +287,7 @@ impl Oauth2 {
|
||||
None
|
||||
}
|
||||
|
||||
async fn get_addr(&self, context: &Context, access_token: &str) -> Option<String> {
|
||||
async fn get_addr(&self, context: &Context, access_token: &str) -> Result<Option<String>> {
|
||||
let userinfo_url = self.get_userinfo.unwrap_or("");
|
||||
let userinfo_url = replace_in_uri(userinfo_url, "$ACCESS_TOKEN", access_token);
|
||||
|
||||
@@ -290,41 +298,22 @@ 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) {
|
||||
Ok(cl) => cl,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to get HTTP client: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let response = match client.get(userinfo_url).send().await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
warn!(context, "failed to get userinfo: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let response: Result<HashMap<String, serde_json::Value>, _> = response.json().await;
|
||||
let parsed = match response {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) => {
|
||||
warn!(context, "Error getting userinfo: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let response = read_url_blob(context, &userinfo_url).await?;
|
||||
let parsed: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_slice(&response.blob).context("Error getting userinfo")?;
|
||||
// CAVE: serde_json::Value.as_str() removes the quotes of json-strings
|
||||
// but serde_json::Value.to_string() does not!
|
||||
if let Some(addr) = parsed.get("email") {
|
||||
if let Some(s) = addr.as_str() {
|
||||
Some(s.to_string())
|
||||
Ok(Some(s.to_string()))
|
||||
} else {
|
||||
warn!(context, "E-mail in userinfo is not a string: {}", addr);
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
warn!(context, "E-mail missing in userinfo.");
|
||||
None
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +87,10 @@ impl Iroh {
|
||||
&self,
|
||||
ctx: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<Option<JoinTopicFut>> {
|
||||
let topic = get_iroh_topic_for_msg(ctx, msg_id).await?;
|
||||
) -> 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"))?;
|
||||
|
||||
// Take exclusive lock to make sure
|
||||
// no other thread can create a second gossip subscription
|
||||
@@ -91,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(())
|
||||
}
|
||||
@@ -152,14 +157,21 @@ impl Iroh {
|
||||
msg_id: MsgId,
|
||||
mut data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let topic = get_iroh_topic_for_msg(ctx, msg_id).await?;
|
||||
let topic = get_iroh_topic_for_msg(ctx, msg_id)
|
||||
.await?
|
||||
.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");
|
||||
@@ -168,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(())
|
||||
}
|
||||
}
|
||||
@@ -199,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();
|
||||
@@ -242,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
|
||||
@@ -250,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,
|
||||
})
|
||||
@@ -268,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,
|
||||
@@ -325,16 +351,28 @@ async fn get_iroh_gossip_peers(ctx: &Context, msg_id: MsgId) -> Result<Vec<NodeA
|
||||
}
|
||||
|
||||
/// Get the topic for a given [MsgId].
|
||||
pub(crate) async fn get_iroh_topic_for_msg(ctx: &Context, msg_id: MsgId) -> Result<TopicId> {
|
||||
let bytes: Vec<u8> = ctx
|
||||
pub(crate) async fn get_iroh_topic_for_msg(
|
||||
ctx: &Context,
|
||||
msg_id: MsgId,
|
||||
) -> Result<Option<TopicId>> {
|
||||
if let Some(bytes) = ctx
|
||||
.sql
|
||||
.query_get_value(
|
||||
.query_get_value::<Vec<u8>>(
|
||||
"SELECT topic FROM iroh_gossip_peers WHERE msg_id = ? LIMIT 1",
|
||||
(msg_id,),
|
||||
)
|
||||
.await?
|
||||
.context("couldn't restore topic from db")?;
|
||||
Ok(TopicId::from_bytes(bytes.try_into().unwrap()))
|
||||
.await
|
||||
.context("Couldn't restore topic from db")?
|
||||
{
|
||||
let topic_id = TopicId::from_bytes(
|
||||
bytes
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("Could not convert stored topic ID"))?,
|
||||
);
|
||||
Ok(Some(topic_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a gossip advertisement to the chat that [MsgId] belongs to.
|
||||
@@ -342,7 +380,7 @@ pub(crate) async fn get_iroh_topic_for_msg(ctx: &Context, msg_id: MsgId) -> Resu
|
||||
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);
|
||||
}
|
||||
@@ -376,10 +414,11 @@ pub async fn leave_webxdc_realtime(ctx: &Context, msg_id: MsgId) -> Result<()> {
|
||||
if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let topic = get_iroh_topic_for_msg(ctx, msg_id)
|
||||
.await?
|
||||
.with_context(|| format!("Message {msg_id} has no gossip topic"))?;
|
||||
let iroh = ctx.get_or_try_init_peer_channel().await?;
|
||||
iroh.leave_realtime(get_iroh_topic_for_msg(ctx, msg_id).await?)
|
||||
.await?;
|
||||
iroh.leave_realtime(topic).await?;
|
||||
info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}");
|
||||
|
||||
Ok(())
|
||||
@@ -423,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"
|
||||
@@ -438,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)]
|
||||
@@ -511,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)
|
||||
@@ -525,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()
|
||||
@@ -539,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()
|
||||
@@ -567,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();
|
||||
|
||||
@@ -599,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();
|
||||
|
||||
@@ -657,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)
|
||||
@@ -671,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()
|
||||
@@ -685,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()
|
||||
@@ -712,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()
|
||||
@@ -724,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();
|
||||
|
||||
@@ -752,8 +830,9 @@ mod tests {
|
||||
leave_webxdc_realtime(alice, alice_webxdc.id).await.unwrap();
|
||||
let topic = get_iroh_topic_for_msg(alice, alice_webxdc.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(if let Some(state) = alice
|
||||
assert!(alice
|
||||
.iroh
|
||||
.get()
|
||||
.unwrap()
|
||||
@@ -761,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)]
|
||||
|
||||
68
src/pgp.rs
68
src/pgp.rs
@@ -11,6 +11,7 @@ use pgp::composed::{
|
||||
Deserializable, KeyType as PgpKeyType, Message, SecretKeyParamsBuilder, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, StandaloneSignature, SubkeyParamsBuilder,
|
||||
};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::types::{
|
||||
@@ -115,7 +116,14 @@ pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, Str
|
||||
let headers = dearmor
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key.trim().to_lowercase(), value.trim().to_string()))
|
||||
.map(|(key, values)| {
|
||||
(
|
||||
key.trim().to_lowercase(),
|
||||
values
|
||||
.last()
|
||||
.map_or_else(String::new, |s| s.trim().to_string()),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((typ, headers, bytes))
|
||||
@@ -145,7 +153,9 @@ pub(crate) fn create_keypair(addr: EmailAddress, keygen_type: KeyGenType) -> Res
|
||||
let (signing_key_type, encryption_key_type) = match keygen_type {
|
||||
KeyGenType::Rsa2048 => (PgpKeyType::Rsa(2048), PgpKeyType::Rsa(2048)),
|
||||
KeyGenType::Rsa4096 => (PgpKeyType::Rsa(4096), PgpKeyType::Rsa(4096)),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => (PgpKeyType::EdDSA, PgpKeyType::ECDH),
|
||||
KeyGenType::Ed25519 | KeyGenType::Default => {
|
||||
(PgpKeyType::EdDSA, PgpKeyType::ECDH(ECCCurve::Curve25519))
|
||||
}
|
||||
};
|
||||
|
||||
let user_id = format!("<{addr}>");
|
||||
@@ -262,7 +272,7 @@ pub async fn pk_encrypt(
|
||||
lit_msg.encrypt_to_keys(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
|
||||
};
|
||||
|
||||
let encoded_msg = encrypted_msg.to_armored_string(None)?;
|
||||
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
@@ -279,7 +289,7 @@ pub fn pk_calc_signature(
|
||||
|| "".into(),
|
||||
HASH_ALGORITHM,
|
||||
)?;
|
||||
let signature = msg.into_signature().to_armored_string(None)?;
|
||||
let signature = msg.into_signature().to_armored_string(Default::default())?;
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
@@ -304,31 +314,26 @@ pub fn pk_decrypt(
|
||||
|
||||
let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
|
||||
|
||||
let (decryptor, _) = msg.decrypt(|| "".into(), &skeys[..])?;
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
let (msg, _) = msg.decrypt(|| "".into(), &skeys[..])?;
|
||||
|
||||
if let Some(msg) = msgs.into_iter().next() {
|
||||
// get_content() will decompress the message if needed,
|
||||
// but this avoids decompressing it again to check signatures
|
||||
let msg = msg.decompress()?;
|
||||
// get_content() will decompress the message if needed,
|
||||
// but this avoids decompressing it again to check signatures
|
||||
let msg = msg.decompress()?;
|
||||
|
||||
let content = match msg.get_content()? {
|
||||
Some(content) => content,
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
let content = match msg.get_content()? {
|
||||
Some(content) => content,
|
||||
None => bail!("The decrypted message is empty"),
|
||||
};
|
||||
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in public_keys_for_validation {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret_signature_fingerprints.insert(fp);
|
||||
}
|
||||
if let signed_msg @ pgp::composed::Message::Signed { .. } = msg {
|
||||
for pkey in public_keys_for_validation {
|
||||
if signed_msg.verify(&pkey.primary_key).is_ok() {
|
||||
let fp = DcKey::fingerprint(pkey);
|
||||
ret_signature_fingerprints.insert(fp);
|
||||
}
|
||||
}
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
} else {
|
||||
bail!("No valid messages found");
|
||||
}
|
||||
Ok((content, ret_signature_fingerprints))
|
||||
}
|
||||
|
||||
/// Validates detached signature.
|
||||
@@ -368,7 +373,7 @@ pub async fn symm_encrypt(passphrase: &str, plain: &[u8]) -> Result<String> {
|
||||
let msg =
|
||||
lit_msg.encrypt_with_password(&mut rng, s2k, SYMMETRIC_KEY_ALGORITHM, || passphrase)?;
|
||||
|
||||
let encoded_msg = msg.to_armored_string(None)?;
|
||||
let encoded_msg = msg.to_armored_string(Default::default())?;
|
||||
|
||||
Ok(encoded_msg)
|
||||
})
|
||||
@@ -384,16 +389,11 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
|
||||
let passphrase = passphrase.to_string();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let decryptor = enc_msg.decrypt_with_password(|| passphrase)?;
|
||||
let msg = enc_msg.decrypt_with_password(|| passphrase)?;
|
||||
|
||||
let msgs = decryptor.collect::<pgp::errors::Result<Vec<_>>>()?;
|
||||
if let Some(msg) = msgs.first() {
|
||||
match msg.get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
} else {
|
||||
bail!("No valid messages found")
|
||||
match msg.get_content()? {
|
||||
Some(content) => Ok(content),
|
||||
None => bail!("Decrypted message is empty"),
|
||||
}
|
||||
})
|
||||
.await?
|
||||
@@ -410,7 +410,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_split_armored_data_1() {
|
||||
let (typ, _headers, base64) = split_armored_data(
|
||||
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE----",
|
||||
b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE-----",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! [Provider database](https://providers.delta.chat/) module.
|
||||
|
||||
mod data;
|
||||
pub(crate) mod data;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
|
||||
@@ -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: &[
|
||||
@@ -874,6 +874,20 @@ static P_MEHL_CLOUD: Provider = Provider {
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mehl-cloud",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 443,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 443,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
@@ -1009,6 +1023,20 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/nine-testrun-org",
|
||||
server: &[
|
||||
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,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
@@ -1260,14 +1288,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 +1329,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 +1614,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 +1818,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 +2273,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 +2407,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 +2431,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, 23).unwrap());
|
||||
|
||||
30
src/push.rs
30
src/push.rs
@@ -5,7 +5,6 @@ use anyhow::Result;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::net::http;
|
||||
|
||||
/// Manages subscription to Apple Push Notification services.
|
||||
///
|
||||
@@ -48,7 +47,10 @@ impl PushSubscriber {
|
||||
}
|
||||
|
||||
/// Subscribes for heartbeat notifications with previously set device token.
|
||||
pub(crate) async fn subscribe(&self) -> Result<()> {
|
||||
#[cfg(target_os = "ios")]
|
||||
pub(crate) async fn subscribe(&self, context: &Context) -> Result<()> {
|
||||
use crate::net::http;
|
||||
|
||||
let mut state = self.inner.write().await;
|
||||
|
||||
if state.heartbeat_subscribed {
|
||||
@@ -59,20 +61,26 @@ impl PushSubscriber {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let socks5_config = None;
|
||||
let response = http::get_client(socks5_config)?
|
||||
.post("https://notifications.delta.chat/register")
|
||||
.body(format!("{{\"token\":\"{token}\"}}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response_status = response.status();
|
||||
if response_status.is_success() {
|
||||
if http::post_string(
|
||||
context,
|
||||
"https://notifications.delta.chat/register",
|
||||
format!("{{\"token\":\"{token}\"}}"),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
state.heartbeat_subscribed = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Placeholder to skip subscribing to heartbeat notifications outside iOS.
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub(crate) async fn subscribe(&self, _context: &Context) -> Result<()> {
|
||||
let mut state = self.inner.write().await;
|
||||
state.heartbeat_subscribed = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn heartbeat_subscribed(&self) -> bool {
|
||||
self.inner.read().await.heartbeat_subscribed
|
||||
}
|
||||
|
||||
289
src/qr.rs
289
src/qr.rs
@@ -19,11 +19,10 @@ use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::Fingerprint;
|
||||
use crate::message::Message;
|
||||
use crate::net::http::post_empty;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::socks::Socks5Config;
|
||||
use crate::token;
|
||||
use crate::tools::validate_id;
|
||||
use iroh_old as iroh;
|
||||
|
||||
const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase
|
||||
const IDELTACHAT_SCHEME: &str = "https://i.delta.chat/#";
|
||||
@@ -31,13 +30,16 @@ const IDELTACHAT_NOSLASH_SCHEME: &str = "https://i.delta.chat#";
|
||||
const DCACCOUNT_SCHEME: &str = "DCACCOUNT:";
|
||||
pub(super) const DCLOGIN_SCHEME: &str = "DCLOGIN:";
|
||||
const DCWEBRTC_SCHEME: &str = "DCWEBRTC:";
|
||||
const TG_SOCKS_SCHEME: &str = "https://t.me/socks";
|
||||
const MAILTO_SCHEME: &str = "mailto:";
|
||||
const MATMSG_SCHEME: &str = "MATMSG:";
|
||||
const VCARD_SCHEME: &str = "BEGIN:VCARD";
|
||||
const SMTP_SCHEME: &str = "SMTP:";
|
||||
const HTTP_SCHEME: &str = "http://";
|
||||
const HTTPS_SCHEME: &str = "https://";
|
||||
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)]
|
||||
@@ -106,18 +108,13 @@ pub enum Qr {
|
||||
domain: String,
|
||||
},
|
||||
|
||||
/// Provides a backup that can be retrieve.
|
||||
///
|
||||
/// 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.
|
||||
Backup {
|
||||
/// Printable version of the provider information.
|
||||
///
|
||||
/// This is the printable version of a `sendme` ticket, which contains all the
|
||||
/// information to connect to and authenticate a backup provider.
|
||||
///
|
||||
/// The format is somewhat opaque, but `sendme` can deserialise this.
|
||||
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.
|
||||
@@ -129,6 +126,21 @@ pub enum Qr {
|
||||
instance_pattern: String,
|
||||
},
|
||||
|
||||
/// Ask the user if they want to add or use the given SOCKS5 proxy
|
||||
Socks5Proxy {
|
||||
/// SOCKS5 server
|
||||
host: String,
|
||||
|
||||
/// SOCKS5 port
|
||||
port: u16,
|
||||
|
||||
/// SOCKS5 user
|
||||
user: Option<String>,
|
||||
|
||||
/// SOCKS5 password
|
||||
pass: Option<String>,
|
||||
},
|
||||
|
||||
/// Contact address is scanned.
|
||||
///
|
||||
/// Optionally, a draft message could be provided.
|
||||
@@ -264,8 +276,10 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
dclogin_scheme::decode_login(qr)?
|
||||
} else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) {
|
||||
decode_webrtc_instance(context, qr)?
|
||||
} else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) {
|
||||
decode_backup(qr)?
|
||||
} else if starts_with_ignore_case(qr, TG_SOCKS_SCHEME) {
|
||||
decode_tg_socks_proxy(context, 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) {
|
||||
@@ -286,7 +300,7 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Ok(qrcode)
|
||||
}
|
||||
|
||||
/// Formats the text of the [`Qr::Backup`] variant.
|
||||
/// Formats the text of the [`Qr::Backup2`] variant.
|
||||
///
|
||||
/// This is the inverse of [`check_qr`] for that variant only.
|
||||
///
|
||||
@@ -294,7 +308,13 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result<Qr> {
|
||||
/// into `FromStr`.
|
||||
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 +507,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 {
|
||||
@@ -517,16 +537,55 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a [`DCBACKUP_SCHEME`] QR code.
|
||||
///
|
||||
/// The format of this scheme is `DCBACKUP:<encoded ticket>`. The encoding is the
|
||||
/// [`iroh::provider::Ticket`]'s `Display` impl.
|
||||
fn decode_backup(qr: &str) -> Result<Qr> {
|
||||
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
|
||||
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
|
||||
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
|
||||
|
||||
const SOCKS5_DEFAULT_PORT: u16 = 1080;
|
||||
let mut host: Option<String> = None;
|
||||
let mut port: u16 = SOCKS5_DEFAULT_PORT;
|
||||
let mut user: Option<String> = None;
|
||||
let mut pass: Option<String> = None;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "server" {
|
||||
host = Some(value.to_string());
|
||||
} else if key == "port" {
|
||||
port = value.parse().unwrap_or(SOCKS5_DEFAULT_PORT);
|
||||
} else if key == "user" {
|
||||
user = Some(value.to_string());
|
||||
} else if key == "pass" {
|
||||
pass = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(host) = host {
|
||||
Ok(Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
})
|
||||
} else {
|
||||
bail!("Bad t.me/socks url: {:?}", url);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decodes a [`DCBACKUP2_SCHEME`] QR code.
|
||||
fn decode_backup2(qr: &str) -> Result<Qr> {
|
||||
let payload = qr
|
||||
.strip_prefix(DCBACKUP_SCHEME)
|
||||
.strip_prefix(DCBACKUP2_SCHEME)
|
||||
.ok_or_else(|| anyhow!("invalid DCBACKUP scheme"))?;
|
||||
let ticket: iroh::provider::Ticket = payload.parse().context("invalid DCBACKUP payload")?;
|
||||
Ok(Qr::Backup { ticket })
|
||||
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)]
|
||||
@@ -549,18 +608,13 @@ 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)?
|
||||
.post(url_str)
|
||||
.send()
|
||||
.await?;
|
||||
let response_status = response.status();
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.context("Cannot create account, request failed: empty response")?;
|
||||
|
||||
if response_status.is_success() {
|
||||
if !url_str.starts_with(HTTPS_SCHEME) {
|
||||
bail!("DCACCOUNT QR codes must use HTTPS scheme");
|
||||
}
|
||||
|
||||
let (response_text, response_success) = post_empty(context, url_str).await?;
|
||||
if response_success {
|
||||
let CreateAccountSuccessResponse { password, email } = serde_json::from_str(&response_text)
|
||||
.with_context(|| {
|
||||
format!("Cannot create account, response is malformed:\n{response_text:?}")
|
||||
@@ -601,6 +655,29 @@ pub async fn set_config_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
.set_config_internal(Config::WebrtcInstance, Some(&instance_pattern))
|
||||
.await?;
|
||||
}
|
||||
Qr::Socks5Proxy {
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
pass,
|
||||
} => {
|
||||
// disable proxy before changing settings to not use a combination of old and new
|
||||
context
|
||||
.set_config_bool(Config::Socks5Enabled, false)
|
||||
.await?;
|
||||
|
||||
context.set_config(Config::Socks5Host, Some(&host)).await?;
|
||||
context
|
||||
.set_config_u32(Config::Socks5Port, u32::from(port))
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::Socks5User, user.as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::Socks5Password, pass.as_deref())
|
||||
.await?;
|
||||
context.set_config_bool(Config::Socks5Enabled, true).await?;
|
||||
}
|
||||
Qr::WithdrawVerifyContact {
|
||||
invitenumber,
|
||||
authcode,
|
||||
@@ -611,7 +688,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 +699,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 +708,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 +728,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?
|
||||
@@ -824,6 +899,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::{create_group_chat, ProtectionStatus};
|
||||
use crate::config::Config;
|
||||
use crate::key::DcKey;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
@@ -1359,9 +1435,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?,
|
||||
@@ -1429,6 +1508,73 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_tg_socks_proxy() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
host: "84.53.239.95".to_string(),
|
||||
port: 4145,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.bar&port=123").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
host: "foo.bar".to_string(),
|
||||
port: 123,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?server=foo.baz").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
host: "foo.baz".to_string(),
|
||||
port: 1080,
|
||||
user: None,
|
||||
pass: None,
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(
|
||||
&t,
|
||||
"https://t.me/socks?server=foo.baz&port=12345&user=ada&pass=ms%21%2F%24",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Socks5Proxy {
|
||||
host: "foo.baz".to_string(),
|
||||
port: 12345,
|
||||
user: Some("ada".to_string()),
|
||||
pass: Some("ms!/$".to_string()),
|
||||
}
|
||||
);
|
||||
|
||||
// wrong domain results in Qr:Url instead of Qr::Socks5Proxy
|
||||
let qr = check_qr(&t, "https://not.me/socks?noserver=84.53.239.95&port=4145").await?;
|
||||
assert_eq!(
|
||||
qr,
|
||||
Qr::Url {
|
||||
url: "https://not.me/socks?noserver=84.53.239.95&port=4145".to_string()
|
||||
}
|
||||
);
|
||||
|
||||
let qr = check_qr(&t, "https://t.me/socks?noserver=84.53.239.95&port=4145").await;
|
||||
assert!(qr.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_decode_account_bad_scheme() {
|
||||
let ctx = TestContext::new().await;
|
||||
@@ -1449,7 +1595,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_from_qr() -> Result<()> {
|
||||
async fn test_set_webrtc_instance_config_from_qr() -> Result<()> {
|
||||
let ctx = TestContext::new().await;
|
||||
|
||||
assert!(ctx.ctx.get_config(Config::WebrtcInstance).await?.is_none());
|
||||
@@ -1479,4 +1625,57 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_socks5_proxy_config_from_qr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, false);
|
||||
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=foo&port=666").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("foo".to_string())
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 666);
|
||||
assert_eq!(t.get_config(Config::Socks5User).await?, None);
|
||||
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
|
||||
|
||||
// make sure, user&password are reset when not specified in the URL
|
||||
t.set_config(Config::Socks5User, Some("alice")).await?;
|
||||
t.set_config(Config::Socks5Password, Some("secret")).await?;
|
||||
let res = set_config_from_qr(&t, "https://t.me/socks?server=1.2.3.4").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("1.2.3.4".to_string())
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
|
||||
assert_eq!(t.get_config(Config::Socks5User).await?, None);
|
||||
assert_eq!(t.get_config(Config::Socks5Password).await?, None);
|
||||
|
||||
// make sure, user&password are set when specified in the URL
|
||||
let res =
|
||||
set_config_from_qr(&t, "https://t.me/socks?server=jau&user=Da&pass=x%26%25%24X").await;
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(t.get_config_bool(Config::Socks5Enabled).await?, true);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Host).await?,
|
||||
Some("jau".to_string())
|
||||
);
|
||||
assert_eq!(t.get_config_u32(Config::Socks5Port).await?, 1080);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5User).await?,
|
||||
Some("Da".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
t.get_config(Config::Socks5Password).await?,
|
||||
Some("x&%$X".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use super::{Qr, DCLOGIN_SCHEME};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::login_param::CertificateChecks;
|
||||
use crate::login_param::EnteredCertificateChecks;
|
||||
use crate::provider::Socket;
|
||||
|
||||
/// Options for `dclogin:` scheme.
|
||||
@@ -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<EnteredCertificateChecks>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
@@ -150,11 +146,12 @@ fn parse_socket_security(security: Option<&String>) -> Result<Option<Socket>> {
|
||||
|
||||
fn parse_certificate_checks(
|
||||
certificate_checks: Option<&String>,
|
||||
) -> Result<Option<CertificateChecks>> {
|
||||
) -> Result<Option<EnteredCertificateChecks>> {
|
||||
Ok(match certificate_checks.map(|s| s.as_str()) {
|
||||
Some("0") => Some(CertificateChecks::Automatic),
|
||||
Some("1") => Some(CertificateChecks::Strict),
|
||||
Some("3") => Some(CertificateChecks::AcceptInvalidCertificates),
|
||||
Some("0") => Some(EnteredCertificateChecks::Automatic),
|
||||
Some("1") => Some(EnteredCertificateChecks::Strict),
|
||||
Some("2") => Some(EnteredCertificateChecks::AcceptInvalidCertificates),
|
||||
Some("3") => Some(EnteredCertificateChecks::AcceptInvalidCertificates2),
|
||||
Some(other) => bail!("Unknown certificatecheck level: {}", other),
|
||||
None => None,
|
||||
})
|
||||
@@ -177,13 +174,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 +212,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 +240,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?;
|
||||
@@ -273,7 +264,7 @@ mod test {
|
||||
use anyhow::bail;
|
||||
|
||||
use super::{decode_login, LoginOptions};
|
||||
use crate::{login_param::CertificateChecks, provider::Socket, qr::Qr};
|
||||
use crate::{login_param::EnteredCertificateChecks, provider::Socket, qr::Qr};
|
||||
|
||||
macro_rules! login_options_just_pw {
|
||||
($pw: expr) => {
|
||||
@@ -284,13 +275,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 +382,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(EnteredCertificateChecks::Strict),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
34
src/quota.rs
34
src/quota.rs
@@ -1,6 +1,7 @@
|
||||
//! # Support for IMAP QUOTA extension.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_imap::types::{Quota, QuotaResource};
|
||||
@@ -11,7 +12,7 @@ use crate::context::Context;
|
||||
use crate::imap::scan_folders::get_watched_folders;
|
||||
use crate::imap::session::Session as ImapSession;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::tools;
|
||||
use crate::tools::{self, time_elapsed};
|
||||
use crate::{stock_str, EventType};
|
||||
|
||||
/// warn about a nearly full mailbox after this usage percentage is reached.
|
||||
@@ -102,6 +103,16 @@ pub fn needs_quota_warning(curr_percentage: u64, warned_at_percentage: u64) -> b
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns whether the quota value needs an update. If so, `update_recent_quota()` should be
|
||||
/// called.
|
||||
pub(crate) async fn quota_needs_update(&self, ratelimit_secs: u64) -> bool {
|
||||
let quota = self.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(ratelimit_secs))
|
||||
.is_none()
|
||||
}
|
||||
|
||||
/// Updates `quota.recent`, sets `quota.modified` to the current time
|
||||
/// and emits an event to let the UIs update connectivity view.
|
||||
///
|
||||
@@ -155,6 +166,7 @@ impl Context {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::TestContextManager;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_needs_quota_warning() -> Result<()> {
|
||||
@@ -183,4 +195,24 @@ mod tests {
|
||||
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_quota_needs_update() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.unconfigured().await;
|
||||
const TIMEOUT: u64 = 60;
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now() - Duration::from_secs(TIMEOUT + 1),
|
||||
});
|
||||
assert!(t.quota_needs_update(TIMEOUT).await);
|
||||
|
||||
*t.quota.write().await = Some(QuotaInfo {
|
||||
recent: Ok(Default::default()),
|
||||
modified: tools::Time::now(),
|
||||
});
|
||||
assert!(!t.quota_needs_update(TIMEOUT).await);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::{
|
||||
addr_cmp, may_be_valid_addr, normalize_name, strip_rtlo_characters, ContactAddress,
|
||||
};
|
||||
use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line, ContactAddress};
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mailparse::{parse_mail, SingleInfo};
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -634,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(
|
||||
@@ -660,10 +658,10 @@ pub async fn from_field_to_contact_id(
|
||||
}
|
||||
};
|
||||
|
||||
let from_id = add_or_lookup_contact_by_addr(
|
||||
let (from_id, _) = Contact::add_or_lookup(
|
||||
context,
|
||||
display_name,
|
||||
from_addr,
|
||||
display_name.unwrap_or_default(),
|
||||
&from_addr,
|
||||
Origin::IncomingUnknownFrom,
|
||||
)
|
||||
.await?;
|
||||
@@ -809,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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1446,12 +1444,19 @@ async fn add_parts(
|
||||
Ok(node_addr) => {
|
||||
info!(context, "Adding iroh peer with address {node_addr:?}.");
|
||||
let instance_id = parent.context("Failed to get parent message")?.id;
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
let topic = get_iroh_topic_for_msg(context, instance_id).await?;
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server).await?;
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
if let Some(topic) = get_iroh_topic_for_msg(context, instance_id).await? {
|
||||
let node_id = node_addr.node_id;
|
||||
let relay_server = node_addr.relay_url().map(|relay| relay.as_str());
|
||||
iroh_add_peer_for_topic(context, instance_id, topic, node_id, relay_server)
|
||||
.await?;
|
||||
let iroh = context.get_or_try_init_peer_channel().await?;
|
||||
iroh.maybe_add_gossip_peers(topic, vec![node_addr]).await?;
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Could not add iroh peer because {instance_id} has no topic"
|
||||
);
|
||||
}
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1659,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,
|
||||
@@ -1819,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,
|
||||
@@ -1828,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)
|
||||
}
|
||||
@@ -2141,6 +2147,8 @@ async fn apply_group_changes(
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
let grpname = &sanitize_single_line(grpname);
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
if chat_id
|
||||
.update_timestamp(
|
||||
context,
|
||||
@@ -2152,10 +2160,7 @@ async fn apply_group_changes(
|
||||
info!(context, "Updating grpname for chat {chat_id}.");
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE chats SET name=? WHERE id=?;",
|
||||
(strip_rtlo_characters(grpname), chat_id),
|
||||
)
|
||||
.execute("UPDATE chats SET name=? WHERE id=?;", (grpname, chat_id))
|
||||
.await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
@@ -2428,7 +2433,7 @@ fn compute_mailinglist_name(
|
||||
}
|
||||
}
|
||||
|
||||
strip_rtlo_characters(&name)
|
||||
sanitize_single_line(&name)
|
||||
}
|
||||
|
||||
/// Set ListId param on the contact and ListPost param the chat.
|
||||
@@ -2508,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.
|
||||
//
|
||||
@@ -2547,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);
|
||||
}
|
||||
@@ -2579,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)]
|
||||
@@ -2850,8 +2864,9 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
}
|
||||
let display_name = info.display_name.as_deref();
|
||||
if let Ok(addr) = ContactAddress::new(addr) {
|
||||
let contact_id =
|
||||
add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?;
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, display_name.unwrap_or_default(), &addr, origin)
|
||||
.await?;
|
||||
contact_ids.insert(contact_id);
|
||||
} else {
|
||||
warn!(context, "Contact with address {:?} cannot exist.", addr);
|
||||
@@ -2861,22 +2876,5 @@ async fn add_or_lookup_contacts_by_address_list(
|
||||
Ok(contact_ids.into_iter().collect::<Vec<ContactId>>())
|
||||
}
|
||||
|
||||
/// Add contacts to database on receiving messages.
|
||||
async fn add_or_lookup_contact_by_addr(
|
||||
context: &Context,
|
||||
display_name: Option<&str>,
|
||||
addr: ContactAddress,
|
||||
origin: Origin,
|
||||
) -> Result<ContactId> {
|
||||
if context.is_self_addr(&addr).await? {
|
||||
return Ok(ContactId::SELF);
|
||||
}
|
||||
let display_name_normalized = display_name.map(normalize_name).unwrap_or_default();
|
||||
|
||||
let (contact_id, _modified) =
|
||||
Contact::add_or_lookup(context, &display_name_normalized, &addr, origin).await?;
|
||||
Ok(contact_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -14,8 +14,9 @@ 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::SystemTime;
|
||||
use crate::tools::{time, SystemTime};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing() -> Result<()> {
|
||||
@@ -2094,6 +2095,18 @@ Message content",
|
||||
assert_ne!(msg.chat_id, t.get_self_chat().await.id);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_unencrypted_name_in_self_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Smith"))
|
||||
.await?;
|
||||
let chat_id = bob.get_self_chat().await.id;
|
||||
let msg = bob.send_text(chat_id, "Happy birthday to me").await;
|
||||
assert_eq!(msg.payload.contains("Bob Smith"), false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outgoing_classic_mail_creates_chat() {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -3263,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();
|
||||
@@ -3326,6 +3379,22 @@ async fn test_bot_recv_existing_msg() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_date_in_imf_section() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = tcm.send_recv_accept(bob, alice, "hi").await.chat_id;
|
||||
let time_before_sending = time();
|
||||
let mut sent_msg = alice.send_text(alice_chat_id, "hi").await;
|
||||
sent_msg.payload = sent_msg.payload.replace(
|
||||
"Date:",
|
||||
"Date: Tue, 29 Feb 1972 22:37:57 +0000\nX-Microsoft-Original-Date:",
|
||||
);
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.timestamp_sent >= time_before_sending);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3531,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;
|
||||
@@ -4630,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();
|
||||
@@ -4886,19 +5049,63 @@ async fn test_make_n_send_vcard() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_no_recipients() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
let raw = b"From: alice@example.org\n\
|
||||
Subject: Group\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Chat-Group-Name: Group\n\
|
||||
Chat-Group-ID: GePFDkwEj2K\n\
|
||||
Message-ID: <foobar@localhost>\n\
|
||||
\n\
|
||||
Hello!";
|
||||
let raw = "From: alice@example.org
|
||||
Subject: Group
|
||||
Chat-Version: 1.0
|
||||
Chat-Group-Name: Group
|
||||
name\u{202B}
|
||||
Chat-Group-ID: GePFDkwEj2K
|
||||
Message-ID: <foobar@localhost>
|
||||
|
||||
Hello!"
|
||||
.as_bytes();
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
|
||||
let chat = Chat::load_from_db(t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
|
||||
// Check that the weird group name is sanitzied correctly:
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert_eq!(
|
||||
mail.headers
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.unwrap()
|
||||
.get_value_raw(),
|
||||
"Group\n name\u{202B}".as_bytes()
|
||||
);
|
||||
assert_eq!(chat.name, "Group name");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_name_with_newline() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
let raw = "From: alice@example.org
|
||||
Subject: Group
|
||||
Chat-Version: 1.0
|
||||
Chat-Group-Name: =?utf-8?q?Delta=0D=0AChat?=
|
||||
Chat-Group-ID: GePFDkwEj2K
|
||||
Message-ID: <foobar@localhost>
|
||||
|
||||
Hello!"
|
||||
.as_bytes();
|
||||
let received = receive_imf(t, raw, false).await?.unwrap();
|
||||
let msg = Message::load_from_db(t, *received.msg_ids.last().unwrap()).await?;
|
||||
let chat = Chat::load_from_db(t, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
|
||||
// Check that the weird group name is sanitzied correctly:
|
||||
let mail = mailparse::parse_mail(raw).unwrap();
|
||||
assert_eq!(
|
||||
mail.headers
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.unwrap()
|
||||
.get_value(),
|
||||
"Delta\r\nChat"
|
||||
);
|
||||
assert_eq!(chat.name, "Delta Chat");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::cmp;
|
||||
use std::iter::{self, once};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
@@ -111,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);
|
||||
@@ -466,21 +466,16 @@ 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.
|
||||
let quota_needs_update = {
|
||||
let quota = ctx.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.filter(|quota| time_elapsed("a.modified) > Duration::from_secs(60))
|
||||
.is_none()
|
||||
};
|
||||
if quota_needs_update {
|
||||
if ctx.quota_needs_update(60).await {
|
||||
if let Err(err) = ctx.update_recent_quota(&mut session).await {
|
||||
warn!(ctx, "Failed to update quota: {:#}.", err);
|
||||
}
|
||||
@@ -982,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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={}",
|
||||
@@ -367,60 +369,42 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
==========================================================*/
|
||||
|
||||
// verify that Secure-Join-Fingerprint:-header matches the fingerprint of Bob
|
||||
let fingerprint: Fingerprint =
|
||||
match mime_message.get_header(HeaderDef::SecureJoinFingerprint) {
|
||||
Some(fp) => fp.parse()?,
|
||||
None => {
|
||||
could_not_establish_secure_connection(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint not provided.",
|
||||
)
|
||||
.await?;
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
};
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
could_not_establish_secure_connection(
|
||||
let Some(fp) = mime_message.get_header(HeaderDef::SecureJoinFingerprint) else {
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth not encrypted.",
|
||||
)
|
||||
.await?;
|
||||
"Ignoring {step} message because fingerprint is not provided."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
let fingerprint: Fingerprint = fp.parse()?;
|
||||
if !encrypted_and_signed(context, mime_message, Some(&fingerprint)) {
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring {step} message because the message is not encrypted."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
if !verify_sender_by_fingerprint(context, &fingerprint, contact_id).await? {
|
||||
could_not_establish_secure_connection(
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
"Ignoring {step} message because of fingerprint mismatch."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
info!(context, "Fingerprint verified.",);
|
||||
// verify that the `Secure-Join-Auth:`-header matches the secret written to the QR code
|
||||
let Some(auth) = mime_message.get_header(HeaderDef::SecureJoinAuth) else {
|
||||
could_not_establish_secure_connection(
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth not provided.",
|
||||
)
|
||||
.await?;
|
||||
"Ignoring {step} message because of missing auth code."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
let Some(group_chat_id) = token::auth_chat_id(context, auth).await? else {
|
||||
could_not_establish_secure_connection(
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Auth invalid.",
|
||||
)
|
||||
.await?;
|
||||
"Ignoring {step} message because of invalid auth code."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
};
|
||||
|
||||
@@ -437,13 +421,10 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
)
|
||||
.await?;
|
||||
if !fingerprint_found {
|
||||
could_not_establish_secure_connection(
|
||||
warn!(
|
||||
context,
|
||||
contact_id,
|
||||
info_chat_id(context, contact_id).await?,
|
||||
"Fingerprint mismatch on inviter-side.",
|
||||
)
|
||||
.await?;
|
||||
"Ignoring {step} message because of the failure to find matching peerstate."
|
||||
);
|
||||
return Ok(HandshakeMessage::Ignore);
|
||||
}
|
||||
contact_id.regossip_keys(context).await?;
|
||||
@@ -635,6 +616,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?;
|
||||
|
||||
@@ -775,6 +760,7 @@ mod tests {
|
||||
CheckProtectionTimestamp,
|
||||
WrongAliceGossip,
|
||||
SecurejoinWaitTimeout,
|
||||
AliceIsBot,
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -797,6 +783,11 @@ mod tests {
|
||||
test_setup_contact_ex(SetupContactCase::SecurejoinWaitTimeout).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_setup_contact_alice_is_bot() {
|
||||
test_setup_contact_ex(SetupContactCase::AliceIsBot).await
|
||||
}
|
||||
|
||||
async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
@@ -805,13 +796,19 @@ mod tests {
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
.unwrap();
|
||||
alice
|
||||
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
bob.set_config(Config::VerifiedOneOnOneChats, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let alice_auto_submitted_hdr;
|
||||
match case {
|
||||
SetupContactCase::AliceIsBot => {
|
||||
alice.set_config_bool(Config::Bot, true).await.unwrap();
|
||||
alice_auto_submitted_hdr = "Auto-Submitted: auto-generated";
|
||||
}
|
||||
_ => alice_auto_submitted_hdr = "Auto-Submitted: auto-replied",
|
||||
};
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&alice, 0, None, None)
|
||||
@@ -841,11 +838,13 @@ 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());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
@@ -858,6 +857,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
@@ -903,6 +904,7 @@ mod tests {
|
||||
|
||||
// Check Bob sent the right message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
let mut msg = alice.parse_msg(&sent).await;
|
||||
let vc_request_with_auth_ts_sent = msg
|
||||
@@ -968,6 +970,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
assert_eq!(contact_bob.is_bot(), false);
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
@@ -1007,6 +1010,7 @@ mod tests {
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains(alice_auto_submitted_hdr));
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
@@ -1025,6 +1029,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
assert_eq!(contact_alice.is_bot(), case == SetupContactCase::AliceIsBot);
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
@@ -1239,6 +1244,7 @@ mod tests {
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
||||
// but it was only used by Alice in `vg-request-with-auth`.
|
||||
@@ -1252,6 +1258,7 @@ mod tests {
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
@@ -1285,6 +1292,7 @@ mod tests {
|
||||
}
|
||||
|
||||
// Check Bob sent the right handshake message.
|
||||
assert!(sent.payload.contains("Auto-Submitted: auto-replied"));
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
@@ -1317,6 +1325,10 @@ mod tests {
|
||||
msg.get_header(HeaderDef::SecureJoin).unwrap(),
|
||||
"vg-member-added"
|
||||
);
|
||||
// Formally this message is auto-submitted, but as the member addition is a result of an
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
{
|
||||
// Now Alice's chat with Bob should still be hidden, the verified message should
|
||||
@@ -1443,13 +1455,11 @@ First thread."#;
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
alice
|
||||
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
bob.set_config(Config::VerifiedOneOnOneChats, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
for t in [&alice, &bob] {
|
||||
t.set_config_bool(Config::VerifiedOneOnOneChats, true)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
|
||||
@@ -117,7 +117,7 @@ pub(crate) fn simplify(mut input: String, is_chat_message: bool) -> SimplifiedTe
|
||||
let lines = split_lines(&input);
|
||||
let (lines, is_forwarded) = skip_forward_header(&lines);
|
||||
|
||||
let (lines, mut top_quote) = remove_top_quote(lines);
|
||||
let (lines, mut top_quote) = remove_top_quote(lines, is_chat_message);
|
||||
let original_lines = &lines;
|
||||
let (lines, footer_lines) = remove_message_footer(lines);
|
||||
let footer = footer_lines.map(|footer_lines| render_message(footer_lines, false));
|
||||
@@ -210,7 +210,10 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
fn remove_top_quote<'a>(
|
||||
lines: &'a [&str],
|
||||
is_chat_message: bool,
|
||||
) -> (&'a [&'a str], Option<String>) {
|
||||
let mut first_quoted_line = 0;
|
||||
let mut last_quoted_line = None;
|
||||
let mut has_quoted_headline = false;
|
||||
@@ -220,7 +223,11 @@ fn remove_top_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
|
||||
first_quoted_line = l;
|
||||
}
|
||||
last_quoted_line = Some(l)
|
||||
} else if is_quoted_headline(line) && !has_quoted_headline && last_quoted_line.is_none() {
|
||||
} else if !is_chat_message
|
||||
&& is_quoted_headline(line)
|
||||
&& !has_quoted_headline
|
||||
&& last_quoted_line.is_none()
|
||||
{
|
||||
has_quoted_headline = true
|
||||
} else {
|
||||
/* non-quoting line found */
|
||||
@@ -396,17 +403,34 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_remove_top_quote() {
|
||||
let (lines, top_quote) = remove_top_quote(&["> first", "> second"]);
|
||||
let (lines, top_quote) = remove_top_quote(&["> first", "> second"], true);
|
||||
assert!(lines.is_empty());
|
||||
assert_eq!(top_quote.unwrap(), "first\nsecond");
|
||||
|
||||
let (lines, top_quote) = remove_top_quote(&["> first", "> second", "not a quote"]);
|
||||
let (lines, top_quote) = remove_top_quote(&["> first", "> second", "not a quote"], true);
|
||||
assert_eq!(lines, &["not a quote"]);
|
||||
assert_eq!(top_quote.unwrap(), "first\nsecond");
|
||||
|
||||
let (lines, top_quote) = remove_top_quote(&["not a quote", "> first", "> second"]);
|
||||
let (lines, top_quote) = remove_top_quote(&["not a quote", "> first", "> second"], true);
|
||||
assert_eq!(lines, &["not a quote", "> first", "> second"]);
|
||||
assert!(top_quote.is_none());
|
||||
|
||||
let (lines, top_quote) = remove_top_quote(
|
||||
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"],
|
||||
false,
|
||||
);
|
||||
assert_eq!(lines, &["not a quote"]);
|
||||
assert_eq!(top_quote.unwrap(), "quote");
|
||||
|
||||
let (lines, top_quote) = remove_top_quote(
|
||||
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"],
|
||||
true,
|
||||
);
|
||||
assert_eq!(
|
||||
lines,
|
||||
&["On 2024-08-28, Bob wrote:", "> quote", "not a quote"]
|
||||
);
|
||||
assert!(top_quote.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
279
src/smtp.rs
279
src/smtp.rs
@@ -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 async_smtp::{EmailAddress, SmtpTransport};
|
||||
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::prioritize_server_login_params;
|
||||
use crate::login_param::{ConfiguredLoginParam, ConfiguredServerLoginParam};
|
||||
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.
|
||||
@@ -96,251 +88,76 @@ impl Smtp {
|
||||
}
|
||||
|
||||
self.connectivity.set_connecting(context).await;
|
||||
let lp = LoginParam::load_configured_params(context).await?;
|
||||
let lp = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
self.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.smtp_password,
|
||||
&lp.socks5_config,
|
||||
&lp.addr,
|
||||
lp.provider.map_or(lp.socks5_config.is_some(), |provider| {
|
||||
provider.opt.strict_tls
|
||||
}),
|
||||
lp.strict_tls(),
|
||||
lp.oauth2,
|
||||
)
|
||||
.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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn connect(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
lp: &ServerLoginParam,
|
||||
login_params: &[ConfiguredServerLoginParam],
|
||||
password: &str,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
strict_tls: bool,
|
||||
oauth2: bool,
|
||||
) -> Result<()> {
|
||||
if self.is_connected() {
|
||||
warn!(context, "SMTP already connected.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if lp.server.is_empty() || lp.port == 0 {
|
||||
bail!("bad connection parameters");
|
||||
}
|
||||
|
||||
let from = EmailAddress::new(addr.to_string())
|
||||
.with_context(|| format!("invalid login address {addr}"))?;
|
||||
|
||||
.with_context(|| format!("Invalid address {addr:?}"))?;
|
||||
self.from = Some(from);
|
||||
|
||||
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?
|
||||
let login_params =
|
||||
prioritize_server_login_params(&context.sql, login_params, "smtp").await?;
|
||||
for lp in login_params {
|
||||
info!(context, "SMTP trying to connect to {}.", &lp.connection);
|
||||
let transport = match connect::connect_and_auth(
|
||||
context,
|
||||
socks5_config,
|
||||
strict_tls,
|
||||
lp.connection.clone(),
|
||||
oauth2,
|
||||
addr,
|
||||
&lp.user,
|
||||
password,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(transport) => transport,
|
||||
Err(err) => {
|
||||
warn!(context, "SMTP failed to connect: {err:#}.");
|
||||
continue;
|
||||
}
|
||||
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?,
|
||||
}
|
||||
};
|
||||
|
||||
// Authenticate.
|
||||
{
|
||||
let (creds, mechanism) = if lp.oauth2 {
|
||||
// oauth2
|
||||
let send_pw = &lp.password;
|
||||
let access_token = get_oauth2_access_token(context, addr, send_pw, false).await?;
|
||||
if access_token.is_none() {
|
||||
bail!("SMTP OAuth 2 error {}", addr);
|
||||
}
|
||||
let user = &lp.user;
|
||||
(
|
||||
smtp::authentication::Credentials::new(
|
||||
user.to_string(),
|
||||
access_token.unwrap_or_default(),
|
||||
),
|
||||
vec![smtp::authentication::Mechanism::Xoauth2],
|
||||
)
|
||||
} else {
|
||||
// plain
|
||||
let user = lp.user.clone();
|
||||
let pw = lp.password.clone();
|
||||
(
|
||||
smtp::authentication::Credentials::new(user, pw),
|
||||
vec![
|
||||
smtp::authentication::Mechanism::Plain,
|
||||
smtp::authentication::Mechanism::Login,
|
||||
],
|
||||
)
|
||||
};
|
||||
transport.try_login(&creds, &mechanism).await?;
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.last_success = Some(tools::Time::now());
|
||||
|
||||
context.emit_event(EventType::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
lp.user,
|
||||
)));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.transport = Some(transport);
|
||||
self.last_success = Some(tools::Time::now());
|
||||
|
||||
context.emit_event(EventType::SmtpConnected(format!(
|
||||
"SMTP-LOGIN as {} ok",
|
||||
lp.user,
|
||||
)));
|
||||
|
||||
Ok(())
|
||||
Err(format_err!("SMTP failed to connect"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,9 +611,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);
|
||||
}
|
||||
|
||||
304
src/smtp/connect.rs
Normal file
304
src/smtp/connect.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
//! SMTP connection establishment.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use async_smtp::{SmtpClient, SmtpTransport};
|
||||
use tokio::io::BufStream;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::login_param::{ConnectionCandidate, ConnectionSecurity};
|
||||
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::{
|
||||
connect_tcp_inner, connect_tls_inner, run_connection_attempts, update_connection_history,
|
||||
};
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
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"]
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn connect_and_auth(
|
||||
context: &Context,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
oauth2: bool,
|
||||
addr: &str,
|
||||
user: &str,
|
||||
password: &str,
|
||||
) -> Result<SmtpTransport<Box<dyn SessionBufStream>>> {
|
||||
let session_stream =
|
||||
connect_stream(context, socks5_config.clone(), strict_tls, candidate).await?;
|
||||
let client = async_smtp::SmtpClient::new()
|
||||
.smtp_utf8(true)
|
||||
.without_greeting();
|
||||
let mut transport = SmtpTransport::new(client, session_stream).await?;
|
||||
|
||||
// Authenticate.
|
||||
let (creds, mechanism) = if oauth2 {
|
||||
// oauth2
|
||||
let access_token = get_oauth2_access_token(context, addr, password, false).await?;
|
||||
if access_token.is_none() {
|
||||
bail!("SMTP OAuth 2 error {}", addr);
|
||||
}
|
||||
(
|
||||
async_smtp::authentication::Credentials::new(
|
||||
user.to_string(),
|
||||
access_token.unwrap_or_default(),
|
||||
),
|
||||
vec![async_smtp::authentication::Mechanism::Xoauth2],
|
||||
)
|
||||
} else {
|
||||
// plain
|
||||
(
|
||||
async_smtp::authentication::Credentials::new(user.to_string(), password.to_string()),
|
||||
vec![
|
||||
async_smtp::authentication::Mechanism::Plain,
|
||||
async_smtp::authentication::Mechanism::Login,
|
||||
],
|
||||
)
|
||||
};
|
||||
transport.try_login(&creds, &mechanism).await?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
async fn connection_attempt(
|
||||
context: Context,
|
||||
host: String,
|
||||
security: ConnectionSecurity,
|
||||
resolved_addr: SocketAddr,
|
||||
strict_tls: bool,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let context = &context;
|
||||
let host = &host;
|
||||
info!(
|
||||
context,
|
||||
"Attempting SMTP connection to {host} ({resolved_addr})."
|
||||
);
|
||||
let res = match security {
|
||||
ConnectionSecurity::Tls => connect_secure(resolved_addr, host, strict_tls).await,
|
||||
ConnectionSecurity::Starttls => connect_starttls(resolved_addr, host, strict_tls).await,
|
||||
ConnectionSecurity::Plain => connect_insecure(resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(stream) => {
|
||||
let ip_addr = resolved_addr.ip().to_string();
|
||||
let port = resolved_addr.port();
|
||||
|
||||
let save_cache = match security {
|
||||
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
|
||||
ConnectionSecurity::Plain => false,
|
||||
};
|
||||
if save_cache {
|
||||
update_connect_timestamp(context, host, &ip_addr).await?;
|
||||
}
|
||||
update_connection_history(context, "smtp", host, port, &ip_addr, time()).await?;
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
context,
|
||||
"Failed to connect to {host} ({resolved_addr}): {err:#}."
|
||||
);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
async fn connect_stream(
|
||||
context: &Context,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
strict_tls: bool,
|
||||
candidate: ConnectionCandidate,
|
||||
) -> Result<Box<dyn SessionBufStream>> {
|
||||
let host = &candidate.host;
|
||||
let port = candidate.port;
|
||||
let security = candidate.security;
|
||||
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
let stream = match security {
|
||||
ConnectionSecurity::Tls => {
|
||||
connect_secure_socks5(context, host, port, strict_tls, socks5_config.clone())
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Starttls => {
|
||||
connect_starttls_socks5(context, host, port, strict_tls, socks5_config.clone())
|
||||
.await?
|
||||
}
|
||||
ConnectionSecurity::Plain => {
|
||||
connect_insecure_socks5(context, host, port, socks5_config.clone()).await?
|
||||
}
|
||||
};
|
||||
Ok(stream)
|
||||
} else {
|
||||
let load_cache = match security {
|
||||
ConnectionSecurity::Tls | ConnectionSecurity::Starttls => strict_tls,
|
||||
ConnectionSecurity::Plain => false,
|
||||
};
|
||||
|
||||
let connection_futures = lookup_host_with_cache(context, host, port, "smtp", load_cache)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|resolved_addr| {
|
||||
let context = context.clone();
|
||||
let host = host.to_string();
|
||||
connection_attempt(context, host, security, resolved_addr, strict_tls)
|
||||
});
|
||||
run_connection_attempts(connection_futures).await
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
52
src/socks.rs
52
src/socks.rs
@@ -2,14 +2,12 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use fast_socks5::client::{Config, Socks5Stream};
|
||||
use fast_socks5::util::target_addr::ToTargetAddr;
|
||||
use fast_socks5::AuthenticationMethod;
|
||||
use fast_socks5::Socks5Command;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_io_timeout::TimeoutStream;
|
||||
|
||||
@@ -55,20 +53,6 @@ impl Socks5Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts SOCKS5 configuration into URL.
|
||||
pub fn to_url(&self) -> String {
|
||||
// `socks5h` means that hostname is resolved into address by the proxy
|
||||
// and DNS requests should not leak.
|
||||
let mut url = "socks5h://".to_string();
|
||||
if let Some((username, password)) = &self.user_password {
|
||||
let username_urlencoded = utf8_percent_encode(username, NON_ALPHANUMERIC).to_string();
|
||||
let password_urlencoded = utf8_percent_encode(password, NON_ALPHANUMERIC).to_string();
|
||||
url += &format!("{username_urlencoded}:{password_urlencoded}@");
|
||||
}
|
||||
url += &format!("{}:{}", self.host, self.port);
|
||||
url
|
||||
}
|
||||
|
||||
/// If `load_dns_cache` is true, loads cached DNS resolution results.
|
||||
/// Use this only if the connection is going to be protected with TLS checks.
|
||||
pub async fn connect(
|
||||
@@ -76,11 +60,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()
|
||||
{
|
||||
@@ -117,35 +99,3 @@ impl fmt::Display for Socks5Config {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_socks5h_url() {
|
||||
let config = Socks5Config {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 9050,
|
||||
user_password: None,
|
||||
};
|
||||
assert_eq!(config.to_url(), "socks5h://127.0.0.1:9050");
|
||||
|
||||
let config = Socks5Config {
|
||||
host: "example.org".to_string(),
|
||||
port: 1080,
|
||||
user_password: Some(("root".to_string(), "toor".to_string())),
|
||||
};
|
||||
assert_eq!(config.to_url(), "socks5h://root:toor@example.org:1080");
|
||||
|
||||
let config = Socks5Config {
|
||||
host: "example.org".to_string(),
|
||||
port: 1080,
|
||||
user_password: Some(("root".to_string(), "foo/?\\@".to_string())),
|
||||
};
|
||||
assert_eq!(
|
||||
config.to_url(),
|
||||
"socks5h://root:foo%2F%3F%5C%40@example.org:1080"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
115
src/sql.rs
115
src/sql.rs
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Migrations module.
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
@@ -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";
|
||||
@@ -907,15 +908,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 111 {
|
||||
// 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.
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE msgs ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0",
|
||||
111,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 111 {
|
||||
sql.execute_migration(
|
||||
@@ -950,6 +942,34 @@ 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 mut migration_version: i32 = 115;
|
||||
|
||||
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.
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE msgs ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0",
|
||||
migration_version,
|
||||
)
|
||||
.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)
|
||||
@@ -959,13 +979,11 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
let created_db = if exists_before_update {
|
||||
""
|
||||
} else {
|
||||
"Created new database; "
|
||||
"Created new database. "
|
||||
};
|
||||
info!(
|
||||
context,
|
||||
"{}[migration] v{}-v{}", created_db, dbversion, new_version
|
||||
);
|
||||
info!(context, "{}Migration done from v{}.", created_db, dbversion);
|
||||
}
|
||||
info!(context, "Database version: v{new_version}.");
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
@@ -998,6 +1016,13 @@ impl Sql {
|
||||
|
||||
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
|
||||
self.transaction(move |transaction| {
|
||||
let curr_version: String = transaction.query_row(
|
||||
"SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
|
||||
("0", VERSION_CFG),
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
let curr_version: i32 = curr_version.parse()?;
|
||||
ensure!(curr_version < version, "Db version must be increased");
|
||||
Self::set_db_version_trans(transaction, version)?;
|
||||
transaction.execute_batch(query)?;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user