mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 14:02:10 +03:00
Compare commits
67 Commits
link2xt/py
...
v1.141.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
8
.github/workflows/ci.yml
vendored
8
.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.79.0
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -95,11 +95,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.79.0
|
||||
- os: windows-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.79.0
|
||||
- os: macos-latest
|
||||
rust: 1.78.0
|
||||
rust: 1.79.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.77.0
|
||||
- os: ubuntu-latest
|
||||
|
||||
122
CHANGELOG.md
122
CHANGELOG.md
@@ -1,5 +1,123 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
@@ -4371,3 +4489,7 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[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
|
||||
|
||||
223
Cargo.lock
generated
223
Cargo.lock
generated
@@ -17,9 +17,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||
checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
@@ -174,9 +174,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.82"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
@@ -251,7 +251,7 @@ checksum = "7378575ff571966e99a744addeff0bff98b8ada0dedf1956d59e634db95eaac1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
"synstructure 0.13.1",
|
||||
]
|
||||
|
||||
@@ -274,7 +274,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -302,12 +302,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.2.1"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener 5.2.0",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -332,7 +331,7 @@ version = "0.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98892ebee4c05fc66757e600a7466f0d9bfcde338f645d64add323789f26cb36"
|
||||
dependencies = [
|
||||
"async-channel 2.2.1",
|
||||
"async-channel 2.3.1",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"chrono",
|
||||
@@ -394,7 +393,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -499,9 +498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.71"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cc",
|
||||
@@ -780,9 +779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
||||
|
||||
[[package]]
|
||||
name = "cfb-mode"
|
||||
@@ -933,9 +932,9 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -1152,7 +1151,7 @@ dependencies = [
|
||||
"aead",
|
||||
"chacha20",
|
||||
"crypto_secretbox",
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"salsa20",
|
||||
"serdect",
|
||||
"subtle",
|
||||
@@ -1190,16 +1189,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.2"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"platforms",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
@@ -1213,7 +1211,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1324,12 +1322,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
"async-channel 2.2.1",
|
||||
"async-channel 2.3.1",
|
||||
"async-imap",
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
@@ -1418,10 +1416,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.2.1",
|
||||
"async-channel 2.3.1",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"deltachat",
|
||||
@@ -1443,7 +1441,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1458,7 +1456,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1482,12 +1480,12 @@ name = "deltachat_derive"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1575,7 +1573,7 @@ checksum = "5fe87ce4529967e0ba1dcf8450bab64d97dfd5010a6256187ffe2e43e6f0e049"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1649,7 +1647,7 @@ checksum = "2bba3e9872d7c58ce7ef0fcf1844fcc3e23ef2a58377b50df35dd98e42a5726e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1739,7 +1737,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1884,7 +1882,7 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"ed25519 2.2.3",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
@@ -2079,7 +2077,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2092,7 +2090,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2112,7 +2110,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2214,9 +2212,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291"
|
||||
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
|
||||
dependencies = [
|
||||
"event-listener 5.2.0",
|
||||
"pin-project-lite",
|
||||
@@ -2501,7 +2499,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2603,9 +2601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.28.1"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
@@ -3555,9 +3553,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.153"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -3981,7 +3979,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4006,9 +4004,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"libm",
|
||||
@@ -4042,14 +4040,14 @@ dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.32.2"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -4113,7 +4111,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4245,9 +4243,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
|
||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
@@ -4346,7 +4344,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4381,7 +4379,7 @@ dependencies = [
|
||||
"cipher",
|
||||
"const-oid",
|
||||
"crc24",
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"derive_builder",
|
||||
"des",
|
||||
"digest 0.10.7",
|
||||
@@ -4433,7 +4431,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4514,12 +4512,6 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "platforms"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.5"
|
||||
@@ -4566,7 +4558,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4744,9 +4736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -4771,7 +4763,7 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5387,9 +5379,9 @@ checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
@@ -5585,9 +5577,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.19"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
|
||||
checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"schemars_derive",
|
||||
@@ -5597,14 +5589,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.19"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
|
||||
checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5691,9 +5683,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.200"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
|
||||
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -5718,13 +5710,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.200"
|
||||
version = "1.0.203"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
|
||||
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5735,14 +5727,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -5761,9 +5753,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -6093,7 +6085,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"struct_iterable_internal",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6121,7 +6113,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6183,9 +6175,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
version = "2.0.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6224,7 +6216,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6334,22 +6326,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.59"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.59"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6431,9 +6423,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
version = "1.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -6460,13 +6452,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6583,35 +6575,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.10"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
|
||||
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.12"
|
||||
version = "0.8.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
|
||||
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.9",
|
||||
"toml_edit 0.22.13",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -6629,9 +6620,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.9"
|
||||
version = "0.22.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
|
||||
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -6688,7 +6679,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7023,7 +7014,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -7057,7 +7048,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -7240,7 +7231,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7251,7 +7242,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7499,7 +7490,7 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
@@ -7657,7 +7648,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7677,7 +7668,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
62
Cargo.toml
62
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.77"
|
||||
@@ -40,22 +40,22 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.0"
|
||||
async-channel = "2.2.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"
|
||||
@@ -66,12 +66,12 @@ iroh-gossip = { version = "0.17.0", features = ["net"] }
|
||||
quinn = "0.10.0"
|
||||
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"
|
||||
@@ -79,14 +79,14 @@ pgp = { version = "0.11", default-features = false }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.31"
|
||||
quoted_printable = "0.5"
|
||||
rand = "0.8"
|
||||
rand = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
reqwest = { version = "0.11.27", 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"
|
||||
@@ -94,26 +94,26 @@ 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"] }
|
||||
|
||||
[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"
|
||||
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]
|
||||
@@ -159,10 +159,28 @@ 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 }
|
||||
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"] }
|
||||
sanitize-filename = "0.5"
|
||||
serde_json = "1"
|
||||
serde = "1.0"
|
||||
tempfile = "3.10.1"
|
||||
thiserror = "1"
|
||||
tokio = "1.38.0"
|
||||
tokio-util = "0.7.11"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.5.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -16,16 +16,16 @@ crate-type = ["cdylib", "staticlib"]
|
||||
[dependencies]
|
||||
deltachat = { path = "../", default-features = false }
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||
libc = "0.2"
|
||||
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"]
|
||||
|
||||
@@ -481,8 +481,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 +494,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.
|
||||
@@ -6648,6 +6649,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
///
|
||||
/// Used as message text of outgoing read receipts.
|
||||
/// - %1$s will be replaced by the subject of the displayed message
|
||||
///
|
||||
/// @deprecated Deprecated 2024-06-23, use DC_STR_READRCPT_MAILBODY2 instead.
|
||||
#define DC_STR_READRCPT_MAILBODY 32
|
||||
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
@@ -7366,7 +7369,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "Contact"
|
||||
/// "The message is a receipt notification."
|
||||
///
|
||||
/// Used as message text of outgoing read receipts.
|
||||
#define DC_STR_READRCPT_MAILBODY2 192
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
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"
|
||||
anyhow = { workspace = true }
|
||||
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"] }
|
||||
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 }
|
||||
|
||||
[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
|
||||
@@ -1476,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
|
||||
// ---------------------------------------------
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.140.0"
|
||||
"version": "1.141.1"
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
ansi_term = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.21"
|
||||
rusqlite = "0.31"
|
||||
log = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
rustyline = "14"
|
||||
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
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:
|
||||
|
||||
@@ -96,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)
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ def wait_receive_realtime_data(msg_data_list):
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
for i, (msg, data) in enumerate(msg_data_list):
|
||||
if msg.id == event.msg_id:
|
||||
assert data == event.data
|
||||
assert list(data) == event.data
|
||||
log(f"msg {msg.id}: got correct realtime data {data}")
|
||||
del msg_data_list[i]
|
||||
break
|
||||
@@ -184,3 +184,26 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
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")
|
||||
|
||||
@@ -7,7 +7,7 @@ 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()
|
||||
@@ -46,7 +46,7 @@ def test_qr_securejoin(acfactory, 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.
|
||||
@@ -91,7 +91,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 +106,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 +168,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 +188,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 +205,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 +252,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 +269,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 +336,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 +346,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 +359,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 +381,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 +402,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 +425,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 +464,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 +520,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 +589,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()
|
||||
|
||||
|
||||
@@ -28,5 +28,5 @@ commands =
|
||||
|
||||
[pytest]
|
||||
timeout = 300
|
||||
#log_cli = true
|
||||
log_cli = true
|
||||
log_level = debug
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.140.0"
|
||||
version = "1.141.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -13,15 +13,15 @@ categories = ["cryptography", "std", "email"]
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
|
||||
deltachat = { path = "..", default-features = false }
|
||||
|
||||
anyhow = "1"
|
||||
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"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||
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
|
||||
|
||||
@@ -11,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() {
|
||||
@@ -25,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());
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.140.0"
|
||||
"version": "1.141.1"
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -266,6 +266,7 @@ module.exports = {
|
||||
DC_STR_REACTED_BY: 177,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_READRCPT_MAILBODY2: 192,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
|
||||
DC_STR_REPLY_NOUN: 90,
|
||||
|
||||
@@ -266,6 +266,7 @@ export enum C {
|
||||
DC_STR_REACTED_BY = 177,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_READRCPT_MAILBODY2 = 192,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
"test:mocha": "mocha node/test/test.mjs --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.140.0"
|
||||
"version": "1.141.1"
|
||||
}
|
||||
|
||||
@@ -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.140.0"
|
||||
version = "1.141.1"
|
||||
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
|
||||
|
||||
@@ -1 +1 @@
|
||||
2024-06-04
|
||||
2024-06-27
|
||||
@@ -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.79.0
|
||||
|
||||
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 += (
|
||||
|
||||
@@ -6,7 +6,7 @@ set -euo pipefail
|
||||
export TZ=UTC
|
||||
|
||||
# Provider database revision.
|
||||
REV=2f3db24107e4802c2df0aa0a40f0e144006c0a9b
|
||||
REV=828e5ddc7e6609b582fbd7f063cc3f60b580ce96
|
||||
|
||||
CORE_ROOT="$PWD"
|
||||
TMP="$(mktemp -d)"
|
||||
|
||||
387
src/blob.rs
387
src/blob.rs
@@ -1101,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();
|
||||
}
|
||||
@@ -1135,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);
|
||||
@@ -1155,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);
|
||||
@@ -1176,64 +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();
|
||||
|
||||
send_image_check_mediaquality(
|
||||
Viewtype::File,
|
||||
Some("1"),
|
||||
SendImageCheckMediaquality {
|
||||
viewtype: Viewtype::File,
|
||||
media_quality_config: "1",
|
||||
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();
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -1244,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();
|
||||
}
|
||||
@@ -1263,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();
|
||||
}
|
||||
@@ -1296,71 +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 (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
||||
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?;
|
||||
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)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
79
src/chat.rs
79
src/chat.rs
@@ -49,7 +49,6 @@ use crate::tools::{
|
||||
create_smeared_timestamps, get_abs_path, gm2local_offset, improve_single_line_input,
|
||||
smeared_time, time, IsNoneOrEmpty, SystemTime,
|
||||
};
|
||||
use crate::webxdc::WEBXDC_SUFFIX;
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
@@ -894,8 +893,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 +927,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 +957,11 @@ impl ChatId {
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
param,
|
||||
hidden,
|
||||
mime_in_reply_to)
|
||||
VALUES (?,?,?, ?,?,?,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?);",
|
||||
(
|
||||
self,
|
||||
ContactId::SELF,
|
||||
@@ -956,6 +969,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(),
|
||||
@@ -2064,7 +2078,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=?
|
||||
@@ -2078,6 +2092,7 @@ impl Chat {
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -2106,6 +2121,7 @@ impl Chat {
|
||||
type,
|
||||
state,
|
||||
txt,
|
||||
txt_normalized,
|
||||
subject,
|
||||
param,
|
||||
hidden,
|
||||
@@ -2117,7 +2133,7 @@ impl Chat {
|
||||
location_id,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?);",
|
||||
params_slice![
|
||||
msg.rfc724_mid,
|
||||
msg.chat_id,
|
||||
@@ -2127,6 +2143,7 @@ impl Chat {
|
||||
msg.viewtype,
|
||||
msg.state,
|
||||
msg.text,
|
||||
message::normalize_text(&msg.text),
|
||||
&msg.subject,
|
||||
msg.param.to_string(),
|
||||
msg.hidden,
|
||||
@@ -2649,6 +2666,10 @@ 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 !send_as_is
|
||||
&& (msg.viewtype == Viewtype::Image
|
||||
@@ -2887,7 +2908,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();
|
||||
|
||||
@@ -3760,7 +3781,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));
|
||||
@@ -3916,8 +3940,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(
|
||||
@@ -3929,17 +3952,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
|
||||
@@ -4346,9 +4376,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,
|
||||
@@ -4359,6 +4390,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,
|
||||
),
|
||||
@@ -4462,8 +4494,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),
|
||||
@@ -4474,6 +4506,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(),
|
||||
@@ -4518,8 +4551,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);
|
||||
@@ -4611,7 +4644,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)
|
||||
@@ -5023,6 +5056,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;
|
||||
@@ -5075,6 +5109,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;
|
||||
|
||||
@@ -378,14 +378,14 @@ 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;
|
||||
}
|
||||
// NB: We don't restart IO from the synchronisation code, so `MvboxMove` isn't effective
|
||||
// immediately if `ConfiguredMvboxFolder` is unset, but only after a reconnect (see
|
||||
// `Imap::prepare()`).
|
||||
matches!(
|
||||
self,
|
||||
Self::Displayname
|
||||
| Self::MdnsEnabled
|
||||
| Self::MvboxMove
|
||||
| Self::ShowEmails
|
||||
| Self::Selfavatar
|
||||
| Self::Selfstatus,
|
||||
@@ -493,6 +493,14 @@ impl Context {
|
||||
.is_some())
|
||||
}
|
||||
|
||||
/// Returns true if sync messages should be sent.
|
||||
///
|
||||
/// This requires that both `SyncMsgs` and `BccSelf` settings are enabled.
|
||||
pub(crate) async fn should_send_sync_msgs(&self) -> Result<bool> {
|
||||
Ok(self.get_config_bool(Config::SyncMsgs).await?
|
||||
&& self.get_config_bool(Config::BccSelf).await?)
|
||||
}
|
||||
|
||||
/// Gets configured "delete_server_after" value.
|
||||
///
|
||||
/// `None` means never delete the message, `Some(0)` means delete
|
||||
@@ -600,7 +608,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 {
|
||||
@@ -973,15 +981,12 @@ mod tests {
|
||||
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?;
|
||||
|
||||
@@ -459,6 +459,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 900);
|
||||
|
||||
if imap_session.is_chatmail() {
|
||||
ctx.set_config(Config::IsChatmail, Some("1")).await?;
|
||||
ctx.set_config(Config::SentboxWatch, None).await?;
|
||||
ctx.set_config(Config::MvboxMove, Some("0")).await?;
|
||||
ctx.set_config(Config::OnlyFetchMvbox, None).await?;
|
||||
@@ -512,8 +513,8 @@ 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,
|
||||
@@ -624,14 +625,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))
|
||||
}
|
||||
}
|
||||
@@ -665,13 +666,13 @@ async fn try_smtp_one_param(
|
||||
.connect(context, param, socks5_config, addr, provider_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(())
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ impl Context {
|
||||
.as_ref()
|
||||
.filter(|quota| {
|
||||
time_elapsed("a.modified)
|
||||
> Duration::from_secs(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
< Duration::from_secs(DC_BACKGROUND_FETCH_QUOTA_CHECK_RATELIMIT)
|
||||
})
|
||||
.is_none()
|
||||
};
|
||||
@@ -1259,12 +1259,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());
|
||||
}
|
||||
@@ -1280,7 +1280,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"),
|
||||
@@ -1316,7 +1316,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"),
|
||||
@@ -1346,7 +1346,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))
|
||||
@@ -1721,6 +1721,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);
|
||||
@@ -1733,6 +1735,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());
|
||||
|
||||
@@ -184,7 +184,7 @@ impl Session {
|
||||
bail!("Attempt to fetch UID 0");
|
||||
}
|
||||
|
||||
self.select_folder(context, 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
|
||||
|
||||
37
src/imap.rs
37
src/imap.rs
@@ -313,7 +313,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 {
|
||||
@@ -543,15 +543,16 @@ impl Imap {
|
||||
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 +599,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 +772,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 +841,7 @@ impl Session {
|
||||
// Collect pairs of UID and Message-ID.
|
||||
let mut msgs = BTreeMap::new();
|
||||
|
||||
self.select_folder(context, folder).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
let mut list = self
|
||||
.uid_fetch("1:*", RFC724MID_UID)
|
||||
@@ -1039,7 +1042,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, folder).await?;
|
||||
self.select_with_uidvalidity(context, folder).await?;
|
||||
|
||||
// Empty target folder name means messages should be deleted.
|
||||
if target.is_empty() {
|
||||
@@ -1087,7 +1090,7 @@ impl Session {
|
||||
.await?;
|
||||
|
||||
for (folder, rowid_set, uid_set) in UidGrouper::from(rows) {
|
||||
self.select_folder(context, &folder)
|
||||
self.select_with_uidvalidity(context, &folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
@@ -1131,7 +1134,7 @@ impl Session {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.select_folder(context, folder)
|
||||
self.select_with_uidvalidity(context, folder)
|
||||
.await
|
||||
.context("failed to select folder")?;
|
||||
|
||||
|
||||
@@ -29,9 +29,13 @@ impl Session {
|
||||
) -> Result<Self> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
self.select_folder(context, 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);
|
||||
}
|
||||
|
||||
@@ -92,6 +96,9 @@ impl Session {
|
||||
session.as_mut().set_read_timeout(Some(IMAP_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,11 +48,7 @@ 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: &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(selected_folder) = &self.selected_folder {
|
||||
@@ -85,10 +77,6 @@ impl ImapSession {
|
||||
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))
|
||||
}
|
||||
@@ -128,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
|
||||
@@ -191,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.
|
||||
@@ -223,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
|
||||
@@ -245,7 +233,7 @@ impl ImapSession {
|
||||
old_uid_next,
|
||||
old_uid_validity,
|
||||
);
|
||||
Ok(false)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,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 +72,7 @@ impl Session {
|
||||
selected_folder: None,
|
||||
selected_mailbox: None,
|
||||
selected_folder_needs_expunge: false,
|
||||
new_mail: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
132
src/message.rs
132
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};
|
||||
@@ -16,7 +17,7 @@ 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;
|
||||
@@ -102,23 +103,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?;
|
||||
@@ -1081,6 +1090,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>) {
|
||||
@@ -1524,8 +1557,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}"))?;
|
||||
|
||||
@@ -1869,23 +1903,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 +1932,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 +1948,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 +2086,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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,8 @@ use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::{
|
||||
self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype,
|
||||
self, get_vcard_summary, set_msg_failed, update_msg_state, Message, MessageState, MsgId,
|
||||
Viewtype,
|
||||
};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::Peerstate;
|
||||
@@ -302,7 +303,7 @@ impl MimeMessage {
|
||||
// them in signed-only emails, but has no value currently.
|
||||
Self::remove_secured_headers(&mut headers);
|
||||
|
||||
let from = from.context("No from in message")?;
|
||||
let mut from = from.context("No from in message")?;
|
||||
let private_keyring = load_self_secret_keyring(context).await?;
|
||||
|
||||
let mut decryption_info =
|
||||
@@ -398,6 +399,7 @@ impl MimeMessage {
|
||||
if let (Some(inner_from), true) = (inner_from, !signatures.is_empty()) {
|
||||
if addr_cmp(&inner_from.addr, &from.addr) {
|
||||
from_is_signed = true;
|
||||
from = inner_from;
|
||||
} else {
|
||||
// There is a From: header in the encrypted &
|
||||
// signed part, but it doesn't match the outer one.
|
||||
@@ -1233,6 +1235,7 @@ impl MimeMessage {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let mut part = Part::default();
|
||||
let msg_type = if context
|
||||
.is_webxdc_file(filename, decoded_data)
|
||||
.await
|
||||
@@ -1276,6 +1279,13 @@ impl MimeMessage {
|
||||
.unwrap_or_default();
|
||||
self.webxdc_status_update = Some(serialized);
|
||||
return Ok(());
|
||||
} else if msg_type == Viewtype::Vcard {
|
||||
if let Some(summary) = get_vcard_summary(decoded_data) {
|
||||
part.param.set(Param::Summary1, summary);
|
||||
msg_type
|
||||
} else {
|
||||
Viewtype::File
|
||||
}
|
||||
} else {
|
||||
msg_type
|
||||
};
|
||||
@@ -1295,8 +1305,6 @@ impl MimeMessage {
|
||||
};
|
||||
info!(context, "added blobfile: {:?}", blob.as_name());
|
||||
|
||||
/* create and register Mime part referencing the new Blob object */
|
||||
let mut part = Part::default();
|
||||
if mime_type.type_() == mime::IMAGE {
|
||||
if let Ok((width, height)) = get_filemeta(decoded_data) {
|
||||
part.param.set_int(Param::Width, width as i32);
|
||||
@@ -1928,7 +1936,10 @@ pub struct Part {
|
||||
pub(crate) is_reaction: bool,
|
||||
}
|
||||
|
||||
/// return mimetype and viewtype for a parsed mail
|
||||
/// Returns the mimetype and viewtype for a parsed mail.
|
||||
///
|
||||
/// This only looks at the metadata, not at the content;
|
||||
/// the viewtype may later be corrected in `do_add_single_file_part()`.
|
||||
fn get_mime_type(
|
||||
mail: &mailparse::ParsedMail<'_>,
|
||||
filename: &Option<String>,
|
||||
@@ -1937,7 +1948,7 @@ fn get_mime_type(
|
||||
|
||||
let viewtype = match mimetype.type_() {
|
||||
mime::TEXT => match mimetype.subtype() {
|
||||
mime::VCARD if is_valid_deltachat_vcard(mail) => Viewtype::Vcard,
|
||||
mime::VCARD => Viewtype::Vcard,
|
||||
mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
|
||||
_ => Viewtype::File,
|
||||
},
|
||||
@@ -1988,17 +1999,6 @@ fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
|
||||
.any(|(key, _value)| key.starts_with("filename"))
|
||||
}
|
||||
|
||||
fn is_valid_deltachat_vcard(mail: &mailparse::ParsedMail) -> bool {
|
||||
let Ok(body) = &mail.get_body() else {
|
||||
return false;
|
||||
};
|
||||
let contacts = deltachat_contact_tools::parse_vcard(body);
|
||||
if let [c] = &contacts[..] {
|
||||
return deltachat_contact_tools::may_be_valid_addr(&c.addr);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Tries to get attachment filename.
|
||||
///
|
||||
/// If filename is explicitly specified in Content-Disposition, it is
|
||||
|
||||
@@ -24,7 +24,7 @@ pub struct Response {
|
||||
/// Response body.
|
||||
pub blob: Vec<u8>,
|
||||
|
||||
/// MIME type exntracted from the `Content-Type` header, if any.
|
||||
/// MIME type extracted from the `Content-Type` header, if any.
|
||||
pub mimetype: Option<String>,
|
||||
|
||||
/// Encoding extracted from the `Content-Type` header, if any.
|
||||
|
||||
@@ -88,6 +88,9 @@ pub enum Param {
|
||||
/// For Messages: quoted text.
|
||||
Quote = b'q',
|
||||
|
||||
/// For Messages: the 1st part of summary text (i.e. before the dash if any).
|
||||
Summary1 = b'4',
|
||||
|
||||
/// For Messages
|
||||
Cmd = b'S',
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
use email::Header;
|
||||
use iroh_gossip::net::{Gossip, JoinTopicFut, GOSSIP_ALPN};
|
||||
use iroh_gossip::proto::{Event as IrohEvent, TopicId};
|
||||
use iroh_net::key::{PublicKey, SecretKey};
|
||||
use iroh_net::relay::{RelayMap, RelayUrl};
|
||||
use iroh_net::{key::SecretKey, relay::RelayMode, Endpoint};
|
||||
use iroh_net::{relay::RelayMode, Endpoint};
|
||||
use iroh_net::{NodeAddr, NodeId};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::env;
|
||||
@@ -60,8 +61,10 @@ pub struct Iroh {
|
||||
/// Topics for which an advertisement has already been sent.
|
||||
pub(crate) iroh_channels: RwLock<HashMap<TopicId, ChannelState>>,
|
||||
|
||||
/// Currently used Iroh secret key
|
||||
pub(crate) secret_key: SecretKey,
|
||||
/// Currently used Iroh public key.
|
||||
///
|
||||
/// This is attached to every message to work around `iroh_gossip` deduplication.
|
||||
pub(crate) public_key: PublicKey,
|
||||
}
|
||||
|
||||
impl Iroh {
|
||||
@@ -154,7 +157,7 @@ impl Iroh {
|
||||
|
||||
let seq_num = self.get_and_incr(&topic).await;
|
||||
data.extend(seq_num.to_le_bytes());
|
||||
data.extend(self.secret_key.public().as_bytes());
|
||||
data.extend(self.public_key.as_bytes());
|
||||
|
||||
self.gossip.broadcast(topic, data.into()).await?;
|
||||
|
||||
@@ -214,7 +217,8 @@ impl ChannelState {
|
||||
impl Context {
|
||||
/// Create magic endpoint and gossip.
|
||||
async fn init_peer_channels(&self) -> Result<Iroh> {
|
||||
let secret_key: SecretKey = SecretKey::generate();
|
||||
let secret_key = SecretKey::generate();
|
||||
let public_key = secret_key.public();
|
||||
|
||||
let relay_mode = if let Some(relay_url) = self
|
||||
.metadata
|
||||
@@ -231,7 +235,7 @@ impl Context {
|
||||
};
|
||||
|
||||
let endpoint = Endpoint::builder()
|
||||
.secret_key(secret_key.clone())
|
||||
.secret_key(secret_key)
|
||||
.alpns(vec![GOSSIP_ALPN.to_vec()])
|
||||
.relay_mode(relay_mode)
|
||||
.bind(0)
|
||||
@@ -251,7 +255,7 @@ impl Context {
|
||||
endpoint,
|
||||
gossip,
|
||||
iroh_channels: RwLock::new(HashMap::new()),
|
||||
secret_key,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,34 @@ static P_AKTIVIX_ORG: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// aliyun.md: aliyun.com
|
||||
static P_ALIYUN: Provider = Provider {
|
||||
id: "aliyun",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/aliyun",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "imap.aliyun.com",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "smtp.aliyun.com",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// aol.md: aol.com
|
||||
static P_AOL: Provider = Provider {
|
||||
id: "aol",
|
||||
@@ -222,99 +250,6 @@ static P_BUZON_UY: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c1.testrun.org.md: c1.testrun.org
|
||||
static P_C1_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c1.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c1-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c1.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c2.testrun.org.md: c2.testrun.org
|
||||
static P_C2_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c2.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c2-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c2.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// c3.testrun.org.md: c3.testrun.org
|
||||
static P_C3_TESTRUN_ORG: Provider = Provider {
|
||||
id: "c3.testrun.org",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/c3-testrun-org",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "c3.testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
key: Config::MvboxMove,
|
||||
value: "0",
|
||||
}]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// chello.at.md: chello.at
|
||||
static P_CHELLO_AT: Provider = Provider {
|
||||
id: "chello.at",
|
||||
@@ -356,6 +291,48 @@ static P_COMCAST: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// daleth.cafe.md: daleth.cafe
|
||||
static P_DALETH_CAFE: Provider = Provider {
|
||||
id: "daleth.cafe",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/daleth-cafe",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "daleth.cafe",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "daleth.cafe",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "daleth.cafe",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "daleth.cafe",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// dismail.de.md: dismail.de
|
||||
static P_DISMAIL_DE: Provider = Provider {
|
||||
id: "dismail.de",
|
||||
@@ -382,14 +359,14 @@ static P_DISROOT: Provider = Provider {
|
||||
socket: Ssl,
|
||||
hostname: "disroot.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
username_pattern: Emaillocalpart,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "disroot.org",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
username_pattern: Emaillocalpart,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
@@ -446,7 +423,7 @@ static P_EXAMPLE_COM: Provider = Provider {
|
||||
after_login_hint: "This provider doesn't really exist, so you can't use it :/ If you need an email provider for Delta Chat, take a look at providers.delta.chat!",
|
||||
overview_page: "https://providers.delta.chat/example-com",
|
||||
server: &[
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "imap.example.com", port: 1337, username_pattern: Email },
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "imap.example.com", port: 1337, username_pattern: Emaillocalpart },
|
||||
Server { protocol: Smtp, socket: Starttls, hostname: "smtp.example.com", port: 1337, username_pattern: Email },
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
@@ -747,6 +724,20 @@ static P_KONTENT_COM: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mail.com.md: email.com, groupmail.com, post.com, homemail.com, housemail.com, writeme.com, mail.com, mail-me.com, workmail.com, accountant.com, activist.com, adexec.com, allergist.com, alumni.com, alumnidirector.com, archaeologist.com, auctioneer.net, bartender.net, brew-master.com, chef.net, chemist.com, collector.org, columnist.com, comic.com, consultant.com, contractor.net, counsellor.com, deliveryman.com, diplomats.com, dr.com, engineer.com, financier.com, fireman.net, gardener.com, geologist.com, graphic-designer.com, graduate.org, hairdresser.net, instructor.net, insurer.com, journalist.com, legislator.com, lobbyist.com, minister.com, musician.org, optician.com, orthodontist.net, pediatrician.com, photographer.net, physicist.net, politician.com, presidency.com, priest.com, programmer.net, publicist.com, radiologist.net, realtyagent.com, registerednurses.com, repairman.com, representative.com, salesperson.net, secretary.net, socialworker.net, sociologist.com, songwriter.net, teachers.org, techie.com, technologist.com, therapist.net, umpire.com, worker.com, artlover.com, bikerider.com, birdlover.com, blader.com, kittymail.com, lovecat.com, marchmail.com, boardermail.com, catlover.com, clubmember.org, nonpartisan.com, petlover.com, doglover.com, greenmail.net, hackermail.com, theplate.com, bsdmail.com, computer4u.com, coolsite.net, cyberdude.com, cybergal.com, cyberservices.com, cyber-wizard.com, linuxmail.org, null.net, solution4u.com, tech-center.com, webname.com, acdcfan.com, angelic.com, discofan.com, elvisfan.com, hiphopfan.com, kissfans.com, madonnafan.com, metalfan.com, ninfan.com, ravemail.com, reggaefan.com, snakebite.com, bellair.net, californiamail.com, dallasmail.com, nycmail.com, pacific-ocean.com, pacificwest.com, sanfranmail.com, usa.com, africamail.com, asia-mail.com, australiamail.com, berlin.com, brazilmail.com, chinamail.com, dublin.com, dutchmail.com, englandmail.com, europe.com, arcticmail.com, europemail.com, germanymail.com, irelandmail.com, israelmail.com, italymail.com, koreamail.com, mexicomail.com, moscowmail.com, munich.com, asia.com, polandmail.com, safrica.com, samerica.com, scotlandmail.com, spainmail.com, swedenmail.com, swissmail.com, torontomail.com, aircraftmail.com, cash4u.com, disposable.com, execs.com, fastservice.com, instruction.com, job4u.com, net-shopping.com, planetmail.com, planetmail.net, qualityservice.com, rescueteam.com, surgical.net, atheist.com, disciples.com, muslim.com, protestant.com, reborn.com, reincarnate.com, religious.com, saintly.com, brew-meister.com, cutey.com, dbzmail.com, doramail.com, galaxyhit.com, hilarious.com, humanoid.net, hot-shot.com, inorbit.com, iname.com, innocent.com, keromail.com, myself.com, rocketship.com, toothfairy.com, toke.com, tvstar.com, uymail.com, 2trom.com
|
||||
static P_MAIL_COM: Provider = Provider {
|
||||
id: "mail.com",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "To log in with Delta Chat, you first need to activate POP3/IMAP in your mail.com settings. Note that this is a mail.com Premium feature only.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mail-com",
|
||||
server: &[
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mail.de.md: mail.de
|
||||
static P_MAIL_DE: Provider = Provider {
|
||||
id: "mail.de",
|
||||
@@ -875,6 +866,64 @@ static P_MAILO_COM: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mehl.cloud.md: mehl.cloud
|
||||
static P_MEHL_CLOUD: Provider = Provider {
|
||||
id: "mehl.cloud",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/mehl-cloud",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mehl.cloud",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "mehl.cloud",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "mehl.cloud",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// mehl.store.md: mehl.store, ende.in.net, l2i.top, szh.homes, sls.post.in, ente.quest, ente.cfd, nein.jetzt
|
||||
static P_MEHL_STORE: Provider = Provider {
|
||||
id: "mehl.store",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "This account provides 3GB storage for eMails and the possibility to access a NEXTCLOUD-instance by using the email-credits!",
|
||||
overview_page: "https://providers.delta.chat/mehl-store",
|
||||
server: &[
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "mail.ende.in.net", port: 993, username_pattern: Email },
|
||||
Server { protocol: Smtp, socket: Starttls, hostname: "mail.ende.in.net", port: 587, username_pattern: Email },
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// nauta.cu.md: nauta.cu
|
||||
static P_NAUTA_CU: Provider = Provider {
|
||||
id: "nauta.cu",
|
||||
@@ -908,10 +957,6 @@ static P_NAUTA_CU: Provider = Provider {
|
||||
key: Config::DeleteServerAfter,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::BccSelf,
|
||||
value: "0",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::SentboxWatch,
|
||||
value: "0",
|
||||
@@ -924,10 +969,6 @@ static P_NAUTA_CU: Provider = Provider {
|
||||
key: Config::MediaQuality,
|
||||
value: "1",
|
||||
},
|
||||
ConfigDefault {
|
||||
key: Config::FetchExistingMsgs,
|
||||
value: "0",
|
||||
},
|
||||
]),
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
@@ -982,6 +1023,20 @@ static P_NINE_TESTRUN_ORG: Provider = Provider {
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 143,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Starttls,
|
||||
hostname: "nine.testrun.org",
|
||||
port: 587,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: Some(&[ConfigDefault {
|
||||
@@ -1131,6 +1186,34 @@ static P_PROTONMAIL: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// purelymail.com.md: purelymail.com, cheapermail.com, placeq.com, rethinkmail.com, worldofmail.com
|
||||
static P_PURELYMAIL_COM: Provider = Provider {
|
||||
id: "purelymail.com",
|
||||
status: Status::Ok,
|
||||
before_login_hint: "",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/purelymail-com",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "imap.purelymail.com",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "smtp.purelymail.com",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// qq.md: qq.com, foxmail.com
|
||||
static P_QQ: Provider = Provider {
|
||||
id: "qq",
|
||||
@@ -1147,6 +1230,23 @@ static P_QQ: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// rambler.ru.md: rambler.ru, autorambler.ru, myrambler.ru, rambler.ua, lenta.ru, ro.ru, r0.ru
|
||||
static P_RAMBLER_RU: Provider = Provider {
|
||||
id: "rambler.ru",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Чтобы войти в Рамблер/почта через Delta Chat, необходимо предварительно включить доступ с помощью почтовых клиентов на сайте mail.rambler.ru",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/rambler-ru",
|
||||
server: &[
|
||||
Server { protocol: Imap, socket: Ssl, hostname: "imap.rambler.ru", port: 993, username_pattern: Email },
|
||||
Server { protocol: Smtp, socket: Ssl, hostname: "smtp.rambler.ru", port: 465, username_pattern: Email },
|
||||
Server { protocol: Imap, socket: Starttls, hostname: "imap.rambler.ru", port: 143, username_pattern: Email },
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// riseup.net.md: riseup.net
|
||||
static P_RISEUP_NET: Provider = Provider {
|
||||
id: "riseup.net",
|
||||
@@ -1160,14 +1260,14 @@ static P_RISEUP_NET: Provider = Provider {
|
||||
socket: Ssl,
|
||||
hostname: "mail.riseup.net",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
username_pattern: Emaillocalpart,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "mail.riseup.net",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
username_pattern: Emaillocalpart,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
@@ -1288,6 +1388,13 @@ static P_TESTRUN: Provider = Provider {
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "testrun.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Starttls,
|
||||
@@ -1445,6 +1552,20 @@ static P_VIVALDI: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// vk.com.md: vk.com
|
||||
static P_VK_COM: Provider = Provider {
|
||||
id: "vk.com",
|
||||
status: Status::Broken,
|
||||
before_login_hint: "К сожалению, VK Почта не поддерживает работу с Delta Chat. См. https://help.vk.mail.ru/vkmail/questions/client",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/vk-com",
|
||||
server: &[
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// vodafone.de.md: vodafone.de, vodafonemail.de
|
||||
static P_VODAFONE_DE: Provider = Provider {
|
||||
id: "vodafone.de",
|
||||
@@ -1490,6 +1611,34 @@ static P_WEB_DE: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// wkpb.de.md: wkpb.de
|
||||
static P_WKPB_DE: Provider = Provider {
|
||||
id: "wkpb.de",
|
||||
status: Status::Preparation,
|
||||
before_login_hint: "Dies sind die gleichen Anmeldedaten wie bei Moodle und Abitur-Online.",
|
||||
after_login_hint: "",
|
||||
overview_page: "https://providers.delta.chat/wkpb-de",
|
||||
server: &[
|
||||
Server {
|
||||
protocol: Imap,
|
||||
socket: Ssl,
|
||||
hostname: "pimap.schulon.org",
|
||||
port: 993,
|
||||
username_pattern: Email,
|
||||
},
|
||||
Server {
|
||||
protocol: Smtp,
|
||||
socket: Ssl,
|
||||
hostname: "psmtp.schulon.org",
|
||||
port: 465,
|
||||
username_pattern: Email,
|
||||
},
|
||||
],
|
||||
opt: ProviderOptions::new(),
|
||||
config_defaults: None,
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
// yahoo.md: yahoo.com, yahoo.de, yahoo.it, yahoo.fr, yahoo.es, yahoo.se, yahoo.co.uk, yahoo.co.nz, yahoo.com.au, yahoo.com.ar, yahoo.com.br, yahoo.com.mx, ymail.com, rocketmail.com, yahoodns.net
|
||||
static P_YAHOO: Provider = Provider {
|
||||
id: "yahoo",
|
||||
@@ -1608,9 +1757,10 @@ static P_ZOHO: Provider = Provider {
|
||||
oauth2_authorizer: None,
|
||||
};
|
||||
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
pub(crate) static PROVIDER_DATA: [(&str, &Provider); 528] = [
|
||||
("163.com", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun.com", &P_ALIYUN),
|
||||
("aol.com", &P_AOL),
|
||||
("arcor.de", &P_ARCOR_DE),
|
||||
("autistici.org", &P_AUTISTICI_ORG),
|
||||
@@ -1618,12 +1768,10 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("delta.blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("xfinity.com", &P_COMCAST),
|
||||
("comcast.net", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot.org", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
@@ -1775,6 +1923,194 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("ik.me", &P_INFOMANIAK_COM),
|
||||
("kolst.com", &P_KOLST_COM),
|
||||
("kontent.com", &P_KONTENT_COM),
|
||||
("email.com", &P_MAIL_COM),
|
||||
("groupmail.com", &P_MAIL_COM),
|
||||
("post.com", &P_MAIL_COM),
|
||||
("homemail.com", &P_MAIL_COM),
|
||||
("housemail.com", &P_MAIL_COM),
|
||||
("writeme.com", &P_MAIL_COM),
|
||||
("mail.com", &P_MAIL_COM),
|
||||
("mail-me.com", &P_MAIL_COM),
|
||||
("workmail.com", &P_MAIL_COM),
|
||||
("accountant.com", &P_MAIL_COM),
|
||||
("activist.com", &P_MAIL_COM),
|
||||
("adexec.com", &P_MAIL_COM),
|
||||
("allergist.com", &P_MAIL_COM),
|
||||
("alumni.com", &P_MAIL_COM),
|
||||
("alumnidirector.com", &P_MAIL_COM),
|
||||
("archaeologist.com", &P_MAIL_COM),
|
||||
("auctioneer.net", &P_MAIL_COM),
|
||||
("bartender.net", &P_MAIL_COM),
|
||||
("brew-master.com", &P_MAIL_COM),
|
||||
("chef.net", &P_MAIL_COM),
|
||||
("chemist.com", &P_MAIL_COM),
|
||||
("collector.org", &P_MAIL_COM),
|
||||
("columnist.com", &P_MAIL_COM),
|
||||
("comic.com", &P_MAIL_COM),
|
||||
("consultant.com", &P_MAIL_COM),
|
||||
("contractor.net", &P_MAIL_COM),
|
||||
("counsellor.com", &P_MAIL_COM),
|
||||
("deliveryman.com", &P_MAIL_COM),
|
||||
("diplomats.com", &P_MAIL_COM),
|
||||
("dr.com", &P_MAIL_COM),
|
||||
("engineer.com", &P_MAIL_COM),
|
||||
("financier.com", &P_MAIL_COM),
|
||||
("fireman.net", &P_MAIL_COM),
|
||||
("gardener.com", &P_MAIL_COM),
|
||||
("geologist.com", &P_MAIL_COM),
|
||||
("graphic-designer.com", &P_MAIL_COM),
|
||||
("graduate.org", &P_MAIL_COM),
|
||||
("hairdresser.net", &P_MAIL_COM),
|
||||
("instructor.net", &P_MAIL_COM),
|
||||
("insurer.com", &P_MAIL_COM),
|
||||
("journalist.com", &P_MAIL_COM),
|
||||
("legislator.com", &P_MAIL_COM),
|
||||
("lobbyist.com", &P_MAIL_COM),
|
||||
("minister.com", &P_MAIL_COM),
|
||||
("musician.org", &P_MAIL_COM),
|
||||
("optician.com", &P_MAIL_COM),
|
||||
("orthodontist.net", &P_MAIL_COM),
|
||||
("pediatrician.com", &P_MAIL_COM),
|
||||
("photographer.net", &P_MAIL_COM),
|
||||
("physicist.net", &P_MAIL_COM),
|
||||
("politician.com", &P_MAIL_COM),
|
||||
("presidency.com", &P_MAIL_COM),
|
||||
("priest.com", &P_MAIL_COM),
|
||||
("programmer.net", &P_MAIL_COM),
|
||||
("publicist.com", &P_MAIL_COM),
|
||||
("radiologist.net", &P_MAIL_COM),
|
||||
("realtyagent.com", &P_MAIL_COM),
|
||||
("registerednurses.com", &P_MAIL_COM),
|
||||
("repairman.com", &P_MAIL_COM),
|
||||
("representative.com", &P_MAIL_COM),
|
||||
("salesperson.net", &P_MAIL_COM),
|
||||
("secretary.net", &P_MAIL_COM),
|
||||
("socialworker.net", &P_MAIL_COM),
|
||||
("sociologist.com", &P_MAIL_COM),
|
||||
("songwriter.net", &P_MAIL_COM),
|
||||
("teachers.org", &P_MAIL_COM),
|
||||
("techie.com", &P_MAIL_COM),
|
||||
("technologist.com", &P_MAIL_COM),
|
||||
("therapist.net", &P_MAIL_COM),
|
||||
("umpire.com", &P_MAIL_COM),
|
||||
("worker.com", &P_MAIL_COM),
|
||||
("artlover.com", &P_MAIL_COM),
|
||||
("bikerider.com", &P_MAIL_COM),
|
||||
("birdlover.com", &P_MAIL_COM),
|
||||
("blader.com", &P_MAIL_COM),
|
||||
("kittymail.com", &P_MAIL_COM),
|
||||
("lovecat.com", &P_MAIL_COM),
|
||||
("marchmail.com", &P_MAIL_COM),
|
||||
("boardermail.com", &P_MAIL_COM),
|
||||
("catlover.com", &P_MAIL_COM),
|
||||
("clubmember.org", &P_MAIL_COM),
|
||||
("nonpartisan.com", &P_MAIL_COM),
|
||||
("petlover.com", &P_MAIL_COM),
|
||||
("doglover.com", &P_MAIL_COM),
|
||||
("greenmail.net", &P_MAIL_COM),
|
||||
("hackermail.com", &P_MAIL_COM),
|
||||
("theplate.com", &P_MAIL_COM),
|
||||
("bsdmail.com", &P_MAIL_COM),
|
||||
("computer4u.com", &P_MAIL_COM),
|
||||
("coolsite.net", &P_MAIL_COM),
|
||||
("cyberdude.com", &P_MAIL_COM),
|
||||
("cybergal.com", &P_MAIL_COM),
|
||||
("cyberservices.com", &P_MAIL_COM),
|
||||
("cyber-wizard.com", &P_MAIL_COM),
|
||||
("linuxmail.org", &P_MAIL_COM),
|
||||
("null.net", &P_MAIL_COM),
|
||||
("solution4u.com", &P_MAIL_COM),
|
||||
("tech-center.com", &P_MAIL_COM),
|
||||
("webname.com", &P_MAIL_COM),
|
||||
("acdcfan.com", &P_MAIL_COM),
|
||||
("angelic.com", &P_MAIL_COM),
|
||||
("discofan.com", &P_MAIL_COM),
|
||||
("elvisfan.com", &P_MAIL_COM),
|
||||
("hiphopfan.com", &P_MAIL_COM),
|
||||
("kissfans.com", &P_MAIL_COM),
|
||||
("madonnafan.com", &P_MAIL_COM),
|
||||
("metalfan.com", &P_MAIL_COM),
|
||||
("ninfan.com", &P_MAIL_COM),
|
||||
("ravemail.com", &P_MAIL_COM),
|
||||
("reggaefan.com", &P_MAIL_COM),
|
||||
("snakebite.com", &P_MAIL_COM),
|
||||
("bellair.net", &P_MAIL_COM),
|
||||
("californiamail.com", &P_MAIL_COM),
|
||||
("dallasmail.com", &P_MAIL_COM),
|
||||
("nycmail.com", &P_MAIL_COM),
|
||||
("pacific-ocean.com", &P_MAIL_COM),
|
||||
("pacificwest.com", &P_MAIL_COM),
|
||||
("sanfranmail.com", &P_MAIL_COM),
|
||||
("usa.com", &P_MAIL_COM),
|
||||
("africamail.com", &P_MAIL_COM),
|
||||
("asia-mail.com", &P_MAIL_COM),
|
||||
("australiamail.com", &P_MAIL_COM),
|
||||
("berlin.com", &P_MAIL_COM),
|
||||
("brazilmail.com", &P_MAIL_COM),
|
||||
("chinamail.com", &P_MAIL_COM),
|
||||
("dublin.com", &P_MAIL_COM),
|
||||
("dutchmail.com", &P_MAIL_COM),
|
||||
("englandmail.com", &P_MAIL_COM),
|
||||
("europe.com", &P_MAIL_COM),
|
||||
("arcticmail.com", &P_MAIL_COM),
|
||||
("europemail.com", &P_MAIL_COM),
|
||||
("germanymail.com", &P_MAIL_COM),
|
||||
("irelandmail.com", &P_MAIL_COM),
|
||||
("israelmail.com", &P_MAIL_COM),
|
||||
("italymail.com", &P_MAIL_COM),
|
||||
("koreamail.com", &P_MAIL_COM),
|
||||
("mexicomail.com", &P_MAIL_COM),
|
||||
("moscowmail.com", &P_MAIL_COM),
|
||||
("munich.com", &P_MAIL_COM),
|
||||
("asia.com", &P_MAIL_COM),
|
||||
("polandmail.com", &P_MAIL_COM),
|
||||
("safrica.com", &P_MAIL_COM),
|
||||
("samerica.com", &P_MAIL_COM),
|
||||
("scotlandmail.com", &P_MAIL_COM),
|
||||
("spainmail.com", &P_MAIL_COM),
|
||||
("swedenmail.com", &P_MAIL_COM),
|
||||
("swissmail.com", &P_MAIL_COM),
|
||||
("torontomail.com", &P_MAIL_COM),
|
||||
("aircraftmail.com", &P_MAIL_COM),
|
||||
("cash4u.com", &P_MAIL_COM),
|
||||
("disposable.com", &P_MAIL_COM),
|
||||
("execs.com", &P_MAIL_COM),
|
||||
("fastservice.com", &P_MAIL_COM),
|
||||
("instruction.com", &P_MAIL_COM),
|
||||
("job4u.com", &P_MAIL_COM),
|
||||
("net-shopping.com", &P_MAIL_COM),
|
||||
("planetmail.com", &P_MAIL_COM),
|
||||
("planetmail.net", &P_MAIL_COM),
|
||||
("qualityservice.com", &P_MAIL_COM),
|
||||
("rescueteam.com", &P_MAIL_COM),
|
||||
("surgical.net", &P_MAIL_COM),
|
||||
("atheist.com", &P_MAIL_COM),
|
||||
("disciples.com", &P_MAIL_COM),
|
||||
("muslim.com", &P_MAIL_COM),
|
||||
("protestant.com", &P_MAIL_COM),
|
||||
("reborn.com", &P_MAIL_COM),
|
||||
("reincarnate.com", &P_MAIL_COM),
|
||||
("religious.com", &P_MAIL_COM),
|
||||
("saintly.com", &P_MAIL_COM),
|
||||
("brew-meister.com", &P_MAIL_COM),
|
||||
("cutey.com", &P_MAIL_COM),
|
||||
("dbzmail.com", &P_MAIL_COM),
|
||||
("doramail.com", &P_MAIL_COM),
|
||||
("galaxyhit.com", &P_MAIL_COM),
|
||||
("hilarious.com", &P_MAIL_COM),
|
||||
("humanoid.net", &P_MAIL_COM),
|
||||
("hot-shot.com", &P_MAIL_COM),
|
||||
("inorbit.com", &P_MAIL_COM),
|
||||
("iname.com", &P_MAIL_COM),
|
||||
("innocent.com", &P_MAIL_COM),
|
||||
("keromail.com", &P_MAIL_COM),
|
||||
("myself.com", &P_MAIL_COM),
|
||||
("rocketship.com", &P_MAIL_COM),
|
||||
("toothfairy.com", &P_MAIL_COM),
|
||||
("toke.com", &P_MAIL_COM),
|
||||
("tvstar.com", &P_MAIL_COM),
|
||||
("uymail.com", &P_MAIL_COM),
|
||||
("2trom.com", &P_MAIL_COM),
|
||||
("mail.de", &P_MAIL_DE),
|
||||
("mail.ru", &P_MAIL_RU),
|
||||
("inbox.ru", &P_MAIL_RU),
|
||||
@@ -1785,6 +2121,15 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("secure.mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("ende.in.net", &P_MEHL_STORE),
|
||||
("l2i.top", &P_MEHL_STORE),
|
||||
("szh.homes", &P_MEHL_STORE),
|
||||
("sls.post.in", &P_MEHL_STORE),
|
||||
("ente.quest", &P_MEHL_STORE),
|
||||
("ente.cfd", &P_MEHL_STORE),
|
||||
("nein.jetzt", &P_MEHL_STORE),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver.com", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
@@ -1850,8 +2195,20 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("protonmail.com", &P_PROTONMAIL),
|
||||
("protonmail.ch", &P_PROTONMAIL),
|
||||
("pm.me", &P_PROTONMAIL),
|
||||
("purelymail.com", &P_PURELYMAIL_COM),
|
||||
("cheapermail.com", &P_PURELYMAIL_COM),
|
||||
("placeq.com", &P_PURELYMAIL_COM),
|
||||
("rethinkmail.com", &P_PURELYMAIL_COM),
|
||||
("worldofmail.com", &P_PURELYMAIL_COM),
|
||||
("qq.com", &P_QQ),
|
||||
("foxmail.com", &P_QQ),
|
||||
("rambler.ru", &P_RAMBLER_RU),
|
||||
("autorambler.ru", &P_RAMBLER_RU),
|
||||
("myrambler.ru", &P_RAMBLER_RU),
|
||||
("rambler.ua", &P_RAMBLER_RU),
|
||||
("lenta.ru", &P_RAMBLER_RU),
|
||||
("ro.ru", &P_RAMBLER_RU),
|
||||
("r0.ru", &P_RAMBLER_RU),
|
||||
("riseup.net", &P_RISEUP_NET),
|
||||
("rogers.com", &P_ROGERS_COM),
|
||||
("sonic.net", &P_SONIC),
|
||||
@@ -1871,6 +2228,7 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("undernet.uy", &P_UNDERNET_UY),
|
||||
("vfemail.net", &P_VFEMAIL),
|
||||
("vivaldi.net", &P_VIVALDI),
|
||||
("vk.com", &P_VK_COM),
|
||||
("vodafone.de", &P_VODAFONE_DE),
|
||||
("vodafonemail.de", &P_VODAFONE_DE),
|
||||
("web.de", &P_WEB_DE),
|
||||
@@ -1900,6 +2258,7 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 318] = [
|
||||
("joker.ms", &P_WEB_DE),
|
||||
("planet.ms", &P_WEB_DE),
|
||||
("power.ms", &P_WEB_DE),
|
||||
("wkpb.de", &P_WKPB_DE),
|
||||
("yahoo.com", &P_YAHOO),
|
||||
("yahoo.de", &P_YAHOO),
|
||||
("yahoo.it", &P_YAHOO),
|
||||
@@ -1933,17 +2292,16 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
HashMap::from([
|
||||
("163", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun", &P_ALIYUN),
|
||||
("aol", &P_AOL),
|
||||
("arcor.de", &P_ARCOR_DE),
|
||||
("autistici.org", &P_AUTISTICI_ORG),
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("c1.testrun.org", &P_C1_TESTRUN_ORG),
|
||||
("c2.testrun.org", &P_C2_TESTRUN_ORG),
|
||||
("c3.testrun.org", &P_C3_TESTRUN_ORG),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
@@ -1963,11 +2321,14 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("infomaniak.com", &P_INFOMANIAK_COM),
|
||||
("kolst.com", &P_KOLST_COM),
|
||||
("kontent.com", &P_KONTENT_COM),
|
||||
("mail.com", &P_MAIL_COM),
|
||||
("mail.de", &P_MAIL_DE),
|
||||
("mail.ru", &P_MAIL_RU),
|
||||
("mail2tor", &P_MAIL2TOR),
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
@@ -1976,7 +2337,9 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("ouvaton.coop", &P_OUVATON_COOP),
|
||||
("posteo", &P_POSTEO),
|
||||
("protonmail", &P_PROTONMAIL),
|
||||
("purelymail.com", &P_PURELYMAIL_COM),
|
||||
("qq", &P_QQ),
|
||||
("rambler.ru", &P_RAMBLER_RU),
|
||||
("riseup.net", &P_RISEUP_NET),
|
||||
("rogers.com", &P_ROGERS_COM),
|
||||
("sonic", &P_SONIC),
|
||||
@@ -1990,8 +2353,10 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
("undernet.uy", &P_UNDERNET_UY),
|
||||
("vfemail", &P_VFEMAIL),
|
||||
("vivaldi", &P_VIVALDI),
|
||||
("vk.com", &P_VK_COM),
|
||||
("vodafone.de", &P_VODAFONE_DE),
|
||||
("web.de", &P_WEB_DE),
|
||||
("wkpb.de", &P_WKPB_DE),
|
||||
("yahoo", &P_YAHOO),
|
||||
("yandex.ru", &P_YANDEX_RU),
|
||||
("yggmail", &P_YGGMAIL),
|
||||
@@ -2001,4 +2366,4 @@ pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> =
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 2, 5).unwrap());
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_and, Message, MessageState, MessengerMessage, MsgId,
|
||||
self, rfc724_mid_exists, rfc724_mid_exists_ex, Message, MessageState, MessengerMessage, MsgId,
|
||||
Viewtype,
|
||||
};
|
||||
use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMessage};
|
||||
@@ -40,7 +40,7 @@ use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::{self, buf_compress};
|
||||
use crate::tools::{self, buf_compress, remove_subject_prefix};
|
||||
use crate::{chatlist_events, location};
|
||||
use crate::{contact, imap};
|
||||
use iroh_net::NodeAddr;
|
||||
@@ -489,7 +489,9 @@ pub(crate) async fn receive_imf_inner(
|
||||
can_info_msg = false;
|
||||
Some(Message::load_from_db(context, insert_msg_id).await?)
|
||||
} else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
if let Some(instance) = get_rfc724_mid_in_list(context, field).await? {
|
||||
if let Some(instance) =
|
||||
message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
|
||||
{
|
||||
can_info_msg = instance.download_state() == DownloadState::Done;
|
||||
Some(instance)
|
||||
} else {
|
||||
@@ -694,6 +696,9 @@ async fn add_parts(
|
||||
prevent_rename: bool,
|
||||
verified_encryption: VerifiedEncryption,
|
||||
) -> Result<ReceivedMsg> {
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
// Bots handle existing messages the same way as new ones.
|
||||
let fetching_existing_messages = fetching_existing_messages && !is_bot;
|
||||
let rfc724_mid_orig = &mime_parser
|
||||
.get_rfc724_mid()
|
||||
.unwrap_or(rfc724_mid.to_string());
|
||||
@@ -707,9 +712,13 @@ async fn add_parts(
|
||||
better_msg = Some(stock_str::msg_location_enabled_by(context, from_id).await);
|
||||
}
|
||||
|
||||
let parent = get_parent_message(context, mime_parser)
|
||||
.await?
|
||||
.filter(|p| Some(p.id) != replace_msg_id);
|
||||
let parent = get_parent_message(
|
||||
context,
|
||||
mime_parser.get_header(HeaderDef::References),
|
||||
mime_parser.get_header(HeaderDef::InReplyTo),
|
||||
)
|
||||
.await?
|
||||
.filter(|p| Some(p.id) != replace_msg_id);
|
||||
|
||||
let is_dc_message = if mime_parser.has_chat_version() {
|
||||
MessengerMessage::Yes
|
||||
@@ -782,9 +791,6 @@ async fn add_parts(
|
||||
info!(context, "Message is an MDN (TRASH).",);
|
||||
}
|
||||
|
||||
// signals whether the current user is a bot
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
|
||||
let create_blocked_default = if is_bot {
|
||||
Blocked::Not
|
||||
} else {
|
||||
@@ -1534,7 +1540,7 @@ INSERT INTO msgs
|
||||
rfc724_mid, chat_id,
|
||||
from_id, to_id, timestamp, timestamp_sent,
|
||||
timestamp_rcvd, type, state, msgrmsg,
|
||||
txt, subject, txt_raw, param, hidden,
|
||||
txt, txt_normalized, subject, txt_raw, param, hidden,
|
||||
bytes, mime_headers, mime_compressed, mime_in_reply_to,
|
||||
mime_references, mime_modified, error, ephemeral_timer,
|
||||
ephemeral_timestamp, download_state, hop_info
|
||||
@@ -1544,7 +1550,7 @@ INSERT INTO msgs
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, 1,
|
||||
?, ?, ?, ?, ?, 1,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?
|
||||
)
|
||||
@@ -1552,7 +1558,8 @@ ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
|
||||
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
|
||||
@@ -1572,6 +1579,7 @@ RETURNING id
|
||||
state,
|
||||
is_dc_message,
|
||||
if trash { "" } else { msg },
|
||||
if trash { None } else { message::normalize_text(msg) },
|
||||
if trash { "" } else { &subject },
|
||||
// txt_raw might contain invalid utf8
|
||||
if trash { "" } else { &txt_raw },
|
||||
@@ -1644,8 +1652,11 @@ RETURNING id
|
||||
}
|
||||
|
||||
if let Some(replace_msg_id) = replace_msg_id {
|
||||
// "Replace" placeholder with a message that has no parts.
|
||||
replace_msg_id.trash(context).await?;
|
||||
// Trash the "replace" placeholder with a message that has no parts. If it has the original
|
||||
// "Message-ID", mark the placeholder for server-side deletion so as if the user deletes the
|
||||
// fully downloaded message later, the server-side deletion is issued.
|
||||
let on_server = rfc724_mid == rfc724_mid_orig;
|
||||
replace_msg_id.trash(context, on_server).await?;
|
||||
}
|
||||
|
||||
chat_id.unarchive_if_not_muted(context, state).await?;
|
||||
@@ -1930,8 +1941,9 @@ async fn create_group(
|
||||
let grpname = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
.context("Chat-Group-Name vanished")?
|
||||
// W/a for "Space added before long group names after MIME serialization/deserialization
|
||||
// #3650" issue. DC itself never creates group names with leading/trailing whitespace.
|
||||
// Workaround for the "Space added before long group names after MIME
|
||||
// serialization/deserialization #3650" issue. DC itself never creates group names with
|
||||
// leading/trailing whitespace.
|
||||
.trim();
|
||||
let new_chat_id = ChatId::create_multiuser_record(
|
||||
context,
|
||||
@@ -2049,8 +2061,9 @@ async fn apply_group_changes(
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
Some(reply_to) => rfc724_mid_exists_and(context, reply_to, "download_state=0")
|
||||
Some(reply_to) => rfc724_mid_exists_ex(context, reply_to, "download_state=0")
|
||||
.await?
|
||||
.filter(|(_, _, downloaded)| *downloaded)
|
||||
.is_none(),
|
||||
None => false,
|
||||
}
|
||||
@@ -2121,12 +2134,10 @@ async fn apply_group_changes(
|
||||
}
|
||||
} else if let Some(old_name) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|s| s.trim())
|
||||
{
|
||||
if let Some(grpname) = mime_parser
|
||||
.get_header(HeaderDef::ChatGroupName)
|
||||
// See create_or_lookup_group() for explanation
|
||||
.map(|grpname| grpname.trim())
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
@@ -2541,10 +2552,10 @@ async fn create_adhoc_group(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// use subject as initial chat name
|
||||
let grpname = mime_parser
|
||||
.get_subject()
|
||||
.unwrap_or_else(|| "Unnamed group".to_string());
|
||||
.map(|s| remove_subject_prefix(&s))
|
||||
.unwrap_or_else(|| "👥📧".to_string());
|
||||
|
||||
let new_chat_id: ChatId = ChatId::create_multiuser_record(
|
||||
context,
|
||||
@@ -2792,53 +2803,35 @@ async fn get_previous_message(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Given a list of Message-IDs, returns the latest message found in the database.
|
||||
///
|
||||
/// Only messages that are not in the trash chat are considered.
|
||||
async fn get_rfc724_mid_in_list(context: &Context, mid_list: &str) -> Result<Option<Message>> {
|
||||
message::get_latest_by_rfc724_mids(context, &parse_message_ids(mid_list)).await
|
||||
}
|
||||
|
||||
/// Returns the last message referenced from References: header found in the database.
|
||||
///
|
||||
/// If none found, tries In-Reply-To: as a fallback for classic MUAs that don't set the
|
||||
/// References: header.
|
||||
async fn get_parent_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
references: Option<&str>,
|
||||
in_reply_to: Option<&str>,
|
||||
) -> Result<Option<Message>> {
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::References) {
|
||||
if let Some(msg) = get_rfc724_mid_in_list(context, field).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
let mut mids = Vec::new();
|
||||
if let Some(field) = in_reply_to {
|
||||
mids = parse_message_ids(field);
|
||||
}
|
||||
|
||||
if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
if let Some(msg) = get_rfc724_mid_in_list(context, field).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
if let Some(field) = references {
|
||||
mids.append(&mut parse_message_ids(field));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
message::get_by_rfc724_mids(context, &mids).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_prefetch_parent_message(
|
||||
context: &Context,
|
||||
headers: &[mailparse::MailHeader<'_>],
|
||||
) -> Result<Option<Message>> {
|
||||
if let Some(field) = headers.get_header_value(HeaderDef::References) {
|
||||
if let Some(msg) = get_rfc724_mid_in_list(context, &field).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(field) = headers.get_header_value(HeaderDef::InReplyTo) {
|
||||
if let Some(msg) = get_rfc724_mid_in_list(context, &field).await? {
|
||||
return Ok(Some(msg));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
get_parent_message(
|
||||
context,
|
||||
headers.get_header_value(HeaderDef::References).as_deref(),
|
||||
headers.get_header_value(HeaderDef::InReplyTo).as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Looks up contact IDs from the database given the list of recipients.
|
||||
|
||||
@@ -10,10 +10,11 @@ use crate::chat::{
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
|
||||
use crate::contact;
|
||||
use crate::download::MIN_DOWNLOAD_LIMIT;
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::imex::{imex, ImexMode};
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2042,7 +2043,7 @@ async fn test_dont_assign_to_trash_by_parent() {
|
||||
assert_eq!(msg.text, "Hi – hello");
|
||||
|
||||
println!("\n========= Delete the message ==========");
|
||||
msg.id.trash(&t).await.unwrap();
|
||||
msg.id.trash(&t, false).await.unwrap();
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id).await.unwrap();
|
||||
assert_eq!(msgs.len(), 0);
|
||||
@@ -2799,8 +2800,13 @@ async fn test_read_receipts_dont_create_chats() -> Result<()> {
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory =
|
||||
crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?;
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
&bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(&bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
@@ -2829,8 +2835,13 @@ async fn test_read_receipts_dont_unmark_bots() -> Result<()> {
|
||||
let received_msg = bob.get_last_msg().await;
|
||||
|
||||
// Bob sends a read receipt.
|
||||
let mdn_mimefactory =
|
||||
crate::mimefactory::MimeFactory::from_mdn(bob, &received_msg, vec![]).await?;
|
||||
let mdn_mimefactory = crate::mimefactory::MimeFactory::from_mdn(
|
||||
bob,
|
||||
received_msg.from_id,
|
||||
received_msg.rfc724_mid,
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
let rendered_mdn = mdn_mimefactory.render(bob).await?;
|
||||
let mdn_body = rendered_mdn.message;
|
||||
|
||||
@@ -2886,6 +2897,18 @@ async fn test_incoming_contact_request() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_parent_message(
|
||||
context: &Context,
|
||||
mime_parser: &MimeMessage,
|
||||
) -> Result<Option<Message>> {
|
||||
super::get_parent_message(
|
||||
context,
|
||||
mime_parser.get_header(HeaderDef::References),
|
||||
mime_parser.get_header(HeaderDef::InReplyTo),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_parent_message() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -3263,6 +3286,46 @@ async fn test_send_as_bot() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_recv_existing_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
bob.set_config(Config::FetchExistingMsgs, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let fetching_existing_messages = true;
|
||||
let msg = receive_imf_from_inbox(
|
||||
bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
None,
|
||||
fetching_existing_messages,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::IncomingMsg { .. }))
|
||||
.await;
|
||||
let EventType::IncomingMsg { chat_id, msg_id } = event else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(chat_id, msg.chat_id);
|
||||
assert_eq!(msg_id, msg.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_private_reply_to_blocked_account() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -3923,6 +3986,7 @@ async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
@@ -4085,6 +4149,7 @@ async fn test_mua_can_readd() -> Result<()> {
|
||||
|
||||
// And leaves it.
|
||||
remove_contact_from_chat(&alice, alice_chat.id, ContactId::SELF).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
assert!(!is_contact_in_chat(&alice, alice_chat.id, ContactId::SELF).await?);
|
||||
|
||||
// Bob uses a classical MUA to answer, adding Alice back.
|
||||
@@ -4159,6 +4224,7 @@ async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
// But if Bob just left, they mustn't recreate the member list even after missing a message.
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
send_text_msg(&alice, alice_chat_id, "3rd message".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?;
|
||||
@@ -4212,6 +4278,33 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_adhoc_grp_name_no_prefix() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let chat_id = receive_imf(
|
||||
alice,
|
||||
b"Subject: Re: Once upon a time this was with the only Re: here\n\
|
||||
From: <bob@example.net>\n\
|
||||
To: <claire@example.org>, <alice@example.org>\n\
|
||||
Date: Mon, 12 Dec 3000 14:32:39 +0000\n\
|
||||
Message-ID: <thisone@example.net>\n\
|
||||
In-Reply-To: <previous@example.net>\n\
|
||||
\n\
|
||||
Adding Alice the Delta Chat lover",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap()
|
||||
.chat_id;
|
||||
let chat = Chat::load_from_db(alice, chat_id).await.unwrap();
|
||||
assert_eq!(
|
||||
chat.get_name(),
|
||||
"Once upon a time this was with the only Re: here"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_later() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4464,6 +4557,79 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_protected_group_missing_member_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
mark_as_verified(alice, bob).await;
|
||||
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
|
||||
let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
add_contact_to_chat(alice, group_id, alice_bob_id).await?;
|
||||
alice.send_text(group_id, "Hello!").await;
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE acpeerstates SET addr=? WHERE addr=?",
|
||||
("b@b", "bob@example.net"),
|
||||
)
|
||||
.await?;
|
||||
assert!(remove_contact_from_chat(alice, group_id, ContactId::SELF)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
|
||||
alice
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE acpeerstates SET addr=? WHERE addr=?",
|
||||
("bob@example.net", "b@b"),
|
||||
)
|
||||
.await?;
|
||||
remove_contact_from_chat(alice, group_id, ContactId::SELF).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
assert!(!is_contact_in_chat(alice, group_id, ContactId::SELF).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
mark_as_verified(alice, bob).await;
|
||||
let group_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
let alice_bob_id = alice.add_or_lookup_contact(bob).await.id;
|
||||
add_contact_to_chat(alice, group_id, alice_bob_id).await?;
|
||||
alice.send_text(group_id, "Hello!").await;
|
||||
alice
|
||||
.sql
|
||||
.execute("DELETE FROM acpeerstates WHERE addr=?", (&bob_addr,))
|
||||
.await?;
|
||||
|
||||
let fiona = &tcm.fiona().await;
|
||||
mark_as_verified(alice, fiona).await;
|
||||
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
|
||||
assert!(add_contact_to_chat(alice, group_id, alice_fiona_id)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(!is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
|
||||
// Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that
|
||||
// may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a
|
||||
// normal scenario anyway.
|
||||
|
||||
remove_contact_from_chat(alice, group_id, alice_bob_id).await?;
|
||||
assert!(!is_contact_in_chat(alice, group_id, alice_bob_id).await?);
|
||||
let msg = alice.get_last_msg_in(group_id).await;
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await
|
||||
);
|
||||
assert!(msg.error().is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4547,6 +4713,50 @@ async fn test_references() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_prefer_references_to_downloaded_msgs() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_bob_id = tcm.send_recv(bob, alice, "hi").await.from_id;
|
||||
let alice_fiona_id = tcm.send_recv(fiona, alice, "hi").await.from_id;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_id).await?;
|
||||
// W/o fiona the test doesn't work -- the last message is assigned to the 1:1 chat due to
|
||||
// `is_probably_private_reply()`.
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_fiona_id).await?;
|
||||
let sent = alice.send_text(alice_chat_id, "Hi").await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Done);
|
||||
let bob_chat_id = received.chat_id;
|
||||
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)
|
||||
.await?;
|
||||
let mut sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
sent.payload = sent
|
||||
.payload
|
||||
.replace("References:", "X-Microsoft-Original-References:")
|
||||
.replace("In-Reply-To:", "X-Microsoft-Original-In-Reply-To:");
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Available);
|
||||
assert_ne!(received.chat_id, bob_chat_id);
|
||||
assert_eq!(received.chat_id, bob.get_chat(alice).await.id);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.set_file_from_bytes(alice, "file", file_bytes, None)
|
||||
.await?;
|
||||
let sent = alice.send_msg(alice_chat_id, &mut msg).await;
|
||||
let received = bob.recv_msg(&sent).await;
|
||||
assert_eq!(received.download_state, DownloadState::Available);
|
||||
assert_eq!(received.chat_id, bob_chat_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_list_from() -> Result<()> {
|
||||
let t = &TestContext::new_alice().await;
|
||||
@@ -4572,10 +4782,15 @@ async fn test_receive_vcard() -> Result<()> {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
for vcard_contains_address in [true, false] {
|
||||
let mut msg = Message::new(Viewtype::Vcard);
|
||||
async fn test(
|
||||
alice: &TestContext,
|
||||
bob: &TestContext,
|
||||
vcard_contains_address: bool,
|
||||
viewtype: Viewtype,
|
||||
) -> Result<()> {
|
||||
let mut msg = Message::new(viewtype);
|
||||
msg.set_file_from_bytes(
|
||||
&alice,
|
||||
alice,
|
||||
"claire.vcf",
|
||||
format!(
|
||||
"BEGIN:VCARD\n\
|
||||
@@ -4595,19 +4810,24 @@ async fn test_receive_vcard() -> Result<()> {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alice_bob_chat = alice.create_chat(&bob).await;
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let sent = alice.send_msg(alice_bob_chat.id, &mut msg).await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
let sent = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
|
||||
if vcard_contains_address {
|
||||
assert_eq!(sent.viewtype, Viewtype::Vcard);
|
||||
assert_eq!(sent.get_summary_text(alice).await, "👤 Claire");
|
||||
assert_eq!(rcvd.viewtype, Viewtype::Vcard);
|
||||
assert_eq!(rcvd.get_summary_text(bob).await, "👤 Claire");
|
||||
} else {
|
||||
// VCards without an email address are not "deltachat contacts",
|
||||
// so they are shown as files
|
||||
assert_eq!(sent.viewtype, Viewtype::File);
|
||||
assert_eq!(rcvd.viewtype, Viewtype::File);
|
||||
}
|
||||
|
||||
let vcard = tokio::fs::read(rcvd.get_file(&bob).unwrap()).await?;
|
||||
let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?;
|
||||
let vcard = std::str::from_utf8(&vcard)?;
|
||||
let parsed = deltachat_contact_tools::parse_vcard(vcard);
|
||||
assert_eq!(parsed.len(), 1);
|
||||
@@ -4616,8 +4836,50 @@ async fn test_receive_vcard() -> Result<()> {
|
||||
} else {
|
||||
assert_eq!(&parsed[0].addr, "");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
for vcard_contains_address in [true, false] {
|
||||
for viewtype in [Viewtype::File, Viewtype::Vcard] {
|
||||
test(&alice, &bob, vcard_contains_address, viewtype).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_make_n_send_vcard() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let vcard = "BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Claire\n\
|
||||
EMAIL;TYPE=work:claire@example.org\n\
|
||||
END:VCARD";
|
||||
let contact_ids = contact::import_vcard(alice, vcard).await?;
|
||||
assert_eq!(contact_ids.len(), 1);
|
||||
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
msg.make_vcard(alice, &contact_ids).await?;
|
||||
|
||||
let alice_bob_chat = alice.create_chat(bob).await;
|
||||
let sent = alice.send_msg(alice_bob_chat.id, &mut msg).await;
|
||||
let rcvd = bob.recv_msg(&sent).await;
|
||||
let sent = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
||||
|
||||
assert_eq!(sent.viewtype, Viewtype::Vcard);
|
||||
assert_eq!(sent.get_summary_text(alice).await, "👤 Claire");
|
||||
assert_eq!(rcvd.viewtype, Viewtype::Vcard);
|
||||
assert_eq!(rcvd.get_summary_text(bob).await, "👤 Claire");
|
||||
|
||||
let vcard = tokio::fs::read(rcvd.get_file(bob).unwrap()).await?;
|
||||
let vcard = std::str::from_utf8(&vcard)?;
|
||||
let parsed = deltachat_contact_tools::parse_vcard(vcard);
|
||||
assert_eq!(parsed.len(), 1);
|
||||
assert_eq!(&parsed[0].addr, "claire@example.org");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
|
||||
let quota = ctx.quota.read().await;
|
||||
quota
|
||||
.as_ref()
|
||||
.filter(|quota| time_elapsed("a.modified) > Duration::from_secs(60))
|
||||
.filter(|quota| time_elapsed("a.modified) < Duration::from_secs(60))
|
||||
.is_none()
|
||||
};
|
||||
if quota_needs_update {
|
||||
@@ -749,7 +749,7 @@ async fn smtp_loop(
|
||||
) {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
info!(ctx, "starting smtp loop");
|
||||
info!(ctx, "Starting SMTP loop.");
|
||||
let SmtpConnectionHandlers {
|
||||
mut connection,
|
||||
stop_receiver,
|
||||
@@ -760,14 +760,14 @@ async fn smtp_loop(
|
||||
let fut = async move {
|
||||
let ctx = ctx1;
|
||||
if let Err(()) = started.send(()) {
|
||||
warn!(&ctx, "smtp loop, missing started receiver");
|
||||
warn!(&ctx, "SMTP loop, missing started receiver.");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut timeout = None;
|
||||
loop {
|
||||
if let Err(err) = send_smtp_messages(&ctx, &mut connection).await {
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}", err);
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}.", err);
|
||||
timeout = Some(timeout.unwrap_or(30));
|
||||
} else {
|
||||
timeout = None;
|
||||
@@ -784,7 +784,7 @@ async fn smtp_loop(
|
||||
}
|
||||
|
||||
// Fake Idle
|
||||
info!(ctx, "smtp fake idle - started");
|
||||
info!(ctx, "SMTP fake idle started.");
|
||||
match &connection.last_send_error {
|
||||
None => connection.connectivity.set_idle(&ctx).await,
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err).await,
|
||||
@@ -798,7 +798,7 @@ async fn smtp_loop(
|
||||
let now = tools::Time::now();
|
||||
info!(
|
||||
ctx,
|
||||
"smtp has messages to retry, planning to retry {} seconds later", t,
|
||||
"SMTP has messages to retry, planning to retry {t} seconds later."
|
||||
);
|
||||
let duration = std::time::Duration::from_secs(t);
|
||||
tokio::time::timeout(duration, async {
|
||||
@@ -812,18 +812,18 @@ async fn smtp_loop(
|
||||
slept.saturating_add(rand::thread_rng().gen_range((slept / 2)..=slept)),
|
||||
));
|
||||
} else {
|
||||
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
|
||||
info!(ctx, "SMTP has no messages to retry, waiting for interrupt.");
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default();
|
||||
};
|
||||
|
||||
info!(ctx, "smtp fake idle - interrupted")
|
||||
info!(ctx, "SMTP fake idle interrupted.")
|
||||
}
|
||||
};
|
||||
|
||||
stop_receiver
|
||||
.recv()
|
||||
.map(|_| {
|
||||
info!(ctx, "shutting down smtp loop");
|
||||
info!(ctx, "Shutting down SMTP loop.");
|
||||
})
|
||||
.race(fut)
|
||||
.await;
|
||||
|
||||
@@ -802,6 +802,9 @@ mod tests {
|
||||
let alice = tcm.alice().await;
|
||||
let alice_addr = &alice.get_config(Config::Addr).await.unwrap().unwrap();
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Examplenet"))
|
||||
.await
|
||||
.unwrap();
|
||||
alice
|
||||
.set_config(Config::VerifiedOneOnOneChats, Some("1"))
|
||||
.await
|
||||
@@ -824,6 +827,11 @@ mod tests {
|
||||
|
||||
// Step 1: Generate QR-code, ChatId(0) indicates setup-contact
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
// We want Bob to learn Alice's name from their messages, not from the QR code.
|
||||
alice
|
||||
.set_config(Config::Displayname, Some("Alice Exampleorg"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Step 2: Bob scans QR-code, sends vc-request
|
||||
join_securejoin(&bob.ctx, &qr).await.unwrap();
|
||||
@@ -895,6 +903,7 @@ mod tests {
|
||||
|
||||
// Check Bob sent the right message.
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Bob Examplenet"));
|
||||
let mut msg = alice.parse_msg(&sent).await;
|
||||
let vc_request_with_auth_ts_sent = msg
|
||||
.get_header(HeaderDef::Date)
|
||||
@@ -946,6 +955,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), false);
|
||||
assert_eq!(contact_bob.get_authname(), "");
|
||||
|
||||
if case == SetupContactCase::CheckProtectionTimestamp {
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
@@ -954,6 +964,10 @@ mod tests {
|
||||
// Step 5+6: Alice receives vc-request-with-auth, sends vc-contact-confirm
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_bob.is_verified(&alice.ctx).await.unwrap(), true);
|
||||
let contact_bob = Contact::get_by_id(&alice.ctx, contact_bob_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.get_authname(), "Bob Examplenet");
|
||||
|
||||
// exactly one one-to-one chat should be visible for both now
|
||||
// (check this before calling alice.create_chat() explicitly below)
|
||||
@@ -981,8 +995,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure Alice hasn't yet sent their name to Bob.
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "");
|
||||
|
||||
// Check Alice sent the right message to Bob.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(!sent.payload.contains("Alice Exampleorg"));
|
||||
let msg = bob.parse_msg(&sent).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert_eq!(
|
||||
@@ -991,18 +1016,15 @@ mod tests {
|
||||
);
|
||||
|
||||
// Bob should not yet have Alice verified
|
||||
let contact_alice_id = Contact::lookup_id_by_addr(&bob.ctx, alice_addr, Origin::Unknown)
|
||||
.await
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await.unwrap(), true);
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(contact_alice.get_authname(), "Alice Exampleorg");
|
||||
|
||||
if case != SetupContactCase::SecurejoinWaitTimeout {
|
||||
// Later we check that the timeout message isn't added to the already protected chat.
|
||||
@@ -1119,7 +1141,7 @@ mod tests {
|
||||
// Alice should not yet have Bob verified
|
||||
let (contact_bob_id, _modified) = Contact::add_or_lookup(
|
||||
&alice.ctx,
|
||||
"Bob",
|
||||
"",
|
||||
&ContactAddress::new("bob@example.net")?,
|
||||
Origin::ManuallyCreated,
|
||||
)
|
||||
@@ -1403,6 +1425,7 @@ First thread."#;
|
||||
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// The message from Bob is delivered late, Bob is already removed.
|
||||
let msg = alice.recv_msg(&sent).await;
|
||||
|
||||
142
src/smtp.rs
142
src/smtp.rs
@@ -87,7 +87,7 @@ impl Smtp {
|
||||
/// Connect using configured parameters.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.has_maybe_stale_connection() {
|
||||
info!(context, "Closing stale connection");
|
||||
info!(context, "Closing stale connection.");
|
||||
self.disconnect();
|
||||
}
|
||||
|
||||
@@ -361,11 +361,10 @@ pub(crate) async fn smtp_send(
|
||||
recipients: &[async_smtp::EmailAddress],
|
||||
message: &str,
|
||||
smtp: &mut Smtp,
|
||||
msg_id: MsgId,
|
||||
msg_id: Option<MsgId>,
|
||||
) -> SendResult {
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(context, "smtp-sending out mime message:");
|
||||
println!("{message}");
|
||||
info!(context, "SMTP-sending out mime message:\n{message}");
|
||||
}
|
||||
|
||||
smtp.connectivity.set_working(context).await;
|
||||
@@ -385,7 +384,7 @@ pub(crate) async fn smtp_send(
|
||||
let status = match send_result {
|
||||
Err(crate::smtp::send::Error::SmtpSend(err)) => {
|
||||
// Remote error, retry later.
|
||||
info!(context, "SMTP failed to send: {:?}", &err);
|
||||
info!(context, "SMTP failed to send: {:?}.", &err);
|
||||
|
||||
let res = match err {
|
||||
async_smtp::error::Error::Permanent(ref response) => {
|
||||
@@ -412,10 +411,10 @@ pub(crate) async fn smtp_send(
|
||||
};
|
||||
|
||||
if maybe_transient {
|
||||
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later");
|
||||
info!(context, "Permanent error that is likely to actually be transient, postponing retry for later.");
|
||||
SendResult::Retry
|
||||
} else {
|
||||
info!(context, "Permanent error, message sending failed");
|
||||
info!(context, "Permanent error, message sending failed.");
|
||||
// If we do not retry, add an info message to the chat.
|
||||
// Yandex error "554 5.7.1 [2] Message rejected under suspicion of SPAM; https://ya.cc/..."
|
||||
// should definitely go here, because user has to open the link to
|
||||
@@ -436,20 +435,19 @@ pub(crate) async fn smtp_send(
|
||||
// Any extended smtp status codes like x.1.1, x.1.2 or x.1.3 that we
|
||||
// receive as a transient error are misconfigurations of the smtp server.
|
||||
// See <https://tools.ietf.org/html/rfc3463#section-3.2>
|
||||
info!(context, "Received extended status code {} for a transient error. This looks like a misconfigured SMTP server, let's fail immediately", first_word);
|
||||
info!(context, "Received extended status code {first_word} for a transient error. This looks like a misconfigured SMTP server, let's fail immediately.");
|
||||
SendResult::Failure(format_err!("Permanent SMTP error: {}", err))
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error with status code {}, postponing retry for later",
|
||||
first_word
|
||||
"Transient error with status code {first_word}, postponing retry for later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Transient error without status code, postponing retry for later"
|
||||
"Transient error without status code, postponing retry for later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
@@ -457,14 +455,14 @@ pub(crate) async fn smtp_send(
|
||||
_ => {
|
||||
info!(
|
||||
context,
|
||||
"Message sending failed without error returned by the server, retry later"
|
||||
"Message sending failed without error returned by the server, retry later."
|
||||
);
|
||||
SendResult::Retry
|
||||
}
|
||||
};
|
||||
|
||||
// this clears last_success info
|
||||
info!(context, "Failed to send message over SMTP, disconnecting");
|
||||
info!(context, "Failed to send message over SMTP, disconnecting.");
|
||||
smtp.disconnect();
|
||||
|
||||
res
|
||||
@@ -472,38 +470,41 @@ pub(crate) async fn smtp_send(
|
||||
Err(crate::smtp::send::Error::Envelope(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "SMTP job is invalid: {}", err);
|
||||
warn!(context, "SMTP job is invalid: {err:#}.");
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
Err(crate::smtp::send::Error::NoTransport) => {
|
||||
// Should never happen.
|
||||
// It does not even make sense to disconnect here.
|
||||
error!(context, "SMTP job failed because SMTP has no transport");
|
||||
error!(context, "SMTP job failed because SMTP has no transport.");
|
||||
SendResult::Failure(format_err!("SMTP has not transport"))
|
||||
}
|
||||
Err(crate::smtp::send::Error::Other(err)) => {
|
||||
// Local error, job is invalid, do not retry.
|
||||
smtp.disconnect();
|
||||
warn!(context, "unable to load job: {}", err);
|
||||
warn!(context, "Unable to load SMTP job: {err:#}.");
|
||||
SendResult::Failure(err)
|
||||
}
|
||||
Ok(()) => SendResult::Success,
|
||||
};
|
||||
|
||||
if let SendResult::Failure(err) = &status {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) = message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
if let Some(msg_id) = msg_id {
|
||||
// We couldn't send the message, so mark it as failed
|
||||
match Message::load_from_db(context, msg_id).await {
|
||||
Ok(mut msg) => {
|
||||
if let Err(err) =
|
||||
message::set_msg_failed(context, &mut msg, &err.to_string()).await
|
||||
{
|
||||
error!(context, "Failed to mark {msg_id} as failed: {err:#}.");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(
|
||||
context,
|
||||
"Failed to load {msg_id} to mark it as failed: {err:#}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,12 +559,12 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
.sql
|
||||
.execute("DELETE FROM smtp WHERE id=?", (rowid,))
|
||||
.await
|
||||
.context("failed to remove message with exceeded retry limit from smtp table")?;
|
||||
.context("Failed to remove message with exceeded retry limit from smtp table")?;
|
||||
return Ok(());
|
||||
}
|
||||
info!(
|
||||
context,
|
||||
"Try number {retries} to send message {msg_id} (entry {rowid}) over SMTP"
|
||||
"Try number {retries} to send message {msg_id} (entry {rowid}) over SMTP."
|
||||
);
|
||||
|
||||
let recipients_list = recipients
|
||||
@@ -572,14 +573,14 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
|addr| match async_smtp::EmailAddress::new(addr.to_string()) {
|
||||
Ok(addr) => Some(addr),
|
||||
Err(err) => {
|
||||
warn!(context, "invalid recipient: {} {:?}", addr, err);
|
||||
warn!(context, "Invalid recipient: {} {:?}.", addr, err);
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, msg_id).await;
|
||||
let status = smtp_send(context, &recipients_list, body.as_str(), smtp, Some(msg_id)).await;
|
||||
|
||||
match status {
|
||||
SendResult::Retry => {}
|
||||
@@ -650,7 +651,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
|
||||
loop {
|
||||
if !context.ratelimit.read().await.can_send() {
|
||||
info!(context, "Ratelimiter does not allow sending MDNs now");
|
||||
info!(context, "Ratelimiter does not allow sending MDNs now.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -663,9 +664,6 @@ async fn send_mdns(context: &Context, connection: &mut Smtp) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Tries to send all messages currently in `smtp`, `smtp_status_updates` and `smtp_mdns` tables.
|
||||
///
|
||||
/// Logs and ignores SMTP errors to ensure that a single SMTP message constantly failing to be sent
|
||||
/// does not block other messages in the queue from being sent.
|
||||
pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp) -> Result<()> {
|
||||
let ratelimited = if context.ratelimit.read().await.can_send() {
|
||||
// add status updates and sync messages to end of sending queue
|
||||
@@ -697,7 +695,7 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
for rowid in rowids {
|
||||
send_msg_to_smtp(context, connection, rowid)
|
||||
.await
|
||||
.context("failed to send message")?;
|
||||
.context("Failed to send message")?;
|
||||
}
|
||||
|
||||
// although by slow sending, ratelimit may have been expired meanwhile,
|
||||
@@ -706,12 +704,12 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
if !ratelimited {
|
||||
send_mdns(context, connection)
|
||||
.await
|
||||
.context("failed to send MDNs")?;
|
||||
.context("Failed to send MDNs")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tries to send MDN for message `msg_id` to `contact_id`.
|
||||
/// Tries to send MDN for message identified by `rfc724_mdn` to `contact_id`.
|
||||
///
|
||||
/// Attempts to aggregate additional MDNs for `contact_id` into sent MDN.
|
||||
///
|
||||
@@ -720,9 +718,9 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
/// points to non-existent message or contact.
|
||||
///
|
||||
/// Returns true on success, false on temporary error.
|
||||
async fn send_mdn_msg_id(
|
||||
async fn send_mdn_rfc724_mid(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
rfc724_mid: &str,
|
||||
contact_id: ContactId,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<bool> {
|
||||
@@ -732,26 +730,30 @@ async fn send_mdn_msg_id(
|
||||
}
|
||||
|
||||
// Try to aggregate additional MDNs into this MDN.
|
||||
let (additional_msg_ids, additional_rfc724_mids): (Vec<MsgId>, Vec<String>) = context
|
||||
let additional_rfc724_mids: Vec<String> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT msg_id, rfc724_mid
|
||||
"SELECT rfc724_mid
|
||||
FROM smtp_mdns
|
||||
WHERE from_id=? AND msg_id!=?",
|
||||
(contact_id, msg_id),
|
||||
WHERE from_id=? AND rfc724_mid!=?",
|
||||
(contact_id, &rfc724_mid),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
Ok((msg_id, rfc724_mid))
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
Ok(rfc724_mid)
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unzip();
|
||||
.collect();
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let mimefactory = MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await?;
|
||||
let mimefactory = MimeFactory::from_mdn(
|
||||
context,
|
||||
contact_id,
|
||||
rfc724_mid.to_string(),
|
||||
additional_rfc724_mids.clone(),
|
||||
)
|
||||
.await?;
|
||||
let rendered_msg = mimefactory.render(context).await?;
|
||||
let body = rendered_msg.message;
|
||||
|
||||
@@ -760,21 +762,21 @@ async fn send_mdn_msg_id(
|
||||
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))?;
|
||||
let recipients = vec![recipient];
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, msg_id).await {
|
||||
match smtp_send(context, &recipients, &body, smtp, None).await {
|
||||
SendResult::Success => {
|
||||
info!(context, "Successfully sent MDN for {}", msg_id);
|
||||
info!(context, "Successfully sent MDN for {rfc724_mid}.");
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,))
|
||||
.await?;
|
||||
if !additional_msg_ids.is_empty() {
|
||||
if !additional_rfc724_mids.is_empty() {
|
||||
let q = format!(
|
||||
"DELETE FROM smtp_mdns WHERE msg_id IN({})",
|
||||
sql::repeat_vars(additional_msg_ids.len())
|
||||
"DELETE FROM smtp_mdns WHERE rfc724_mid IN({})",
|
||||
sql::repeat_vars(additional_rfc724_mids.len())
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(&q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.execute(&q, rusqlite::params_from_iter(additional_rfc724_mids))
|
||||
.await?;
|
||||
}
|
||||
Ok(true)
|
||||
@@ -782,7 +784,7 @@ async fn send_mdn_msg_id(
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {}", msg_id
|
||||
"Temporary SMTP failure while sending an MDN for {rfc724_mid}."
|
||||
);
|
||||
Ok(false)
|
||||
}
|
||||
@@ -798,7 +800,7 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
context.sql.execute("DELETE FROM smtp_mdns", []).await?;
|
||||
return Ok(false);
|
||||
}
|
||||
info!(context, "Sending MDNs");
|
||||
info!(context, "Sending MDNs.");
|
||||
|
||||
context
|
||||
.sql
|
||||
@@ -807,40 +809,40 @@ async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
let Some(msg_row) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT msg_id, from_id FROM smtp_mdns ORDER BY retries LIMIT 1",
|
||||
"SELECT rfc724_mid, from_id FROM smtp_mdns ORDER BY retries LIMIT 1",
|
||||
[],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(0)?;
|
||||
let from_id: ContactId = row.get(1)?;
|
||||
Ok((msg_id, from_id))
|
||||
Ok((rfc724_mid, from_id))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let (msg_id, contact_id) = msg_row;
|
||||
let (rfc724_mid, contact_id) = msg_row;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp_mdns SET retries=retries+1 WHERE msg_id=?",
|
||||
(msg_id,),
|
||||
"UPDATE smtp_mdns SET retries=retries+1 WHERE rfc724_mid=?",
|
||||
(rfc724_mid.clone(),),
|
||||
)
|
||||
.await
|
||||
.context("failed to update MDN retries count")?;
|
||||
.context("Failed to update MDN retries count")?;
|
||||
|
||||
match send_mdn_msg_id(context, msg_id, contact_id, smtp).await {
|
||||
match send_mdn_rfc724_mid(context, &rfc724_mid, contact_id, smtp).await {
|
||||
Err(err) => {
|
||||
// If there is an error, for example there is no message corresponding to the msg_id in the
|
||||
// database, do not try to send this MDN again.
|
||||
warn!(
|
||||
context,
|
||||
"Error sending MDN for {msg_id}, removing it: {err:#}."
|
||||
"Error sending MDN for {rfc724_mid}, removing it: {err:#}."
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", (msg_id,))
|
||||
.execute("DELETE FROM smtp_mdns WHERE rfc724_mid = ?", (rfc724_mid,))
|
||||
.await?;
|
||||
Err(err)
|
||||
}
|
||||
|
||||
17
src/sql.rs
17
src/sql.rs
@@ -762,8 +762,9 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_mdns WHERE msg_id NOT IN (SELECT id FROM msgs)",
|
||||
(),
|
||||
"DELETE FROM msgs_mdns WHERE msg_id NOT IN \
|
||||
(SELECT id FROM msgs WHERE chat_id!=?)",
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await
|
||||
.context("failed to remove old MDNs")
|
||||
@@ -773,8 +774,9 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"DELETE FROM msgs_status_updates WHERE msg_id NOT IN (SELECT id FROM msgs)",
|
||||
(),
|
||||
"DELETE FROM msgs_status_updates WHERE msg_id NOT IN \
|
||||
(SELECT id FROM msgs WHERE chat_id!=?)",
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
)
|
||||
.await
|
||||
.context("failed to remove old webxdc status updates")
|
||||
@@ -990,16 +992,19 @@ async fn maybe_add_from_param(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes from the database locally deleted messages that also don't
|
||||
/// Removes from the database stale locally deleted messages that also don't
|
||||
/// have a server UID.
|
||||
async fn prune_tombstones(sql: &Sql) -> Result<()> {
|
||||
// Keep tombstones for the last two days to prevent redownloading locally deleted messages.
|
||||
let timestamp_max = time().saturating_sub(2 * 24 * 3600);
|
||||
sql.execute(
|
||||
"DELETE FROM msgs
|
||||
WHERE chat_id=?
|
||||
AND timestamp<=?
|
||||
AND NOT EXISTS (
|
||||
SELECT * FROM imap WHERE msgs.rfc724_mid=rfc724_mid AND target!=''
|
||||
)",
|
||||
(DC_CHAT_ID_TRASH,),
|
||||
(DC_CHAT_ID_TRASH, timestamp_max),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -578,7 +578,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
if dbversion < 90 {
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE smtp_mdns (
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
|
||||
from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
|
||||
@@ -937,6 +937,21 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 115 {
|
||||
sql.execute_migration("ALTER TABLE msgs ADD COLUMN txt_normalized TEXT", 115)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if dbversion < 116 {
|
||||
// Whether the message part doesn't need to be stored on the server. If all parts are marked
|
||||
// deleted, a server-side deletion is issued.
|
||||
sql.execute_migration(
|
||||
"ALTER TABLE msgs ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0",
|
||||
116,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
|
||||
@@ -83,9 +83,6 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Return receipt"))]
|
||||
ReadRcpt = 31,
|
||||
|
||||
#[strum(props(fallback = "This is a return receipt for the message \"%1$s\"."))]
|
||||
ReadRcptMailBody = 32,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption preferred"))]
|
||||
E2ePreferred = 34,
|
||||
|
||||
@@ -444,8 +441,8 @@ pub enum StockMessage {
|
||||
))]
|
||||
SecurejoinWaitTimeout = 191,
|
||||
|
||||
#[strum(props(fallback = "Contact"))]
|
||||
Contact = 200,
|
||||
#[strum(props(fallback = "This message is a receipt notification."))]
|
||||
ReadRcptMailBody = 192,
|
||||
}
|
||||
|
||||
impl StockMessage {
|
||||
@@ -773,11 +770,6 @@ pub(crate) async fn gif(context: &Context) -> String {
|
||||
translated(context, StockMessage::Gif).await
|
||||
}
|
||||
|
||||
/// Stock string: `Encrypted message`.
|
||||
pub(crate) async fn encrypted_msg(context: &Context) -> String {
|
||||
translated(context, StockMessage::EncryptedMsg).await
|
||||
}
|
||||
|
||||
/// Stock string: `End-to-end encryption available.`.
|
||||
pub(crate) async fn e2e_available(context: &Context) -> String {
|
||||
translated(context, StockMessage::E2eAvailable).await
|
||||
@@ -808,11 +800,9 @@ pub(crate) async fn read_rcpt(context: &Context) -> String {
|
||||
translated(context, StockMessage::ReadRcpt).await
|
||||
}
|
||||
|
||||
/// Stock string: `This is a return receipt for the message "%1$s".`.
|
||||
pub(crate) async fn read_rcpt_mail_body(context: &Context, message: &str) -> String {
|
||||
translated(context, StockMessage::ReadRcptMailBody)
|
||||
.await
|
||||
.replace1(message)
|
||||
/// Stock string: `This message is a receipt notification.`.
|
||||
pub(crate) async fn read_rcpt_mail_body(context: &Context) -> String {
|
||||
translated(context, StockMessage::ReadRcptMailBody).await
|
||||
}
|
||||
|
||||
/// Stock string: `Group image deleted.`.
|
||||
@@ -1101,11 +1091,6 @@ pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> S
|
||||
.replace1(url)
|
||||
}
|
||||
|
||||
/// Stock string: `Contact`.
|
||||
pub(crate) async fn contact(context: &Context) -> String {
|
||||
translated(context, StockMessage::Contact).await
|
||||
}
|
||||
|
||||
/// Stock string: `Error:\n\n“%1$s”`.
|
||||
pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
|
||||
translated(context, StockMessage::ConfigurationFailed)
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::contact::{Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::message::{Message, MessageState, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::stock_str;
|
||||
use crate::stock_str::msg_reacted;
|
||||
use crate::tools::truncate;
|
||||
@@ -149,7 +150,7 @@ impl Summary {
|
||||
|
||||
impl Message {
|
||||
/// Returns a summary text.
|
||||
async fn get_summary_text(&self, context: &Context) -> String {
|
||||
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
|
||||
let summary = self.get_summary_text_without_prefix(context).await;
|
||||
|
||||
if self.is_forwarded() {
|
||||
@@ -231,8 +232,8 @@ impl Message {
|
||||
}
|
||||
Viewtype::Vcard => {
|
||||
emoji = Some("👤");
|
||||
type_name = Some(stock_str::contact(context).await);
|
||||
type_file = None;
|
||||
type_name = None;
|
||||
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
|
||||
append_text = true;
|
||||
}
|
||||
Viewtype::Text | Viewtype::Unknown => {
|
||||
@@ -284,6 +285,7 @@ impl Message {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
use crate::param::Param;
|
||||
use crate::test_utils as test;
|
||||
|
||||
@@ -296,7 +298,9 @@ mod tests {
|
||||
async fn test_get_summary_text() {
|
||||
let d = test::TestContext::new().await;
|
||||
let ctx = &d.ctx;
|
||||
|
||||
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
|
||||
.await
|
||||
.unwrap();
|
||||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
@@ -367,25 +371,34 @@ mod tests {
|
||||
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
|
||||
|
||||
let mut msg = Message::new(Viewtype::Vcard);
|
||||
msg.set_file("foo.vcf", None);
|
||||
assert_summary_texts(&msg, ctx, "👤 Contact").await;
|
||||
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None)
|
||||
.await
|
||||
.unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
// If a vCard can't be parsed, the message becomes `Viewtype::File`.
|
||||
assert_eq!(msg.viewtype, Viewtype::File);
|
||||
assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
|
||||
msg.set_text(some_text.clone());
|
||||
assert_summary_texts(&msg, ctx, "👤 bla bla").await;
|
||||
assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Vcard);
|
||||
msg.set_file_from_bytes(
|
||||
ctx,
|
||||
"alice.vcf",
|
||||
b"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Alice Wonderland\n\
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
END:VCARD",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_summary_texts(&msg, ctx, "👤 Contact").await;
|
||||
for vt in [Viewtype::Vcard, Viewtype::File] {
|
||||
let mut msg = Message::new(vt);
|
||||
msg.set_file_from_bytes(
|
||||
ctx,
|
||||
"alice.vcf",
|
||||
b"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Alice Wonderland\n\
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
END:VCARD",
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||||
assert_eq!(msg.viewtype, Viewtype::Vcard);
|
||||
assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
|
||||
}
|
||||
|
||||
// Forwarded
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
|
||||
23
src/sync.rs
23
src/sync.rs
@@ -101,7 +101,7 @@ impl Context {
|
||||
/// Adds item and timestamp to the list of items that should be synchronized to other devices.
|
||||
/// If device synchronization is disabled, the function does nothing.
|
||||
async fn add_sync_item_with_timestamp(&self, data: SyncData, timestamp: i64) -> Result<()> {
|
||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||
if !self.should_send_sync_msgs().await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ impl Context {
|
||||
/// If device synchronization is disabled,
|
||||
/// no tokens exist or the chat is unpromoted, the function does nothing.
|
||||
pub(crate) async fn sync_qr_code_tokens(&self, chat_id: Option<ChatId>) -> Result<()> {
|
||||
if !self.get_config_bool(Config::SyncMsgs).await? {
|
||||
if !self.should_send_sync_msgs().await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -328,11 +328,24 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_config_sync_msgs() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
|
||||
assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
|
||||
assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(t.should_send_sync_msgs().await?, false);
|
||||
|
||||
t.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
assert!(t.get_config_bool(Config::SyncMsgs).await?);
|
||||
assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
|
||||
assert_eq!(t.get_config_bool(Config::BccSelf).await?, true);
|
||||
assert_eq!(t.should_send_sync_msgs().await?, true);
|
||||
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, true);
|
||||
assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
|
||||
assert_eq!(t.should_send_sync_msgs().await?, false);
|
||||
|
||||
t.set_config_bool(Config::SyncMsgs, false).await?;
|
||||
assert!(!t.get_config_bool(Config::SyncMsgs).await?);
|
||||
assert_eq!(t.get_config_bool(Config::SyncMsgs).await?, false);
|
||||
assert_eq!(t.get_config_bool(Config::BccSelf).await?, false);
|
||||
assert_eq!(t.should_send_sync_msgs().await?, false);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -566,18 +566,12 @@ impl TestContext {
|
||||
|
||||
/// Returns the [`Contact`] for the other [`TestContext`], creating it if necessary.
|
||||
pub async fn add_or_lookup_contact(&self, other: &TestContext) -> Contact {
|
||||
let name = other
|
||||
.ctx
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let primary_self_addr = other.ctx.get_primary_self_addr().await.unwrap();
|
||||
let addr = ContactAddress::new(&primary_self_addr).unwrap();
|
||||
// MailinglistAddress is the lowest allowed origin, we'd prefer to not modify the
|
||||
// origin when creating this contact.
|
||||
let (contact_id, modified) =
|
||||
Contact::add_or_lookup(self, &name, &addr, Origin::MailinglistAddress)
|
||||
Contact::add_or_lookup(self, "", &addr, Origin::MailinglistAddress)
|
||||
.await
|
||||
.expect("add_or_lookup");
|
||||
match modified {
|
||||
|
||||
@@ -542,7 +542,7 @@ async fn test_mdn_doesnt_disable_verification() -> Result<()> {
|
||||
let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await;
|
||||
message::markseen_msgs(&bob, vec![rcvd.id]).await?;
|
||||
|
||||
let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?;
|
||||
let mimefactory = MimeFactory::from_mdn(&bob, rcvd.from_id, rcvd.rfc724_mid, vec![]).await?;
|
||||
let rendered_msg = mimefactory.render(&bob).await?;
|
||||
let body = rendered_msg.message;
|
||||
receive_imf(&alice, body.as_bytes(), false).await.unwrap();
|
||||
@@ -860,6 +860,35 @@ async fn test_verified_member_added_reordering() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_no_unencrypted_name_if_verified() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
for verified in [false, true] {
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob Smith"))
|
||||
.await?;
|
||||
if verified {
|
||||
enable_verified_oneonone_chats(&[&bob]).await;
|
||||
mark_as_verified(&bob, &alice).await;
|
||||
} else {
|
||||
tcm.send_recv_accept(&alice, &bob, "hi").await;
|
||||
}
|
||||
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
let msg = &bob.send_text(chat_id, "hi").await;
|
||||
|
||||
assert_eq!(msg.payload.contains("Bob Smith"), !verified);
|
||||
assert!(msg.payload.contains("BEGIN PGP MESSAGE"));
|
||||
|
||||
let msg = alice.recv_msg(msg).await;
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
|
||||
assert_eq!(Contact::get_display_name(&contact), "Bob Smith");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============== Helper Functions ==============
|
||||
|
||||
async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) {
|
||||
|
||||
@@ -2588,6 +2588,7 @@ sth_for_the = "future""#
|
||||
);
|
||||
|
||||
remove_contact_from_chat(&alice, chat_id, contact_bob).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
let status =
|
||||
helper_send_receive_status_update(&bob, &alice, &bob_instance, &instance).await?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Group#Chat#10: Group chat [3 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#11): I created a group [FRESH]
|
||||
Msg#11: (Contact#Contact#11): Member Fiona (fiona@example.net) added by alice@example.org. [FRESH][INFO]
|
||||
Msg#12: Me (Contact#Contact#Self): You removed member Fiona (fiona@example.net). [INFO] o
|
||||
Msg#12: Me (Contact#Contact#Self): You removed member Fiona (fiona@example.net). [INFO] √
|
||||
Msg#13: (Contact#Contact#11): Welcome, Fiona! [FRESH]
|
||||
Msg#14: info (Contact#Contact#Info): Member Fiona (fiona@example.net) added. [NOTICED][INFO]
|
||||
Msg#15: (Contact#Contact#11): Welcome back, Fiona! [FRESH]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Group#Chat#10: Group chat [4 member(s)]
|
||||
--------------------------------------------------------------------------------
|
||||
Msg#10: (Contact#Contact#10): Hi! I created a group. [FRESH]
|
||||
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] o
|
||||
Msg#11: Me (Contact#Contact#Self): You left the group. [INFO] √
|
||||
Msg#12: (Contact#Contact#10): Member claire@example.net added by alice@example.org. [FRESH][INFO]
|
||||
Msg#13: info (Contact#Contact#Info): Member Me (bob@example.net) added. [NOTICED][INFO]
|
||||
Msg#14: (Contact#Contact#10): What a silence! [FRESH]
|
||||
|
||||
Reference in New Issue
Block a user