mirror of
https://github.com/chatmail/core.git
synced 2026-04-02 13:32:11 +03:00
Compare commits
334 Commits
debug-iroh
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4ebb91712 | ||
|
|
eb3c1b3c25 | ||
|
|
fcf3dbbad4 | ||
|
|
d344cc3bdd | ||
|
|
2dd85afdc2 | ||
|
|
cdeca9ed9d | ||
|
|
495337743a | ||
|
|
775edab7b1 | ||
|
|
fe9fa17005 | ||
|
|
0d0f556f21 | ||
|
|
0e365395bf | ||
|
|
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 | ||
|
|
6607b7fd62 | ||
|
|
8d862b5ad3 | ||
|
|
d40ec88b94 | ||
|
|
a82eb7def6 | ||
|
|
92e8b80da8 | ||
|
|
76a84ec9b1 | ||
|
|
7109692791 | ||
|
|
7ad3c70b68 | ||
|
|
0b20f69959 | ||
|
|
be0ebc7847 | ||
|
|
b5e2ded47a | ||
|
|
8953c2a7de | ||
|
|
13f58e0ca5 | ||
|
|
f436e915d3 | ||
|
|
72bfae9448 | ||
|
|
6aaed3b524 | ||
|
|
501f41fca1 | ||
|
|
06d80e5da3 | ||
|
|
8ddc05923b | ||
|
|
9cbc9bf2bc | ||
|
|
5489b49cc1 | ||
|
|
f6f4ccc6ea | ||
|
|
a5d14b377d | ||
|
|
3b91815240 | ||
|
|
aa30afbeda | ||
|
|
bdc2c8f456 | ||
|
|
37831f82a4 | ||
|
|
4049d3451a | ||
|
|
6614864d78 | ||
|
|
b771311593 | ||
|
|
78fe2beefb | ||
|
|
6a3902d90d | ||
|
|
d412887bf4 | ||
|
|
9c2526bbdd | ||
|
|
889b947792 | ||
|
|
0a0e7156e0 | ||
|
|
24a06d175e | ||
|
|
980bab3040 | ||
|
|
b6dceb4271 | ||
|
|
87a57cd63b | ||
|
|
25b8a482bc | ||
|
|
d7dd563df4 | ||
|
|
6d720b793d | ||
|
|
6cc3e0a19a | ||
|
|
380116d107 | ||
|
|
216b295f52 | ||
|
|
388980ed6c | ||
|
|
2a2983ace0 | ||
|
|
a7f56e164e | ||
|
|
db4183596c | ||
|
|
2b06e672de | ||
|
|
e596664753 | ||
|
|
79d1c96db4 | ||
|
|
cc7c235556 | ||
|
|
56960882ce | ||
|
|
b11c2c6cc5 | ||
|
|
12e0a1962d | ||
|
|
f379bea669 | ||
|
|
bf674151cc | ||
|
|
c11cb5fb3e | ||
|
|
941208cc64 | ||
|
|
9f3cbdc873 | ||
|
|
90c30879b1 | ||
|
|
0ca1318118 | ||
|
|
0be639b244 | ||
|
|
48b4cfc247 | ||
|
|
a4037b8278 | ||
|
|
30405056e3 | ||
|
|
0fbab7147a | ||
|
|
de57ef5ac7 | ||
|
|
f48a047fe0 | ||
|
|
8ba08432c5 | ||
|
|
bf34bd3a62 | ||
|
|
21845ca5ea | ||
|
|
768ef772bb | ||
|
|
69842c18f7 | ||
|
|
42a7cd3eea | ||
|
|
b7e5b906d1 | ||
|
|
ad271fac80 | ||
|
|
70ad323c9a | ||
|
|
27bf4c37a7 | ||
|
|
1cc31c1038 | ||
|
|
adb0dd43a7 | ||
|
|
d29538beb0 | ||
|
|
b99e4649a4 | ||
|
|
68daa3550e | ||
|
|
9d65282710 | ||
|
|
d8f3368b3c | ||
|
|
5755fe7bef | ||
|
|
4f071e3b31 | ||
|
|
f4dfc79808 | ||
|
|
518d5bc4c7 | ||
|
|
0e1f62a38d | ||
|
|
af4b59fe0a | ||
|
|
8c3c0484ed | ||
|
|
97828234dd | ||
|
|
20e64c71f8 | ||
|
|
2214d140c3 | ||
|
|
907d3efcd0 | ||
|
|
9573e02c32 | ||
|
|
8cb699290a | ||
|
|
31d7b4f9ce | ||
|
|
2e5ad3f3a0 | ||
|
|
5d3d5d23a1 | ||
|
|
469ff799ad | ||
|
|
18f2a09b35 | ||
|
|
81f6aec1a0 | ||
|
|
ff60605a7f | ||
|
|
7010e80336 | ||
|
|
5f790c1dbc | ||
|
|
8c5d8477fb | ||
|
|
10fe6929b0 | ||
|
|
6fc0000c8a | ||
|
|
e84a5589df | ||
|
|
e7d9ff12ec | ||
|
|
607f5959ab | ||
|
|
11546a1ce9 | ||
|
|
ee671836ca | ||
|
|
dd77d32446 | ||
|
|
b32fb05ab8 | ||
|
|
918d87dcb6 | ||
|
|
98ae05ee59 | ||
|
|
cff5c064a6 | ||
|
|
e9cef4b0ba | ||
|
|
7f2c8ff53d | ||
|
|
46d6b81058 | ||
|
|
6d59fb49aa | ||
|
|
97602f3fd7 | ||
|
|
f17987743e | ||
|
|
5767cce178 | ||
|
|
20a4bb1a88 | ||
|
|
578f29f215 | ||
|
|
6c9643e39e | ||
|
|
502ae7fd9f | ||
|
|
8cb527342a | ||
|
|
964c943dd9 | ||
|
|
a971ad1f85 | ||
|
|
e66b9de922 | ||
|
|
5db202169b | ||
|
|
b292b191ff | ||
|
|
450ff411ec | ||
|
|
8de92e54eb | ||
|
|
d0844c3e62 |
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"
|
||||
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.78.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.78.0
|
||||
rust: 1.80.1
|
||||
- os: windows-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.80.1
|
||||
- os: macos-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.80.1
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
@@ -125,9 +125,6 @@ jobs:
|
||||
- name: Tests
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
|
||||
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
|
||||
run: cargo nextest run --workspace
|
||||
|
||||
- name: Doc-Tests
|
||||
|
||||
2
.github/workflows/deltachat-rpc-server.yml
vendored
2
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -401,6 +401,6 @@ jobs:
|
||||
working-directory: deltachat-rpc-server/npm-package
|
||||
run: |
|
||||
ls -lah platform_package
|
||||
for platform in *.tgz; do npm publish --provenance "$platform"; done
|
||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
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
|
||||
|
||||
@@ -33,6 +33,6 @@ jobs:
|
||||
|
||||
- name: Publish
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-*
|
||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -50,4 +50,4 @@ result
|
||||
|
||||
# direnv
|
||||
.envrc
|
||||
.direnv
|
||||
.direnv
|
||||
|
||||
641
CHANGELOG.md
641
CHANGELOG.md
@@ -1,5 +1,620 @@
|
||||
# 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
|
||||
|
||||
- deltachat-jsonrpc: Add `get_chat_securejoin_qr_code()`.
|
||||
- api!(deltachat-rpc-client): make {Account,Chat}.get_qr_code() return no SVG
|
||||
This is a breaking change, old method is renamed into `get_qr_code_svg()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Prefer references to fully downloaded messages for chat assignment ([#5645](https://github.com/deltachat/deltachat-core-rust/pull/5645)).
|
||||
- Protect From name for verified chats and To names for encrypted chats ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
- Display vCard contact name in the message summary.
|
||||
- Case-insensitive search for non-ASCII messages ([#5052](https://github.com/deltachat/deltachat-core-rust/pull/5052)).
|
||||
- Remove subject prefix from ad-hoc group names ([#5385](https://github.com/deltachat/deltachat-core-rust/pull/5385)).
|
||||
- Replace "Unnamed group" with "👥📧" to avoid translation.
|
||||
- Sync `Config::MvboxMove` across devices ([#5680](https://github.com/deltachat/deltachat-core-rust/pull/5680)).
|
||||
- Don't reveal profile data to a not yet verified contact ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
- Don't reveal profile data in MDNs ([#5166](https://github.com/deltachat/deltachat-core-rust/pull/5166)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Fetch existing messages for bots as `InFresh` ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Keep tombstones for two days before deleting ([#3685](https://github.com/deltachat/deltachat-core-rust/pull/3685)).
|
||||
- Housekeeping: Delete MDNs and webxdc status updates for tombstones.
|
||||
- Delete user-deleted messages on the server even if they show up on IMAP later.
|
||||
- Do not send sync messages if bcc_self is disabled.
|
||||
- Don't generate Config sync messages for unconfigured accounts.
|
||||
- Do not require the Message to render MDN.
|
||||
|
||||
### CI
|
||||
|
||||
- Update Rust to 1.79.0.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Remove outdated documentation comment from `send_smtp_messages`.
|
||||
- Remove misleading configuration comment.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update curve25519-dalek 4.1.x and suppress 3.2.0 warning.
|
||||
- Update provider database.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Deduplicate dependency versions ([#5691](https://github.com/deltachat/deltachat-core-rust/pull/5691)).
|
||||
- Store public key instead of secret key for peer channels.
|
||||
|
||||
### Tests
|
||||
|
||||
- Image drafted as Viewtype::File is sent as is.
|
||||
- python: Set delete_server_after=1 ("delete immediately") for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- deltachat-rpc-client: Test that webxdc realtime data is not reordered on the sender.
|
||||
- python: Wait for bot's DC_EVENT_IMAP_INBOX_IDLE before sending messages to it ([#5699](https://github.com/deltachat/deltachat-core-rust/pull/5699)).
|
||||
|
||||
## [1.140.2] - 2024-06-07
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add set_draft_vcard(.., msg_id, contacts).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Remove group member locally even if send_msg() fails ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
- Revert member addition if the corresponding message couldn't be sent ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
- @deltachat/stdio-rpc-server: Make local non-symlinked installation possible by using absolute paths for local dev version ([#5679](https://github.com/deltachat/deltachat-core-rust/pull/5679)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump schemars from 0.8.19 to 0.8.21.
|
||||
- cargo: Bump backtrace from 0.3.71 to 0.3.72.
|
||||
|
||||
### Refactor
|
||||
|
||||
- @deltachat/stdio-rpc-server: Use old school require instead of the experimental json import ([#5628](https://github.com/deltachat/deltachat-core-rust/pull/5628)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Set fetch_existing_msgs for bots ([#4976](https://github.com/deltachat/deltachat-core-rust/pull/4976)).
|
||||
- Don't leave protected group if some member's key is missing ([#5508](https://github.com/deltachat/deltachat-core-rust/pull/5508)).
|
||||
|
||||
## [1.140.1] - 2024-06-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Retry sending MDNs on temporary error.
|
||||
- Set Config::IsChatmail in configure().
|
||||
- Do not miss new messages while expunging the folder.
|
||||
- Log messages with `info!` instead of `println!`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- imap: Document why CLOSE is faster than EXPUNGE.
|
||||
|
||||
### Refactor
|
||||
|
||||
- imap: Make select_folder() accept non-optional folder.
|
||||
- Improve SMTP logs and errors.
|
||||
- Remove unused `select_folder::Error` variants.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: reenable `log_cli`.
|
||||
|
||||
## [1.140.0] - 2024-06-04
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove limit on number of email recipients for chatmail clients ([#5598](https://github.com/deltachat/deltachat-core-rust/pull/5598)).
|
||||
- Add config option to enable iroh ([#5607](https://github.com/deltachat/deltachat-core-rust/pull/5607)).
|
||||
- Map `*.wav` to Viewtype::Audio ([#5633](https://github.com/deltachat/deltachat-core-rust/pull/5633)).
|
||||
- Add a db index for reactions by msg_id ([#5507](https://github.com/deltachat/deltachat-core-rust/pull/5507)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set Param::Bot for messages on the sender side as well ([#5615](https://github.com/deltachat/deltachat-core-rust/pull/5615)).
|
||||
- AEAP: Remove old peerstate verified_key instead of removing the whole peerstate ([#5535](https://github.com/deltachat/deltachat-core-rust/pull/5535)).
|
||||
- Allow creation of groups by outgoing messages without recipients.
|
||||
- Prefer `Chat-Group-ID` over references for new groups.
|
||||
- Do not fail to send images with wrong extensions.
|
||||
|
||||
### Build system
|
||||
|
||||
- Unpin OpenSSL version and update to OpenSSL 3.3.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Remove cargo-nextest bug workaround.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add vCard as supported standard.
|
||||
- Create_group() does not find chats, only creates them.
|
||||
- Fix a typo in test_partial_group_consistency().
|
||||
|
||||
### Refactor
|
||||
|
||||
- Factor create_adhoc_group() call out of create_group().
|
||||
- Put duplicate code into `lookup_chat_or_create_adhoc_group`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Fix logging of TestContext created using TestContext::new_alice().
|
||||
- Refactor `test_alias_*` into 8 separate tests.
|
||||
|
||||
## [1.139.6] - 2024-05-25
|
||||
|
||||
### Build system
|
||||
|
||||
- Update `iroh` to the git version.
|
||||
- nix: Add iroh-base output hash.
|
||||
- Upgrade iroh to 0.17.0.
|
||||
|
||||
### Fixes
|
||||
|
||||
- @deltachat/stdio-rpc-server: Do not set RUST_LOG to "info" by default.
|
||||
- Acquire write lock on iroh_channels before checking for subscribe_loop.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Fix python lint.
|
||||
- cargo-deny: Remove unused entry from deny.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Log IMAP connection type on connection failure.
|
||||
|
||||
### Tests
|
||||
|
||||
- Viewtype::File attachments are sent unchanged and preserve extensions.
|
||||
- deltachat-rpc-client: Add realtime channel tests.
|
||||
- deltachat-rpc-client: Regression test for double gossip subscription.
|
||||
|
||||
## [1.139.5] - 2024-05-23
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-ffi: Make WebXdcRealtimeData data usable in CFFI.
|
||||
- Add event channel overflow event.
|
||||
- deltachat-rpc-client: Add EventType.WEBXDC_REALTIME_DATA constant.
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_advertisement().
|
||||
- deltachat-rpc-client: Add Message.send_webxdc_realtime_data().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-repl: Add start-realtime and send-realtime commands.
|
||||
|
||||
### Fixes
|
||||
|
||||
- peer_channels: Connect to peers that advertise to you.
|
||||
- Don't recode images in `Viewtype::File` messages ([#5617](https://github.com/deltachat/deltachat-core-rust/pull/5617)).
|
||||
|
||||
### Tests
|
||||
|
||||
- peer_channels: Add test_parallel_connect().
|
||||
- "SecureJoin wait" state and info messages.
|
||||
|
||||
## [1.139.4] - 2024-05-21
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Scale up contact origins to OutgoingTo when sending a message.
|
||||
- Add import_vcard() ([#5202](https://github.com/deltachat/deltachat-core-rust/pull/5202)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not log warning if iroh relay metadata is NIL.
|
||||
- contact-tools: Parse_vcard: Support `\r\n` newlines.
|
||||
- Make_vcard: Add authname and key for ContactId::SELF.
|
||||
|
||||
### Other
|
||||
|
||||
- nix: Add nextest ([#5610](https://github.com/deltachat/deltachat-core-rust/pull/5610)).
|
||||
|
||||
## [1.139.3] - 2024-05-20
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] @deltachat/stdio-rpc-server: change api: don't search in path unless `options.takeVersionFromPATH` is set to `true`
|
||||
- @deltachat/stdio-rpc-server: remove `DELTA_CHAT_SKIP_PATH` environment variable
|
||||
- @deltachat/stdio-rpc-server: remove version check / search for dc rpc server in $PATH
|
||||
- @deltachat/stdio-rpc-server: remove `options.skipSearchInPath`
|
||||
- @deltachat/stdio-rpc-server: add `options.takeVersionFromPATH`
|
||||
- deltachat-rpc-client: Add Account.wait_for_incoming_msg().
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Replace env_logger with tracing_subscriber.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Ignore event channel overflows.
|
||||
- mimeparser: Take the last header of multiple ones with the same name.
|
||||
- Db migration version 59, it contained an sql syntax error.
|
||||
- Sql syntax error in db migration 27.
|
||||
- Log/print exit error of deltachat-rpc-server ([#5601](https://github.com/deltachat/deltachat-core-rust/pull/5601)).
|
||||
- @deltachat/stdio-rpc-server: set default options for `startDeltaChat`.
|
||||
- Always convert absolute paths to relative in accounts.toml.
|
||||
|
||||
### Refactor
|
||||
|
||||
- receive_imf: Do not check for ContactId::UNDEFINED.
|
||||
- receive_imf: Remove unnecessary check for is_mdn.
|
||||
- receive_imf: Only call create_or_lookup_group() with allow_creation=true.
|
||||
- Use let..else in create_or_lookup_group().
|
||||
- Stop trying to extract chat ID from Message-IDs.
|
||||
- Do not try to lookup group in create_or_lookup_group().
|
||||
|
||||
## [1.139.2] - 2024-05-18
|
||||
|
||||
### Build system
|
||||
|
||||
- Add repository URL to @deltachat/jsonrpc-client.
|
||||
|
||||
## [1.139.1] - 2024-05-18
|
||||
|
||||
### CI
|
||||
|
||||
- Set `--access public` when publishing to npm.
|
||||
|
||||
## [1.139.0] - 2024-05-18
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Ephemeral peer channels ([#5346](https://github.com/deltachat/deltachat-core-rust/pull/5346)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Save override sender displayname for outgoing messages.
|
||||
- Do not mark the message as seen if it has `location.kml`.
|
||||
- @deltachat/stdio-rpc-server: fix version check when deltachat-rpc-server is found in path ([#5579](https://github.com/deltachat/deltachat-core-rust/pull/5579)).
|
||||
- @deltachat/stdio-rpc-server: fix local desktop development ([#5583](https://github.com/deltachat/deltachat-core-rust/pull/5583)).
|
||||
- @deltachat/stdio-rpc-server: rename `shutdown` method to `close` and add `muteStdErr` option to mute the stderr output ([#5588](https://github.com/deltachat/deltachat-core-rust/pull/5588))
|
||||
- @deltachat/stdio-rpc-server: fix `convert_platform.py`: 32bit `i32` -> `ia32` ([#5589](https://github.com/deltachat/deltachat-core-rust/pull/5589))
|
||||
- @deltachat/stdio-rpc-server: fix example ([#5580](https://github.com/deltachat/deltachat-core-rust/pull/5580))
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-jsonrpc: Return vcard contact directly in MessageObject.
|
||||
- deltachat-jsonrpc: Add api `migrate_account` and `get_blob_dir` ([#5584](https://github.com/deltachat/deltachat-core-rust/pull/5584)).
|
||||
- deltachat-rpc-client: Add ViewType.VCARD constant.
|
||||
- deltachat-rpc-client: Add Contact.make_vcard().
|
||||
- deltachat-rpc-client: Add Chat.send_contact().
|
||||
|
||||
### CI
|
||||
|
||||
- Publish @deltachat/jsonrpc-client directly to npm.
|
||||
- Check that constants are always up-to-date.
|
||||
|
||||
### Build system
|
||||
|
||||
- nix: Add git-cliff to flake.
|
||||
- nix: Use rust-analyzer nightly
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Downgrade libc from 0.2.154 to 0.2.153.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: Test sending vCard.
|
||||
|
||||
## [1.138.5] - 2024-05-16
|
||||
|
||||
### API-Changes
|
||||
@@ -4165,3 +4780,29 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.138.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.2...v1.138.3
|
||||
[1.138.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.3...v1.138.4
|
||||
[1.138.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.4...v1.138.5
|
||||
[1.139.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.138.5...v1.139.0
|
||||
[1.139.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.0...v1.139.1
|
||||
[1.139.2]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.1...v1.139.2
|
||||
[1.139.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.2...v1.139.3
|
||||
[1.139.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.3...v1.139.4
|
||||
[1.139.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.4...v1.139.5
|
||||
[1.139.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.5...v1.139.6
|
||||
[1.140.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.139.6...v1.140.0
|
||||
[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
|
||||
|
||||
1330
Cargo.lock
generated
1330
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
107
Cargo.toml
107
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.138.5"
|
||||
version = "1.142.12"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -34,99 +34,83 @@ 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-channel = "2.2.1"
|
||||
async-broadcast = "0.7.1"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.9.7", 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 = "0.22"
|
||||
base64 = { workspace = true }
|
||||
brotli = { version = "6", default-features=false, features = ["std"] }
|
||||
chrono = { workspace = true }
|
||||
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" }
|
||||
escaper = "0.1"
|
||||
fast-socks5 = "0.9"
|
||||
fd-lock = "4"
|
||||
futures = "0.3"
|
||||
futures-lite = "2.3.0"
|
||||
futures = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "0.24"
|
||||
humansize = "2"
|
||||
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"}
|
||||
iroh-net = { git = "https://github.com/link2xt/iroh", branch="link2xt/keep-connection" }
|
||||
iroh-gossip = { git = "https://github.com/link2xt/iroh", branch="link2xt/keep-connection", features = ["net"] }
|
||||
quinn = "0.10.0"
|
||||
iroh-net = { version = "0.22.0", default-features = false }
|
||||
iroh-gossip = { version = "0.22.0", default-features = false, features = ["net"] }
|
||||
kamadak-exif = "0.5.3"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
libc = { workspace = true }
|
||||
mailparse = "0.15"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.11", default-features = false }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
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 = "0.8"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.11.27", features = ["json"] }
|
||||
reqwest = { version = "0.12.5", features = ["json"] }
|
||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
sanitize-filename = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1.13.2"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
tokio-util = "0.7.9"
|
||||
tokio-util = { workspace = true }
|
||||
toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Pin OpenSSL to 3.1 releases.
|
||||
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||
# 3.1 branch will be supported until 2025-03-14.
|
||||
openssl-src = "~300.1"
|
||||
tracing = "0.1.40"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
ansi_term = { workspace = true }
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
tempfile = { workspace = true }
|
||||
testdir = "0.9.0"
|
||||
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
@@ -172,10 +156,38 @@ 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"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.18.0"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
rusqlite = "0.31"
|
||||
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
|
||||
rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = "1.0"
|
||||
tempfile = "3.10.1"
|
||||
thiserror = "1"
|
||||
|
||||
# 1.38 is the latest version before `mio` dependency update
|
||||
# that broke compilation with Android NDK r23c and r24.
|
||||
# Version 1.39.0 cannot be compiled using these NDKs,
|
||||
# see issue <https://github.com/tokio-rs/tokio/issues/6748>
|
||||
# for details.
|
||||
tokio = "~1.38.1"
|
||||
|
||||
tokio-util = "0.7.11"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
@@ -185,3 +197,6 @@ vendored = [
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
]
|
||||
|
||||
[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
|
||||
```
|
||||
|
||||
@@ -12,7 +12,7 @@ anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true }
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
|
||||
@@ -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`
|
||||
@@ -155,7 +154,7 @@ pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\n[\t ]").unwrap());
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
@@ -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.
|
||||
@@ -643,10 +697,10 @@ END:VCARD
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_android_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
fn test_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
||||
// This one is Android-like.
|
||||
let vcard0 = "BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
@@ -656,13 +710,101 @@ PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
||||
|
||||
END:VCARD
|
||||
",
|
||||
";
|
||||
// This one is DOS-like.
|
||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
||||
for vcard in [vcard0, vcard1.as_str()] {
|
||||
let contacts = parse_vcard(vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
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, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
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.138.5"
|
||||
version = "1.142.12"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -14,21 +14,21 @@ name = "deltachat"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||
libc = "0.2"
|
||||
deltachat = { workspace = true, default-features = false }
|
||||
deltachat-jsonrpc = { workspace = true, optional = true }
|
||||
libc = { workspace = true }
|
||||
human-panic = { version = "2", default-features = false }
|
||||
num-traits = "0.2"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.37.0", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
once_cell = "1.18.0"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
num-traits = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
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,7 +420,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* and also recoded to a reasonable size.
|
||||
* - `e2ee_enabled` = 0=no end-to-end-encryption, 1=prefer end-to-end-encryption (default)
|
||||
* - `mdns_enabled` = 0=do not send or request read receipts,
|
||||
* 1=send and request read receipts (default)
|
||||
* 1=send and request read receipts
|
||||
* default=send and request read receipts, only send but not reuqest if `bot` is set
|
||||
* - `bcc_self` = 0=do not send a copy of outgoing messages to self (default),
|
||||
* 1=send a copy of outgoing messages to self.
|
||||
* Sending messages to self is needed for a proper multi-account setup,
|
||||
@@ -481,8 +482,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages,
|
||||
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
|
||||
* and does not cut large incoming text messages.
|
||||
* accepts contact requests automatically (calling dc_accept_chat() is not needed),
|
||||
* does not cut large incoming text messages,
|
||||
* handles existing messages the same way as new ones if `fetch_existing_msgs=1`.
|
||||
* - `last_msg_id` = database ID of the last message processed by the bot.
|
||||
* This ID and IDs below it are guaranteed not to be returned
|
||||
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
||||
@@ -493,8 +495,8 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* For most bots calling `dc_markseen_msgs()` is the
|
||||
* recommended way to update this value
|
||||
* even for self-sent messages.
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* - `fetch_existing_msgs` = 0=do not fetch existing messages on configure (default),
|
||||
* 1=fetch most recent existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
@@ -518,11 +520,19 @@ 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`.
|
||||
* These keys go to backups and allow easy per-account settings when using @ref dc_accounts_t,
|
||||
* however, are not handled by the core otherwise.
|
||||
* - `webxdc_realtime_enabled` = Whether the realtime APIs should be enabled.
|
||||
* 0 = WebXDC realtime API is disabled and behaves as noop (default).
|
||||
* 1 = WebXDC realtime API is enabled.
|
||||
*
|
||||
* If you want to retrieve a value, use dc_get_config().
|
||||
*
|
||||
@@ -2495,6 +2505,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
|
||||
#define DC_QR_ACCOUNT 250 // text1=domain
|
||||
#define DC_QR_BACKUP 251
|
||||
#define DC_QR_BACKUP2 252
|
||||
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
|
||||
#define DC_QR_ADDR 320 // id=contact
|
||||
#define DC_QR_TEXT 330 // text1=text
|
||||
@@ -2541,6 +2552,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* - DC_QR_BACKUP:
|
||||
* - DC_QR_BACKUP2:
|
||||
* ask the user if they want to set up a new device.
|
||||
* If so, pass the qr-code to dc_receive_backup().
|
||||
*
|
||||
@@ -6284,6 +6296,18 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
|
||||
/**
|
||||
* Data received over an ephemeral peer channel.
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 (int) + (char*) binary data.
|
||||
* length is returned as integer with dc_event_get_data2_int()
|
||||
* and binary data is returned as dc_event_get_data2_str().
|
||||
* Binary data must be passed to dc_str_unref() afterwards.
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_REALTIME_DATA 2150
|
||||
|
||||
/**
|
||||
* Tells that the Background fetch was completed (or timed out).
|
||||
*
|
||||
@@ -6312,6 +6336,14 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
#define DC_EVENT_CHATLIST_ITEM_CHANGED 2301
|
||||
|
||||
|
||||
/**
|
||||
* Inform that some events have been skipped due to event channel overflow.
|
||||
*
|
||||
* @param data1 (int) number of events that have been skipped
|
||||
*/
|
||||
#define DC_EVENT_CHANNEL_OVERFLOW 2400
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
@@ -6619,12 +6651,16 @@ 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."
|
||||
///
|
||||
/// 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
|
||||
#define DC_STR_READRCPT_MAILBODY 32
|
||||
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
@@ -7343,7 +7379,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "Contact"
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
/**
|
||||
|
||||
@@ -566,6 +566,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::AccountsBackgroundFetchDone => 2200,
|
||||
EventType::ChatlistChanged => 2300,
|
||||
EventType::ChatlistItemChanged { .. } => 2301,
|
||||
EventType::EventChannelOverflow { .. } => 2400,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,6 +625,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
EventType::EventChannelOverflow { n } => *n as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,13 +660,13 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::WebxdcRealtimeData { .. }
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ConfigSynced { .. } => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::EventChannelOverflow { .. } => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
@@ -679,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
status_update_serial,
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
EventType::WebxdcRealtimeData { data, .. } => data.len() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,12 +728,12 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::WebxdcRealtimeData { .. }
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged => ptr::null_mut(),
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
@@ -746,6 +749,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = key.to_string().to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::WebxdcRealtimeData { data, .. } => {
|
||||
let ptr = libc::malloc(data.len());
|
||||
libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len());
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4356,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();
|
||||
@@ -4410,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::Backup { .. } => None,
|
||||
Qr::Backup2 { .. } => None,
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Url { url } => Some(url),
|
||||
@@ -102,6 +103,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||
Qr::Account { .. } => LotState::QrAccount,
|
||||
Qr::Backup { .. } => LotState::QrBackup,
|
||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||
Qr::Addr { .. } => LotState::QrAddr,
|
||||
Qr::Url { .. } => LotState::QrUrl,
|
||||
@@ -127,6 +129,7 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::Backup { .. } => Default::default(),
|
||||
Qr::Backup2 { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
@@ -177,6 +180,8 @@ pub enum LotState {
|
||||
|
||||
QrBackup = 251,
|
||||
|
||||
QrBackup2 = 252,
|
||||
|
||||
/// text1=domain, text2=instance pattern
|
||||
QrWebrtcInstance = 260,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.138.5"
|
||||
version = "1.142.12"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -13,30 +13,30 @@ path = "src/webserver.rs"
|
||||
required-features = ["webserver"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.19"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.10.1"
|
||||
log = "0.4"
|
||||
async-channel = { version = "2.2.1" }
|
||||
futures = { version = "0.3.30" }
|
||||
serde_json = "1"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true }
|
||||
deltachat-contact-tools = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
schemars = "0.8.21"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
log = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||
tokio = { version = "1.37.0" }
|
||||
sanitize-filename = "0.5"
|
||||
tokio = { workspace = true }
|
||||
sanitize-filename = { workspace = true }
|
||||
walkdir = "2.5.0"
|
||||
base64 = "0.22"
|
||||
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 = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -707,7 +707,22 @@ impl CommandApi {
|
||||
ChatId::new(chat_id).get_encryption_info(&ctx).await
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
/// Get QR code text that will offer a [SecureJoin](https://securejoin.delta.chat/) invitation.
|
||||
///
|
||||
/// If `chat_id` is a group chat ID, SecureJoin QR code for the group is returned.
|
||||
/// If `chat_id` is unset, setup contact QR code is returned.
|
||||
async fn get_chat_securejoin_qr_code(
|
||||
&self,
|
||||
account_id: u32,
|
||||
chat_id: Option<u32>,
|
||||
) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = chat_id.map(ChatId::new);
|
||||
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
||||
Ok(qr)
|
||||
}
|
||||
|
||||
/// Get QR code (text and SVG) that will offer a Setup-Contact or Verified-Group invitation.
|
||||
/// The QR code is compatible to the OPENPGP4FPR format
|
||||
/// so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
///
|
||||
@@ -729,10 +744,9 @@ impl CommandApi {
|
||||
) -> Result<(String, String)> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat = chat_id.map(ChatId::new);
|
||||
Ok((
|
||||
securejoin::get_securejoin_qr(&ctx, chat).await?,
|
||||
get_securejoin_qr_svg(&ctx, chat).await?,
|
||||
))
|
||||
let qr = securejoin::get_securejoin_qr(&ctx, chat).await?;
|
||||
let svg = get_securejoin_qr_svg(&ctx, chat).await?;
|
||||
Ok((qr, svg))
|
||||
}
|
||||
|
||||
/// Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
@@ -1447,7 +1461,7 @@ impl CommandApi {
|
||||
|
||||
/// Parses a vCard file located at the given path. Returns contacts in their original order.
|
||||
async fn parse_vcard(&self, path: String) -> Result<Vec<VcardContact>> {
|
||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||
let vcard = fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat_contact_tools::parse_vcard(vcard)
|
||||
.into_iter()
|
||||
@@ -1455,6 +1469,20 @@ impl CommandApi {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Imports contacts from a vCard file located at the given path.
|
||||
///
|
||||
/// Returns the ids of created/modified contacts in the order they appear in the vCard.
|
||||
async fn import_vcard(&self, account_id: u32, path: String) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let vcard = tokio::fs::read(Path::new(&path)).await?;
|
||||
let vcard = str::from_utf8(&vcard)?;
|
||||
Ok(deltachat::contact::import_vcard(&ctx, vcard)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|c| c.to_u32())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns a vCard containing contacts with the given ids.
|
||||
async fn make_vcard(&self, account_id: u32, contacts: Vec<u32>) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1462,6 +1490,20 @@ impl CommandApi {
|
||||
deltachat::contact::make_vcard(&ctx, &contacts).await
|
||||
}
|
||||
|
||||
/// Sets vCard containing the given contacts to the message draft.
|
||||
async fn set_draft_vcard(
|
||||
&self,
|
||||
account_id: u32,
|
||||
msg_id: u32,
|
||||
contacts: Vec<u32>,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contacts: Vec<_> = contacts.iter().map(|&c| ContactId::new(c)).collect();
|
||||
let mut msg = Message::load_from_db(&ctx, MsgId::new(msg_id)).await?;
|
||||
msg.make_vcard(&ctx, &contacts).await?;
|
||||
msg.get_chat_id().set_draft(&ctx, Some(&mut msg)).await
|
||||
}
|
||||
|
||||
// ---------------------------------------------
|
||||
// chat
|
||||
// ---------------------------------------------
|
||||
@@ -1630,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
|
||||
@@ -1649,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,
|
||||
|
||||
@@ -263,6 +263,9 @@ pub enum EventType {
|
||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatlistItemChanged { chat_id: Option<u32> },
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow { n: u64 },
|
||||
}
|
||||
|
||||
impl From<CoreEventType> for EventType {
|
||||
@@ -378,6 +381,7 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.map(|id| id.to_u32()),
|
||||
},
|
||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ pub enum QrObject {
|
||||
Backup {
|
||||
ticket: String,
|
||||
},
|
||||
Backup2 {
|
||||
auth_token: String,
|
||||
|
||||
node_addr: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
@@ -132,6 +137,14 @@ impl From<Qr> for QrObject {
|
||||
Qr::Backup { ticket } => QrObject::Backup {
|
||||
ticket: ticket.to_string(),
|
||||
},
|
||||
Qr::Backup2 {
|
||||
ref node_addr,
|
||||
auth_token,
|
||||
} => QrObject::Backup2 {
|
||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
||||
|
||||
auth_token,
|
||||
},
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
|
||||
@@ -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",
|
||||
@@ -32,6 +32,10 @@
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
@@ -54,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.138.5"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.138.5"
|
||||
version = "1.142.12"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
ansi_term = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { workspace = true, features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.31"
|
||||
log = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ use rustyline::{
|
||||
};
|
||||
use tokio::fs;
|
||||
use tokio::runtime::Handle;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
mod cmdline;
|
||||
use self::cmdline::*;
|
||||
@@ -151,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",
|
||||
@@ -162,7 +163,6 @@ const IMEX_COMMANDS: [&str; 14] = [
|
||||
"receive-backup",
|
||||
"export-keys",
|
||||
"import-keys",
|
||||
"export-setup",
|
||||
"poke",
|
||||
"reset",
|
||||
"stop",
|
||||
@@ -483,9 +483,10 @@ async fn handle_cmd(
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Error> {
|
||||
pretty_env_logger::formatted_timed_builder()
|
||||
.parse_default_env()
|
||||
.filter_module("deltachat_repl", log::LevelFilter::Info)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = std::env::args().collect();
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.138.5"
|
||||
version = "1.142.12"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -250,12 +250,16 @@ class Account:
|
||||
"""
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
def get_qr_code(self) -> str:
|
||||
"""Get Setup-Contact QR Code text.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
This data needs to be transferred to another Delta Chat account
|
||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||
"""
|
||||
return self._rpc.get_chat_securejoin_qr_code(self.id, None)
|
||||
|
||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
||||
"""Get Setup-Contact QR code text and SVG."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
@@ -297,6 +301,12 @@ class Account:
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
|
||||
Consumes all events before the next incoming message event."""
|
||||
return self.get_message_by_id(self.wait_for_incoming_msg_event().msg_id)
|
||||
|
||||
def wait_for_securejoin_inviter_success(self):
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from dataclasses import dataclass
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
@@ -95,7 +96,11 @@ class Chat:
|
||||
"""Return encryption info for this chat."""
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
def get_qr_code(self) -> tuple[str, str]:
|
||||
def get_qr_code(self) -> str:
|
||||
"""Get Join-Group QR code text."""
|
||||
return self._rpc.get_chat_securejoin_qr_code(self.account.id, self.id)
|
||||
|
||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
@@ -265,3 +270,11 @@ class Chat:
|
||||
location["message"] = Message(self.account, location.msg_id)
|
||||
locations.append(location)
|
||||
return locations
|
||||
|
||||
def send_contact(self, contact: Contact):
|
||||
"""Send contact to the chat."""
|
||||
vcard = contact.make_vcard()
|
||||
with NamedTemporaryFile(suffix=".vcard") as f:
|
||||
f.write(vcard.encode())
|
||||
f.flush()
|
||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
||||
|
||||
@@ -115,6 +115,7 @@ class ViewType(str, Enum):
|
||||
FILE = "File"
|
||||
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||
WEBXDC = "Webxdc"
|
||||
VCARD = "Vcard"
|
||||
|
||||
|
||||
class SystemMessageType(str, Enum):
|
||||
|
||||
@@ -60,3 +60,6 @@ class Contact:
|
||||
self.account,
|
||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
return self._rpc.make_vcard(self.account.id, [self.id])
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from ._utils import AttrDict
|
||||
from ._utils import AttrDict, futuremethod
|
||||
from .const import EventType
|
||||
from .contact import Contact
|
||||
|
||||
@@ -70,3 +70,11 @@ class Message:
|
||||
event = self.account.wait_for_event()
|
||||
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
|
||||
break
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_advertisement(self):
|
||||
yield self._rpc.send_webxdc_realtime_advertisement.future(self.account.id, self.id)
|
||||
|
||||
@futuremethod
|
||||
def send_webxdc_realtime_data(self, data) -> None:
|
||||
yield self._rpc.send_webxdc_realtime_data.future(self.account.id, self.id, list(data))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -177,7 +177,7 @@ class Rpc:
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
event = event["event"]
|
||||
print("account_id=%d got an event %s" % (account_id, event), file=sys.stderr)
|
||||
logging.debug("account_id=%d got an event %s", account_id, event)
|
||||
queue.put(event)
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
|
||||
@@ -126,8 +126,7 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
|
||||
alice.set_config("download_limit", "1")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
bob.get_chat_by_id(chat_id).send_message(
|
||||
@@ -135,13 +134,15 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
|
||||
)
|
||||
|
||||
msg_id = alice.wait_for_incoming_msg_event().msg_id
|
||||
|
||||
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
|
||||
message = alice.wait_for_incoming_msg()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.download_state == const.DownloadState.AVAILABLE
|
||||
|
||||
alice.clear_all_events()
|
||||
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
|
||||
alice._rpc.download_full_message(alice.id, msg_id)
|
||||
|
||||
snapshot = message.get_snapshot()
|
||||
chat_id = snapshot.chat_id
|
||||
alice._rpc.download_full_message(alice.id, message.id)
|
||||
|
||||
wait_for_chatlist_specific_item(alice, chat_id)
|
||||
|
||||
@@ -177,8 +178,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
|
||||
alice_chat_bob.send_text("hello")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
msg = bob.get_message_by_id(event.msg_id)
|
||||
msg = bob.wait_for_incoming_msg()
|
||||
bob_chat_id = msg.get_snapshot().chat_id
|
||||
msg.get_snapshot().chat.accept()
|
||||
|
||||
@@ -189,8 +189,7 @@ def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
|
||||
# make sure alice_second_device already received the message
|
||||
alice_second_device.wait_for_incoming_msg_event()
|
||||
|
||||
event = alice.wait_for_incoming_msg_event()
|
||||
msg = alice.get_message_by_id(event.msg_id)
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
alice_second_device.clear_all_events()
|
||||
msg.mark_seen()
|
||||
|
||||
@@ -211,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()
|
||||
|
||||
210
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
210
deltachat-rpc-client/tests/test_iroh_webxdc.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Testing webxdc iroh connectivity
|
||||
|
||||
If you want to debug iroh at rust-trace/log level set
|
||||
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def path_to_webxdc(request):
|
||||
p = request.path.parent.parent.parent.joinpath("test-data/webxdc/chess.xdc")
|
||||
assert p.exists()
|
||||
return str(p)
|
||||
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
|
||||
def setup_realtime_webxdc(ac1, ac2, path_to_webxdc):
|
||||
assert ac1.get_config("webxdc_realtime_enabled") == "1"
|
||||
assert ac2.get_config("webxdc_realtime_enabled") == "1"
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "play"
|
||||
|
||||
# send iroh announcements simultaneously
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
return ac1_webxdc_msg, ac2_webxdc_msg
|
||||
|
||||
|
||||
def setup_thread_send_realtime_data(msg, data):
|
||||
def thread_run():
|
||||
for _i in range(10):
|
||||
msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
|
||||
def wait_receive_realtime_data(msg_data_list):
|
||||
account = msg_data_list[0][0].account
|
||||
msg_data_list = msg_data_list[:]
|
||||
|
||||
log(f"account {account.id}: waiting for realtime data {msg_data_list}")
|
||||
while msg_data_list:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
for i, (msg, data) in enumerate(msg_data_list):
|
||||
if msg.id == event.msg_id:
|
||||
assert list(data) == event.data
|
||||
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||
del msg_data_list[i]
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
# send iroh announcements sequentially
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(b"foo")
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == list(b"foo")
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10])])
|
||||
|
||||
|
||||
def test_two_parallel_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
ac1_webxdc_msg2, ac2_webxdc_msg2 = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, [10])
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg2, [20])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg, [30])
|
||||
setup_thread_send_realtime_data(ac2_webxdc_msg2, [40])
|
||||
|
||||
wait_receive_realtime_data([(ac1_webxdc_msg, [30]), (ac1_webxdc_msg2, [40])])
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, [10]), (ac2_webxdc_msg2, [20])])
|
||||
|
||||
|
||||
def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
"""Test that messages are received only once."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
|
||||
ac1_webxdc_msg = ac1_ac2_chat.send_message(text="webxdc", file=path_to_webxdc)
|
||||
|
||||
ac2_webxdc_msg = ac2.wait_for_incoming_msg()
|
||||
ac2_webxdc_msg.get_snapshot().chat.accept()
|
||||
assert ac2_webxdc_msg.get_snapshot().text == "webxdc"
|
||||
|
||||
# Issue a "send" call in parallel with sending advertisement.
|
||||
# Previously due to a bug this caused subscribing to the channel twice.
|
||||
ac2_webxdc_msg.send_webxdc_realtime_data.future(b"foobar")
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
|
||||
def thread_run():
|
||||
for i in range(10):
|
||||
data = str(i).encode()
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data(data)
|
||||
time.sleep(1)
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
n = int(bytes(event.data).decode())
|
||||
break
|
||||
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
break
|
||||
|
||||
|
||||
def test_no_reordering(acfactory, path_to_webxdc):
|
||||
"""Test that sending a lot of realtime messages does not result in reordering."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("webxdc_realtime_enabled", "1")
|
||||
ac2.set_config("webxdc_realtime_enabled", "1")
|
||||
|
||||
ac1_webxdc_msg, ac2_webxdc_msg = setup_realtime_webxdc(ac1, ac2, path_to_webxdc)
|
||||
|
||||
setup_thread_send_realtime_data(ac1_webxdc_msg, b"hello")
|
||||
wait_receive_realtime_data([(ac2_webxdc_msg, b"hello")])
|
||||
|
||||
for i in range(200):
|
||||
ac1_webxdc_msg.send_webxdc_realtime_data([i])
|
||||
|
||||
for i in range(200):
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA and bytes(event.data) != b"hello":
|
||||
if event.data[0] == i:
|
||||
break
|
||||
pytest.fail("Reordering detected")
|
||||
@@ -1,13 +1,14 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Chat, EventType, SpecialContactId
|
||||
|
||||
|
||||
def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
@@ -30,23 +31,52 @@ def test_qr_setup_contact(acfactory, tmp_path) -> None:
|
||||
bob2.export_self_keys(tmp_path)
|
||||
|
||||
logging.info("Bob imports a key")
|
||||
bob.import_self_keys(tmp_path / "private-key-default.asc")
|
||||
bob.import_self_keys(tmp_path)
|
||||
|
||||
assert bob.get_config("key_id") == "2"
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert not bob_contact_alice_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_setup_contact_svg(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
_, _, domain = alice.get_config("addr").rpartition("@")
|
||||
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
|
||||
# Test that email address is in SVG
|
||||
# when we have no display name.
|
||||
# Check only the domain name, because
|
||||
# long address may be split over multiple lines
|
||||
# and not matched.
|
||||
assert domain in svg
|
||||
|
||||
alice.set_config("displayname", "Alice")
|
||||
|
||||
# Test that display name is used
|
||||
# in SVG and no address is visible.
|
||||
_qr_code, svg = alice.get_qr_code_svg()
|
||||
assert domain not in svg
|
||||
assert "Alice" in svg
|
||||
|
||||
|
||||
@pytest.mark.parametrize("protect", [True, False])
|
||||
def test_qr_securejoin(acfactory, protect):
|
||||
def test_qr_securejoin(acfactory, protect, tmp_path):
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
# Setup second device for Alice
|
||||
# to test observing securejoin protocol.
|
||||
alice.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
logging.info("Alice creates a verified group")
|
||||
alice_chat = alice.create_group("Verified group", protect=protect)
|
||||
assert alice_chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Check that at least some of the handshake messages are deleted.
|
||||
@@ -74,6 +104,14 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
|
||||
assert bob_contact_alice_snapshot.is_verified
|
||||
|
||||
# Start second Alice device.
|
||||
# Alice observes securejoin protocol and verifies Bob on second device.
|
||||
alice2.start_io()
|
||||
alice2.wait_for_securejoin_inviter_success()
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob.get_config("addr"))
|
||||
alice2_contact_bob_snapshot = alice2_contact_bob.get_snapshot()
|
||||
assert alice2_contact_bob_snapshot.is_verified
|
||||
|
||||
|
||||
def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
@@ -91,7 +129,7 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
|
||||
alice_chat = alice.create_group("Verified group", protect=True)
|
||||
logging.info("Bob joins verified group")
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
@@ -106,7 +144,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
alice, bob, charlie = acfactory.get_online_accounts(3)
|
||||
|
||||
logging.info("Bob and Charlie setup contact with Alice")
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
|
||||
bob.secure_join(qr_code)
|
||||
charlie.secure_join(qr_code)
|
||||
@@ -168,13 +206,13 @@ def test_setup_contact_resetup(acfactory) -> None:
|
||||
"""Tests that setup contact works after Alice resets the device and changes the key."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
alice = acfactory.resetup_account(alice)
|
||||
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -188,7 +226,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -205,7 +243,7 @@ def test_verified_group_recovery(acfactory) -> None:
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -252,7 +290,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
|
||||
logging.info("ac2 joins verified group")
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -269,7 +307,7 @@ def test_verified_group_member_added_recovery(acfactory) -> None:
|
||||
ac2 = acfactory.resetup_account(ac2)
|
||||
|
||||
logging.info("ac2 reverifies with ac3")
|
||||
qr_code, _svg = ac3.get_qr_code()
|
||||
qr_code = ac3.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -336,7 +374,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
|
||||
|
||||
logging.info("ac3: verify with ac2")
|
||||
qr_code, _svg = ac2.get_qr_code()
|
||||
qr_code = ac2.get_qr_code()
|
||||
ac3.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_inviter_success()
|
||||
|
||||
@@ -346,7 +384,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
|
||||
logging.info("ac1: create verified group that ac2 fully joins")
|
||||
ch1 = ac1.create_group("Group", protect=True)
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
|
||||
@@ -359,7 +397,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
break
|
||||
|
||||
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
|
||||
qr_code, _svg = ch1.get_qr_code()
|
||||
qr_code = ch1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.remove()
|
||||
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
|
||||
@@ -381,7 +419,7 @@ def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
|
||||
break
|
||||
|
||||
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
|
||||
qr_code, _svg = vg.get_qr_code()
|
||||
qr_code = vg.get_qr_code()
|
||||
ac4.secure_join(qr_code)
|
||||
ac3.wait_for_securejoin_inviter_success()
|
||||
while 1:
|
||||
@@ -402,7 +440,7 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group("Group for joining", protect=True)
|
||||
qr_code, _svg = ac1_chat.get_qr_code()
|
||||
qr_code = ac1_chat.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -425,7 +463,7 @@ def test_aeap_flow_verified(acfactory):
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
assert chat.get_basic_snapshot().is_protected
|
||||
qr_code, _svg = chat.get_qr_code()
|
||||
qr_code = chat.get_qr_code()
|
||||
logging.info("ac2: start QR-code based join-group protocol")
|
||||
ac2.secure_join(qr_code)
|
||||
ac1.wait_for_securejoin_inviter_success()
|
||||
@@ -464,12 +502,12 @@ def test_gossip_verification(acfactory) -> None:
|
||||
alice, bob, carol = acfactory.get_online_accounts(3)
|
||||
|
||||
# Bob verifies Alice.
|
||||
qr_code, _svg = alice.get_qr_code()
|
||||
qr_code = alice.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Bob verifies Carol.
|
||||
qr_code, _svg = carol.get_qr_code()
|
||||
qr_code = carol.get_qr_code()
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -520,16 +558,16 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac3_chat = ac3.create_group("Verified group", protect=True)
|
||||
|
||||
# ac1 joins ac3 group.
|
||||
ac3_qr_code, _svg = ac3_chat.get_qr_code()
|
||||
ac3_qr_code = ac3_chat.get_qr_code()
|
||||
ac1.secure_join(ac3_qr_code)
|
||||
ac1.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
qr_code, _svg = ac1.get_qr_code()
|
||||
qr_code = ac1.get_qr_code()
|
||||
ac2.secure_join(qr_code)
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
@@ -589,7 +627,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
assert alice_chat.get_basic_snapshot().is_protected
|
||||
logging.info("Bob joins verified group")
|
||||
|
||||
qr_code, _svg = alice_chat.get_qr_code()
|
||||
qr_code = alice_chat.get_qr_code()
|
||||
bob_chat = bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import base64
|
||||
import concurrent.futures
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat_rpc_client import Contact, EventType, Message, events
|
||||
from deltachat_rpc_client.const import DownloadState, MessageState
|
||||
from deltachat_rpc_client.direct_imap import DirectImap
|
||||
@@ -68,6 +71,18 @@ def test_configure_starttls(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
domain = account.get_config("addr").rsplit("@")[-1]
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
|
||||
# This should fail TLS check.
|
||||
account.set_config("mail_server", ip_address)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.configure()
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -103,12 +118,12 @@ def test_account(acfactory) -> None:
|
||||
assert alice.get_chatlist(snapshot=True)
|
||||
assert alice.get_qr_code()
|
||||
assert alice.get_fresh_messages()
|
||||
assert alice.get_next_messages()
|
||||
|
||||
# Test sending empty message.
|
||||
assert len(bob.wait_next_messages()) == 0
|
||||
alice_chat_bob.send_text("")
|
||||
messages = bob.wait_next_messages()
|
||||
assert bob.get_next_messages() == messages
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = message.get_snapshot()
|
||||
@@ -613,3 +628,31 @@ def test_markseen_contact_request(acfactory, tmp_path):
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
def test_get_http_response(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
http_response = alice._rpc.get_http_response(alice.id, "https://example.org")
|
||||
assert http_response["mimetype"] == "text/html"
|
||||
assert b"<title>Example Domain</title>" in base64.b64decode((http_response["blob"] + "==").encode())
|
||||
|
||||
|
||||
def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert configured_certificate_checks
|
||||
|
||||
# 0 is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
# and configuration failed to use strict TLS checks
|
||||
# so it switched strict TLS checks off.
|
||||
#
|
||||
# New versions of Delta Chat are not disabling TLS checks
|
||||
# unless users explicitly disables them
|
||||
# or provider database says provider has invalid certificates.
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
15
deltachat-rpc-client/tests/test_vcard.py
Normal file
15
deltachat-rpc-client/tests/test_vcard.py
Normal file
@@ -0,0 +1,15 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_contact(alice_contact_charlie)
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.vcard_contact
|
||||
assert snapshot.vcard_contact.addr == "charlie@example.org"
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Testing webxdc iroh connectivity
|
||||
|
||||
If you want to debug iroh at rust-trace/log level set
|
||||
|
||||
RUST_LOG=iroh_net=trace,iroh_gossip=trace
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import random
|
||||
import itertools
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, SpecialContactId
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def path_to_webxdc():
|
||||
return "../test-data/webxdc/chess.xdc"
|
||||
|
||||
|
||||
def test_realtime_sequentially(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection sequentially."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping0")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping0"
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
# send iroh announcements sequentially
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1._rpc.send_webxdc_realtime_advertisement(ac1.id, ac1_webxdc_msg.id)
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2._rpc.send_webxdc_realtime_advertisement(ac2.id, ac2_webxdc_msg.id)
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
ac1._rpc.send_webxdc_realtime_data(ac1.id, ac1_webxdc_msg.id, [13, 15, 17])
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == [13, 15, 17]
|
||||
break
|
||||
|
||||
|
||||
def test_realtime_simultaneously(acfactory, path_to_webxdc):
|
||||
"""Test two peers trying to establish connection simultaneously."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.create_chat(ac2)
|
||||
ac2.create_chat(ac1)
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping0")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping0"
|
||||
|
||||
def log(msg):
|
||||
print()
|
||||
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
|
||||
print()
|
||||
|
||||
# share a webxdc app between ac1 and ac2
|
||||
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
|
||||
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
|
||||
snapshot = ac2_webxdc_msg.get_snapshot()
|
||||
assert snapshot.text == "play"
|
||||
|
||||
# send iroh announcements simultaneously
|
||||
log("sending ac1 -> ac2 realtime advertisement and additional message")
|
||||
ac1._rpc.send_webxdc_realtime_advertisement(ac1.id, ac1_webxdc_msg.id)
|
||||
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
|
||||
|
||||
log("sending ac2 -> ac1 realtime advertisement and additional message")
|
||||
ac2._rpc.send_webxdc_realtime_advertisement(ac2.id, ac2_webxdc_msg.id)
|
||||
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
|
||||
|
||||
# Ensure that advertisements have been received.
|
||||
|
||||
log("waiting for incoming message on ac2")
|
||||
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping1"
|
||||
|
||||
log("waiting for incoming message on ac1")
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "ping2"
|
||||
|
||||
log("sending realtime data ac1 -> ac2")
|
||||
ac1._rpc.send_webxdc_realtime_data(ac1.id, ac1_webxdc_msg.id, [13, 15, 17])
|
||||
|
||||
log("ac2: waiting for realtime data")
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert event.data == [13, 15, 17]
|
||||
break
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.138.5"
|
||||
version = "1.142.12"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -10,19 +10,18 @@ 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 = "1"
|
||||
env_logger = { version = "0.11.3" }
|
||||
futures-lite = "2.3.0"
|
||||
log = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.37.0", features = ["io-std"] }
|
||||
tokio-util = "0.7.9"
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
anyhow = { workspace = true }
|
||||
futures-lite = { workspace = true }
|
||||
log = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tokio = { workspace = true, features = ["io-std"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -7,7 +7,7 @@ This simplifies cross-compilation and even reduces binary size (no CFFI layer an
|
||||
|
||||
## Usage
|
||||
|
||||
> The **minimum** nodejs version for this package is `20.11`
|
||||
> The **minimum** nodejs version for this package is `16`
|
||||
|
||||
```
|
||||
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
|
||||
@@ -20,7 +20,9 @@ import { C } from "@deltachat/jsonrpc-client";
|
||||
async function main() {
|
||||
const dc = await startDeltaChat("deltachat-data");
|
||||
console.log(await dc.rpc.getSystemInfo());
|
||||
dc.close()
|
||||
}
|
||||
main()
|
||||
```
|
||||
|
||||
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
|
||||
@@ -44,12 +46,11 @@ references:
|
||||
When you import this package it searches for the rpc server in the following locations and order:
|
||||
|
||||
1. `DELTA_CHAT_RPC_SERVER` environment variable
|
||||
2. in PATH
|
||||
- unless `DELTA_CHAT_SKIP_PATH=1` is specified
|
||||
- searches in .cargo/bin directory first
|
||||
- but there an additional version check is performed
|
||||
2. use the PATH when `{takeVersionFromPATH: true}` is supplied in the options.
|
||||
3. prebuilds in npm packages
|
||||
|
||||
so by default it uses the prebuilds.
|
||||
|
||||
## How do you built this package in CI
|
||||
|
||||
- To build platform packages, run the `build_platform_package.py` script:
|
||||
|
||||
4
deltachat-rpc-server/npm-package/index.d.ts
vendored
4
deltachat-rpc-server/npm-package/index.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
export interface SearchOptions {
|
||||
/** whether to disable looking for deltachat-rpc-server inside of $PATH */
|
||||
skipSearchInPath: boolean;
|
||||
/** whether take deltachat-rpc-server inside of $PATH*/
|
||||
takeVersionFromPATH: boolean;
|
||||
|
||||
/** whether to disable the DELTA_CHAT_RPC_SERVER environment variable */
|
||||
disableEnvPath: boolean;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
//@ts-check
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { stat, readdir } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import { join, basename } from "node:path";
|
||||
import process from "node:process";
|
||||
import { promisify } from "node:util";
|
||||
import {
|
||||
ENV_VAR_NAME,
|
||||
PATH_EXECUTABLE_NAME,
|
||||
SKIP_SEARCH_IN_PATH,
|
||||
} from "./src/const.js";
|
||||
import { ENV_VAR_NAME, PATH_EXECUTABLE_NAME } from "./src/const.js";
|
||||
import {
|
||||
ENV_VAR_LOCATION_NOT_FOUND,
|
||||
FAILED_TO_START_SERVER_EXECUTABLE,
|
||||
@@ -17,9 +11,6 @@ import {
|
||||
NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR,
|
||||
} from "./src/errors.js";
|
||||
|
||||
// Because this is not compiled by typescript, esm needs this stuff (` with { type: "json" };`,
|
||||
// nodejs still complains about it being experimental, but deno also uses it, so treefit bets taht it will become standard)
|
||||
import package_json from "./package.json" with { type: "json" };
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
function findRPCServerInNodeModules() {
|
||||
@@ -31,7 +22,12 @@ function findRPCServerInNodeModules() {
|
||||
return resolve(package_name);
|
||||
} catch (error) {
|
||||
console.debug("findRpcServerInNodeModules", error);
|
||||
if (Object.keys(package_json.optionalDependencies).includes(package_name)) {
|
||||
const require = createRequire(import.meta.url);
|
||||
if (
|
||||
Object.keys(require("./package.json").optionalDependencies).includes(
|
||||
package_name
|
||||
)
|
||||
) {
|
||||
throw new Error(NPM_NOT_FOUND_SUPPORTED_PLATFORM_ERROR(package_name));
|
||||
} else {
|
||||
throw new Error(NPM_NOT_FOUND_UNSUPPORTED_PLATFORM_ERROR());
|
||||
@@ -39,38 +35,13 @@ function findRPCServerInNodeModules() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function getLocationInPath() {
|
||||
const exec = promisify(execFile);
|
||||
|
||||
if (os.platform() === "win32") {
|
||||
const { stdout: executable } = await exec("where", [PATH_EXECUTABLE_NAME], {
|
||||
shell: true,
|
||||
});
|
||||
return executable;
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout: executable } = await exec(
|
||||
"command",
|
||||
["-v", PATH_EXECUTABLE_NAME],
|
||||
{ shell: true }
|
||||
);
|
||||
return executable;
|
||||
} catch (error) {
|
||||
if (error.code > 0) return "";
|
||||
else throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import("./index").FnTypes.getRPCServerPath} */
|
||||
export async function getRPCServerPath(
|
||||
options = { skipSearchInPath: false, disableEnvPath: false }
|
||||
) {
|
||||
// @TODO: improve confusing naming of these options
|
||||
const { skipSearchInPath, disableEnvPath } = options;
|
||||
export async function getRPCServerPath(options = {}) {
|
||||
const { takeVersionFromPATH, disableEnvPath } = {
|
||||
takeVersionFromPATH: false,
|
||||
disableEnvPath: false,
|
||||
...options,
|
||||
};
|
||||
// 1. check if it is set as env var
|
||||
if (process.env[ENV_VAR_NAME] && !disableEnvPath) {
|
||||
try {
|
||||
@@ -85,35 +56,9 @@ export async function getRPCServerPath(
|
||||
return process.env[ENV_VAR_NAME];
|
||||
}
|
||||
|
||||
// 2. check if it can be found in PATH
|
||||
if (!process.env[SKIP_SEARCH_IN_PATH] && !skipSearchInPath) {
|
||||
const executable = await getLocationInPath();
|
||||
|
||||
// by just trying to execute it and then use "command -v deltachat-rpc-server" (unix) or "where deltachat-rpc-server" (windows) to get the path to the executable
|
||||
if (executable.length > 1) {
|
||||
// test if it is the right version
|
||||
try {
|
||||
// for some unknown reason it is in stderr and not in stdout
|
||||
const { stderr } = await promisify(execFile)(
|
||||
executable,
|
||||
["--version"],
|
||||
{ shell: true }
|
||||
);
|
||||
const version = stderr.slice(0, stderr.indexOf("\n"));
|
||||
if (package_json.version !== version) {
|
||||
throw new Error(
|
||||
`version mismatch: (npm package: ${package_json.version}) (installed ${PATH_EXECUTABLE_NAME} version: ${version})`
|
||||
);
|
||||
} else {
|
||||
return executable;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Found executable in PATH, but there was an error: " + error
|
||||
);
|
||||
console.error("So falling back to using prebuild...");
|
||||
}
|
||||
}
|
||||
// 2. check if PATH should be used
|
||||
if (takeVersionFromPATH) {
|
||||
return PATH_EXECUTABLE_NAME;
|
||||
}
|
||||
// 3. check for prebuilds
|
||||
|
||||
@@ -123,11 +68,11 @@ export async function getRPCServerPath(
|
||||
import { StdioDeltaChat } from "@deltachat/jsonrpc-client";
|
||||
|
||||
/** @type {import("./index").FnTypes.startDeltaChat} */
|
||||
export async function startDeltaChat(directory, options) {
|
||||
export async function startDeltaChat(directory, options = {}) {
|
||||
const pathToServerBinary = await getRPCServerPath(options);
|
||||
const server = spawn(pathToServerBinary, {
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG || "info",
|
||||
RUST_LOG: process.env.RUST_LOG,
|
||||
DC_ACCOUNTS_PATH: directory,
|
||||
},
|
||||
stdio: ["pipe", "pipe", options.muteStdErr ? "ignore" : "inherit"],
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.138.5"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -55,9 +55,10 @@ for (const { folder_name, package_name } of platform_package_names) {
|
||||
}
|
||||
|
||||
if (is_local) {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = 'file:../../deltachat-jsonrpc/typescript'
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] =
|
||||
`file:${join(expected_cwd, "/../../deltachat-jsonrpc/typescript")}`;
|
||||
} else {
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*"
|
||||
package_json.peerDependencies["@deltachat/jsonrpc-client"] = "*";
|
||||
}
|
||||
|
||||
await fs.writeFile("./package.json", JSON.stringify(package_json, null, 4));
|
||||
|
||||
@@ -2,5 +2,4 @@
|
||||
|
||||
export const PATH_EXECUTABLE_NAME = 'deltachat-rpc-server'
|
||||
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
export const SKIP_SEARCH_IN_PATH = "DELTA_CHAT_SKIP_PATH"
|
||||
export const ENV_VAR_NAME = "DELTA_CHAT_RPC_SERVER"
|
||||
@@ -11,7 +11,7 @@ use deltachat::constants::DC_VERSION_STR;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tracing_subscriber::{prelude::*, EnvFilter};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use yerpc::RpcServer as _;
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
@@ -29,6 +29,9 @@ async fn main() {
|
||||
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
if let Err(error) = &r {
|
||||
log::error!("Fatal error: {error:#}.")
|
||||
}
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
@@ -61,13 +64,13 @@ async fn main_impl() -> Result<()> {
|
||||
#[cfg(target_family = "unix")]
|
||||
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
|
||||
.with(EnvFilter::builder().from_env_lossy())
|
||||
.try_init()
|
||||
.ok();
|
||||
// Logs from `log` crate and traces from `tracing` crate
|
||||
// are configurable with `RUST_LOG` environment variable
|
||||
// and go to stderr to avoid interferring with JSON-RPC using stdout.
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
|
||||
15
deny.toml
15
deny.toml
@@ -15,6 +15,10 @@ ignore = [
|
||||
|
||||
# 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]
|
||||
@@ -45,12 +49,12 @@ skip = [
|
||||
{ name = "dlopen2", version = "0.4.1" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "0.10.2" },
|
||||
{ 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", version = "0.14.28" },
|
||||
@@ -62,8 +66,6 @@ skip = [
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pem", version = "1.1.1" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "proc-macro-error-attr", version = "0.4.12" },
|
||||
{ name = "proc-macro-error", version = "0.4.12" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "<0.3" },
|
||||
{ name = "rand_core", version = "<0.6" },
|
||||
@@ -73,6 +75,9 @@ skip = [
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ 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" },
|
||||
@@ -80,15 +85,18 @@ skip = [
|
||||
{ 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" },
|
||||
{ name = "windows_aarch64_msvc", version = "<0.52" },
|
||||
{ name = "windows-core", version = "<0.54.0" },
|
||||
@@ -102,6 +110,7 @@ skip = [
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
])
|
||||
cargo-deny
|
||||
rust-analyzer-nightly
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
];
|
||||
|
||||
2675
fuzz/Cargo.lock
generated
2675
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@ module.exports = {
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE: 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE: 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW: 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED: 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED: 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
@@ -66,6 +67,7 @@ module.exports = {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA: 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
@@ -126,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,
|
||||
|
||||
@@ -37,7 +37,9 @@ module.exports = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED'
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW'
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum C {
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_DOWNLOAD_UNDECIPHERABLE = 30,
|
||||
DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE = 2200,
|
||||
DC_EVENT_CHANNEL_OVERFLOW = 2400,
|
||||
DC_EVENT_CHATLIST_CHANGED = 2300,
|
||||
DC_EVENT_CHATLIST_ITEM_CHANGED = 2301,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
@@ -66,6 +67,7 @@ export enum C {
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_REALTIME_DATA = 2150,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
@@ -126,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,
|
||||
@@ -338,7 +341,9 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2111: 'DC_EVENT_CONFIG_SYNCED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
2150: 'DC_EVENT_WEBXDC_REALTIME_DATA',
|
||||
2200: 'DC_EVENT_ACCOUNTS_BACKGROUND_FETCH_DONE',
|
||||
2300: 'DC_EVENT_CHATLIST_CHANGED',
|
||||
2301: 'DC_EVENT_CHATLIST_ITEM_CHANGED',
|
||||
2400: 'DC_EVENT_CHANNEL_OVERFLOW',
|
||||
}
|
||||
|
||||
@@ -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.138.5"
|
||||
"version": "1.142.12"
|
||||
}
|
||||
|
||||
@@ -44,11 +44,6 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
botproc.fnmatch_lines(
|
||||
"""
|
||||
*ac_configure_completed*
|
||||
""",
|
||||
)
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.138.5"
|
||||
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"
|
||||
|
||||
@@ -552,6 +552,15 @@ class ACFactory:
|
||||
|
||||
bot_cfg = self.get_next_liveconfig()
|
||||
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
|
||||
self._acsetup.start_configure(bot_ac)
|
||||
self.wait_configured(bot_ac)
|
||||
bot_ac.start_io()
|
||||
# Wait for DC_EVENT_IMAP_INBOX_IDLE so that all emails appeared in the bot's Inbox later are
|
||||
# considered new and not existing ones, and thus processed by the bot.
|
||||
print(bot_ac._logid, "waiting for inbox IDLE to become ready")
|
||||
bot_ac._evtracker.wait_idle_inbox_ready()
|
||||
bot_ac.stop_io()
|
||||
self._acsetup._account2state[bot_ac] = self._acsetup.IDLEREADY
|
||||
|
||||
# Forget ac as it will be opened by the bot subprocess
|
||||
# but keep something in the list to not confuse account generation
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
@@ -675,3 +676,17 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
|
||||
def test_deleted_msgs_dont_reappear(acfactory):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
acfactory.bring_accounts_online()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.get_self_contact().create_chat()
|
||||
msg = chat.send_text("hello")
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
ac1.delete_messages([msg])
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSG_DELETED")
|
||||
ac1._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
time.sleep(5)
|
||||
assert len(chat.get_messages()) == 0
|
||||
|
||||
@@ -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-05-16
|
||||
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.78.0
|
||||
RUST_VERSION=1.80.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -149,7 +149,7 @@ def process_data(data, file):
|
||||
oauth2 = "Some(Oauth2Authorizer::" + camel(oauth2) + ")" if oauth2 != "" else "None"
|
||||
|
||||
provider = ""
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", ""))
|
||||
before_login_hint = cleanstr(data.get("before_login_hint", "") or "")
|
||||
after_login_hint = cleanstr(data.get("after_login_hint", ""))
|
||||
if (not has_imap and not has_smtp) or (has_imap and has_smtp):
|
||||
provider += (
|
||||
|
||||
@@ -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=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
|
||||
REV=05c1b2029da74718e4bdc3799a46e29c4f794dc7
|
||||
|
||||
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) {
|
||||
@@ -485,10 +498,6 @@ impl Config {
|
||||
|
||||
/// Read a configuration from the given file into memory.
|
||||
pub async fn from_file(file: PathBuf, writable: bool) -> Result<Self> {
|
||||
let dir = file
|
||||
.parent()
|
||||
.context("Cannot get config file directory")?
|
||||
.to_path_buf();
|
||||
let mut config = Self::new_nosync(file, writable).await?;
|
||||
let bytes = fs::read(&config.file)
|
||||
.await
|
||||
@@ -500,9 +509,13 @@ impl Config {
|
||||
// Convert them to relative paths.
|
||||
let mut modified = false;
|
||||
for account in &mut config.inner.accounts {
|
||||
if let Ok(new_dir) = account.dir.strip_prefix(&dir) {
|
||||
account.dir = new_dir.to_path_buf();
|
||||
modified = true;
|
||||
if account.dir.is_absolute() {
|
||||
if let Some(old_path_parent) = account.dir.parent() {
|
||||
if let Ok(new_path) = account.dir.strip_prefix(old_path_parent) {
|
||||
account.dir = new_path.to_path_buf();
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if modified && writable {
|
||||
|
||||
427
src/blob.rs
427
src/blob.rs
@@ -3,7 +3,7 @@
|
||||
use core::cmp::max;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::io::Cursor;
|
||||
use std::io::{Cursor, Seek};
|
||||
use std::iter::FusedIterator;
|
||||
use std::mem;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -12,6 +12,7 @@ use anyhow::{format_err, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use futures::StreamExt;
|
||||
use image::codecs::jpeg::JpegEncoder;
|
||||
use image::ImageReader;
|
||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||
use num_traits::FromPrimitive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
@@ -426,9 +427,25 @@ impl<'a> BlobObject<'a> {
|
||||
let mut no_exif = false;
|
||||
let no_exif_ref = &mut no_exif;
|
||||
let res = tokio::task::block_in_place(move || {
|
||||
let (nr_bytes, exif) = self.metadata()?;
|
||||
let mut file = std::fs::File::open(self.to_abs_path())?;
|
||||
let (nr_bytes, exif) = image_metadata(&file)?;
|
||||
*no_exif_ref = exif.is_none();
|
||||
let mut img = image::open(&blob_abs).context("image decode failure")?;
|
||||
// It's strange that BufReader modifies a file position while it takes a non-mut
|
||||
// reference. Ok, just rewind it.
|
||||
file.rewind()?;
|
||||
let imgreader = ImageReader::new(std::io::BufReader::new(&file)).with_guessed_format();
|
||||
let imgreader = match imgreader {
|
||||
Ok(ir) => ir,
|
||||
_ => {
|
||||
file.rewind()?;
|
||||
ImageReader::with_format(
|
||||
std::io::BufReader::new(&file),
|
||||
ImageFormat::from_path(&blob_abs)?,
|
||||
)
|
||||
}
|
||||
};
|
||||
let fmt = imgreader.format().context("No format??")?;
|
||||
let mut img = imgreader.decode().context("image decode failure")?;
|
||||
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
|
||||
let mut encoded = Vec::new();
|
||||
let mut changed_name = None;
|
||||
@@ -457,10 +474,9 @@ impl<'a> BlobObject<'a> {
|
||||
let exceeds_max_bytes = nr_bytes > max_bytes as u64;
|
||||
|
||||
let jpeg_quality = 75;
|
||||
let fmt = ImageFormat::from_path(&blob_abs);
|
||||
let ofmt = match fmt {
|
||||
Ok(ImageFormat::Png) if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
Ok(ImageFormat::Jpeg) => {
|
||||
ImageFormat::Png if !exceeds_max_bytes => ImageOutputFormat::Png,
|
||||
ImageFormat::Jpeg => {
|
||||
add_white_bg = false;
|
||||
ImageOutputFormat::Jpeg {
|
||||
quality: jpeg_quality,
|
||||
@@ -497,7 +513,7 @@ impl<'a> BlobObject<'a> {
|
||||
img_wh = max(img.width(), img.height());
|
||||
// PNGs and WebPs may be huge because of animation, which is lost by the `image`
|
||||
// crate when recoding, so don't scale them down.
|
||||
if matches!(fmt, Ok(ImageFormat::Jpeg)) || !encoded.is_empty() {
|
||||
if matches!(fmt, ImageFormat::Jpeg) || !encoded.is_empty() {
|
||||
img_wh = img_wh * 2 / 3;
|
||||
}
|
||||
}
|
||||
@@ -538,7 +554,7 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
if do_scale || exif.is_some() {
|
||||
// The file format is JPEG/PNG now, we may have to change the file extension
|
||||
if !matches!(fmt, Ok(ImageFormat::Jpeg))
|
||||
if !matches!(fmt, ImageFormat::Jpeg)
|
||||
&& matches!(ofmt, ImageOutputFormat::Jpeg { .. })
|
||||
{
|
||||
blob_abs = blob_abs.with_extension("jpg");
|
||||
@@ -575,15 +591,14 @@ impl<'a> BlobObject<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns image file size and Exif.
|
||||
pub fn metadata(&self) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let file = std::fs::File::open(self.to_abs_path())?;
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(&file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
/// Returns image file size and Exif.
|
||||
pub fn image_metadata(file: &std::fs::File) -> Result<(u64, Option<exif::Exif>)> {
|
||||
let len = file.metadata()?.len();
|
||||
let mut bufreader = std::io::BufReader::new(file);
|
||||
let exif = exif::Reader::new().read_from_container(&mut bufreader).ok();
|
||||
Ok((len, exif))
|
||||
}
|
||||
|
||||
fn exif_orientation(exif: &exif::Exif, context: &Context) -> i32 {
|
||||
@@ -1086,32 +1101,34 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_1() {
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1000,
|
||||
1000,
|
||||
0,
|
||||
1000,
|
||||
1000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1000,
|
||||
original_height: 1000,
|
||||
compressed_width: 1000,
|
||||
compressed_height: 1000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1120,18 +1137,20 @@ mod tests {
|
||||
async fn test_recode_image_2() {
|
||||
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
||||
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
2000,
|
||||
1800,
|
||||
270,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 2000,
|
||||
original_height: 1800,
|
||||
orientation: 270,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
@@ -1140,18 +1159,18 @@ mod tests {
|
||||
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
||||
let bytes = buf.into_inner();
|
||||
|
||||
let img_rotated = send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
&bytes,
|
||||
"jpg",
|
||||
false, // no Exif
|
||||
1800,
|
||||
2000,
|
||||
0,
|
||||
1800,
|
||||
2000,
|
||||
)
|
||||
let img_rotated = SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes: &bytes,
|
||||
extension: "jpg",
|
||||
original_width: 1800,
|
||||
original_height: 2000,
|
||||
compressed_width: 1800,
|
||||
compressed_height: 2000,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_correct_rotation(&img_rotated);
|
||||
@@ -1161,49 +1180,80 @@ mod tests {
|
||||
async fn test_recode_image_balanced_png() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
set_draft: true,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Sticker,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Sticker,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
1920,
|
||||
1080,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: 1920,
|
||||
compressed_height: 1080,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1214,18 +1264,18 @@ mod tests {
|
||||
async fn test_recode_image_rgba_png_to_jpeg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot-rgba.png");
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "1",
|
||||
bytes,
|
||||
"png",
|
||||
false, // no Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::WORSE_IMAGE_SIZE,
|
||||
constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "png",
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::WORSE_IMAGE_SIZE,
|
||||
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1233,18 +1283,19 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recode_image_huge_jpg() {
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::Image,
|
||||
Some("0"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::Image,
|
||||
media_quality_config: "0",
|
||||
bytes,
|
||||
"jpg",
|
||||
true, // has Exif
|
||||
1920,
|
||||
1080,
|
||||
0,
|
||||
constants::BALANCED_IMAGE_SIZE,
|
||||
constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
)
|
||||
extension: "jpg",
|
||||
has_exif: true,
|
||||
original_width: 1920,
|
||||
original_height: 1080,
|
||||
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
||||
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
||||
..Default::default()
|
||||
}
|
||||
.test()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1266,68 +1317,93 @@ mod tests {
|
||||
assert_eq!(luma, 0);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn send_image_check_mediaquality(
|
||||
viewtype: Viewtype,
|
||||
media_quality_config: Option<&str>,
|
||||
bytes: &[u8],
|
||||
extension: &str,
|
||||
has_exif: bool,
|
||||
original_width: u32,
|
||||
original_height: u32,
|
||||
orientation: i32,
|
||||
compressed_width: u32,
|
||||
compressed_height: u32,
|
||||
) -> anyhow::Result<DynamicImage> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, media_quality_config)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
#[derive(Default)]
|
||||
struct SendImageCheckMediaquality<'a> {
|
||||
pub(crate) viewtype: Viewtype,
|
||||
pub(crate) media_quality_config: &'a str,
|
||||
pub(crate) bytes: &'a [u8],
|
||||
pub(crate) extension: &'a str,
|
||||
pub(crate) has_exif: bool,
|
||||
pub(crate) original_width: u32,
|
||||
pub(crate) original_height: u32,
|
||||
pub(crate) orientation: i32,
|
||||
pub(crate) compressed_width: u32,
|
||||
pub(crate) compressed_height: u32,
|
||||
pub(crate) set_draft: bool,
|
||||
}
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
impl SendImageCheckMediaquality<'_> {
|
||||
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
||||
let viewtype = self.viewtype;
|
||||
let media_quality_config = self.media_quality_config;
|
||||
let bytes = self.bytes;
|
||||
let extension = self.extension;
|
||||
let has_exif = self.has_exif;
|
||||
let original_width = self.original_width;
|
||||
let original_height = self.original_height;
|
||||
let orientation = self.orientation;
|
||||
let compressed_width = self.compressed_width;
|
||||
let compressed_height = self.compressed_height;
|
||||
let set_draft = self.set_draft;
|
||||
|
||||
let blob = BlobObject::new_from_path(&alice, &file).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice
|
||||
.set_config(Config::MediaQuality, Some(media_quality_config))
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("file").with_extension(extension);
|
||||
|
||||
fs::write(&file, &bytes)
|
||||
.await
|
||||
.context("failed to write file")?;
|
||||
check_image_size(&file, original_width, original_height);
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
if has_exif {
|
||||
let exif = exif.unwrap();
|
||||
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
||||
} else {
|
||||
assert!(exif.is_none());
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
if set_draft {
|
||||
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
||||
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
}
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
if viewtype == Viewtype::File {
|
||||
assert_eq!(file_saved.extension().unwrap(), extension);
|
||||
let bytes1 = fs::read(&file_saved).await?;
|
||||
assert_eq!(&bytes1, bytes);
|
||||
}
|
||||
|
||||
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent = alice.send_msg(chat.id, &mut msg).await;
|
||||
let alice_msg = alice.get_last_msg().await;
|
||||
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = alice
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
||||
alice_msg.save_file(&alice, &file_saved).await?;
|
||||
check_image_size(file_saved, compressed_width, compressed_height);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
|
||||
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
||||
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
||||
let file_saved = bob
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (_, exif) = blob.metadata()?;
|
||||
assert!(exif.is_none());
|
||||
|
||||
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1359,8 +1435,7 @@ mod tests {
|
||||
.get_blobdir()
|
||||
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
||||
bob_msg.save_file(&bob, &file_saved).await?;
|
||||
let blob = BlobObject::new_from_path(&bob, &file_saved).await?;
|
||||
let (file_size, _) = blob.metadata()?;
|
||||
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
||||
assert_eq!(file_size, bytes.len() as u64);
|
||||
check_image_size(file_saved, width, height);
|
||||
Ok(())
|
||||
|
||||
252
src/chat.rs
252
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,10 +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::WEBXDC_SUFFIX;
|
||||
use crate::webxdc::StatusUpdateSerial;
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -292,7 +292,7 @@ impl ChatId {
|
||||
ChatIdBlocked::get_for_contact(context, contact_id, create_blocked)
|
||||
.await
|
||||
.map(|chat| chat.id)?;
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat).await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat).await?;
|
||||
chat_id
|
||||
} else {
|
||||
warn!(
|
||||
@@ -322,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(
|
||||
@@ -489,7 +489,7 @@ impl ChatId {
|
||||
// went to "contact requests" list rather than normal chatlist.
|
||||
for contact_id in get_chat_contacts(context, self).await? {
|
||||
if contact_id != ContactId::SELF {
|
||||
Contact::scaleup_origin_by_id(context, contact_id, Origin::CreateChat)
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::CreateChat)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
@@ -894,8 +894,20 @@ impl ChatId {
|
||||
.await?
|
||||
.context("no file stored in params")?;
|
||||
msg.param.set(Param::File, blob.as_name());
|
||||
if blob.suffix() == Some(WEBXDC_SUFFIX) {
|
||||
msg.viewtype = Viewtype::Webxdc;
|
||||
if msg.viewtype == Viewtype::File {
|
||||
if let Some((better_type, _)) =
|
||||
message::guess_msgtype_from_suffix(&blob.to_abs_path())
|
||||
// We do not do an automatic conversion to other viewtypes here so that
|
||||
// users can send images as "files" to preserve the original quality
|
||||
// (usually we compress images). The remaining conversions are done by
|
||||
// `prepare_msg_blob()` later.
|
||||
.filter(|&(vt, _)| vt == Viewtype::Webxdc || vt == Viewtype::Vcard)
|
||||
{
|
||||
msg.viewtype = better_type;
|
||||
}
|
||||
}
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -916,12 +928,13 @@ impl ChatId {
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET timestamp=?,type=?,txt=?, param=?,mime_in_reply_to=?
|
||||
SET timestamp=?,type=?,txt=?,txt_normalized=?,param=?,mime_in_reply_to=?
|
||||
WHERE id=?;",
|
||||
(
|
||||
time(),
|
||||
msg.viewtype,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
msg.id,
|
||||
@@ -945,10 +958,11 @@ impl ChatId {
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
param,
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
@@ -956,6 +970,7 @@ impl ChatId {
|
||||
msg.viewtype,
|
||||
MessageState::OutDraft,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
1,
|
||||
msg.in_reply_to.as_deref().unwrap_or_default(),
|
||||
@@ -1920,7 +1935,7 @@ impl Chat {
|
||||
self.param.remove(Param::Unpromoted);
|
||||
self.update_param(context).await?;
|
||||
// send_sync_msg() is called (usually) a moment later at send_msg_to_smtp()
|
||||
// when the group-creation message is actually sent though SMTP -
|
||||
// when the group creation message is actually sent through SMTP --
|
||||
// this makes sure, the other devices are aware of grpid that is used in the sync-message.
|
||||
context
|
||||
.sync_qr_code_tokens(Some(self.id))
|
||||
@@ -1932,6 +1947,10 @@ impl Chat {
|
||||
// reset encrypt error state eg. for forwarding
|
||||
msg.param.remove(Param::ErroneousE2ee);
|
||||
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
msg.param
|
||||
.set_optional(Param::Bot, Some("1").filter(|_| is_bot));
|
||||
|
||||
// Set "In-Reply-To:" to identify the message to which the composed message is a reply.
|
||||
// Set "References:" to identify the "thread" of the conversation.
|
||||
// Both according to [RFC 5322 3.6.4, page 25](https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4).
|
||||
@@ -2060,7 +2079,7 @@ impl Chat {
|
||||
.execute(
|
||||
"UPDATE msgs
|
||||
SET rfc724_mid=?, chat_id=?, from_id=?, to_id=?, timestamp=?, type=?,
|
||||
state=?, txt=?, subject=?, param=?,
|
||||
state=?, txt=?, txt_normalized=?, subject=?, param=?,
|
||||
hidden=?, mime_in_reply_to=?, mime_references=?, mime_modified=?,
|
||||
mime_headers=?, mime_compressed=1, location_id=?, ephemeral_timer=?,
|
||||
ephemeral_timestamp=?
|
||||
@@ -2074,6 +2093,7 @@ impl Chat {
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -2102,6 +2122,7 @@ impl Chat {
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
subject,
|
||||
param,
|
||||
hidden,
|
||||
@@ -2113,7 +2134,7 @@ impl Chat {
|
||||
location_id,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
@@ -2123,6 +2144,7 @@ impl Chat {
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -2218,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(())
|
||||
}
|
||||
|
||||
@@ -2619,6 +2641,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.get_blob(Param::File, context, !msg.is_increation())
|
||||
.await?
|
||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||
let send_as_is = msg.viewtype == Viewtype::File;
|
||||
|
||||
if msg.viewtype == Viewtype::File || msg.viewtype == Viewtype::Image {
|
||||
// Correct the type, take care not to correct already very special
|
||||
@@ -2644,9 +2667,14 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if msg.viewtype == Viewtype::Vcard {
|
||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
||||
}
|
||||
|
||||
let mut maybe_sticker = msg.viewtype == Viewtype::Sticker;
|
||||
if msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker)
|
||||
if !send_as_is
|
||||
&& (msg.viewtype == Viewtype::Image
|
||||
|| maybe_sticker && !msg.param.exists(Param::ForceSticker))
|
||||
{
|
||||
blob.recode_to_image_size(context, &mut maybe_sticker)
|
||||
.await?;
|
||||
@@ -2831,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() {
|
||||
@@ -2881,7 +2909,7 @@ async fn prepare_send_msg(
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = MimeFactory::from_msg(context, msg).await?;
|
||||
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
let mut recipients = mimefactory.recipients();
|
||||
|
||||
@@ -2989,11 +3017,7 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
|
||||
msg.subject.clone_from(&rendered_msg.subject);
|
||||
msg.update_subject(context).await?;
|
||||
let chunk_size = context
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or(constants::DEFAULT_MAX_SMTP_RCPT_TO, usize::from);
|
||||
let chunk_size = context.get_max_smtp_rcpt_to().await?;
|
||||
let trans_fn = |t: &mut rusqlite::Transaction| {
|
||||
let mut row_ids = Vec::<i64>::new();
|
||||
for recipients_chunk in recipients.chunks(chunk_size) {
|
||||
@@ -3244,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(())
|
||||
}
|
||||
|
||||
@@ -3335,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 {
|
||||
@@ -3480,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();
|
||||
@@ -3714,12 +3729,14 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
chat.param.remove(Param::Unpromoted);
|
||||
chat.update_param(context).await?;
|
||||
let _ = context
|
||||
if context
|
||||
.sync_qr_code_tokens(Some(chat_id))
|
||||
.await
|
||||
.log_err(context)
|
||||
.is_ok()
|
||||
&& context.send_sync_msg().await.log_err(context).is_ok();
|
||||
{
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
}
|
||||
|
||||
if context.is_self_addr(contact.get_addr()).await? {
|
||||
@@ -3758,7 +3775,10 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
if let Err(e) = send_msg(context, chat_id, &mut msg).await {
|
||||
remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
return Err(e);
|
||||
}
|
||||
sync = Nosync;
|
||||
}
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
@@ -3914,8 +3934,7 @@ pub async fn remove_contact_from_chat(
|
||||
if let Some(contact) = Contact::get_by_id_optional(context, contact_id).await? {
|
||||
if chat.typ == Chattype::Group && chat.is_promoted() {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
if contact.id == ContactId::SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
if contact_id == ContactId::SELF {
|
||||
msg.text = stock_str::msg_group_left_local(context, ContactId::SELF).await;
|
||||
} else {
|
||||
msg.text = stock_str::msg_del_member_local(
|
||||
@@ -3927,17 +3946,24 @@ pub async fn remove_contact_from_chat(
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
|
||||
msg.id = send_msg(context, chat_id, &mut msg).await?;
|
||||
let res = send_msg(context, chat_id, &mut msg).await;
|
||||
if contact_id == ContactId::SELF {
|
||||
res?;
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
} else if let Err(e) = res {
|
||||
warn!(context, "remove_contact_from_chat({chat_id}, {contact_id}): send_msg() failed: {e:#}.");
|
||||
}
|
||||
} else {
|
||||
sync = Sync;
|
||||
}
|
||||
}
|
||||
// we remove the member from the chat after constructing the
|
||||
// to-be-send message. If between send_msg() and here the
|
||||
// process dies the user will have to re-do the action. It's
|
||||
// better than the other way round: you removed
|
||||
// someone from DB but no peer or device gets to know about it and
|
||||
// group membership is thus different on different devices.
|
||||
// process dies, the user will be able to redo the action. It's better than the other
|
||||
// way round: you removed someone from DB but no peer or device gets to know about it
|
||||
// and group membership is thus different on different devices. But if send_msg()
|
||||
// failed, we still remove the member locally, otherwise it would be impossible to
|
||||
// remove a member with missing key from a protected group.
|
||||
// Note also that sending a message needs all recipients
|
||||
// in order to correctly determine encryption so if we
|
||||
// removed it first, it would complicate the
|
||||
@@ -3985,7 +4011,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;
|
||||
|
||||
@@ -4016,7 +4042,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 =
|
||||
@@ -4240,9 +4266,39 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
msg.timestamp_sort = create_smeared_timestamp(context);
|
||||
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
|
||||
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
if create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
let conn_fn = |conn: &mut rusqlite::Connection| {
|
||||
let range = conn.query_row(
|
||||
"SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \
|
||||
FROM msgs_status_updates WHERE msg_id=?",
|
||||
(msg.id,),
|
||||
|row| {
|
||||
let min_id: StatusUpdateSerial = row.get(0)?;
|
||||
let max_id: StatusUpdateSerial = row.get(1)?;
|
||||
Ok((min_id, max_id))
|
||||
},
|
||||
)?;
|
||||
if range.0 > range.1 {
|
||||
return Ok(());
|
||||
};
|
||||
// `first_serial` must be decreased, otherwise if `Context::flush_status_updates()`
|
||||
// runs in parallel, it would miss the race and instead of resending just remove the
|
||||
// updates thinking that they have been already sent.
|
||||
conn.execute(
|
||||
"INSERT INTO smtp_status_updates (msg_id, first_serial, last_serial, descr) \
|
||||
VALUES(?, ?, ?, '') \
|
||||
ON CONFLICT(msg_id) \
|
||||
DO UPDATE SET first_serial=min(first_serial - 1, excluded.first_serial)",
|
||||
(msg.id, range.0, range.1),
|
||||
)?;
|
||||
Ok(())
|
||||
};
|
||||
context.sql.call_write(conn_fn).await?;
|
||||
}
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -4344,9 +4400,10 @@ pub async fn add_device_msg_with_importance(
|
||||
timestamp_rcvd,
|
||||
type,state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
param,
|
||||
rfc724_mid)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
chat_id,
|
||||
ContactId::DEVICE,
|
||||
@@ -4357,6 +4414,7 @@ pub async fn add_device_msg_with_importance(
|
||||
msg.viewtype,
|
||||
state,
|
||||
&msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
msg.param.to_string(),
|
||||
rfc724_mid,
|
||||
),
|
||||
@@ -4460,8 +4518,8 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
|
||||
let row_id =
|
||||
context.sql.insert(
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,rfc724_mid,ephemeral_timer, param,mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?, ?,?,?, ?,?);",
|
||||
"INSERT INTO msgs (chat_id,from_id,to_id,timestamp,timestamp_sent,timestamp_rcvd,type,state,txt,txt_normalized,rfc724_mid,ephemeral_timer,param,mime_in_reply_to)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
chat_id,
|
||||
from_id.unwrap_or(ContactId::INFO),
|
||||
@@ -4472,6 +4530,7 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
Viewtype::Text,
|
||||
MessageState::InNoticed,
|
||||
text,
|
||||
message::normalize_text(text),
|
||||
rfc724_mid,
|
||||
ephemeral_timer,
|
||||
param.to_string(),
|
||||
@@ -4516,8 +4575,8 @@ pub(crate) async fn update_msg_text_and_timestamp(
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs SET txt=?, timestamp=? WHERE id=?;",
|
||||
(text, timestamp, msg_id),
|
||||
"UPDATE msgs SET txt=?, txt_normalized=?, timestamp=? WHERE id=?;",
|
||||
(text, message::normalize_text(text), timestamp, msg_id),
|
||||
)
|
||||
.await?;
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
@@ -4609,7 +4668,7 @@ impl Context {
|
||||
.0
|
||||
}
|
||||
SyncId::Msgids(msgids) => {
|
||||
let msg = message::get_latest_by_rfc724_mids(self, msgids)
|
||||
let msg = message::get_by_rfc724_mids(self, msgids)
|
||||
.await?
|
||||
.with_context(|| format!("No message found for Message-IDs {msgids:?}"))?;
|
||||
ChatId::lookup_by_message(&msg)
|
||||
@@ -4629,6 +4688,14 @@ impl Context {
|
||||
SyncAction::SetContacts(addrs) => set_contacts_by_addrs(self, chat_id, addrs).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the appropriate `MsgsChanged` event. Should be called if the number of unnoticed
|
||||
/// archived chats could decrease. In general we don't want to make an extra db query to know if
|
||||
/// a noticied chat is archived. Emitting events should be cheap, a false-positive `MsgsChanged`
|
||||
/// is ok.
|
||||
pub(crate) fn on_archived_chats_maybe_noticed(&self) {
|
||||
self.emit_msgs_changed(DC_CHAT_ID_ARCHIVED_LINK, MsgId::new(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -5021,6 +5088,7 @@ mod tests {
|
||||
|
||||
// Bob leaves the chat.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// Bob receives a msg about Alice adding Claire to the group.
|
||||
bob.recv_msg(&alice_sent_add_msg).await;
|
||||
@@ -5073,6 +5141,7 @@ mod tests {
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// This doesn't add Fiona back because Bob just removed them.
|
||||
let sent_msg = alice.send_text(alice_chat_id, "Welcome, Fiona!").await;
|
||||
@@ -5798,7 +5867,27 @@ mod tests {
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2);
|
||||
|
||||
// mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well
|
||||
t.evtracker.clear_events();
|
||||
marknoticed_chat(&t, claire_chat_id).await?;
|
||||
let ev = t
|
||||
.evtracker
|
||||
.get_matching(|ev| {
|
||||
matches!(
|
||||
ev,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: DC_CHAT_ID_ARCHIVED_LINK,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
ev,
|
||||
EventType::MsgsChanged {
|
||||
chat_id: DC_CHAT_ID_ARCHIVED_LINK,
|
||||
msg_id: MsgId::new(0),
|
||||
}
|
||||
);
|
||||
assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2);
|
||||
assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0);
|
||||
assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1);
|
||||
@@ -7526,4 +7615,27 @@ mod tests {
|
||||
assert_eq!(a1_broadcast_chat.get_name(), "Broadcast list 42");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests sending JPEG image with .png extension.
|
||||
///
|
||||
/// This is a regression test, previously sending failed
|
||||
/// because image was passed to PNG decoder
|
||||
/// and it failed to decode image.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_jpeg_with_png_ext() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
|
||||
let file = alice.get_blobdir().join("screenshot.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_msg(alice_chat.get_id(), &mut msg).await;
|
||||
let _msg = bob.recv_msg(&sent_msg).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
120
src/config.rs
120
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,
|
||||
|
||||
@@ -209,7 +215,12 @@ pub enum Config {
|
||||
/// Configured IMAP server security (e.g. TLS, STARTTLS).
|
||||
ConfiguredMailSecurity,
|
||||
|
||||
/// How to check IMAP server TLS certificates.
|
||||
/// Configured TLS certificate checks.
|
||||
/// This option is saved on successful configuration
|
||||
/// and should not be modified manually.
|
||||
///
|
||||
/// This actually applies to both IMAP and SMTP connections,
|
||||
/// but has "IMAP" in the name for backwards compatibility.
|
||||
ConfiguredImapCertificateChecks,
|
||||
|
||||
/// Configured SMTP server hostname.
|
||||
@@ -224,7 +235,9 @@ pub enum Config {
|
||||
/// Configured SMTP server port.
|
||||
ConfiguredSendPort,
|
||||
|
||||
/// How to check SMTP server TLS certificates.
|
||||
/// Deprecated, stored for backwards compatibility.
|
||||
///
|
||||
/// ConfiguredImapCertificateChecks is actually used.
|
||||
ConfiguredSmtpCertificateChecks,
|
||||
|
||||
/// Whether OAuth 2 is used with configured provider.
|
||||
@@ -257,6 +270,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 +333,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,
|
||||
|
||||
@@ -363,8 +383,8 @@ pub enum Config {
|
||||
/// MsgId of webxdc map integration.
|
||||
WebxdcIntegration,
|
||||
|
||||
/// Iroh secret key.
|
||||
IrohSecretKey,
|
||||
/// Enable webxdc realtime features.
|
||||
WebxdcRealtimeEnabled,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -378,14 +398,11 @@ 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 {
|
||||
// We don't restart IO from the synchronisation code, so this is to be on the safe side.
|
||||
if self.needs_io_restart() {
|
||||
return false;
|
||||
}
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::MvboxMove
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
@@ -394,10 +411,7 @@ impl Config {
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
|
||||
)
|
||||
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +437,7 @@ impl Context {
|
||||
.into_owned()
|
||||
})
|
||||
}
|
||||
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
|
||||
Config::SysVersion => Some((*constants::DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key.as_ref()).await?,
|
||||
@@ -481,7 +495,8 @@ impl Context {
|
||||
/// Returns true if movebox ("DeltaChat" folder) should be watched.
|
||||
pub(crate) async fn should_watch_mvbox(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::MvboxMove).await?
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?)
|
||||
|| self.get_config_bool(Config::OnlyFetchMvbox).await?
|
||||
|| !self.get_config_bool(Config::IsChatmail).await?)
|
||||
}
|
||||
|
||||
/// Returns true if sentbox ("Sent" folder) should be watched.
|
||||
@@ -493,6 +508,26 @@ impl Context {
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
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::Bot).await?)
|
||||
}
|
||||
|
||||
/// Returns whether MDNs should be requested.
|
||||
pub(crate) async fn should_request_mdns(&self) -> Result<bool> {
|
||||
match self.config_exists(Config::MdnsEnabled).await? {
|
||||
true => self.get_config_bool(Config::MdnsEnabled).await,
|
||||
false => Ok(!self.get_config_bool(Config::Bot).await?),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether MDNs should be sent.
|
||||
pub(crate) async fn should_send_mdns(&self) -> Result<bool> {
|
||||
self.get_config_bool(Config::MdnsEnabled).await
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
@@ -600,7 +635,7 @@ impl Context {
|
||||
mut value: Option<&str>,
|
||||
) -> Result<()> {
|
||||
Self::check_config(key, value)?;
|
||||
let sync = sync == Sync && key.is_synced();
|
||||
let sync = sync == Sync && key.is_synced() && self.is_configured().await?;
|
||||
let better_value;
|
||||
|
||||
match key {
|
||||
@@ -639,7 +674,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?;
|
||||
@@ -677,7 +712,7 @@ impl Context {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
Box::pin(self.send_sync_msg()).await.log_err(self).ok();
|
||||
self.scheduler.interrupt_smtp().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -942,6 +977,21 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdns_default_behaviour() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
assert!(t.should_request_mdns().await?);
|
||||
assert!(t.should_send_mdns().await?);
|
||||
// The setting should be displayed correctly.
|
||||
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
|
||||
|
||||
t.set_config_bool(Config::Bot, true).await?;
|
||||
assert!(!t.should_request_mdns().await?);
|
||||
assert!(t.should_send_mdns().await?);
|
||||
assert!(t.get_config_bool(Config::MdnsEnabled).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync() -> Result<()> {
|
||||
let alice0 = TestContext::new_alice().await;
|
||||
@@ -968,20 +1018,16 @@ 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);
|
||||
|
||||
let show_emails = alice0.get_config_bool(Config::ShowEmails).await?;
|
||||
alice0
|
||||
.set_config_bool(Config::ShowEmails, !show_emails)
|
||||
.await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(
|
||||
alice1.get_config_bool(Config::ShowEmails).await?,
|
||||
!show_emails
|
||||
);
|
||||
for key in [Config::ShowEmails, Config::MvboxMove] {
|
||||
let val = alice0.get_config_bool(key).await?;
|
||||
alice0.set_config_bool(key, !val).await?;
|
||||
sync(&alice0, &alice1).await;
|
||||
assert_eq!(alice1.get_config_bool(key).await?, !val);
|
||||
}
|
||||
|
||||
// `Config::SyncMsgs` mustn't be synced.
|
||||
alice0.set_config_bool(Config::SyncMsgs, false).await?;
|
||||
@@ -1046,7 +1092,8 @@ mod tests {
|
||||
|
||||
let status = "Synced via usual message";
|
||||
alice0.set_config(Config::Selfstatus, Some(status)).await?;
|
||||
alice0.pop_sent_msg().await; // Sync message
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_msg().await;
|
||||
let status1 = "Synced via sync message";
|
||||
alice1.set_config(Config::Selfstatus, Some(status1)).await?;
|
||||
tcm.send_recv(alice0, alice1, "hi Alice!").await;
|
||||
@@ -1069,7 +1116,8 @@ mod tests {
|
||||
alice0
|
||||
.set_config(Config::Selfavatar, Some(file.to_str().unwrap()))
|
||||
.await?;
|
||||
alice0.pop_sent_msg().await; // Sync message
|
||||
alice0.send_sync_msg().await?;
|
||||
alice0.pop_sent_msg().await;
|
||||
let file = alice1.dir.path().join("avatar.jpg");
|
||||
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
|
||||
129
src/configure.rs
129
src/configure.rs
@@ -113,10 +113,6 @@ impl Context {
|
||||
let mut param = LoginParam::load_candidate_params(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
|
||||
// Reset our knowledge about whether the server is a chatmail server.
|
||||
// We will update it when we connect to IMAP.
|
||||
self.set_config_internal(Config::IsChatmail, None).await?;
|
||||
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, None)
|
||||
.await?;
|
||||
@@ -193,10 +189,8 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
// Step 1: Load the parameters and check email-address and password
|
||||
|
||||
// Do oauth2 only if socks5 is disabled. As soon as we have a http library that can do
|
||||
// socks5 requests, this can work with socks5 too. OAuth is always set either for both
|
||||
// IMAP and SMTP or not at all.
|
||||
if param.imap.oauth2 && !socks5_enabled {
|
||||
// OAuth is always set either for both IMAP and SMTP or not at all.
|
||||
if param.imap.oauth2 {
|
||||
// the used oauth2 addr may differ, check this.
|
||||
// if get_oauth2_addr() is not available in the oauth2 implementation, just use the given one.
|
||||
progress!(ctx, 10);
|
||||
@@ -216,7 +210,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
let parsed = EmailAddress::new(¶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);
|
||||
@@ -267,7 +260,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
}
|
||||
},
|
||||
strict_tls: Some(provider.opt.strict_tls),
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -282,19 +274,28 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
} else {
|
||||
// Try receiving autoconfig
|
||||
info!(ctx, "no offline autoconfig found");
|
||||
param_autoconfig = if socks5_enabled {
|
||||
// Currently we can't do http requests through socks5, to not leak
|
||||
// the ip, just don't do online autoconfig
|
||||
info!(ctx, "socks5 enabled, skipping autoconfig");
|
||||
None
|
||||
} else {
|
||||
get_autoconfig(ctx, param, ¶m_domain, ¶m_addr_urlencoded).await
|
||||
}
|
||||
param_autoconfig = get_autoconfig(ctx, param, ¶m_domain).await;
|
||||
}
|
||||
} else {
|
||||
param_autoconfig = None;
|
||||
}
|
||||
|
||||
let user_strict_tls = match param.certificate_checks {
|
||||
CertificateChecks::Automatic => None,
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
|
||||
};
|
||||
let provider_strict_tls = param.provider.map(|provider| provider.opt.strict_tls);
|
||||
let strict_tls = user_strict_tls.or(provider_strict_tls).unwrap_or(true);
|
||||
|
||||
// Do not save `CertificateChecks::Automatic` into `configured_imap_certificate_checks`.
|
||||
param.certificate_checks = if strict_tls {
|
||||
CertificateChecks::Strict
|
||||
} else {
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
};
|
||||
|
||||
progress!(ctx, 500);
|
||||
|
||||
let mut servers = param_autoconfig.unwrap_or_default();
|
||||
@@ -308,7 +309,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.imap.port,
|
||||
socket: param.imap.security,
|
||||
username: param.imap.user.clone(),
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
if !servers
|
||||
@@ -321,24 +321,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
port: param.smtp.port,
|
||||
socket: param.smtp.security,
|
||||
username: param.smtp.user.clone(),
|
||||
strict_tls: None,
|
||||
})
|
||||
}
|
||||
|
||||
// respect certificate setting from function parameters
|
||||
for server in &mut servers {
|
||||
let certificate_checks = match server.protocol {
|
||||
Protocol::Imap => param.imap.certificate_checks,
|
||||
Protocol::Smtp => param.smtp.certificate_checks,
|
||||
};
|
||||
server.strict_tls = match certificate_checks {
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::Automatic => server.strict_tls,
|
||||
};
|
||||
}
|
||||
|
||||
let servers = expand_param_vector(servers, ¶m.addr, ¶m_domain);
|
||||
|
||||
progress!(ctx, 550);
|
||||
@@ -354,9 +339,6 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
.filter(|params| params.protocol == Protocol::Smtp)
|
||||
.cloned()
|
||||
.collect();
|
||||
let provider_strict_tls = param
|
||||
.provider
|
||||
.map_or(socks5_config.is_some(), |provider| provider.opt.strict_tls);
|
||||
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp_configured = false;
|
||||
@@ -366,18 +348,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
smtp_param.server.clone_from(&smtp_server.hostname);
|
||||
smtp_param.port = smtp_server.port;
|
||||
smtp_param.security = smtp_server.socket;
|
||||
smtp_param.certificate_checks = match smtp_server.strict_tls {
|
||||
Some(true) => CertificateChecks::Strict,
|
||||
Some(false) => CertificateChecks::AcceptInvalidCertificates,
|
||||
None => CertificateChecks::Automatic,
|
||||
};
|
||||
|
||||
match try_smtp_one_param(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&socks5_config,
|
||||
&smtp_addr,
|
||||
provider_strict_tls,
|
||||
strict_tls,
|
||||
&mut smtp,
|
||||
)
|
||||
.await
|
||||
@@ -413,18 +390,13 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
param.imap.server.clone_from(&imap_server.hostname);
|
||||
param.imap.port = imap_server.port;
|
||||
param.imap.security = imap_server.socket;
|
||||
param.imap.certificate_checks = match imap_server.strict_tls {
|
||||
Some(true) => CertificateChecks::Strict,
|
||||
Some(false) => CertificateChecks::AcceptInvalidCertificates,
|
||||
None => CertificateChecks::Automatic,
|
||||
};
|
||||
|
||||
match try_imap_one_param(
|
||||
ctx,
|
||||
¶m.imap,
|
||||
¶m.socks5_config,
|
||||
¶m.addr,
|
||||
provider_strict_tls,
|
||||
strict_tls,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -458,7 +430,22 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
progress!(ctx, 900);
|
||||
|
||||
if imap_session.is_chatmail() {
|
||||
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?;
|
||||
@@ -466,8 +453,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
ctx.set_config(Config::E2eeEnabled, Some("1")).await?;
|
||||
}
|
||||
|
||||
let create_mvbox = ctx.should_watch_mvbox().await?;
|
||||
|
||||
let create_mvbox = !is_chatmail;
|
||||
imap.configure_folders(ctx, &mut imap_session, create_mvbox)
|
||||
.await?;
|
||||
|
||||
@@ -512,20 +498,21 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
///
|
||||
/// A Search configurations from the domain used in the email-address, prefer encrypted
|
||||
/// B. If we have no configuration yet, search configuration in Thunderbird's centeral database
|
||||
/// A. Search configurations from the domain used in the email-address
|
||||
/// B. If we have no configuration yet, search configuration in Thunderbird's central database
|
||||
async fn get_autoconfig(
|
||||
ctx: &Context,
|
||||
param: &LoginParam,
|
||||
param_domain: &str,
|
||||
param_addr_urlencoded: &str,
|
||||
) -> Option<Vec<ServerParams>> {
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶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
|
||||
{
|
||||
@@ -540,7 +527,7 @@ async fn get_autoconfig(
|
||||
"https://{}/.well-known/autoconfig/mail/config-v1.1.xml?emailaddress={}",
|
||||
¶m_domain, ¶m_addr_urlencoded
|
||||
),
|
||||
param,
|
||||
¶m.addr,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -576,7 +563,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
|
||||
{
|
||||
@@ -591,15 +578,15 @@ async fn try_imap_one_param(
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
strict_tls: bool,
|
||||
) -> Result<(Imap, ImapSession), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"imap: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
"imap: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
strict_tls,
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
@@ -611,7 +598,7 @@ async fn try_imap_one_param(
|
||||
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {:#}", err);
|
||||
return Err(ConfigurationError {
|
||||
@@ -624,14 +611,14 @@ async fn try_imap_one_param(
|
||||
|
||||
match imap.connect(context).await {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {:#}", err);
|
||||
info!(context, "IMAP failure: {err:#}.");
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{err:#}"),
|
||||
})
|
||||
}
|
||||
Ok(session) => {
|
||||
info!(context, "success: {}", inf);
|
||||
info!(context, "IMAP success: {inf}.");
|
||||
Ok((imap, session))
|
||||
}
|
||||
}
|
||||
@@ -642,16 +629,16 @@ async fn try_smtp_one_param(
|
||||
param: &ServerLoginParam,
|
||||
socks5_config: &Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
strict_tls: bool,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let inf = format!(
|
||||
"smtp: {}@{}:{} security={} certificate_checks={} oauth2={} socks5_config={}",
|
||||
"smtp: {}@{}:{} security={} strict_tls={} oauth2={} socks5_config={}",
|
||||
param.user,
|
||||
param.server,
|
||||
param.port,
|
||||
param.security,
|
||||
param.certificate_checks,
|
||||
strict_tls,
|
||||
param.oauth2,
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
socks5_config.to_string()
|
||||
@@ -662,16 +649,16 @@ async fn try_smtp_one_param(
|
||||
info!(context, "Trying: {}", inf);
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect(context, param, socks5_config, addr, provider_strict_tls)
|
||||
.connect(context, param, socks5_config, addr, strict_tls)
|
||||
.await
|
||||
{
|
||||
info!(context, "failure: {}", err);
|
||||
info!(context, "SMTP failure: {err:#}.");
|
||||
Err(ConfigurationError {
|
||||
config: inf,
|
||||
msg: format!("{err:#}"),
|
||||
})
|
||||
} else {
|
||||
info!(context, "success: {}", inf);
|
||||
info!(context, "SMTP success: {inf}.");
|
||||
smtp.disconnect();
|
||||
Ok(())
|
||||
}
|
||||
@@ -729,7 +716,7 @@ pub enum Error {
|
||||
|
||||
#[error("XML error at position {position}: {error}")]
|
||||
InvalidXml {
|
||||
position: usize,
|
||||
position: u64,
|
||||
#[source]
|
||||
error: quick_xml::Error,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
343
src/contact.rs
343
src/contact.rs
@@ -1,6 +1,6 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::cmp::{min, Reverse};
|
||||
use std::collections::BinaryHeap;
|
||||
use std::fmt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -11,8 +11,8 @@ 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, normalize_name, sanitize_name_and_addr,
|
||||
strip_rtlo_characters, ContactAddress, VcardContact,
|
||||
self as contact_tools, addr_cmp, addr_normalize, sanitize_name, sanitize_name_and_addr,
|
||||
ContactAddress, VcardContact,
|
||||
};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rusqlite::OptionalExtension;
|
||||
@@ -20,14 +20,15 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{ChatId, ChatIdBlocked, ProtectionStatus};
|
||||
use crate::color::str_to_color;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{load_self_public_key, DcKey};
|
||||
use crate::key::{load_self_public_key, DcKey, SignedPublicKey};
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::LoginParam;
|
||||
use crate::message::MessageState;
|
||||
@@ -36,7 +37,7 @@ 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, 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.
|
||||
@@ -120,6 +121,29 @@ impl ContactId {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the origin of the contacts, but only if `origin` is higher than the current one.
|
||||
pub(crate) async fn scaleup_origin(
|
||||
context: &Context,
|
||||
ids: &[Self],
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
&format!(
|
||||
"UPDATE contacts SET origin=? WHERE id IN ({}) AND origin<?",
|
||||
sql::repeat_vars(ids.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&[origin])
|
||||
.chain(params_iter(ids))
|
||||
.chain(params_iter(&[origin])),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContactId {
|
||||
@@ -166,9 +190,13 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
let mut vcard_contacts = Vec::with_capacity(contacts.len());
|
||||
for id in contacts {
|
||||
let c = Contact::get_by_id(context, *id).await?;
|
||||
let key = Peerstate::from_addr(context, &c.addr)
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.peek_key(false).map(|k| k.to_base64()));
|
||||
let key = match *id {
|
||||
ContactId::SELF => Some(load_self_public_key(context).await?),
|
||||
_ => Peerstate::from_addr(context, &c.addr)
|
||||
.await?
|
||||
.and_then(|peerstate| peerstate.take_key(false)),
|
||||
};
|
||||
let key = key.map(|k| k.to_base64());
|
||||
let profile_image = match c.get_profile_image(context).await? {
|
||||
None => None,
|
||||
Some(path) => tokio::fs::read(path)
|
||||
@@ -189,6 +217,129 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
Ok(contact_tools::make_vcard(&vcard_contacts))
|
||||
}
|
||||
|
||||
/// Imports contacts from the given vCard.
|
||||
///
|
||||
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
|
||||
/// regardless of whether they are just created, modified or left untouched.
|
||||
pub async fn import_vcard(context: &Context, vcard: &str) -> Result<Vec<ContactId>> {
|
||||
let contacts = contact_tools::parse_vcard(vcard);
|
||||
let mut contact_ids = Vec::with_capacity(contacts.len());
|
||||
for c in &contacts {
|
||||
let Ok(id) = import_vcard_contact(context, c)
|
||||
.await
|
||||
.with_context(|| format!("import_vcard_contact() failed for {}", c.addr))
|
||||
.log_err(context)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
contact_ids.push(id);
|
||||
}
|
||||
Ok(contact_ids)
|
||||
}
|
||||
|
||||
async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Result<ContactId> {
|
||||
let addr = ContactAddress::new(&contact.addr).context("Invalid address")?;
|
||||
// Importing a vCard is also an explicit user action like creating a chat with the contact. We
|
||||
// mustn't use `Origin::AddressBook` here because the vCard may be created not by us, also we
|
||||
// want `contact.authname` to be saved as the authname and not a locally given name.
|
||||
let origin = Origin::CreateChat;
|
||||
let (id, modified) =
|
||||
match Contact::add_or_lookup(context, &contact.authname, &addr, origin).await {
|
||||
Err(e) => return Err(e).context("Contact::add_or_lookup() failed"),
|
||||
Ok((ContactId::SELF, _)) => return Ok(ContactId::SELF),
|
||||
Ok(val) => val,
|
||||
};
|
||||
if modified != Modifier::None {
|
||||
context.emit_event(EventType::ContactsChanged(Some(id)));
|
||||
}
|
||||
let key = contact.key.as_ref().and_then(|k| {
|
||||
SignedPublicKey::from_base64(k)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"import_vcard_contact: Cannot decode key for {}",
|
||||
contact.addr
|
||||
)
|
||||
})
|
||||
.log_err(context)
|
||||
.ok()
|
||||
});
|
||||
if let Some(public_key) = key {
|
||||
let timestamp = contact
|
||||
.timestamp
|
||||
.as_ref()
|
||||
.map_or(0, |&t| min(t, smeared_time(context)));
|
||||
let aheader = Aheader {
|
||||
addr: contact.addr.clone(),
|
||||
public_key,
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
};
|
||||
let peerstate = match Peerstate::from_addr(context, &aheader.addr).await {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Cannot create peerstate from {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
Ok(p) => p,
|
||||
};
|
||||
let peerstate = if let Some(mut p) = peerstate {
|
||||
p.apply_gossip(&aheader, timestamp);
|
||||
p
|
||||
} else {
|
||||
Peerstate::from_gossip(&aheader, timestamp)
|
||||
};
|
||||
if let Err(e) = peerstate.save_to_db(&context.sql).await {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not save peerstate for {}: {e:#}.", contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
if let Err(e) = peerstate
|
||||
.handle_fingerprint_change(context, timestamp)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: handle_fingerprint_change() failed for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
if modified != Modifier::Created {
|
||||
return Ok(id);
|
||||
}
|
||||
let path = match &contact.profile_image {
|
||||
Some(image) => match BlobObject::store_from_base64(context, image, "avatar").await {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not decode and save avatar for {}: {e:#}.",
|
||||
contact.addr
|
||||
);
|
||||
None
|
||||
}
|
||||
Ok(path) => Some(path),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
if let Some(path) = path {
|
||||
// Currently this value doesn't matter as we don't import the contact of self.
|
||||
let was_encrypted = false;
|
||||
if let Err(e) =
|
||||
set_profile_image(context, id, &AvatarAction::Change(path), was_encrypted).await
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"import_vcard_contact: Could not set avatar for {}: {e:#}.", contact.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// An object representing a single contact in memory.
|
||||
///
|
||||
/// The contact object is not updated.
|
||||
@@ -394,6 +545,10 @@ impl Contact {
|
||||
{
|
||||
if contact_id == ContactId::SELF {
|
||||
contact.name = stock_str::self_msg(context).await;
|
||||
contact.authname = context
|
||||
.get_config(Config::Displayname)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
contact.addr = context
|
||||
.get_config(Config::ConfiguredAddr)
|
||||
.await?
|
||||
@@ -469,9 +624,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) =
|
||||
@@ -489,7 +642,7 @@ impl Contact {
|
||||
set_blocked(context, Nosync, contact_id, false).await?;
|
||||
}
|
||||
|
||||
if sync.into() {
|
||||
if sync.into() && sth_modified != Modifier::None {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr.to_string()),
|
||||
@@ -594,7 +747,7 @@ impl Contact {
|
||||
/// - "name": name passed as function argument, belonging to the given origin
|
||||
/// - "row_name": current name used in the database, typically set to "name"
|
||||
/// - "row_authname": name as authorized from a contact, set only through a From-header
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
///
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
|
||||
pub(crate) async fn add_or_lookup(
|
||||
@@ -612,7 +765,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:
|
||||
@@ -808,7 +961,6 @@ impl Contact {
|
||||
|
||||
for (name, addr) in split_address_book(addr_book) {
|
||||
let (name, addr) = sanitize_name_and_addr(name, addr);
|
||||
let name = normalize_name(&name);
|
||||
match ContactAddress::new(&addr) {
|
||||
Ok(addr) => {
|
||||
match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await {
|
||||
@@ -845,7 +997,7 @@ impl Contact {
|
||||
/// - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters
|
||||
/// - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned.
|
||||
/// if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned.
|
||||
/// `query` is a string to filter the list.
|
||||
/// `query` is a string to filter the list.
|
||||
pub async fn get_all(
|
||||
context: &Context,
|
||||
listflags: u32,
|
||||
@@ -1250,6 +1402,17 @@ impl Contact {
|
||||
self.status.as_str()
|
||||
}
|
||||
|
||||
/// Returns whether end-to-end encryption to the contact is available.
|
||||
pub async fn e2ee_avail(&self, context: &Context) -> Result<bool> {
|
||||
if self.id == ContactId::SELF {
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(peerstate.peek_key(false).is_some())
|
||||
}
|
||||
|
||||
/// Returns true if the contact
|
||||
/// can be added to verified chats,
|
||||
/// i.e. has a verified key
|
||||
@@ -1383,22 +1546,6 @@ impl Contact {
|
||||
.await?;
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Updates the origin of the contact, but only if new origin is higher than the current one.
|
||||
pub async fn scaleup_origin_by_id(
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
origin: Origin,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=? AND origin<?;",
|
||||
(origin, contact_id, origin),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
@@ -1777,8 +1924,13 @@ impl RecentlySeenLoop {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) fn abort(self) {
|
||||
pub(crate) async fn abort(self) {
|
||||
self.handle.abort();
|
||||
|
||||
// Await aborted task to ensure the `Future` is dropped
|
||||
// with all resources moved inside such as the `Context`
|
||||
// reference to `InnerContext`.
|
||||
self.handle.await.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1823,15 +1975,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");
|
||||
@@ -2541,6 +2684,8 @@ mod tests {
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(!contact.e2ee_avail(&alice).await?);
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
@@ -2564,6 +2709,8 @@ bob@example.net:
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
let contact = Contact::get_by_id(&alice, contact_bob_id).await?;
|
||||
assert!(contact.e2ee_avail(&alice).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2849,7 +2996,7 @@ Until the false-positive is fixed:
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_make_vcard() -> Result<()> {
|
||||
async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
@@ -2883,8 +3030,8 @@ Until the false-positive is fixed:
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0].addr, bob_addr);
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, Some(key_base64));
|
||||
assert_eq!(contacts[0].profile_image, Some(avatar_base64));
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
let timestamp = *contacts[0].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
@@ -2894,6 +3041,110 @@ Until the false-positive is fixed:
|
||||
let timestamp = *contacts[1].timestamp.as_ref().unwrap();
|
||||
assert!(t0 <= timestamp && timestamp <= t1);
|
||||
|
||||
let alice = &TestContext::new_alice().await;
|
||||
alice.evtracker.clear_events();
|
||||
let contact_ids = import_vcard(alice, &vcard).await?;
|
||||
assert_eq!(contact_ids.len(), 2);
|
||||
for _ in 0..contact_ids.len() {
|
||||
alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged(Some(_))))
|
||||
.await;
|
||||
}
|
||||
|
||||
let vcard = make_vcard(alice, &[contact_ids[0], contact_ids[1]]).await?;
|
||||
// This should be the same vCard except timestamps, check that roughly.
|
||||
let contacts = contact_tools::parse_vcard(&vcard);
|
||||
assert_eq!(contacts.len(), 2);
|
||||
assert_eq!(contacts[0].addr, bob_addr);
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(*contacts[0].key.as_ref().unwrap(), key_base64);
|
||||
assert_eq!(*contacts[0].profile_image.as_ref().unwrap(), avatar_base64);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
assert_eq!(contacts[1].addr, "fiona@example.net".to_string());
|
||||
|
||||
let chat_id = ChatId::create_for_contact(alice, contact_ids[0]).await?;
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// Bob only actually imports Fiona, though `ContactId::SELF` is also returned.
|
||||
bob.evtracker.clear_events();
|
||||
let contact_ids = import_vcard(bob, &vcard).await?;
|
||||
bob.emit_event(EventType::Test);
|
||||
assert_eq!(contact_ids.len(), 2);
|
||||
assert_eq!(contact_ids[0], ContactId::SELF);
|
||||
let ev = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::ContactsChanged(Some(contact_ids[1])));
|
||||
let ev = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. } | EventType::Test))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::Test);
|
||||
let vcard = make_vcard(bob, &[contact_ids[1]]).await?;
|
||||
let contacts = contact_tools::parse_vcard(&vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "fiona@example.net");
|
||||
assert_eq!(contacts[0].authname, "".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_ok());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_import_vcard_updates_only_key() -> Result<()> {
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
let bob_addr = &bob.get_config(Config::Addr).await?.unwrap();
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let vcard = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
alice.evtracker.clear_events();
|
||||
let alice_bob_id = import_vcard(alice, &vcard).await?[0];
|
||||
let ev = alice
|
||||
.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::ContactsChanged { .. }))
|
||||
.await;
|
||||
assert_eq!(ev, EventType::ContactsChanged(Some(alice_bob_id)));
|
||||
let chat_id = ChatId::create_for_contact(alice, alice_bob_id).await?;
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let bob = &TestContext::new().await;
|
||||
bob.configure_addr(bob_addr).await;
|
||||
bob.set_config(Config::Displayname, Some("Not Bob")).await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&avatar_path, avatar_bytes).await?;
|
||||
bob.set_config(Config::Selfavatar, Some(avatar_path.to_str().unwrap()))
|
||||
.await?;
|
||||
SystemTime::shift(Duration::from_secs(1));
|
||||
let vcard1 = make_vcard(bob, &[ContactId::SELF]).await?;
|
||||
assert_eq!(import_vcard(alice, &vcard1).await?, vec![alice_bob_id]);
|
||||
let alice_bob_contact = Contact::get_by_id(alice, alice_bob_id).await?;
|
||||
assert_eq!(alice_bob_contact.get_authname(), "Bob");
|
||||
assert_eq!(alice_bob_contact.get_profile_image(alice).await?, None);
|
||||
let msg = alice.get_last_msg_in(chat_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::contact_setup_changed(alice, bob_addr).await
|
||||
);
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
// The old vCard is imported, but doesn't change Bob's key for Alice.
|
||||
import_vcard(alice, &vcard).await?.first().unwrap();
|
||||
let sent_msg = alice.send_text(chat_id, "moin").await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
122
src/context.rs
122
src/context.rs
@@ -339,6 +339,7 @@ impl Context {
|
||||
) -> Result<Context> {
|
||||
let context =
|
||||
Self::new_closed(dbfile, id, events, stock_strings, Default::default()).await?;
|
||||
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
context.sql.open(&context, "".to_string()).await?;
|
||||
@@ -497,6 +498,23 @@ impl Context {
|
||||
self.get_config_bool(Config::IsChatmail).await
|
||||
}
|
||||
|
||||
/// Returns maximum number of recipients the provider allows to send a single email to.
|
||||
pub(crate) async fn get_max_smtp_rcpt_to(&self) -> Result<usize> {
|
||||
let is_chatmail = self.is_chatmail().await?;
|
||||
let val = self
|
||||
.get_configured_provider()
|
||||
.await?
|
||||
.and_then(|provider| provider.opt.max_smtp_rcpt_to)
|
||||
.map_or_else(
|
||||
|| match is_chatmail {
|
||||
true => usize::MAX,
|
||||
false => constants::DEFAULT_MAX_SMTP_RCPT_TO,
|
||||
},
|
||||
usize::from,
|
||||
);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
/// Does a background fetch
|
||||
/// pauses the scheduler and does one imap fetch, then unpauses and returns
|
||||
pub async fn background_fetch(&self) -> Result<()> {
|
||||
@@ -523,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:#}.");
|
||||
}
|
||||
@@ -804,6 +814,16 @@ impl Context {
|
||||
}
|
||||
|
||||
res.insert("is_chatmail", self.is_chatmail().await?.to_string());
|
||||
res.insert(
|
||||
"fix_is_chatmail",
|
||||
self.get_config_bool(Config::FixIsChatmail)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"is_muted",
|
||||
self.get_config_bool(Config::IsMuted).await?.to_string(),
|
||||
);
|
||||
|
||||
if let Some(metadata) = &*self.metadata.read().await {
|
||||
if let Some(comment) = &metadata.comment {
|
||||
@@ -949,6 +969,12 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"webxdc_realtime_enabled",
|
||||
self.get_config_bool(Config::WebxdcRealtimeEnabled)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
res.insert("uptime", duration_to_str(elapsed));
|
||||
@@ -1235,12 +1261,12 @@ impl Context {
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Searches for messages containing the query string.
|
||||
/// Searches for messages containing the query string case-insensitively.
|
||||
///
|
||||
/// If `chat_id` is provided this searches only for messages in this chat, if `chat_id`
|
||||
/// is `None` this searches messages from all chats.
|
||||
pub async fn search_msgs(&self, chat_id: Option<ChatId>, query: &str) -> Result<Vec<MsgId>> {
|
||||
let real_query = query.trim();
|
||||
let real_query = query.trim().to_lowercase();
|
||||
if real_query.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -1256,7 +1282,7 @@ impl Context {
|
||||
WHERE m.chat_id=?
|
||||
AND m.hidden=0
|
||||
AND ct.blocked=0
|
||||
AND txt LIKE ?
|
||||
AND IFNULL(txt_normalized, txt) LIKE ?
|
||||
ORDER BY m.timestamp,m.id;",
|
||||
(chat_id, str_like_in_text),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
@@ -1292,7 +1318,7 @@ impl Context {
|
||||
AND m.hidden=0
|
||||
AND c.blocked!=1
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
AND IFNULL(txt_normalized, txt) LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
(str_like_in_text,),
|
||||
|row| row.get::<_, MsgId>("id"),
|
||||
@@ -1322,7 +1348,7 @@ impl Context {
|
||||
Ok(sentbox.as_deref() == Some(folder_name))
|
||||
}
|
||||
|
||||
/// Returns true if given folder name is the name of the "Delta Chat" folder.
|
||||
/// Returns true if given folder name is the name of the "DeltaChat" folder.
|
||||
pub async fn is_mvbox(&self, folder_name: &str) -> Result<bool> {
|
||||
let mvbox = self.get_config(Config::ConfiguredMvboxFolder).await?;
|
||||
Ok(mvbox.as_deref() == Some(folder_name))
|
||||
@@ -1375,43 +1401,6 @@ pub fn get_version_str() -> &'static str {
|
||||
&DC_VERSION_STR
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct CollectVisitor(HashMap<String, String>);
|
||||
|
||||
impl tracing::field::Visit for CollectVisitor {
|
||||
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_error(
|
||||
&mut self,
|
||||
field: &tracing::field::Field,
|
||||
value: &(dyn std::error::Error + 'static),
|
||||
) {
|
||||
self.0.insert(field.to_string(), value.to_string());
|
||||
}
|
||||
|
||||
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||
self.0.insert(field.to_string(), format!("{:?}", value));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Context as _;
|
||||
@@ -1571,6 +1560,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();
|
||||
@@ -1692,7 +1697,6 @@ mod tests {
|
||||
"socks5_password",
|
||||
"key_id",
|
||||
"webxdc_integration",
|
||||
"iroh_secret_key",
|
||||
];
|
||||
let t = TestContext::new().await;
|
||||
let info = t.get_info().await.unwrap();
|
||||
@@ -1735,6 +1739,8 @@ mod tests {
|
||||
msg2.set_text("barbaz".to_string());
|
||||
send_msg(&alice, chat.id, &mut msg2).await?;
|
||||
|
||||
alice.send_text(chat.id, "Δ-Chat").await;
|
||||
|
||||
// Global search with a part of text finds the message.
|
||||
let res = alice.search_msgs(None, "ob").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
@@ -1747,6 +1753,12 @@ mod tests {
|
||||
assert_eq!(res.first(), Some(&msg2.id));
|
||||
assert_eq!(res.get(1), Some(&msg1.id));
|
||||
|
||||
// Search is case-insensitive.
|
||||
for chat_id in [None, Some(chat.id)] {
|
||||
let res = alice.search_msgs(chat_id, "δ-chat").await?;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
// Global search with longer text does not find any message.
|
||||
let res = alice.search_msgs(None, "foobarbaz").await?;
|
||||
assert!(res.is_empty());
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -184,7 +184,7 @@ impl Session {
|
||||
bail!("Attempt to fetch UID 0");
|
||||
}
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
// we are connected, and the folder is selected
|
||||
info!(context, "Downloading message {}/{} fully...", folder, uid);
|
||||
|
||||
@@ -447,7 +447,7 @@ pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Resu
|
||||
for (msg_id, chat_id, viewtype, location_id) in rows {
|
||||
transaction.execute(
|
||||
"UPDATE msgs
|
||||
SET chat_id=?, txt='', subject='', txt_raw='',
|
||||
SET chat_id=?, txt='', txt_normalized=NULL, subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE id=?",
|
||||
(DC_CHAT_ID_TRASH, msg_id),
|
||||
@@ -1024,7 +1024,7 @@ mod tests {
|
||||
t.send_text(self_chat.id, "Saved message, which we delete manually")
|
||||
.await;
|
||||
let msg = t.get_last_msg_in(self_chat.id).await;
|
||||
msg.id.trash(&t).await?;
|
||||
msg.id.trash(&t, false).await?;
|
||||
check_msg_is_deleted(&t, &self_chat, msg.id).await;
|
||||
|
||||
self_chat
|
||||
@@ -1304,7 +1304,7 @@ mod tests {
|
||||
let msg = alice.get_last_msg().await;
|
||||
|
||||
// Message is deleted when its timer expires.
|
||||
msg.id.trash(&alice).await?;
|
||||
msg.id.trash(&alice, false).await?;
|
||||
|
||||
// Message with Message-ID <third@example.com>, referencing <first@example.com> and
|
||||
// <second@example.com>, is received. The message <second@example.come> is not in the
|
||||
|
||||
@@ -71,7 +71,14 @@ impl EventEmitter {
|
||||
/// [`try_recv`]: Self::try_recv
|
||||
pub async fn recv(&self) -> Option<Event> {
|
||||
let mut lock = self.0.lock().await;
|
||||
lock.recv().await.ok()
|
||||
match lock.recv().await {
|
||||
Err(async_broadcast::RecvError::Overflowed(n)) => Some(Event {
|
||||
id: 0,
|
||||
typ: EventType::EventChannelOverflow { n },
|
||||
}),
|
||||
Err(async_broadcast::RecvError::Closed) => None,
|
||||
Ok(event) => Some(event),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to receive an event without blocking.
|
||||
@@ -86,8 +93,19 @@ impl EventEmitter {
|
||||
// to avoid blocking
|
||||
// in case there is a concurrent call to `recv`.
|
||||
let mut lock = self.0.try_lock()?;
|
||||
let event = lock.try_recv()?;
|
||||
Ok(event)
|
||||
match lock.try_recv() {
|
||||
Err(async_broadcast::TryRecvError::Overflowed(n)) => {
|
||||
// Some events have been lost,
|
||||
// but the channel is not closed.
|
||||
Ok(Event {
|
||||
id: 0,
|
||||
typ: EventType::EventChannelOverflow { n },
|
||||
})
|
||||
}
|
||||
res @ (Err(async_broadcast::TryRecvError::Empty)
|
||||
| Err(async_broadcast::TryRecvError::Closed)
|
||||
| Ok(_)) => Ok(res?),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,4 +311,14 @@ pub enum EventType {
|
||||
/// ID of the changed chat
|
||||
chat_id: Option<ChatId>,
|
||||
},
|
||||
|
||||
/// Event for using in tests, e.g. as a fence between normally generated events.
|
||||
#[cfg(test)]
|
||||
Test,
|
||||
|
||||
/// Inform than some events have been skipped due to event channel overflow.
|
||||
EventChannelOverflow {
|
||||
/// Number of events skipped.
|
||||
n: u64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum HeaderDef {
|
||||
Date,
|
||||
From_,
|
||||
To,
|
||||
AutoSubmitted,
|
||||
|
||||
/// Carbon copy.
|
||||
Cc,
|
||||
|
||||
204
src/imap.rs
204
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,10 @@ use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam};
|
||||
use crate::login_param::{LoginParam, ServerLoginParam};
|
||||
use crate::message::{self, Message, MessageState, MessengerMessage, MsgId, Viewtype};
|
||||
use crate::mimeparser;
|
||||
use crate::oauth2::get_oauth2_access_token;
|
||||
use crate::provider::Socket;
|
||||
use crate::receive_imf::{
|
||||
from_field_to_contact_id, get_prefetch_parent_message, receive_imf_inner, ReceivedMsg,
|
||||
};
|
||||
@@ -232,20 +231,13 @@ impl Imap {
|
||||
lp: &ServerLoginParam,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
addr: &str,
|
||||
provider_strict_tls: bool,
|
||||
strict_tls: bool,
|
||||
idle_interrupt_receiver: Receiver<()>,
|
||||
) -> Result<Self> {
|
||||
if lp.server.is_empty() || lp.user.is_empty() || lp.password.is_empty() {
|
||||
bail!("Incomplete IMAP connection parameters");
|
||||
}
|
||||
|
||||
let strict_tls = match lp.certificate_checks {
|
||||
CertificateChecks::Automatic => provider_strict_tls,
|
||||
CertificateChecks::Strict => true,
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => false,
|
||||
};
|
||||
|
||||
let imap = Imap {
|
||||
idle_interrupt_receiver,
|
||||
addr: addr.to_string(),
|
||||
@@ -273,17 +265,11 @@ impl Imap {
|
||||
}
|
||||
|
||||
let param = LoginParam::load_configured_params(context).await?;
|
||||
// the trailing underscore is correct
|
||||
|
||||
let imap = Self::new(
|
||||
¶m.imap,
|
||||
param.socks5_config.clone(),
|
||||
¶m.addr,
|
||||
param
|
||||
.provider
|
||||
.map_or(param.socks5_config.is_some(), |provider| {
|
||||
provider.opt.strict_tls
|
||||
}),
|
||||
param.strict_tls(),
|
||||
idle_interrupt_receiver,
|
||||
)?;
|
||||
Ok(imap)
|
||||
@@ -313,7 +299,7 @@ impl Imap {
|
||||
if !ratelimit_duration.is_zero() {
|
||||
warn!(
|
||||
context,
|
||||
"IMAP got rate limited, waiting for {} until can connect",
|
||||
"IMAP got rate limited, waiting for {} until can connect.",
|
||||
duration_to_str(ratelimit_duration),
|
||||
);
|
||||
let interrupted = async {
|
||||
@@ -342,52 +328,16 @@ impl Imap {
|
||||
);
|
||||
self.conn_backoff_ms = max(BACKOFF_MIN_MS, self.conn_backoff_ms);
|
||||
|
||||
let connection_res: Result<Client> =
|
||||
if self.lp.security == Socket::Starttls || self.lp.security == Socket::Plain {
|
||||
let imap_server: &str = self.lp.server.as_ref();
|
||||
let imap_port = self.lp.port;
|
||||
let connection_res = Client::connect(
|
||||
context,
|
||||
self.lp.server.as_ref(),
|
||||
self.lp.port,
|
||||
self.strict_tls,
|
||||
self.socks5_config.clone(),
|
||||
self.lp.security,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Some(socks5_config) = &self.socks5_config {
|
||||
if self.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
socks5_config.clone(),
|
||||
self.strict_tls,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Client::connect_insecure_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
socks5_config.clone(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
} else if self.lp.security == Socket::Starttls {
|
||||
Client::connect_starttls(context, imap_server, imap_port, self.strict_tls).await
|
||||
} else {
|
||||
Client::connect_insecure(context, imap_server, imap_port).await
|
||||
}
|
||||
} else {
|
||||
let imap_server: &str = self.lp.server.as_ref();
|
||||
let imap_port = self.lp.port;
|
||||
|
||||
if let Some(socks5_config) = &self.socks5_config {
|
||||
Client::connect_secure_socks5(
|
||||
context,
|
||||
imap_server,
|
||||
imap_port,
|
||||
self.strict_tls,
|
||||
socks5_config.clone(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Client::connect_secure(context, imap_server, imap_port, self.strict_tls).await
|
||||
}
|
||||
};
|
||||
let client = connection_res?;
|
||||
self.conn_backoff_ms = BACKOFF_MIN_MS;
|
||||
self.ratelimit.send();
|
||||
@@ -483,7 +433,11 @@ impl Imap {
|
||||
.get_raw_config_int(constants::DC_FOLDERS_CONFIGURED_KEY)
|
||||
.await?;
|
||||
if folders_configured.unwrap_or_default() < constants::DC_FOLDERS_CONFIGURED_VERSION {
|
||||
let create_mvbox = true;
|
||||
let is_chatmail = match context.get_config_bool(Config::FixIsChatmail).await? {
|
||||
false => session.is_chatmail(),
|
||||
true => context.get_config_bool(Config::IsChatmail).await?,
|
||||
};
|
||||
let create_mvbox = !is_chatmail || context.get_config_bool(Config::MvboxMove).await?;
|
||||
self.configure_folders(context, &mut session, create_mvbox)
|
||||
.await?;
|
||||
}
|
||||
@@ -540,18 +494,20 @@ 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);
|
||||
}
|
||||
|
||||
let new_emails = session
|
||||
session
|
||||
.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.with_context(|| format!("Failed to select folder {folder:?}"))?;
|
||||
|
||||
if !new_emails && !fetch_existing_msgs {
|
||||
if !session.new_mail && !fetch_existing_msgs {
|
||||
info!(context, "No new emails in folder {folder:?}.");
|
||||
return Ok(false);
|
||||
}
|
||||
session.new_mail = false;
|
||||
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, folder).await?;
|
||||
@@ -598,20 +554,26 @@ impl Imap {
|
||||
// in the `INBOX.DeltaChat` folder again.
|
||||
let _target;
|
||||
let target = if let Some(message_id) = &message_id {
|
||||
let is_dup = if let Some((_, ts_sent_old)) =
|
||||
message::rfc724_mid_exists(context, message_id).await?
|
||||
{
|
||||
let msg_info =
|
||||
message::rfc724_mid_exists_ex(context, message_id, "deleted=1").await?;
|
||||
let delete = if let Some((_, _, true)) = msg_info {
|
||||
info!(context, "Deleting locally deleted message {message_id}.");
|
||||
true
|
||||
} else if let Some((_, ts_sent_old, _)) = msg_info {
|
||||
let is_chat_msg = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
let ts_sent = headers
|
||||
.get_header_value(HeaderDef::Date)
|
||||
.and_then(|v| mailparse::dateparse(&v).ok())
|
||||
.unwrap_or_default();
|
||||
is_dup_msg(is_chat_msg, ts_sent, ts_sent_old)
|
||||
let is_dup = is_dup_msg(is_chat_msg, ts_sent, ts_sent_old);
|
||||
if is_dup {
|
||||
info!(context, "Deleting duplicate message {message_id}.");
|
||||
}
|
||||
is_dup
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if is_dup {
|
||||
info!(context, "Deleting duplicate message {message_id}.");
|
||||
if delete {
|
||||
&delete_target
|
||||
} else if context
|
||||
.sql
|
||||
@@ -765,10 +727,6 @@ impl Imap {
|
||||
context: &Context,
|
||||
session: &mut Session,
|
||||
) -> Result<()> {
|
||||
if context.get_config_bool(Config::Bot).await? {
|
||||
return Ok(()); // Bots don't want those messages
|
||||
}
|
||||
|
||||
add_all_recipients_as_contacts(context, session, Config::ConfiguredSentboxFolder)
|
||||
.await
|
||||
.context("failed to get recipients from the sentbox")?;
|
||||
@@ -838,7 +796,7 @@ impl Session {
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msgs = BTreeMap::new();
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
@@ -1039,7 +997,7 @@ impl Session {
|
||||
// MOVE/DELETE operations. This does not result in multiple SELECT commands
|
||||
// being sent because `select_folder()` does nothing if the folder is already
|
||||
// selected.
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
@@ -1087,18 +1045,12 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
self.select_folder(context, Some(&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,
|
||||
@@ -1131,7 +1083,7 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.select_folder(context, Some(folder))
|
||||
self.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
@@ -1197,6 +1149,9 @@ impl Session {
|
||||
set_modseq(context, folder, highest_modseq)
|
||||
.await
|
||||
.with_context(|| format!("failed to set MODSEQ for folder {folder}"))?;
|
||||
if !updated_chat_ids.is_empty() {
|
||||
context.on_archived_chats_maybe_noticed();
|
||||
}
|
||||
for updated_chat_id in updated_chat_ids {
|
||||
context.emit_event(EventType::MsgsNoticed(updated_chat_id));
|
||||
chatlist_events::emit_chatlist_item_changed(context, updated_chat_id);
|
||||
@@ -1472,13 +1427,15 @@ impl Session {
|
||||
admin = m.value;
|
||||
}
|
||||
"/shared/vendor/deltachat/irohrelay" => {
|
||||
if let Some(url) = m.value.as_deref().and_then(|s| Url::parse(s).ok()) {
|
||||
iroh_relay = Some(url);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Got invalid URL from iroh relay metadata: {:?}.", m.value
|
||||
);
|
||||
if let Some(value) = m.value {
|
||||
if let Ok(url) = Url::parse(&value) {
|
||||
iroh_relay = Some(url);
|
||||
} else {
|
||||
warn!(
|
||||
context,
|
||||
"Got invalid URL from iroh relay metadata: {:?}.", value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -1517,7 +1474,7 @@ impl Session {
|
||||
} else if !context.push_subscriber.heartbeat_subscribed().await {
|
||||
let context = context.clone();
|
||||
// Subscribe for heartbeat notifications.
|
||||
tokio::spawn(async move { context.push_subscriber.subscribe().await });
|
||||
tokio::spawn(async move { context.push_subscriber.subscribe(&context).await });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1547,8 +1504,8 @@ impl Session {
|
||||
|
||||
/// Attempts to configure mvbox.
|
||||
///
|
||||
/// Tries to find any folder in the given list of `folders`. If none is found, tries to create
|
||||
/// `folders[0]`. This method does not use LIST command to ensure that
|
||||
/// Tries to find any folder examining `folders` in the order they go. If none is found, tries
|
||||
/// to create any folder in the same order. This method does not use LIST command to ensure that
|
||||
/// configuration works even if mailbox lookup is forbidden via Access Control List (see
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc4314>).
|
||||
///
|
||||
@@ -1561,7 +1518,7 @@ impl Session {
|
||||
) -> Result<Option<&'a str>> {
|
||||
// Close currently selected folder if needed.
|
||||
// We are going to select folders using low-level EXAMINE operations below.
|
||||
self.select_folder(context, None).await?;
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
for folder in folders {
|
||||
info!(context, "Looking for MVBOX-folder \"{}\"...", &folder);
|
||||
@@ -1582,16 +1539,17 @@ impl Session {
|
||||
if !create_mvbox {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(folder) = folders.first() else {
|
||||
return Ok(None);
|
||||
};
|
||||
match self.select_with_uidvalidity(context, folder).await {
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
|
||||
// Some servers require namespace-style folder names like "INBOX.DeltaChat", so we try all
|
||||
// the variants here.
|
||||
for folder in folders {
|
||||
match self.select_with_uidvalidity(context, folder).await {
|
||||
Ok(_) => {
|
||||
info!(context, "MVBOX-folder {} created.", folder);
|
||||
return Ok(Some(folder));
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Cannot create MVBOX-folder {:?}: {}", folder, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
@@ -1839,6 +1797,20 @@ async fn needs_move_to_mvbox(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<bool> {
|
||||
let has_chat_version = headers.get_header_value(HeaderDef::ChatVersion).is_some();
|
||||
if !context.get_config_bool(Config::IsChatmail).await?
|
||||
&& has_chat_version
|
||||
&& headers
|
||||
.get_header_value(HeaderDef::AutoSubmitted)
|
||||
.filter(|val| val.to_ascii_lowercase() == "auto-generated")
|
||||
.is_some()
|
||||
{
|
||||
if let Some(from) = mimeparser::get_from(headers) {
|
||||
if context.is_self_addr(&from.addr).await? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !context.get_config_bool(Config::MvboxMove).await? {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -1852,7 +1824,7 @@ async fn needs_move_to_mvbox(
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if headers.get_header_value(HeaderDef::ChatVersion).is_some() {
|
||||
if has_chat_version {
|
||||
Ok(true)
|
||||
} else if let Some(parent) = get_prefetch_parent_message(context, headers).await? {
|
||||
match parent.is_dc_message {
|
||||
@@ -2419,12 +2391,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!(
|
||||
@@ -2440,7 +2406,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,23 @@
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{bail, format_err, Context as _, Result};
|
||||
use async_imap::Client as ImapClient;
|
||||
use async_imap::Session as ImapSession;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
use tokio::io::BufWriter;
|
||||
|
||||
use super::capabilities::Capabilities;
|
||||
use super::session::Session;
|
||||
use crate::context::Context;
|
||||
use crate::net::connect_tcp;
|
||||
use crate::net::dns::{lookup_host_with_cache, update_connect_timestamp};
|
||||
use crate::net::session::SessionStream;
|
||||
use crate::net::tls::wrap_tls;
|
||||
use crate::net::update_connection_history;
|
||||
use crate::net::{connect_tcp_inner, connect_tls_inner};
|
||||
use crate::provider::Socket;
|
||||
use crate::socks::Socks5Config;
|
||||
use fast_socks5::client::Socks5Stream;
|
||||
|
||||
/// IMAP connection, write and read timeout.
|
||||
pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
use crate::tools::time;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Client {
|
||||
@@ -39,6 +38,16 @@ impl DerefMut for Client {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts port number to ALPN list.
|
||||
fn alpn(port: u16) -> &'static [&'static str] {
|
||||
if port == 993 {
|
||||
// Do not request ALPN on standard port.
|
||||
&[]
|
||||
} else {
|
||||
&["imap"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine server capabilities.
|
||||
///
|
||||
/// If server supports ID capability, send our client ID.
|
||||
@@ -98,14 +107,67 @@ impl Client {
|
||||
Ok(Session::new(session, capabilities))
|
||||
}
|
||||
|
||||
pub async fn connect_secure(
|
||||
pub async fn connect(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
security: Socket,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream).await?;
|
||||
if let Some(socks5_config) = socks5_config {
|
||||
let client = match security {
|
||||
Socket::Automatic => bail!("IMAP port security is not configured"),
|
||||
Socket::Ssl => {
|
||||
Client::connect_secure_socks5(context, host, port, strict_tls, socks5_config)
|
||||
.await?
|
||||
}
|
||||
Socket::Starttls => {
|
||||
Client::connect_starttls_socks5(context, host, port, socks5_config, strict_tls)
|
||||
.await?
|
||||
}
|
||||
Socket::Plain => {
|
||||
Client::connect_insecure_socks5(context, host, port, socks5_config).await?
|
||||
}
|
||||
};
|
||||
Ok(client)
|
||||
} else {
|
||||
let mut first_error = None;
|
||||
let load_cache =
|
||||
strict_tls && (security == Socket::Ssl || security == Socket::Starttls);
|
||||
for resolved_addr in
|
||||
lookup_host_with_cache(context, host, port, "imap", load_cache).await?
|
||||
{
|
||||
let res = match security {
|
||||
Socket::Automatic => bail!("IMAP port security is not configured"),
|
||||
Socket::Ssl => Client::connect_secure(resolved_addr, host, strict_tls).await,
|
||||
Socket::Starttls => {
|
||||
Client::connect_starttls(resolved_addr, host, strict_tls).await
|
||||
}
|
||||
Socket::Plain => Client::connect_insecure(resolved_addr).await,
|
||||
};
|
||||
match res {
|
||||
Ok(client) => {
|
||||
let ip_addr = resolved_addr.ip().to_string();
|
||||
if load_cache {
|
||||
update_connect_timestamp(context, host, &ip_addr).await?;
|
||||
}
|
||||
update_connection_history(context, "imap", host, port, &ip_addr, time())
|
||||
.await?;
|
||||
return Ok(client);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(context, "Failed to connect to {resolved_addr}: {err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(first_error.unwrap_or_else(|| format_err!("no DNS resolution results for {host}")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_secure(addr: SocketAddr, hostname: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tls_stream = connect_tls_inner(addr, hostname, strict_tls, alpn(addr.port())).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -116,8 +178,8 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_insecure(context: &Context, hostname: &str, port: u16) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, false).await?;
|
||||
async fn connect_insecure(addr: SocketAddr) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
let buffered_stream = BufWriter::new(tcp_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -128,17 +190,12 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_starttls(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp(context, hostname, port, IMAP_TIMEOUT, strict_tls).await?;
|
||||
async fn connect_starttls(addr: SocketAddr, host: &str, strict_tls: bool) -> Result<Self> {
|
||||
let tcp_stream = connect_tcp_inner(addr).await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
let buffered_tcp_stream = BufWriter::new(tcp_stream);
|
||||
let mut client = ImapClient::new(buffered_tcp_stream);
|
||||
let mut client = async_imap::Client::new(buffered_tcp_stream);
|
||||
let _greeting = client
|
||||
.read_response()
|
||||
.await
|
||||
@@ -150,7 +207,7 @@ impl Client {
|
||||
let buffered_tcp_stream = client.into_inner();
|
||||
let tcp_stream = buffered_tcp_stream.into_inner();
|
||||
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, tcp_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, host, &[], tcp_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
|
||||
@@ -160,7 +217,7 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_secure_socks5(
|
||||
async fn connect_secure_socks5(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
@@ -168,9 +225,9 @@ impl Client {
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, strict_tls)
|
||||
.connect(context, domain, port, strict_tls)
|
||||
.await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, socks5_stream).await?;
|
||||
let tls_stream = wrap_tls(strict_tls, domain, alpn(port), socks5_stream).await?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -181,15 +238,13 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_insecure_socks5(
|
||||
async fn connect_insecure_socks5(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
port: u16,
|
||||
socks5_config: Socks5Config,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, domain, port, IMAP_TIMEOUT, false)
|
||||
.await?;
|
||||
let socks5_stream = socks5_config.connect(context, domain, port, false).await?;
|
||||
let buffered_stream = BufWriter::new(socks5_stream);
|
||||
let session_stream: Box<dyn SessionStream> = Box::new(buffered_stream);
|
||||
let mut client = Client::new(session_stream);
|
||||
@@ -200,7 +255,7 @@ impl Client {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn connect_starttls_socks5(
|
||||
async fn connect_starttls_socks5(
|
||||
context: &Context,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
@@ -208,7 +263,7 @@ impl Client {
|
||||
strict_tls: bool,
|
||||
) -> Result<Self> {
|
||||
let socks5_stream = socks5_config
|
||||
.connect(context, hostname, port, IMAP_TIMEOUT, strict_tls)
|
||||
.connect(context, hostname, port, strict_tls)
|
||||
.await?;
|
||||
|
||||
// Run STARTTLS command and convert the client back into a stream.
|
||||
@@ -225,7 +280,7 @@ impl Client {
|
||||
let buffered_socks5_stream = client.into_inner();
|
||||
let socks5_stream: Socks5Stream<_> = buffered_socks5_stream.into_inner();
|
||||
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, socks5_stream)
|
||||
let tls_stream = wrap_tls(strict_tls, hostname, &[], socks5_stream)
|
||||
.await
|
||||
.context("STARTTLS upgrade failed")?;
|
||||
let buffered_stream = BufWriter::new(tls_stream);
|
||||
|
||||
@@ -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
|
||||
@@ -29,9 +30,13 @@ impl Session {
|
||||
) -> Result<Self> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
self.select_folder(context, Some(folder)).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
if self.server_sent_unsolicited_exists(context)? {
|
||||
self.new_mail = true;
|
||||
}
|
||||
|
||||
if self.new_mail {
|
||||
return Ok(self);
|
||||
}
|
||||
|
||||
@@ -47,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);
|
||||
@@ -89,9 +94,12 @@ 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.
|
||||
self.new_mail = true;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,6 @@ type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("IMAP Connection Lost or no connection established")]
|
||||
ConnectionLost,
|
||||
|
||||
#[error("IMAP Folder name invalid: {0}")]
|
||||
BadFolderName(String),
|
||||
|
||||
#[error("Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}")]
|
||||
NoFolder(String, String),
|
||||
|
||||
@@ -33,7 +27,8 @@ impl ImapSession {
|
||||
/// Issues a CLOSE command if selected folder needs expunge,
|
||||
/// i.e. if Delta Chat marked a message there as deleted previously.
|
||||
///
|
||||
/// CLOSE is considerably faster than an EXPUNGE, see
|
||||
/// CLOSE is considerably faster than an EXPUNGE
|
||||
/// because no EXPUNGE responses are sent, see
|
||||
/// <https://tools.ietf.org/html/rfc3501#section-6.4.2>
|
||||
pub(super) async fn maybe_close_folder(&mut self, context: &Context) -> anyhow::Result<()> {
|
||||
if let Some(folder) = &self.selected_folder {
|
||||
@@ -44,6 +39,7 @@ impl ImapSession {
|
||||
info!(context, "close/expunge succeeded");
|
||||
self.selected_folder = None;
|
||||
self.selected_folder_needs_expunge = false;
|
||||
self.new_mail = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -52,18 +48,12 @@ impl ImapSession {
|
||||
/// Selects a folder, possibly updating uid_validity and, if needed,
|
||||
/// expunging the folder to remove delete-marked messages.
|
||||
/// Returns whether a new folder was selected.
|
||||
pub(crate) async fn select_folder(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: Option<&str>,
|
||||
) -> Result<NewlySelected> {
|
||||
async fn select_folder(&mut self, context: &Context, folder: &str) -> Result<NewlySelected> {
|
||||
// if there is a new folder and the new folder is equal to the selected one, there's nothing to do.
|
||||
// if there is _no_ new folder, we continue as we might want to expunge below.
|
||||
if let Some(folder) = folder {
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
if let Some(selected_folder) = &self.selected_folder {
|
||||
if folder == selected_folder {
|
||||
return Ok(NewlySelected::No);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,34 +61,26 @@ impl ImapSession {
|
||||
self.maybe_close_folder(context).await?;
|
||||
|
||||
// select new folder
|
||||
if let Some(folder) = folder {
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::ConnectionLost) => Err(Error::ConnectionLost),
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => Err(Error::Other(err.to_string())),
|
||||
}
|
||||
let res = if self.can_condstore() {
|
||||
self.select_condstore(folder).await
|
||||
} else {
|
||||
Ok(NewlySelected::No)
|
||||
self.select(folder).await
|
||||
};
|
||||
|
||||
// <https://tools.ietf.org/html/rfc3501#section-6.3.1>
|
||||
// says that if the server reports select failure we are in
|
||||
// authenticated (not-select) state.
|
||||
|
||||
match res {
|
||||
Ok(mailbox) => {
|
||||
self.selected_folder = Some(folder.to_string());
|
||||
self.selected_mailbox = Some(mailbox);
|
||||
Ok(NewlySelected::Yes)
|
||||
}
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => Err(Error::Other(err.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +90,7 @@ impl ImapSession {
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> anyhow::Result<NewlySelected> {
|
||||
match self.select_folder(context, Some(folder)).await {
|
||||
match self.select_folder(context, folder).await {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(..) => {
|
||||
@@ -118,7 +100,7 @@ impl ImapSession {
|
||||
info!(context, "Couldn't select folder, then create() failed: {err:#}.");
|
||||
// Need to recheck, could have been created in parallel.
|
||||
}
|
||||
let select_res = self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {folder}"));
|
||||
let select_res = self.select_folder(context, folder).await.with_context(|| format!("failed to select newely created folder {folder}"));
|
||||
if select_res.is_err() {
|
||||
create_res?;
|
||||
}
|
||||
@@ -134,13 +116,14 @@ impl ImapSession {
|
||||
/// When selecting a folder for the first time, sets the uid_next to the current
|
||||
/// mailbox.uid_next so that no old emails are fetched.
|
||||
///
|
||||
/// Returns Result<new_emails> (i.e. whether new emails arrived),
|
||||
/// if in doubt, returns new_emails=true so emails are fetched.
|
||||
/// Updates `self.new_mail` if folder was previously unselected
|
||||
/// and new mails are detected after selecting,
|
||||
/// i.e. UIDNEXT advanced while the folder was closed.
|
||||
pub(crate) async fn select_with_uidvalidity(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<bool> {
|
||||
) -> Result<()> {
|
||||
let newly_selected = self
|
||||
.select_or_create_folder(context, folder)
|
||||
.await
|
||||
@@ -197,28 +180,26 @@ impl ImapSession {
|
||||
mailbox.uid_next = new_uid_next;
|
||||
|
||||
if new_uid_validity == old_uid_validity {
|
||||
let new_emails = if newly_selected == NewlySelected::No {
|
||||
// The folder was not newly selected i.e. no SELECT command was run. This means that mailbox.uid_next
|
||||
// was not updated and may contain an incorrect value. So, just return true so that
|
||||
// the caller tries to fetch new messages (we could of course run a SELECT command now, but trying to fetch
|
||||
// new messages is only one command, just as a SELECT command)
|
||||
true
|
||||
} else if let Some(new_uid_next) = new_uid_next {
|
||||
if new_uid_next < old_uid_next {
|
||||
warn!(
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
context.schedule_resync().await?;
|
||||
}
|
||||
new_uid_next != old_uid_next // If UIDNEXT changed, there are new emails
|
||||
} else {
|
||||
// We have no UIDNEXT and if in doubt, return true.
|
||||
true
|
||||
};
|
||||
if newly_selected == NewlySelected::Yes {
|
||||
if let Some(new_uid_next) = new_uid_next {
|
||||
if new_uid_next < old_uid_next {
|
||||
warn!(
|
||||
context,
|
||||
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {new_uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
|
||||
);
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
context.schedule_resync().await?;
|
||||
}
|
||||
|
||||
return Ok(new_emails);
|
||||
// If UIDNEXT changed, there are new emails.
|
||||
self.new_mail |= new_uid_next != old_uid_next;
|
||||
} else {
|
||||
warn!(context, "Folder {folder} was just selected but we failed to determine UIDNEXT, assume that it has new mail.");
|
||||
self.new_mail = true;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// UIDVALIDITY is modified, reset highest seen MODSEQ.
|
||||
@@ -229,6 +210,7 @@ impl ImapSession {
|
||||
let new_uid_next = new_uid_next.unwrap_or_default();
|
||||
set_uid_next(context, folder, new_uid_next).await?;
|
||||
set_uidvalidity(context, folder, new_uid_validity).await?;
|
||||
self.new_mail = true;
|
||||
|
||||
// Collect garbage entries in `imap` table.
|
||||
context
|
||||
@@ -251,7 +233,7 @@ impl ImapSession {
|
||||
old_uid_next,
|
||||
old_uid_validity,
|
||||
);
|
||||
Ok(false)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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\
|
||||
)])";
|
||||
|
||||
@@ -40,6 +41,11 @@ pub(crate) struct Session {
|
||||
pub selected_mailbox: Option<Mailbox>,
|
||||
|
||||
pub selected_folder_needs_expunge: bool,
|
||||
|
||||
/// True if currently selected folder has new messages.
|
||||
///
|
||||
/// Should be false if no folder is currently selected.
|
||||
pub new_mail: bool,
|
||||
}
|
||||
|
||||
impl Deref for Session {
|
||||
@@ -67,6 +73,7 @@ impl Session {
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
new_mail: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,24 +16,30 @@
|
||||
//! Both these are transferred in the QR code offered to the getter. This ensures that the
|
||||
//! getter can not connect to an impersonated provider and the provider does not offer the
|
||||
//! download to an impersonated getter.
|
||||
//!
|
||||
//! Protocol starts by getter opening a bidirectional QUIC stream
|
||||
//! to the provider and sending authentication token.
|
||||
//! Provider verifies received authentication token,
|
||||
//! sends the size of all files in a backup (database and all blobs)
|
||||
//! as an unsigned 64-bit big endian integer and streams the backup in tar format.
|
||||
//! Getter receives the backup and acknowledges successful reception
|
||||
//! by sending a single byte.
|
||||
//! Provider closes the endpoint after receiving an acknowledgment.
|
||||
|
||||
use std::future::Future;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Poll;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result};
|
||||
use async_channel::Receiver;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::blobs::Collection;
|
||||
use iroh::get::DataStream;
|
||||
use iroh::progress::ProgressEmitter;
|
||||
use iroh::protocol::AuthToken;
|
||||
use iroh::provider::{DataSource, Event, Provider, Ticket};
|
||||
use iroh::Hash;
|
||||
use iroh_old as iroh;
|
||||
use iroh_net::relay::RelayMode;
|
||||
use iroh_net::Endpoint;
|
||||
use iroh_old;
|
||||
use iroh_old::blobs::Collection;
|
||||
use iroh_old::get::DataStream;
|
||||
use iroh_old::progress::ProgressEmitter;
|
||||
use iroh_old::provider::Ticket;
|
||||
use tokio::fs::{self, File};
|
||||
use tokio::io::{self, AsyncWriteExt, BufWriter};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
@@ -47,19 +48,22 @@ use tokio::task::{JoinHandle, JoinSet};
|
||||
use tokio_stream::wrappers::ReadDirStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::blob::BlobDirContents;
|
||||
use crate::chat::{add_device_msg, delete_and_reset_all_device_msgs};
|
||||
use crate::context::Context;
|
||||
use crate::imex::BlobDirContents;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::qr::{self, Qr};
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::time;
|
||||
use crate::{e2ee, EventType};
|
||||
use crate::tools::{create_id, time, TempPathGuard};
|
||||
use crate::EventType;
|
||||
|
||||
use super::{export_database, DBFILE_BACKUP_NAME};
|
||||
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
|
||||
|
||||
const MAX_CONCURRENT_DIALS: u8 = 16;
|
||||
|
||||
/// ALPN protocol identifier for the backup transfer protocol.
|
||||
const BACKUP_ALPN: &[u8] = b"/deltachat/backup";
|
||||
|
||||
/// Provide or send a backup of this device.
|
||||
///
|
||||
/// This creates a backup of the current device and starts a service which offers another
|
||||
@@ -70,15 +74,21 @@ const MAX_CONCURRENT_DIALS: u8 = 16;
|
||||
///
|
||||
/// This starts a task which acquires the global "ongoing" mutex. If you need to stop the
|
||||
/// task use the [`Context::stop_ongoing`] mechanism.
|
||||
///
|
||||
/// The task implements [`Future`] and awaiting it will complete once a transfer has been
|
||||
/// either completed or aborted.
|
||||
#[derive(Debug)]
|
||||
pub struct BackupProvider {
|
||||
/// The supervisor task, run by [`BackupProvider::watch_provider`].
|
||||
/// iroh-net endpoint.
|
||||
_endpoint: Endpoint,
|
||||
|
||||
/// iroh-net address.
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
|
||||
/// Authentication token that should be submitted
|
||||
/// to retrieve the backup.
|
||||
auth_token: String,
|
||||
|
||||
/// Handle for the task accepting backup transfer requests.
|
||||
handle: JoinHandle<Result<()>>,
|
||||
/// The ticket to retrieve the backup collection.
|
||||
ticket: Ticket,
|
||||
|
||||
/// Guard to cancel the provider on drop.
|
||||
_drop_guard: tokio_util::sync::DropGuard,
|
||||
}
|
||||
@@ -95,9 +105,13 @@ impl BackupProvider {
|
||||
///
|
||||
/// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io
|
||||
pub async fn prepare(context: &Context) -> Result<Self> {
|
||||
e2ee::ensure_secret_key_exists(context)
|
||||
.await
|
||||
.context("Private key not available, aborting backup export")?;
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
let endpoint = Endpoint::builder()
|
||||
.alpns(vec![BACKUP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind(0)
|
||||
.await?;
|
||||
let node_addr = endpoint.node_addr().await?;
|
||||
|
||||
// Acquire global "ongoing" mutex.
|
||||
let cancel_token = context.alloc_ongoing().await?;
|
||||
@@ -105,195 +119,153 @@ impl BackupProvider {
|
||||
let context_dir = context
|
||||
.get_blobdir()
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("Context dir not found"))?;
|
||||
.context("Context dir not found")?;
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
warn!(context, "Previous database export deleted");
|
||||
}
|
||||
let dbfile = TempPathGuard::new(dbfile);
|
||||
let res = tokio::select! {
|
||||
biased;
|
||||
res = Self::prepare_inner(context, &dbfile) => {
|
||||
match res {
|
||||
Ok(slf) => Ok(slf),
|
||||
Err(err) => {
|
||||
error!(context, "Failed to set up second device setup: {:#}", err);
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => Err(format_err!("cancelled")),
|
||||
};
|
||||
let (provider, ticket) = match res {
|
||||
Ok((provider, ticket)) => (provider, ticket),
|
||||
Err(err) => {
|
||||
context.free_ongoing().await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
// Authentication token that receiver should send us to receive a backup.
|
||||
let auth_token = create_id();
|
||||
|
||||
let passphrase = String::new();
|
||||
|
||||
export_database(context, &dbfile, passphrase, time())
|
||||
.await
|
||||
.context("Database export failed")?;
|
||||
context.emit_event(EventType::ImexProgress(300));
|
||||
|
||||
let drop_token = CancellationToken::new();
|
||||
let handle = {
|
||||
let context = context.clone();
|
||||
let drop_token = drop_token.clone();
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = Self::watch_provider(&context, provider, cancel_token, drop_token).await;
|
||||
Self::accept_loop(
|
||||
context.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
cancel_token,
|
||||
drop_token,
|
||||
dbfile,
|
||||
)
|
||||
.await;
|
||||
info!(context, "Finished accept loop.");
|
||||
|
||||
context.free_ongoing().await;
|
||||
|
||||
// Explicit drop to move the guards into this future
|
||||
drop(paused_guard);
|
||||
drop(dbfile);
|
||||
res
|
||||
Ok(())
|
||||
})
|
||||
};
|
||||
Ok(Self {
|
||||
_endpoint: endpoint,
|
||||
node_addr,
|
||||
auth_token,
|
||||
handle,
|
||||
ticket,
|
||||
_drop_guard: drop_token.drop_guard(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates the provider task.
|
||||
///
|
||||
/// Having this as a function makes it easier to cancel it when needed.
|
||||
async fn prepare_inner(context: &Context, dbfile: &Path) -> Result<(Provider, Ticket)> {
|
||||
// Generate the token up front: we also use it to encrypt the database.
|
||||
let token = AuthToken::generate();
|
||||
context.emit_event(SendProgress::Started.into());
|
||||
export_database(context, dbfile, token.to_string(), time())
|
||||
.await
|
||||
.context("Database export failed")?;
|
||||
context.emit_event(SendProgress::DatabaseExported.into());
|
||||
async fn handle_connection(
|
||||
context: Context,
|
||||
conn: iroh_net::endpoint::Connecting,
|
||||
auth_token: String,
|
||||
dbfile: Arc<TempPathGuard>,
|
||||
) -> Result<()> {
|
||||
let conn = conn.await?;
|
||||
let (mut send_stream, mut recv_stream) = conn.accept_bi().await?;
|
||||
|
||||
// Now we can be sure IO is not running.
|
||||
let mut files = vec![DataSource::with_name(
|
||||
dbfile.to_owned(),
|
||||
format!("db/{DBFILE_BACKUP_NAME}"),
|
||||
)];
|
||||
let blobdir = BlobDirContents::new(context).await?;
|
||||
for blob in blobdir.iter() {
|
||||
let path = blob.to_abs_path();
|
||||
let name = format!("blob/{}", blob.as_file_name());
|
||||
files.push(DataSource::with_name(path, name));
|
||||
// Read authentication token from the stream.
|
||||
let mut received_auth_token = vec![0u8; auth_token.len()];
|
||||
recv_stream.read_exact(&mut received_auth_token).await?;
|
||||
if received_auth_token.as_slice() != auth_token.as_bytes() {
|
||||
warn!(context, "Received wrong backup authentication token.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Start listening.
|
||||
let (db, hash) = iroh::provider::create_collection(files).await?;
|
||||
context.emit_event(SendProgress::CollectionCreated.into());
|
||||
let provider = Provider::builder(db)
|
||||
.bind_addr((Ipv4Addr::UNSPECIFIED, 0).into())
|
||||
.auth_token(token)
|
||||
.spawn()?;
|
||||
context.emit_event(SendProgress::ProviderListening.into());
|
||||
info!(context, "Waiting for remote to connect");
|
||||
let ticket = provider.ticket(hash)?;
|
||||
Ok((provider, ticket))
|
||||
info!(context, "Received valid backup authentication token.");
|
||||
|
||||
let blobdir = BlobDirContents::new(&context).await?;
|
||||
|
||||
let mut file_size = 0;
|
||||
file_size += dbfile.metadata()?.len();
|
||||
for blob in blobdir.iter() {
|
||||
file_size += blob.to_abs_path().metadata()?.len()
|
||||
}
|
||||
|
||||
send_stream.write_all(&file_size.to_be_bytes()).await?;
|
||||
|
||||
export_backup_stream(&context, &dbfile, blobdir, send_stream)
|
||||
.await
|
||||
.context("Failed to write backup into QUIC stream")?;
|
||||
info!(context, "Finished writing backup into QUIC stream.");
|
||||
let mut buf = [0u8; 1];
|
||||
info!(context, "Waiting for acknowledgment.");
|
||||
recv_stream.read_exact(&mut buf).await?;
|
||||
info!(context, "Received backup reception acknowledgement.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = backup_transfer_msg_body(&context).await;
|
||||
add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Supervises the iroh [`Provider`], terminating it when needed.
|
||||
///
|
||||
/// This will watch the provider and terminate it when:
|
||||
///
|
||||
/// - A transfer is completed, successful or unsuccessful.
|
||||
/// - An event could not be observed to protect against not knowing of a completed event.
|
||||
/// - The ongoing process is cancelled.
|
||||
///
|
||||
/// The *cancel_token* is the handle for the ongoing process mutex, when this completes
|
||||
/// we must cancel this operation.
|
||||
async fn watch_provider(
|
||||
context: &Context,
|
||||
mut provider: Provider,
|
||||
cancel_token: Receiver<()>,
|
||||
async fn accept_loop(
|
||||
context: Context,
|
||||
endpoint: Endpoint,
|
||||
auth_token: String,
|
||||
cancel_token: async_channel::Receiver<()>,
|
||||
drop_token: CancellationToken,
|
||||
) -> Result<()> {
|
||||
let mut events = provider.subscribe();
|
||||
let mut total_size = 0;
|
||||
let mut current_size = 0;
|
||||
let res = loop {
|
||||
dbfile: TempPathGuard,
|
||||
) {
|
||||
let dbfile = Arc::new(dbfile);
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
res = &mut provider => {
|
||||
break res.context("BackupProvider failed");
|
||||
},
|
||||
maybe_event = events.recv() => {
|
||||
match maybe_event {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
Event::ClientConnected { ..} => {
|
||||
context.emit_event(SendProgress::ClientConnected.into());
|
||||
}
|
||||
Event::RequestReceived { .. } => {
|
||||
}
|
||||
Event::TransferCollectionStarted { total_blobs_size, .. } => {
|
||||
total_size = total_blobs_size;
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
}.into());
|
||||
}
|
||||
Event::TransferBlobCompleted { size, .. } => {
|
||||
current_size += size;
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
}.into());
|
||||
}
|
||||
Event::TransferCollectionCompleted { .. } => {
|
||||
context.emit_event(SendProgress::TransferInProgress {
|
||||
current_size: total_size,
|
||||
total_size
|
||||
}.into());
|
||||
provider.shutdown();
|
||||
}
|
||||
Event::TransferAborted { .. } => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider transfer aborted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Closed) => {
|
||||
// We should never see this, provider.join() should complete
|
||||
// first.
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {
|
||||
// We really shouldn't be lagging, if we did we may have missed
|
||||
// a completion event.
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("Missed events from BackupProvider"));
|
||||
|
||||
conn = endpoint.accept() => {
|
||||
if let Some(conn) = conn {
|
||||
// Got a new in-progress connection.
|
||||
let context = context.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
let dbfile = dbfile.clone();
|
||||
if let Err(err) = Self::handle_connection(context.clone(), conn, auth_token, dbfile).await {
|
||||
warn!(context, "Error while handling backup connection: {err:#}.");
|
||||
} else {
|
||||
info!(context, "Backup transfer finished successfully.");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = cancel_token.recv() => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider cancelled"));
|
||||
},
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
_ = drop_token.cancelled() => {
|
||||
provider.shutdown();
|
||||
break Err(anyhow!("BackupProvider dropped"));
|
||||
context.emit_event(EventType::ImexProgress(0));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
match &res {
|
||||
Ok(_) => {
|
||||
context.emit_event(SendProgress::Completed.into());
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = backup_transfer_msg_body(context).await;
|
||||
add_device_msg(context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "Backup transfer failure: {err:#}");
|
||||
context.emit_event(SendProgress::Failed.into())
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Returns a QR code that allows fetching this backup.
|
||||
///
|
||||
/// This QR code can be passed to [`get_backup`] on a (different) device.
|
||||
pub fn qr(&self) -> Qr {
|
||||
Qr::Backup {
|
||||
ticket: self.ticket.clone(),
|
||||
Qr::Backup2 {
|
||||
node_addr: self.node_addr.clone(),
|
||||
|
||||
auth_token: self.auth_token.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,92 +273,14 @@ impl BackupProvider {
|
||||
impl Future for BackupProvider {
|
||||
type Output = Result<()>;
|
||||
|
||||
/// Waits for the backup transfer to complete.
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
|
||||
Pin::new(&mut self.handle).poll(cx)?
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard which will remove the path when dropped.
|
||||
///
|
||||
/// It implements [`Deref`] it it can be used as a `&Path`.
|
||||
#[derive(Debug)]
|
||||
struct TempPathGuard {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempPathGuard {
|
||||
fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempPathGuard {
|
||||
fn drop(&mut self) {
|
||||
let path = self.path.clone();
|
||||
tokio::spawn(async move {
|
||||
fs::remove_file(&path).await.ok();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TempPathGuard {
|
||||
type Target = Path;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
/// Create [`EventType::ImexProgress`] events using readable names.
|
||||
///
|
||||
/// Plus you get warnings if you don't use all variants.
|
||||
#[derive(Debug)]
|
||||
enum SendProgress {
|
||||
Failed,
|
||||
Started,
|
||||
DatabaseExported,
|
||||
CollectionCreated,
|
||||
ProviderListening,
|
||||
ClientConnected,
|
||||
TransferInProgress { current_size: u64, total_size: u64 },
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl From<SendProgress> for EventType {
|
||||
fn from(source: SendProgress) -> Self {
|
||||
use SendProgress::*;
|
||||
let num: u16 = match source {
|
||||
Failed => 0,
|
||||
Started => 100,
|
||||
DatabaseExported => 300,
|
||||
CollectionCreated => 350,
|
||||
ProviderListening => 400,
|
||||
ClientConnected => 450,
|
||||
TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
} => {
|
||||
// the range is 450..=950
|
||||
450 + ((current_size as f64 / total_size as f64) * 500.).floor() as u16
|
||||
}
|
||||
Completed => 1000,
|
||||
};
|
||||
Self::ImexProgress(num.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contacts a backup provider and receives the backup from it.
|
||||
///
|
||||
/// This uses a QR code to contact another instance of deltachat which is providing a backup
|
||||
/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in
|
||||
/// the QR code and retrieve the backup.
|
||||
///
|
||||
/// This is a long running operation which will only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts one specific variant of it. It
|
||||
/// does avoid having [`iroh::provider::Ticket`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
/// Retrieves backup from a legacy backup provider using iroh 0.4.
|
||||
pub async fn get_legacy_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
ensure!(
|
||||
matches!(qr, Qr::Backup { .. }),
|
||||
"QR code for backup must be of type DCBACKUP"
|
||||
@@ -412,6 +306,64 @@ pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn get_backup2(
|
||||
context: &Context,
|
||||
node_addr: iroh_net::NodeAddr,
|
||||
auth_token: String,
|
||||
) -> Result<()> {
|
||||
let relay_mode = RelayMode::Disabled;
|
||||
|
||||
let endpoint = Endpoint::builder().relay_mode(relay_mode).bind(0).await?;
|
||||
|
||||
let conn = endpoint.connect(node_addr, BACKUP_ALPN).await?;
|
||||
let (mut send_stream, mut recv_stream) = conn.open_bi().await?;
|
||||
info!(context, "Sending backup authentication token.");
|
||||
send_stream.write_all(auth_token.as_bytes()).await?;
|
||||
|
||||
let passphrase = String::new();
|
||||
info!(context, "Starting to read backup from the stream.");
|
||||
|
||||
let mut file_size_buf = [0u8; 8];
|
||||
recv_stream.read_exact(&mut file_size_buf).await?;
|
||||
let file_size = u64::from_be_bytes(file_size_buf);
|
||||
import_backup_stream(context, recv_stream, file_size, passphrase)
|
||||
.await
|
||||
.context("Failed to import backup from QUIC stream")?;
|
||||
info!(context, "Finished importing backup from the stream.");
|
||||
context.emit_event(EventType::ImexProgress(1000));
|
||||
|
||||
// Send an acknowledgement, but ignore the errors.
|
||||
// We have imported backup successfully already.
|
||||
send_stream.write_all(b".").await.ok();
|
||||
send_stream.finish().await.ok();
|
||||
info!(context, "Sent backup reception acknowledgment.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Contacts a backup provider and receives the backup from it.
|
||||
///
|
||||
/// This uses a QR code to contact another instance of deltachat which is providing a backup
|
||||
/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in
|
||||
/// the QR code and retrieve the backup.
|
||||
///
|
||||
/// This is a long running operation which will return only when completed.
|
||||
///
|
||||
/// Using [`Qr`] as argument is a bit odd as it only accepts specific variants of it. It
|
||||
/// does avoid having [`iroh_old::provider::Ticket`] in the primary API however, without
|
||||
/// having to revert to untyped bytes.
|
||||
pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> {
|
||||
match qr {
|
||||
Qr::Backup { .. } => get_legacy_backup(context, qr).await?,
|
||||
Qr::Backup2 {
|
||||
node_addr,
|
||||
auth_token,
|
||||
} => get_backup2(context, node_addr, auth_token).await?,
|
||||
_ => bail!("QR code for backup must be of type DCBACKUP or DCBACKUP2"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> {
|
||||
let ticket = match qr {
|
||||
Qr::Backup { ticket } => ticket,
|
||||
@@ -458,7 +410,7 @@ async fn transfer_from_provider(context: &Context, ticket: &Ticket) -> Result<()
|
||||
|
||||
// Perform the transfer.
|
||||
let keylog = false; // Do not enable rustls SSLKEYLOGFILE env var functionality
|
||||
let stats = iroh::get::run_ticket(
|
||||
let stats = iroh_old::get::run_ticket(
|
||||
ticket,
|
||||
keylog,
|
||||
MAX_CONCURRENT_DIALS,
|
||||
@@ -490,7 +442,7 @@ async fn on_blob(
|
||||
progress: &ProgressEmitter,
|
||||
jobs: &Mutex<JoinSet<()>>,
|
||||
ticket: &Ticket,
|
||||
_hash: Hash,
|
||||
_hash: iroh_old::Hash,
|
||||
mut reader: DataStream,
|
||||
name: String,
|
||||
) -> Result<DataStream> {
|
||||
@@ -672,24 +624,6 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_send_progress() {
|
||||
let cases = [
|
||||
((0, 100), 450),
|
||||
((10, 100), 500),
|
||||
((50, 100), 700),
|
||||
((100, 100), 950),
|
||||
];
|
||||
|
||||
for ((current_size, total_size), progress) in cases {
|
||||
let out = EventType::from(SendProgress::TransferInProgress {
|
||||
current_size,
|
||||
total_size,
|
||||
});
|
||||
assert_eq!(out, EventType::ImexProgress(progress));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_drop_provider() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
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();
|
||||
@@ -885,6 +885,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
@@ -1015,6 +1016,54 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that `location.kml` is not hidden and not seen if it contains a message.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn receive_visible_location_kml() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
receive_imf(
|
||||
&alice,
|
||||
br#"Subject: locations
|
||||
MIME-Version: 1.0
|
||||
To: <alice@example.org>
|
||||
From: <bob@example.net>
|
||||
Date: Tue, 21 Dec 2021 00:00:00 +0000
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <foobar@localhost>
|
||||
Content-Type: multipart/mixed; boundary="U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF"
|
||||
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Text message.
|
||||
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF
|
||||
Content-Type: application/vnd.google-earth.kml+xml
|
||||
Content-Disposition: attachment; filename="location.kml"
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document addr="bob@example.net">
|
||||
<Placemark><Timestamp><when>2021-11-21T00:00:00Z</when></Timestamp><Point><coordinates accuracy="1.0000000000000000">10.00000000000000,20.00000000000000</coordinates></Point></Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
|
||||
--U8BOG8qNXfB0GgLiQ3PKUjlvdIuLRF--"#,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let received_msg = alice.get_last_msg().await;
|
||||
assert_eq!(received_msg.text, "Text message.");
|
||||
assert_eq!(received_msg.state, MessageState::InFresh);
|
||||
|
||||
let locations = get_range(&alice, None, None, 0, 0).await?;
|
||||
assert_eq!(locations.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_locations_to_chat() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
@@ -10,7 +10,7 @@ use crate::provider::Socket;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CertificateChecks {
|
||||
@@ -30,6 +30,7 @@ pub enum CertificateChecks {
|
||||
/// means that provider database setting should be taken.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// `Automatic` is the same as `Strict`.
|
||||
#[default]
|
||||
Automatic = 0,
|
||||
|
||||
Strict = 1,
|
||||
@@ -41,12 +42,6 @@ pub enum CertificateChecks {
|
||||
AcceptInvalidCertificates = 3,
|
||||
}
|
||||
|
||||
impl Default for CertificateChecks {
|
||||
fn default() -> Self {
|
||||
Self::Automatic
|
||||
}
|
||||
}
|
||||
|
||||
/// Login parameters for a single server, either IMAP or SMTP
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ServerLoginParam {
|
||||
@@ -56,10 +51,6 @@ pub struct ServerLoginParam {
|
||||
pub port: u16,
|
||||
pub security: Socket,
|
||||
pub oauth2: bool,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: CertificateChecks,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
@@ -69,6 +60,10 @@ pub struct LoginParam {
|
||||
pub smtp: ServerLoginParam,
|
||||
pub provider: Option<&'static Provider>,
|
||||
pub socks5_config: Option<Socks5Config>,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: CertificateChecks,
|
||||
}
|
||||
|
||||
impl LoginParam {
|
||||
@@ -130,10 +125,15 @@ impl LoginParam {
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
// The setting is named `imap_certificate_checks`
|
||||
// for backwards compatibility,
|
||||
// but now it is a global setting applied to all protocols,
|
||||
// while `smtp_certificate_checks` is ignored.
|
||||
let key = &format!("{prefix}imap_certificate_checks");
|
||||
let imap_certificate_checks =
|
||||
let certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap()
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks)
|
||||
.with_context(|| format!("Invalid {key} value"))?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
@@ -157,14 +157,6 @@ impl LoginParam {
|
||||
.and_then(num_traits::FromPrimitive::from_i32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let key = &format!("{prefix}smtp_certificate_checks");
|
||||
let smtp_certificate_checks =
|
||||
if let Some(certificate_checks) = sql.get_raw_config_int(key).await? {
|
||||
num_traits::FromPrimitive::from_i32(certificate_checks).unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
let key = &format!("{prefix}server_flags");
|
||||
let server_flags = sql.get_raw_config_int(key).await?.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
@@ -186,7 +178,6 @@ impl LoginParam {
|
||||
port: mail_port as u16,
|
||||
security: mail_security,
|
||||
oauth2,
|
||||
certificate_checks: imap_certificate_checks,
|
||||
},
|
||||
smtp: ServerLoginParam {
|
||||
server: send_server,
|
||||
@@ -195,8 +186,8 @@ impl LoginParam {
|
||||
port: send_port as u16,
|
||||
security: send_security,
|
||||
oauth2,
|
||||
certificate_checks: smtp_certificate_checks,
|
||||
},
|
||||
certificate_checks,
|
||||
provider,
|
||||
socks5_config,
|
||||
})
|
||||
@@ -227,7 +218,7 @@ impl LoginParam {
|
||||
.await?;
|
||||
|
||||
let key = &format!("{prefix}imap_certificate_checks");
|
||||
sql.set_raw_config_int(key, self.imap.certificate_checks as i32)
|
||||
sql.set_raw_config_int(key, self.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
let key = &format!("{prefix}send_server");
|
||||
@@ -247,8 +238,9 @@ impl LoginParam {
|
||||
sql.set_raw_config_int(key, self.smtp.security as i32)
|
||||
.await?;
|
||||
|
||||
// This is only saved for compatibility reasons, but never loaded.
|
||||
let key = &format!("{prefix}smtp_certificate_checks");
|
||||
sql.set_raw_config_int(key, self.smtp.certificate_checks as i32)
|
||||
sql.set_raw_config_int(key, self.certificate_checks as i32)
|
||||
.await?;
|
||||
|
||||
// The OAuth2 flag is either set for both IMAP and SMTP or not at all.
|
||||
@@ -259,13 +251,25 @@ impl LoginParam {
|
||||
};
|
||||
sql.set_raw_config_int(key, server_flags).await?;
|
||||
|
||||
if let Some(provider) = self.provider {
|
||||
let key = &format!("{prefix}provider");
|
||||
sql.set_raw_config(key, Some(provider.id)).await?;
|
||||
}
|
||||
let key = &format!("{prefix}provider");
|
||||
sql.set_raw_config(key, self.provider.map(|provider| provider.id))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn strict_tls(&self) -> bool {
|
||||
let user_strict_tls = match self.certificate_checks {
|
||||
CertificateChecks::Automatic => None,
|
||||
CertificateChecks::Strict => Some(true),
|
||||
CertificateChecks::AcceptInvalidCertificates
|
||||
| CertificateChecks::AcceptInvalidCertificates2 => Some(false),
|
||||
};
|
||||
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
|
||||
user_strict_tls
|
||||
.or(provider_strict_tls)
|
||||
.unwrap_or(self.socks5_config.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LoginParam {
|
||||
@@ -275,7 +279,7 @@ impl fmt::Display for LoginParam {
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} imap:{}:{}:{}:{}:{}:cert_{}:{} smtp:{}:{}:{}:{}:{}:cert_{}:{}",
|
||||
"{} imap:{}:{}:{}:{}:{}:{} smtp:{}:{}:{}:{}:{}:{} cert_{}",
|
||||
unset_empty(&self.addr),
|
||||
unset_empty(&self.imap.user),
|
||||
if !self.imap.password.is_empty() {
|
||||
@@ -286,7 +290,6 @@ impl fmt::Display for LoginParam {
|
||||
unset_empty(&self.imap.server),
|
||||
self.imap.port,
|
||||
self.imap.security,
|
||||
self.imap.certificate_checks,
|
||||
if self.imap.oauth2 {
|
||||
"OAUTH2"
|
||||
} else {
|
||||
@@ -301,12 +304,12 @@ impl fmt::Display for LoginParam {
|
||||
unset_empty(&self.smtp.server),
|
||||
self.smtp.port,
|
||||
self.smtp.security,
|
||||
self.smtp.certificate_checks,
|
||||
if self.smtp.oauth2 {
|
||||
"OAUTH2"
|
||||
} else {
|
||||
"AUTH_NORMAL"
|
||||
},
|
||||
self.certificate_checks
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -347,7 +350,6 @@ mod tests {
|
||||
port: 123,
|
||||
security: Socket::Starttls,
|
||||
oauth2: false,
|
||||
certificate_checks: CertificateChecks::Strict,
|
||||
},
|
||||
smtp: ServerLoginParam {
|
||||
server: "smtp.example.com".to_string(),
|
||||
@@ -356,16 +358,24 @@ mod tests {
|
||||
port: 456,
|
||||
security: Socket::Ssl,
|
||||
oauth2: false,
|
||||
certificate_checks: CertificateChecks::AcceptInvalidCertificates,
|
||||
},
|
||||
provider: get_provider_by_id("example.com"),
|
||||
// socks5_config is not saved by `save_to_database`, using default value
|
||||
socks5_config: None,
|
||||
certificate_checks: CertificateChecks::Strict,
|
||||
};
|
||||
|
||||
param.save_as_configured_params(&t).await?;
|
||||
let loaded = LoginParam::load_configured_params(&t).await?;
|
||||
assert_eq!(param, loaded);
|
||||
|
||||
// Remove provider.
|
||||
let param = LoginParam {
|
||||
provider: None,
|
||||
..param
|
||||
};
|
||||
param.save_as_configured_params(&t).await?;
|
||||
let loaded = LoginParam::load_configured_params(&t).await?;
|
||||
assert_eq!(param, loaded);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
336
src/message.rs
336
src/message.rs
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str;
|
||||
|
||||
use anyhow::{ensure, format_err, Context as _, Result};
|
||||
use deltachat_contact_tools::{parse_vcard, VcardContact};
|
||||
@@ -10,13 +11,13 @@ 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::{
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
};
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::contact::{self, Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::debug_logging::set_debug_logging_xdc;
|
||||
use crate::download::DownloadState;
|
||||
@@ -80,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)
|
||||
@@ -102,23 +116,31 @@ impl MsgId {
|
||||
/// We keep some infos to
|
||||
/// 1. not download the same message again
|
||||
/// 2. be able to delete the message on the server if we want to
|
||||
pub async fn trash(self, context: &Context) -> Result<()> {
|
||||
///
|
||||
/// * `on_server`: Delete the message on the server also if it is seen on IMAP later, but only
|
||||
/// if all parts of the message are trashed with this flag. `true` if the user explicitly
|
||||
/// deletes the message. As for trashing a partially downloaded message when replacing it with
|
||||
/// a fully downloaded one, see `receive_imf::add_parts()`.
|
||||
pub async fn trash(self, context: &Context, on_server: bool) -> Result<()> {
|
||||
let chat_id = DC_CHAT_ID_TRASH;
|
||||
let deleted_subst = match on_server {
|
||||
true => ", deleted=1",
|
||||
false => "",
|
||||
};
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is removed here, also change delete_expired_messages() and
|
||||
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='',
|
||||
subject='', txt_raw='',
|
||||
mime_headers='',
|
||||
from_id=0, to_id=0,
|
||||
param=''
|
||||
WHERE id=?;
|
||||
"#,
|
||||
&format!(
|
||||
"UPDATE msgs SET \
|
||||
chat_id=?, txt='', txt_normalized=NULL, \
|
||||
subject='', txt_raw='', \
|
||||
mime_headers='', \
|
||||
from_id=0, to_id=0, \
|
||||
param=''{deleted_subst} \
|
||||
WHERE id=?"
|
||||
),
|
||||
(chat_id, self),
|
||||
)
|
||||
.await?;
|
||||
@@ -510,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,",
|
||||
@@ -520,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()) {
|
||||
@@ -559,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()),
|
||||
@@ -1081,6 +1109,30 @@ impl Message {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes message a vCard-containing message using the specified contacts.
|
||||
pub async fn make_vcard(&mut self, context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||
ensure!(
|
||||
matches!(self.viewtype, Viewtype::File | Viewtype::Vcard),
|
||||
"Wrong viewtype for vCard: {}",
|
||||
self.viewtype,
|
||||
);
|
||||
let vcard = contact::make_vcard(context, contacts).await?;
|
||||
self.set_file_from_bytes(context, "vcard.vcf", vcard.as_bytes(), None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Updates message state from the vCard attachment.
|
||||
pub(crate) async fn try_set_vcard(&mut self, context: &Context, path: &Path) -> Result<()> {
|
||||
let vcard = fs::read(path).await.context("Could not read {path}")?;
|
||||
if let Some(summary) = get_vcard_summary(&vcard) {
|
||||
self.param.set(Param::Summary1, summary);
|
||||
} else {
|
||||
warn!(context, "try_set_vcard: Not a valid DeltaChat vCard.");
|
||||
self.viewtype = Viewtype::File;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set different sender name for a message.
|
||||
/// This overrides the name set by the `set_config()`-option `displayname`.
|
||||
pub fn set_override_sender_name(&mut self, name: Option<String>) {
|
||||
@@ -1124,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.
|
||||
@@ -1140,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(())
|
||||
@@ -1303,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,
|
||||
}
|
||||
|
||||
@@ -1346,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.
|
||||
@@ -1433,7 +1510,7 @@ pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)>
|
||||
"txt" => (Viewtype::File, "text/plain"),
|
||||
"vcard" => (Viewtype::Vcard, "text/vcard"),
|
||||
"vcf" => (Viewtype::Vcard, "text/vcard"),
|
||||
"wav" => (Viewtype::File, "audio/wav"),
|
||||
"wav" => (Viewtype::Audio, "audio/wav"),
|
||||
"weba" => (Viewtype::File, "audio/webm"),
|
||||
"webm" => (Viewtype::Video, "video/webm"),
|
||||
"webp" => (Viewtype::Image, "image/webp"), // iOS via SDWebImage, Android since 4.0
|
||||
@@ -1524,8 +1601,9 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
if msg.location_id > 0 {
|
||||
delete_poi_location(context, msg.location_id).await?;
|
||||
}
|
||||
let on_server = true;
|
||||
msg_id
|
||||
.trash(context)
|
||||
.trash(context, on_server)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {msg_id}"))?;
|
||||
|
||||
@@ -1613,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",
|
||||
@@ -1626,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,
|
||||
))
|
||||
},
|
||||
@@ -1643,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
|
||||
{
|
||||
@@ -1683,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(())
|
||||
}
|
||||
@@ -1714,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=''",
|
||||
@@ -1869,23 +1962,26 @@ pub async fn estimate_deletion_cnt(
|
||||
Ok(cnt)
|
||||
}
|
||||
|
||||
/// See [`rfc724_mid_exists_and()`].
|
||||
/// See [`rfc724_mid_exists_ex()`].
|
||||
pub(crate) async fn rfc724_mid_exists(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
) -> Result<Option<(MsgId, i64)>> {
|
||||
rfc724_mid_exists_and(context, rfc724_mid, "1").await
|
||||
Ok(rfc724_mid_exists_ex(context, rfc724_mid, "1")
|
||||
.await?
|
||||
.map(|(id, ts_sent, _)| (id, ts_sent)))
|
||||
}
|
||||
|
||||
/// Returns [MsgId] and "sent" timestamp of the message with given `rfc724_mid` (Message-ID header)
|
||||
/// if it exists in the db.
|
||||
/// Returns [MsgId] and "sent" timestamp of the most recent message with given `rfc724_mid`
|
||||
/// (Message-ID header) and bool `expr` result if such messages exists in the db.
|
||||
///
|
||||
/// @param cond SQL subexpression for filtering messages.
|
||||
pub(crate) async fn rfc724_mid_exists_and(
|
||||
/// * `expr`: SQL expression additionally passed into `SELECT`. Evaluated to `true` iff it is true
|
||||
/// for all messages with the given `rfc724_mid`.
|
||||
pub(crate) async fn rfc724_mid_exists_ex(
|
||||
context: &Context,
|
||||
rfc724_mid: &str,
|
||||
cond: &str,
|
||||
) -> Result<Option<(MsgId, i64)>> {
|
||||
expr: &str,
|
||||
) -> Result<Option<(MsgId, i64, bool)>> {
|
||||
let rfc724_mid = rfc724_mid.trim_start_matches('<').trim_end_matches('>');
|
||||
if rfc724_mid.is_empty() {
|
||||
warn!(context, "Empty rfc724_mid passed to rfc724_mid_exists");
|
||||
@@ -1895,13 +1991,15 @@ pub(crate) async fn rfc724_mid_exists_and(
|
||||
let res = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
&("SELECT id, timestamp_sent FROM msgs WHERE rfc724_mid=? AND ".to_string() + cond),
|
||||
&("SELECT id, timestamp_sent, MIN(".to_string()
|
||||
+ expr
|
||||
+ ") FROM msgs WHERE rfc724_mid=? ORDER BY timestamp_sent DESC"),
|
||||
(rfc724_mid,),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let timestamp_sent: i64 = row.get(1)?;
|
||||
|
||||
Ok((msg_id, timestamp_sent))
|
||||
let expr_res: bool = row.get(2)?;
|
||||
Ok((msg_id, timestamp_sent, expr_res))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -1909,21 +2007,43 @@ pub(crate) async fn rfc724_mid_exists_and(
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Given a list of Message-IDs, returns the latest message found in the database.
|
||||
/// Given a list of Message-IDs, returns the most relevant message found in the database.
|
||||
///
|
||||
/// Relevance here is `(download_state == Done, index)`, where `index` is an index of Message-ID in
|
||||
/// `mids`. This means Message-IDs should be ordered from the least late to the latest one (like in
|
||||
/// the References header).
|
||||
/// Only messages that are not in the trash chat are considered.
|
||||
pub(crate) async fn get_latest_by_rfc724_mids(
|
||||
pub(crate) async fn get_by_rfc724_mids(
|
||||
context: &Context,
|
||||
mids: &[String],
|
||||
) -> Result<Option<Message>> {
|
||||
let mut latest = None;
|
||||
for id in mids.iter().rev() {
|
||||
if let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? {
|
||||
if let Some(msg) = Message::load_from_db_optional(context, msg_id).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
|
||||
continue;
|
||||
};
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
continue;
|
||||
};
|
||||
if msg.download_state == DownloadState::Done {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
latest.get_or_insert(msg);
|
||||
}
|
||||
Ok(None)
|
||||
Ok(latest)
|
||||
}
|
||||
|
||||
/// Returns the 1st part of summary text (i.e. before the dash if any) for a valid DeltaChat vCard.
|
||||
pub(crate) fn get_vcard_summary(vcard: &[u8]) -> Option<String> {
|
||||
let vcard = str::from_utf8(vcard).ok()?;
|
||||
let contacts = deltachat_contact_tools::parse_vcard(vcard);
|
||||
let [c] = &contacts[..] else {
|
||||
return None;
|
||||
};
|
||||
if !deltachat_contact_tools::may_be_valid_addr(&c.addr) {
|
||||
return None;
|
||||
}
|
||||
Some(c.display_name().to_string())
|
||||
}
|
||||
|
||||
/// How a message is primarily displayed.
|
||||
@@ -2025,6 +2145,15 @@ impl Viewtype {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns text for storing in the `msgs.txt_normalized` column (to make case-insensitive search
|
||||
/// possible for non-ASCII messages).
|
||||
pub(crate) fn normalize_text(text: &str) -> Option<String> {
|
||||
if text.is_ascii() {
|
||||
return None;
|
||||
};
|
||||
Some(text.to_lowercase()).filter(|t| t != text)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
@@ -2255,12 +2384,23 @@ mod tests {
|
||||
// Alice quotes encrypted message in unencrypted chat.
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg.set_text("unencrypted".to_string());
|
||||
chat::send_msg(alice, alice_group, &mut msg).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "...");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
// Alice replaces a quote of encrypted message with a quote of unencrypted one.
|
||||
let mut msg1 = Message::new(Viewtype::Text);
|
||||
msg1.set_quote(alice, Some(&alice_received_message)).await?;
|
||||
msg1.set_quote(alice, Some(&msg)).await?;
|
||||
chat::send_msg(alice, alice_group, &mut msg1).await?;
|
||||
|
||||
let bob_received_message = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(bob_received_message.quoted_text().unwrap(), "unencrypted");
|
||||
assert_eq!(bob_received_message.get_showpadlock(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2292,6 +2432,7 @@ mod tests {
|
||||
async fn test_set_override_sender_name() {
|
||||
// send message with overridden sender name
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice2 = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let contact_id = *chat::get_chat_contacts(&alice, chat.id)
|
||||
@@ -2311,6 +2452,7 @@ mod tests {
|
||||
assert_eq!(msg.get_sender_name(&contact), "over ride".to_string());
|
||||
assert_ne!(contact.get_display_name(), "over ride".to_string());
|
||||
chat::send_msg(&alice, chat.id, &mut msg).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// bob receives that message
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
@@ -2320,7 +2462,7 @@ mod tests {
|
||||
.first()
|
||||
.unwrap();
|
||||
let contact = Contact::get_by_id(&bob, contact_id).await.unwrap();
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.chat_id, chat.id);
|
||||
assert_eq!(msg.text, "bla blubb");
|
||||
assert_eq!(
|
||||
@@ -2334,6 +2476,13 @@ mod tests {
|
||||
// (mailing lists may also use `Sender:`-header)
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await.unwrap();
|
||||
assert_ne!(chat.typ, Chattype::Mailinglist);
|
||||
|
||||
// Alice receives message on another device.
|
||||
let msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(
|
||||
msg.get_override_sender_name(),
|
||||
Some("over ride".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2445,9 +2594,6 @@ mod tests {
|
||||
let payload = alice.pop_sent_msg().await;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutDelivered).await;
|
||||
|
||||
update_msg_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutMdnRcvd).await;
|
||||
|
||||
set_msg_failed(&alice, &mut alice_msg, "badly failed").await?;
|
||||
assert_state(&alice, alice_msg.id, MessageState::OutFailed).await;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user