diff --git a/.gitattributes b/.gitattributes index 26f7e3d9b..1359bb5fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,7 @@ # This directory contains email messages verbatim, and changing CRLF to # LF will corrupt them. -test-data/* text=false +test-data/** text=false # binary files should be detected by git, however, to be sure, you can add them here explicitly *.png binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173b342ea..81c0b0077 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: override: true - run: rustup component add rustfmt - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - run: cargo fmt --all -- --check run_clippy: @@ -38,7 +38,7 @@ jobs: components: clippy override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -60,30 +60,31 @@ jobs: components: rust-docs override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - name: Rustdoc run: cargo doc --document-private-items --no-deps build_and_test: name: Build and test strategy: + fail-fast: false matrix: include: - # Currently used Rust version, same as in `rust-toolchain` file. + # Currently used Rust version. - os: ubuntu-latest - rust: 1.65.0 + rust: 1.64.0 python: 3.9 - os: windows-latest - rust: 1.65.0 + rust: 1.64.0 python: false # Python bindings compilation on Windows is not supported. - # Minimum Supported Rust Version = 1.61.0 + # Minimum Supported Rust Version = 1.63.0 # # Minimum Supported Python Version = 3.7 # This is the minimum version for which manylinux Python wheels are # built. - os: ubuntu-latest - rust: 1.61.0 + rust: 1.63.0 python: 3.7 runs-on: ${{ matrix.os }} steps: @@ -96,7 +97,7 @@ jobs: override: true - name: Cache rust cargo artifacts - uses: swatinem/rust-cache@v1 + uses: swatinem/rust-cache@v2 - name: check run: cargo check --all --bins --examples --tests --features repl --benches diff --git a/.github/workflows/jsonrpc.yml b/.github/workflows/jsonrpc.yml index c6819110a..de05f50d6 100644 --- a/.github/workflows/jsonrpc.yml +++ b/.github/workflows/jsonrpc.yml @@ -19,13 +19,8 @@ jobs: uses: actions/setup-node@v3 with: node-version: 16.x - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - name: Add Rust cache - uses: Swatinem/rust-cache@v1.3.0 + uses: Swatinem/rust-cache@v2 - name: npm install run: | cd deltachat-jsonrpc/typescript diff --git a/.github/workflows/node-tests.yml b/.github/workflows/node-tests.yml index 4789de22e..d2f210f39 100644 --- a/.github/workflows/node-tests.yml +++ b/.github/workflows/node-tests.yml @@ -59,6 +59,7 @@ jobs: npm run test env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} + NODE_OPTIONS: '--force-node-api-uncaught-exceptions-policy=true' - name: Run tests on Windows, except lint timeout-minutes: 10 if: runner.os == 'Windows' @@ -67,3 +68,4 @@ jobs: npm run test:mocha env: DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }} + NODE_OPTIONS: '--force-node-api-uncaught-exceptions-policy=true' diff --git a/.github/workflows/repl.yml b/.github/workflows/repl.yml index 38b84e921..c0a76fc37 100644 --- a/.github/workflows/repl.yml +++ b/.github/workflows/repl.yml @@ -16,7 +16,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: 1.50.0 + toolchain: 1.66.0 override: true - name: build diff --git a/.github/workflows/upload-docs.yml b/.github/workflows/upload-docs.yml index fa9b7f6e4..c624cb081 100644 --- a/.github/workflows/upload-docs.yml +++ b/.github/workflows/upload-docs.yml @@ -13,7 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - name: Build the documentation with cargo run: | cargo doc --package deltachat --no-deps diff --git a/.github/workflows/upload-ffi-docs.yml b/.github/workflows/upload-ffi-docs.yml index 96c192803..73fa07cb1 100644 --- a/.github/workflows/upload-ffi-docs.yml +++ b/.github/workflows/upload-ffi-docs.yml @@ -13,7 +13,6 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions-rs/toolchain@v1 - name: Build the documentation with cargo run: | cargo doc --package deltachat_ffi --no-deps diff --git a/CHANGELOG.md b/CHANGELOG.md index 44dbab3a5..441f7a8b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,78 @@ ## Unreleased +### Fixes +- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914 + +### API-Changes +- jsonrpc: add verified-by information to `Contact`-Object +- Remove `attach_selfavatar` config #3951 +- `DC_EVENT_WEBXDC_UPDATE_STATE_CHANGED` is emitted when webxdc update state changes #3320 + +## 1.106.0 + +### Changes +- Only send IncomingMsgBunch if there are more than 0 new messages #3941 + +### Fixes +- fix: only send contact changed event for recently seen if it is relevant (not too old to matter) #3938 +- Immediately save `accounts.toml` if it was modified by a migration from absolute paths to relative paths #3943 +- Do not treat invalid email addresses as an exception #3942 +- Add timeouts to HTTP requests #3948 + + +## 1.105.0 + +### Changes +- Validate signatures in try_decrypt() even if the message isn't encrypted #3859 +- Don't parse the message again after detached signatures validation #3862 +- Move format=flowed support to a separate crate #3869 +- cargo: bump quick-xml from 0.23.0 to 0.26.0 #3722 +- Add fuzzing tests #3853 +- Add mappings for some file types to Viewtype / MIME type #3881 +- Buffer IMAP client writes #3888 +- move `DC_CHAT_ID_ARCHIVED_LINK` to the top of chat lists + and make `dc_get_fresh_msg_cnt()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3918 +- make `dc_marknoticed_chat()` work for `DC_CHAT_ID_ARCHIVED_LINK` #3919 +- Update provider database + +### API-Changes +- jsonrpc: add python API for webxdc updates #3872 +- jsonrpc: add fresh message count to ChatListItemFetchResult::ArchiveLink +- Add ffi functions to retrieve `verified by` information #3786 +- resultify `Message::get_filebytes()` #3925 + +### Fixes +- Do not add an error if the message is encrypted but not signed #3860 +- Do not strip leading spaces from message lines #3867 +- Fix uncaught exception in JSON-RPC tests #3884 +- Fix STARTTLS connection and add a test for it #3907 +- Trigger reconnection when failing to fetch existing messages #3911 +- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913 +- Ensure format=flowed formatting is always reversible on the receiver side #3880 + + +## 1.104.0 + ### Changes - Don't use deprecated `chrono` functions #3798 - Document accounts manager #3837 - If a classical-email-user sends an email to a group and adds new recipients, add the new recipients as group members #3781 -- `DC_EVENT_WEBXDC_UPDATE_STATE_CHANGED` is emitted when update state changes #3320 + ### API-Changes +- Remove `pytest-async` plugin #3846 +- Only send the message about ephemeral timer change if the chat is promoted #3847 +- Use relative paths in `accounts.toml` #3838 ### Fixes - Set read/write timeouts for IMAP over SOCKS5 #3833 +- Treat attached PGP keys as peer keys with mutual encryption preference #3832 +- fix migration of old databases #3842 +- Fix cargo clippy and doc errors after Rust update to 1.66 #3850 +- Don't send GroupNameChanged message if the group name doesn't change in terms of + `improve_single_line_input()` #3852 +- Prefer encryption for the peer if the message is encrypted or signed with the known key #3849 ## 1.103.0 diff --git a/Cargo.lock b/Cargo.lock index 1fb73cb46..8bc55496c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,9 +10,9 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -86,9 +86,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" [[package]] name = "ascii_utils" @@ -340,15 +340,15 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", "cfg-if", "libc", - "miniz_oxide 0.5.3", + "miniz_oxide 0.6.2", "object", "rustc-demangle", ] @@ -371,6 +371,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + [[package]] name = "base64ct" version = "1.5.1" @@ -861,13 +867,13 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" [[package]] name = "deltachat" -version = "1.103.0" +version = "1.106.0" dependencies = [ "ansi_term", "anyhow", @@ -877,7 +883,7 @@ dependencies = [ "async-smtp", "async_zip", "backtrace", - "base64 0.13.1", + "base64 0.20.0", "bitflags", "chrono", "criterion", @@ -887,6 +893,7 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "format-flowed", "futures", "futures-lite", "hex", @@ -940,7 +947,7 @@ dependencies = [ [[package]] name = "deltachat-jsonrpc" -version = "1.103.0" +version = "1.106.0" dependencies = [ "anyhow", "async-channel", @@ -962,7 +969,7 @@ dependencies = [ [[package]] name = "deltachat-rpc-server" -version = "1.103.0" +version = "1.106.0" dependencies = [ "anyhow", "deltachat-jsonrpc", @@ -985,7 +992,7 @@ dependencies = [ [[package]] name = "deltachat_ffi" -version = "1.103.0" +version = "1.106.0" dependencies = [ "anyhow", "deltachat", @@ -1445,6 +1452,10 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "format-flowed" +version = "1.0.0" + [[package]] name = "futures" version = "0.3.25" @@ -1593,9 +1604,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" [[package]] name = "h2" @@ -1747,9 +1758,9 @@ dependencies = [ [[package]] name = "humansize" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" dependencies = [ "libm", ] @@ -2025,9 +2036,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libm" @@ -2095,9 +2106,9 @@ dependencies = [ [[package]] name = "mailparse" -version = "0.13.8" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa" dependencies = [ "charset", "data-encoding", @@ -2331,28 +2342,28 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "object" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "oorandom" @@ -2734,27 +2745,27 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.23.0" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] [[package]] name = "quoted_printable" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" [[package]] name = "r2d2" @@ -3179,18 +3190,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", @@ -3199,9 +3210,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" dependencies = [ "itoa", "ryu", @@ -3408,9 +3419,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -3477,18 +3488,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -3533,9 +3544,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" dependencies = [ "autocfg", "bytes", @@ -3548,7 +3559,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -3636,9 +3647,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 5841fc8b9..2ba38945e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [package] name = "deltachat" -version = "1.103.0" -authors = ["Delta Chat Developers (ML) "] +version = "1.106.0" edition = "2021" license = "MPL-2.0" -rust-version = "1.61" +rust-version = "1.63" [profile.dev] debug = 0 @@ -20,6 +19,7 @@ panic = 'abort' [dependencies] deltachat_derive = { path = "./deltachat_derive" } +format-flowed = { path = "./format-flowed" } ansi_term = { version = "0.12.1", optional = true } anyhow = "1" @@ -30,7 +30,7 @@ trust-dns-resolver = "0.22" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar backtrace = "0.3" -base64 = "0.13" +base64 = "0.20" bitflags = "1.3" chrono = { version = "0.4", default-features=false, features = ["clock", "std"] } dirs = { version = "4", optional=true } @@ -44,16 +44,16 @@ kamadak-exif = "0.5" lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = "0.2" log = {version = "0.4.16", optional = true } -mailparse = "0.13" +mailparse = "0.14" native-tls = "0.2" -num_cpus = "1.14" +num_cpus = "1.15" num-derive = "0.3" num-traits = "0.2" -once_cell = "1.16.0" +once_cell = "1.17.0" percent-encoding = "2.2" pgp = { version = "0.9", default-features = false } pretty_env_logger = { version = "0.4", optional = true } -quick-xml = "0.23" +quick-xml = "0.27" r2d2 = "0.8" r2d2_sqlite = "0.20" rand = "0.8" @@ -100,7 +100,8 @@ members = [ "deltachat-ffi", "deltachat_derive", "deltachat-jsonrpc", - "deltachat-rpc-server" + "deltachat-rpc-server", + "format-flowed", ] [[example]] diff --git a/README.md b/README.md index 71a723eac..3ea334637 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,29 @@ use the `--ignored` argument to the test binary (not to cargo itself): $ cargo test -- --ignored ``` +### Fuzzing + +Install [`cargo-bolero`](https://github.com/camshaft/bolero) with +```sh +$ cargo install cargo-bolero +``` + +Run fuzzing tests with +```sh +$ cd fuzz +$ cargo bolero test fuzz_mailparse --release=false -s NONE +``` + +Corpus is created at `fuzz/fuzz_targets/corpus`, +you can add initial inputs there. +For `fuzz_mailparse` target corpus can be populated with +`../test-data/message/*.eml`. + +To run with AFL instead of libFuzzer: +```sh +$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE +``` + ## Features - `vendored`: When using Openssl for TLS, this bundles a vendored version. diff --git a/assets/icon-archive.png b/assets/icon-archive.png new file mode 100644 index 000000000..95d35e2a5 Binary files /dev/null and b/assets/icon-archive.png differ diff --git a/assets/icon-archive.svg b/assets/icon-archive.svg new file mode 100644 index 000000000..a8ba45d49 --- /dev/null +++ b/assets/icon-archive.svg @@ -0,0 +1,60 @@ + + + + + + + + + + diff --git a/deltachat-ffi/Cargo.toml b/deltachat-ffi/Cargo.toml index 1e967cca4..3488ba82b 100644 --- a/deltachat-ffi/Cargo.toml +++ b/deltachat-ffi/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "deltachat_ffi" -version = "1.103.0" +version = "1.106.0" description = "Deltachat FFI" -authors = ["Delta Chat Developers (ML) "] edition = "2018" readme = "README.md" license = "MPL-2.0" @@ -25,7 +24,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] } anyhow = "1" thiserror = "1" rand = "0.7" -once_cell = "1.16.0" +once_cell = "1.17.0" [features] default = ["vendored"] diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 8f253a660..416f7eafc 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1237,7 +1237,11 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch * Get the number of _fresh_ messages in a chat. * Typically used to implement a badge with a number in the chatlist. * - * If the specified chat is muted, + * As muted archived chats are not unarchived automatically, + * a similar information is needed for the @ref dc_get_chatlist() "archive link" as well: + * here, the number of archived chats containing fresh messages is returned. + * + * If the specified chat is muted or the @ref dc_get_chatlist() "archive link", * the UI should show the badge counter "less obtrusive", * e.g. using "gray" instead of "red" color. * @@ -4745,6 +4749,37 @@ int dc_contact_is_blocked (const dc_contact_t* contact); int dc_contact_is_verified (dc_contact_t* contact); + +/** + * Return the address that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * A string containing the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is an empty string, we don't have verifier + * information or the contact is not verified. + */ +char* dc_contact_get_verifier_addr (dc_contact_t* contact); + + +/** + * Return the `ContactId` that verified a contact + * + * The UI may use this in addition to a checkmark showing the verification status + * + * @memberof dc_contact_t + * @param contact The contact object. + * @return + * The `ContactId` of the verifiers address. If it is the same address as the contact itself, + * we verified the contact ourself. If it is 0, we don't have verifier information or + * the contact is not verified. + */ +uint32_t dc_contact_get_verifier_id (dc_contact_t* contact); + + /** * @class dc_provider_t * @@ -5786,7 +5821,7 @@ void dc_event_unref(dc_event_t* event); * @param data2 (int) The progress as: * 300=vg-/vc-request received, typically shown as "bob@addr joins". * 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - * 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + * 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. * 1000=Protocol finished for this contact. */ #define DC_EVENT_SECUREJOIN_INVITER_PROGRESS 2060 diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 6cd2ea202..877ac2393 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2189,7 +2189,7 @@ pub unsafe extern "C" fn dc_imex( eprintln!("ignoring careless call to dc_imex()"); return; } - let what = match imex::ImexMode::from_i32(what_raw as i32) { + let what = match imex::ImexMode::from_i32(what_raw) { Some(what) => what, None => { eprintln!("ignoring invalid argument {} to dc_imex", what_raw); @@ -2260,10 +2260,7 @@ pub unsafe extern "C" fn dc_continue_key_transfer( msg_id: u32, setup_code: *const libc::c_char, ) -> libc::c_int { - if context.is_null() - || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL as u32 - || setup_code.is_null() - { + if context.is_null() || msg_id <= constants::DC_MSG_ID_LAST_SPECIAL || setup_code.is_null() { eprintln!("ignoring careless call to dc_continue_key_transfer()"); return 0; } @@ -2454,15 +2451,9 @@ pub unsafe extern "C" fn dc_get_locations( }; block_on(async move { - let res = location::get_range( - ctx, - chat_id, - contact_id, - timestamp_begin as i64, - timestamp_end as i64, - ) - .await - .unwrap_or_log_default(ctx, "Failed get_locations"); + let res = location::get_range(ctx, chat_id, contact_id, timestamp_begin, timestamp_end) + .await + .unwrap_or_log_default(ctx, "Failed get_locations"); Box::into_raw(Box::new(dc_array_t::from(res))) }) } @@ -2709,7 +2700,7 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id( } let ffi_list = &*chatlist; let ctx = &*ffi_list.context; - match ffi_list.list.get_chat_id(index as usize) { + match ffi_list.list.get_chat_id(index) { Ok(chat_id) => chat_id.to_u32(), Err(err) => { warn!(ctx, "get_chat_id failed: {}", err); @@ -2729,7 +2720,7 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id( } let ffi_list = &*chatlist; let ctx = &*ffi_list.context; - match ffi_list.list.get_msg_id(index as usize) { + match ffi_list.list.get_msg_id(index) { Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()), Err(err) => { warn!(ctx, "get_msg_id failed: {}", err); @@ -2760,7 +2751,7 @@ pub unsafe extern "C" fn dc_chatlist_get_summary( block_on(async move { let summary = ffi_list .list - .get_summary(ctx, index as usize, maybe_chat) + .get_summary(ctx, index, maybe_chat) .await .log_err(ctx, "get_summary failed") .unwrap_or_default(); @@ -3343,6 +3334,8 @@ pub unsafe extern "C" fn dc_msg_get_filebytes(msg: *mut dc_msg_t) -> u64 { let ctx = &*ffi_msg.context; block_on(ffi_msg.message.get_filebytes(ctx)) + .unwrap_or_log_default(ctx, "Cannot get file size") + .unwrap_or_default() } #[no_mangle] @@ -3997,6 +3990,37 @@ pub unsafe extern "C" fn dc_contact_is_verified(contact: *mut dc_contact_t) -> l .unwrap_or_default() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_addr( + contact: *mut dc_contact_t, +) -> *mut libc::c_char { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_addr()"); + return "".strdup(); + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + block_on(ffi_contact.contact.get_verifier_addr(ctx)) + .log_err(ctx, "failed to get verifier for contact") + .unwrap_or_default() + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_contact_get_verifier_id(contact: *mut dc_contact_t) -> u32 { + if contact.is_null() { + eprintln!("ignoring careless call to dc_contact_get_verifier_id()"); + return 0; + } + let ffi_contact = &*contact; + let ctx = &*ffi_contact.context; + let verifier_contact_id = block_on(ffi_contact.contact.get_verifier_id(ctx)) + .log_err(ctx, "failed to get verifier") + .unwrap_or_default() + .unwrap_or_default(); + + verifier_contact_id.to_u32() +} // dc_lot_t pub type dc_lot_t = lot::Lot; diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index cef348b8f..763548287 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "deltachat-jsonrpc" -version = "1.103.0" +version = "1.106.0" description = "DeltaChat JSON-RPC API" -authors = ["Delta Chat Developers (ML) "] edition = "2021" default-run = "deltachat-jsonrpc-server" license = "MPL-2.0" @@ -21,10 +20,10 @@ tempfile = "3.3.0" log = "0.4" async-channel = { version = "1.8.0" } futures = { version = "0.3.25" } -serde_json = "1.0.89" +serde_json = "1.0.91" yerpc = { version = "^0.3.1", features = ["anyhow_expose"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] } -tokio = { version = "1.22.0" } +tokio = { version = "1.23.1" } sanitize-filename = "0.4" walkdir = "2.3.2" @@ -33,7 +32,7 @@ axum = { version = "0.6.1", optional = true, features = ["ws"] } env_logger = { version = "0.10.0", optional = true } [dev-dependencies] -tokio = { version = "1.22.0", features = ["full", "rt-multi-thread"] } +tokio = { version = "1.23.1", features = ["full", "rt-multi-thread"] } [features] diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index 45933770f..83b84dd3d 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -48,12 +48,10 @@ pub enum ChatListItemFetchResult { dm_chat_contact: Option, was_seen_recently: bool, }, - ArchiveLink, #[serde(rename_all = "camelCase")] - Error { - id: u32, - error: String, - }, + ArchiveLink { fresh_message_counter: usize }, + #[serde(rename_all = "camelCase")] + Error { id: u32, error: String }, } pub(crate) async fn get_chat_list_item_by_id( @@ -66,8 +64,12 @@ pub(crate) async fn get_chat_list_item_by_id( _ => Some(MsgId::new(entry.1)), }; + let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?; + if chat_id.is_archived_link() { - return Ok(ChatListItemFetchResult::ArchiveLink); + return Ok(ChatListItemFetchResult::ArchiveLink { + fresh_message_counter, + }); } let chat = Chat::load_from_db(ctx, chat_id).await?; @@ -111,7 +113,6 @@ pub(crate) async fn get_chat_list_item_by_id( (None, false) }; - let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?; let color = color_int_to_hex_string(chat.get_color(ctx).await?); Ok(ChatListItemFetchResult::ChatListItem { diff --git a/deltachat-jsonrpc/src/api/types/contact.rs b/deltachat-jsonrpc/src/api/types/contact.rs index 4ed4cf435..d67fc7fb1 100644 --- a/deltachat-jsonrpc/src/api/types/contact.rs +++ b/deltachat-jsonrpc/src/api/types/contact.rs @@ -20,6 +20,10 @@ pub struct ContactObject { name_and_addr: String, is_blocked: bool, is_verified: bool, + /// the address that verified this contact + verifier_addr: Option, + /// the id of the contact that verified this contact + verifier_id: Option, /// the contact's last seen timestamp last_seen: i64, was_seen_recently: bool, @@ -36,6 +40,18 @@ impl ContactObject { }; let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified; + let (verifier_addr, verifier_id) = if is_verified { + ( + contact.get_verifier_addr(context).await?, + contact + .get_verifier_id(context) + .await? + .map(|contact_id| contact_id.to_u32()), + ) + } else { + (None, None) + }; + Ok(ContactObject { address: contact.get_addr().to_owned(), color: color_int_to_hex_string(contact.get_color()), @@ -48,6 +64,8 @@ impl ContactObject { name_and_addr: contact.get_name_n_addr(), is_blocked: contact.is_blocked(), is_verified, + verifier_addr, + verifier_id, last_seen: contact.last_seen(), was_seen_recently: contact.was_seen_recently(), }) diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index c7e9cfe8d..0f35f0c6f 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -105,7 +105,7 @@ impl MessageObject { let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?; let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?; - let file_bytes = message.get_filebytes(context).await; + let file_bytes = message.get_filebytes(context).await?.unwrap_or_default(); let override_sender_name = message.get_override_sender_name(); let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc { diff --git a/deltachat-jsonrpc/typescript/package.json b/deltachat-jsonrpc/typescript/package.json index 1ddb8941d..480685078 100644 --- a/deltachat-jsonrpc/typescript/package.json +++ b/deltachat-jsonrpc/typescript/package.json @@ -48,5 +48,5 @@ }, "type": "module", "types": "dist/deltachat.d.ts", - "version": "1.103.0" + "version": "1.106.0" } \ No newline at end of file diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index 4bf7181c9..06c4c8954 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -12,7 +12,7 @@ describe("online tests", function () { let accountId1: number, accountId2: number; before(async function () { - this.timeout(12000); + this.timeout(60000); if (!process.env.DCC_NEW_TMP_EMAIL) { if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) { console.error( diff --git a/deltachat-rpc-client/examples/echobot.py b/deltachat-rpc-client/examples/echobot.py index 792dbae86..65d447bf9 100755 --- a/deltachat-rpc-client/examples/echobot.py +++ b/deltachat-rpc-client/examples/echobot.py @@ -17,8 +17,9 @@ async def log_event(event): @hooks.on(events.NewMessage) -async def echo(msg): - await msg.chat.send_text(msg.text) +async def echo(event): + snapshot = event.message_snapshot + await snapshot.chat.send_text(snapshot.text) if __name__ == "__main__": diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 88ddd1303..48e3d025b 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -25,14 +25,34 @@ async def log_error(event): logging.error(event.msg) -@hooks.on(events.NewMessage(r".+", func=lambda msg: not msg.text.startswith("/"))) -async def echo(msg): - await msg.chat.send_text(msg.text) +@hooks.on(events.MemberListChanged) +async def on_memberlist_changed(event): + logging.info( + "member %s was %s", event.member, "added" if event.member_added else "removed" + ) -@hooks.on(events.NewMessage(r"/help")) -async def help_command(msg): - await msg.chat.send_text("Send me any text message and I will echo it back") +@hooks.on(events.GroupImageChanged) +async def on_group_image_changed(event): + logging.info("group image %s", "deleted" if event.image_deleted else "changed") + + +@hooks.on(events.GroupNameChanged) +async def on_group_name_changed(event): + logging.info("group name changed, old name: %s", event.old_name) + + +@hooks.on(events.NewMessage(func=lambda e: not e.command)) +async def echo(event): + snapshot = event.message_snapshot + if snapshot.text or snapshot.file: + await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) + + +@hooks.on(events.NewMessage(command="/help")) +async def help_command(event): + snapshot = event.message_snapshot + await snapshot.chat.send_text("Send me any message and I will echo it back") async def main(): diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py index a62ae5f63..ff15923b5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/__init__.py @@ -1,4 +1,5 @@ """Delta Chat asynchronous high-level API""" +from ._utils import AttrDict, run_bot_cli, run_client_cli from .account import Account from .chat import Chat from .client import Bot, Client @@ -7,4 +8,3 @@ from .contact import Contact from .deltachat import DeltaChat from .message import Message from .rpc import Rpc -from .utils import AttrDict, run_bot_cli, run_client_cli diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/utils.py b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py similarity index 63% rename from deltachat-rpc-client/src/deltachat_rpc_client/utils.py rename to deltachat-rpc-client/src/deltachat_rpc_client/_utils.py index f7782ad8b..562c62de1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/utils.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/_utils.py @@ -17,6 +17,8 @@ def _camel_to_snake(name: str) -> str: def _to_attrdict(obj): + if isinstance(obj, AttrDict): + return obj if isinstance(obj, dict): return AttrDict(obj) if isinstance(obj, list): @@ -112,3 +114,64 @@ async def _run_cli( client.configure(email=args.email, password=args.password) ) await client.run_forever() + + +def extract_addr(text: str) -> str: + """extract email address from the given text.""" + match = re.match(r".*\((.+@.+)\)", text) + if match: + text = match.group(1) + text = text.rstrip(".") + return text.strip() + + +def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]: + """return image changed/deleted info from parsing the given system message text.""" + text = text.lower() + match = re.match(r"group image (changed|deleted) by (.+).", text) + if match: + action, actor = match.groups() + return (extract_addr(actor), action == "deleted") + return None + + +def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]: + text = text.lower() + match = re.match(r'group name changed from "(.+)" to ".+" by (.+).', text) + if match: + old_title, actor = match.groups() + return (extract_addr(actor), old_title) + return None + + +def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]: + """return add/remove info from parsing the given system message text. + + returns a (action, affected, actor) tuple. + """ + # You removed member a@b. + # You added member a@b. + # Member Me (x@y) removed by a@b. + # Member x@y added by a@b + # Member With space (tmp1@x.org) removed by tmp2@x.org. + # Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).", + # Group left by some one (tmp1@x.org). + # Group left by tmp1@x.org. + text = text.lower() + + match = re.match(r"member (.+) (removed|added) by (.+)", text) + if match: + affected, action, actor = match.groups() + return action, extract_addr(affected), extract_addr(actor) + + match = re.match(r"you (removed|added) member (.+)", text) + if match: + action, affected = match.groups() + return action, extract_addr(affected), "me" + + if text.startswith("group left by "): + addr = extract_addr(text[13:]) + if addr: + return "removed", addr, addr + + return None diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/account.py b/deltachat-rpc-client/src/deltachat_rpc_client/account.py index d75ef67dd..eab67d10d 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/account.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/account.py @@ -1,11 +1,11 @@ from typing import TYPE_CHECKING, List, Optional, Tuple, Union +from ._utils import AttrDict from .chat import Chat from .const import ChatlistFlag, ContactFlag, SpecialContactId from .contact import Contact from .message import Message from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .deltachat import DeltaChat @@ -108,7 +108,7 @@ class Account: obj = (await obj.get_snapshot()).address return Contact(self, await self._rpc.create_contact(self.id, obj, name)) - async def get_contact_by_id(self, contact_id: int) -> Contact: + def get_contact_by_id(self, contact_id: int) -> Contact: """Return Contact instance for the given contact ID.""" return Contact(self, contact_id) @@ -212,7 +212,7 @@ class Account: """ return Chat(self, await self._rpc.create_group_chat(self.id, name, protect)) - async def get_chat_by_id(self, chat_id: int) -> Chat: + def get_chat_by_id(self, chat_id: int) -> Chat: """Return the Chat instance with the given ID.""" return Chat(self, chat_id) @@ -237,7 +237,7 @@ class Account: """ return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) - async def get_message_by_id(self, msg_id: int) -> Message: + def get_message_by_id(self, msg_id: int) -> Message: """Return the Message instance with the given ID.""" return Message(self, msg_id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 9e053b1fc..4a2f4ae77 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -2,11 +2,11 @@ import calendar from datetime import datetime from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union +from ._utils import AttrDict from .const import ChatVisibility from .contact import Contact from .message import Message from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index 2a749eefe..4c6deafa1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -1,12 +1,35 @@ """Event loop implementations offering high level event handling/hooking.""" +import inspect import logging -from typing import Callable, Dict, Iterable, Optional, Set, Tuple, Type, Union +from typing import ( + Callable, + Coroutine, + Dict, + Iterable, + Optional, + Set, + Tuple, + Type, + Union, +) from deltachat_rpc_client.account import Account -from .const import EventType -from .events import EventFilter, NewInfoMessage, NewMessage, RawEvent -from .utils import AttrDict +from ._utils import ( + AttrDict, + parse_system_add_remove, + parse_system_image_changed, + parse_system_title_changed, +) +from .const import COMMAND_PREFIX, EventType, SystemMessageType +from .events import ( + EventFilter, + GroupImageChanged, + GroupNameChanged, + MemberListChanged, + NewMessage, + RawEvent, +) class Client: @@ -21,6 +44,7 @@ class Client: self.account = account self.logger = logger or logging self._hooks: Dict[type, Set[tuple]] = {} + self._should_process_messages = 0 self.add_hooks(hooks or []) def add_hooks( @@ -36,12 +60,24 @@ class Client: if isinstance(event, type): event = event() assert isinstance(event, EventFilter) + self._should_process_messages += int( + isinstance( + event, + (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), + ) + ) self._hooks.setdefault(type(event), set()).add((hook, event)) def remove_hook(self, hook: Callable, event: Union[type, EventFilter]) -> None: """Unregister hook from the given event filter.""" if isinstance(event, type): event = event() + self._should_process_messages -= int( + isinstance( + event, + (NewMessage, MemberListChanged, GroupImageChanged, GroupNameChanged), + ) + ) self._hooks.get(type(event), set()).remove((hook, event)) async def is_configured(self) -> bool: @@ -56,6 +92,18 @@ class Client: self.logger.debug("Account configured") async def run_forever(self) -> None: + """Process events forever.""" + await self.run_until(lambda _: False) + + async def run_until( + self, func: Callable[[AttrDict], Union[bool, Coroutine]] + ) -> AttrDict: + """Process events until the given callable evaluates to True. + + The callable should accept an AttrDict object representing the + last processed event. The event is returned when the callable + evaluates to True. + """ self.logger.debug("Listening to incoming events...") if await self.is_configured(): await self.account.start_io() @@ -68,6 +116,12 @@ class Client: if event.type == EventType.INCOMING_MSG: await self._process_messages() + stop = func(event) + if inspect.isawaitable(stop): + stop = await stop + if stop: + return event + async def _on_event( self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent ) -> None: @@ -78,17 +132,82 @@ class Client: except Exception as ex: self.logger.exception(ex) - def _should_process_messages(self) -> bool: - return any(issubclass(filter_type, NewMessage) for filter_type in self._hooks) + async def _parse_command(self, event: AttrDict) -> None: + cmds = [ + hook[1].command + for hook in self._hooks.get(NewMessage, []) + if hook[1].command + ] + parts = event.message_snapshot.text.split(maxsplit=1) + payload = parts[1] if len(parts) > 1 else "" + cmd = parts.pop(0) + + if "@" in cmd: + suffix = "@" + (await self.account.self_contact.get_snapshot()).address + if cmd.endswith(suffix): + cmd = cmd[: -len(suffix)] + else: + return + + parts = cmd.split("_") + _payload = payload + while parts: + _cmd = "_".join(parts) + if _cmd in cmds: + break + _payload = (parts.pop() + " " + _payload).rstrip() + + if parts: + cmd = _cmd + payload = _payload + + event["command"], event["payload"] = cmd, payload + + async def _on_new_msg(self, snapshot: AttrDict) -> None: + event = AttrDict(command="", payload="", message_snapshot=snapshot) + if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): + await self._parse_command(event) + await self._on_event(event, NewMessage) + + async def _handle_info_msg(self, snapshot: AttrDict) -> None: + event = AttrDict(message_snapshot=snapshot) + + img_changed = parse_system_image_changed(snapshot.text) + if img_changed: + _, event["image_deleted"] = img_changed + await self._on_event(event, GroupImageChanged) + return + + title_changed = parse_system_title_changed(snapshot.text) + if title_changed: + _, event["old_name"] = title_changed + await self._on_event(event, GroupNameChanged) + return + + members_changed = parse_system_add_remove(snapshot.text) + if members_changed: + action, event["member"], _ = members_changed + event["member_added"] = action == "added" + await self._on_event(event, MemberListChanged) + return + + self.logger.warning( + "ignoring unsupported system message id=%s text=%s", + snapshot.id, + snapshot.text, + ) async def _process_messages(self) -> None: - if self._should_process_messages(): + if self._should_process_messages: for message in await self.account.get_fresh_messages_in_arrival_order(): snapshot = await message.get_snapshot() - if snapshot.is_info: - await self._on_event(snapshot, NewInfoMessage) - else: - await self._on_event(snapshot, NewMessage) + await self._on_new_msg(snapshot) + if ( + snapshot.is_info + and snapshot.system_message_type + != SystemMessageType.WEBXDC_INFO_MESSAGE + ): + await self._handle_info_msg(snapshot) await snapshot.message.mark_seen() diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/const.py b/deltachat-rpc-client/src/deltachat_rpc_client/const.py index c8bb925fc..b2a4e7f12 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/const.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/const.py @@ -1,5 +1,7 @@ from enum import Enum, IntEnum +COMMAND_PREFIX = "/" + class ContactFlag(IntEnum): VERIFIED_ONLY = 0x01 diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py index 804ac4af1..7c5267b4f 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/contact.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/contact.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING +from ._utils import AttrDict from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py index 2c926bac5..16afe458b 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/deltachat.py @@ -1,8 +1,8 @@ from typing import Dict, List +from ._utils import AttrDict from .account import Account from .rpc import Rpc -from .utils import AttrDict class DeltaChat: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index 606fe9896..146c89ea5 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -4,8 +4,8 @@ import re from abc import ABC, abstractmethod from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union +from ._utils import AttrDict from .const import EventType -from .utils import AttrDict def _tuple_of(obj, type_: type) -> tuple: @@ -91,8 +91,19 @@ class RawEvent(EventFilter): class NewMessage(EventFilter): """Matches whenever a new message arrives. - Warning: registering a handler for this event or any subclass will cause the messages + Warning: registering a handler for this event will cause the messages to be marked as read. Its usage is mainly intended for bots. + + :param pattern: if set, this Pattern will be used to filter the message by its text + content. + :param command: If set, only match messages with the given command (ex. /help). + Setting this property implies `is_info==False`. + :param is_info: If set to True only match info/system messages, if set to False + only match messages that are not info/system messages. If omitted + info/system messages as well as normal messages will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. """ def __init__( @@ -103,9 +114,17 @@ class NewMessage(EventFilter): Callable[[str], bool], re.Pattern, ] = None, + command: Optional[str] = None, + is_info: Optional[bool] = None, func: Optional[Callable[[AttrDict], bool]] = None, ) -> None: super().__init__(func=func) + self.is_info = is_info + if command is not None and not isinstance(command, str): + raise TypeError("Invalid command") + self.command = command + if self.is_info and self.command: + raise AttributeError("Can not use command and is_info at the same time.") if isinstance(pattern, str): pattern = re.compile(pattern) if isinstance(pattern, re.Pattern): @@ -119,13 +138,22 @@ class NewMessage(EventFilter): return hash((self.pattern, self.func)) def __eq__(self, other) -> bool: - if type(other) is self.__class__: # noqa - return (self.pattern, self.func) == (other.pattern, other.func) + if isinstance(other, NewMessage): + return (self.pattern, self.command, self.is_info, self.func) == ( + other.pattern, + other.command, + other.is_info, + other.func, + ) return False async def filter(self, event: AttrDict) -> bool: + if self.is_info is not None and self.is_info != event.message_snapshot.is_info: + return False + if self.command and self.command != event.command: + return False if self.pattern: - match = self.pattern(event.text) + match = self.pattern(event.message_snapshot.text) if inspect.isawaitable(match): match = await match if not match: @@ -133,8 +161,91 @@ class NewMessage(EventFilter): return await super()._call_func(event) -class NewInfoMessage(NewMessage): - """Matches whenever a new info/system message arrives.""" +class MemberListChanged(EventFilter): + """Matches when a group member is added or removed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param added: If set to True only match if a member was added, if set to False + only match if a member was removed. If omitted both, member additions + and removals, will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __init__(self, added: Optional[bool] = None, **kwargs): + super().__init__(**kwargs) + self.added = added + + def __hash__(self) -> int: + return hash((self.added, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, MemberListChanged): + return (self.added, self.func) == (other.added, other.func) + return False + + async def filter(self, event: AttrDict) -> bool: + if self.added is not None and self.added != event.member_added: + return False + return await self._call_func(event) + + +class GroupImageChanged(EventFilter): + """Matches when the group image is changed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param deleted: If set to True only match if the image was deleted, if set to False + only match if a new image was set. If omitted both, image changes and + removals, will be matched. + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __init__(self, deleted: Optional[bool] = None, **kwargs): + super().__init__(**kwargs) + self.deleted = deleted + + def __hash__(self) -> int: + return hash((self.deleted, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, GroupImageChanged): + return (self.deleted, self.func) == (other.deleted, other.func) + return False + + async def filter(self, event: AttrDict) -> bool: + if self.deleted is not None and self.deleted != event.image_deleted: + return False + return await self._call_func(event) + + +class GroupNameChanged(EventFilter): + """Matches when the group name is changed. + + Warning: registering a handler for this event will cause the messages + to be marked as read. Its usage is mainly intended for bots. + + :param func: A Callable (async or not) function that should accept the event as input + parameter, and return a bool value indicating whether the event + should be dispatched or not. + """ + + def __hash__(self) -> int: + return hash((GroupNameChanged, self.func)) + + def __eq__(self, other) -> bool: + if isinstance(other, GroupNameChanged): + return self.func == other.func + return False + + async def filter(self, event: AttrDict) -> bool: + return await self._call_func(event) class HookCollection: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/message.py b/deltachat-rpc-client/src/deltachat_rpc_client/message.py index 0001dd308..86ddbc95e 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/message.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/message.py @@ -1,8 +1,9 @@ -from typing import TYPE_CHECKING +import json +from typing import TYPE_CHECKING, Union +from ._utils import AttrDict from .contact import Contact from .rpc import Rpc -from .utils import AttrDict if TYPE_CHECKING: from .account import Account @@ -47,3 +48,23 @@ class Message: async def mark_seen(self) -> None: """Mark the message as seen.""" await self._rpc.markseen_msgs(self.account.id, [self.id]) + + async def send_webxdc_status_update( + self, update: Union[dict, str], description: str + ) -> None: + """Send a webxdc status update. This message must be a webxdc.""" + if not isinstance(update, str): + update = json.dumps(update) + await self._rpc.send_webxdc_status_update( + self.account.id, self.id, update, description + ) + + async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: + return json.loads( + await self._rpc.get_webxdc_status_updates( + self.account.id, self.id, last_known_serial + ) + ) + + async def get_webxdc_info(self) -> dict: + return await self._rpc.get_webxdc_info(self.account.id, self.id) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index e8cce5f4c..6bfd446d1 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -1,21 +1,22 @@ import json import os -from typing import AsyncGenerator, List +from typing import AsyncGenerator, List, Optional import aiohttp import pytest_asyncio -from .account import Account -from .client import Bot -from .deltachat import DeltaChat +from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message from .rpc import Rpc async def get_temp_credentials() -> dict: url = os.getenv("DCC_NEW_TMP_EMAIL") assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set" + + # Replace default 5 minute timeout with a 1 minute timeout. + timeout = aiohttp.ClientTimeout(total=60) async with aiohttp.ClientSession() as session: - async with session.post(url) as response: + async with session.post(url, timeout=timeout) as response: return json.loads(await response.text()) @@ -29,12 +30,17 @@ class ACFactory: async def get_unconfigured_bot(self) -> Bot: return Bot(await self.get_unconfigured_account()) - async def new_configured_account(self) -> Account: + async def new_preconfigured_account(self) -> Account: + """Make a new account with configuration options set, but configuration not started.""" credentials = await get_temp_credentials() account = await self.get_unconfigured_account() - assert not await account.is_configured() await account.set_config("addr", credentials["email"]) await account.set_config("mail_pw", credentials["password"]) + assert not await account.is_configured() + return account + + async def new_configured_account(self) -> Account: + account = await self.new_preconfigured_account() await account.configure() assert await account.is_configured() return account @@ -51,6 +57,44 @@ class ACFactory: await account.start_io() return accounts + async def send_message( + self, + to_account: Account, + from_account: Optional[Account] = None, + text: Optional[str] = None, + file: Optional[str] = None, + group: Optional[str] = None, + ) -> Message: + if not from_account: + from_account = (await self.get_online_accounts(1))[0] + to_contact = await from_account.create_contact( + await to_account.get_config("addr") + ) + if group: + to_chat = await from_account.create_group(group) + await to_chat.add_contact(to_contact) + else: + to_chat = await to_contact.create_chat() + return await to_chat.send_message(text=text, file=file) + + async def process_message( + self, + to_client: Client, + from_account: Optional[Account] = None, + text: Optional[str] = None, + file: Optional[str] = None, + group: Optional[str] = None, + ) -> AttrDict: + await self.send_message( + to_account=to_client.account, + from_account=from_account, + text=text, + file=file, + group=group, + ) + + return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) + @pytest_asyncio.fixture async def rpc(tmp_path) -> AsyncGenerator: diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py index 78474aa8b..8a407308d 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/rpc.py @@ -14,9 +14,7 @@ class Rpc: if accounts_dir: kwargs["env"] = { **kwargs.get("env", os.environ), - "DC_ACCOUNTS_PATH": os.path.abspath( - os.path.expanduser(str(accounts_dir)) - ), + "DC_ACCOUNTS_PATH": str(accounts_dir), } self._kwargs = kwargs diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index 6d8136058..d1208042e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,6 +1,8 @@ +from unittest.mock import MagicMock + import pytest -from deltachat_rpc_client import AttrDict, EventType, events +from deltachat_rpc_client import EventType, events from deltachat_rpc_client.rpc import JsonRpcError @@ -39,6 +41,16 @@ async def test_acfactory(acfactory) -> None: print("Successful configuration") +@pytest.mark.asyncio +async def test_configure_starttls(acfactory) -> None: + account = await acfactory.new_preconfigured_account() + + # Use STARTTLS + await account.set_config("mail_security", "2") + await account.configure() + assert await account.is_configured() + + @pytest.mark.asyncio async def test_account(acfactory) -> None: alice, bob = await acfactory.get_online_accounts(2) @@ -55,7 +67,7 @@ async def test_account(acfactory) -> None: msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" @@ -80,8 +92,8 @@ async def test_account(acfactory) -> None: group = await alice.create_group("test group") await group.add_contact(alice_contact_bob) group_msg = await group.send_message(text="hello") - assert group_msg == await alice.get_message_by_id(group_msg.id) - assert group == await alice.get_chat_by_id(group.id) + assert group_msg == alice.get_message_by_id(group_msg.id) + assert group == alice.get_chat_by_id(group.id) await alice.delete_messages([group_msg]) await alice.set_config("selfstatus", "test") @@ -114,11 +126,11 @@ async def test_chat(acfactory) -> None: chat_id = event.chat_id msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" - bob_chat_alice = await bob.get_chat_by_id(chat_id) + bob_chat_alice = bob.get_chat_by_id(chat_id) assert alice_chat_bob != bob_chat_alice assert repr(alice_chat_bob) @@ -172,7 +184,7 @@ async def test_contact(acfactory) -> None: bob_addr = await bob.get_config("addr") alice_contact_bob = await alice.create_contact(bob_addr, "Bob") - assert alice_contact_bob == await alice.get_contact_by_id(alice_contact_bob.id) + assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert repr(alice_contact_bob) await alice_contact_bob.block() await alice_contact_bob.unblock() @@ -199,7 +211,7 @@ async def test_message(acfactory) -> None: msg_id = event.msg_id break - message = await bob.get_message_by_id(msg_id) + message = bob.get_message_by_id(msg_id) snapshot = await message.get_snapshot() assert snapshot.chat_id == chat_id assert snapshot.text == "Hello!" @@ -216,31 +228,42 @@ async def test_message(acfactory) -> None: @pytest.mark.asyncio async def test_bot(acfactory) -> None: - async def callback(e): - res.append(e) - - res = [] + mock = MagicMock() + user = (await acfactory.get_online_accounts(1))[0] bot = await acfactory.new_configured_bot() + assert await bot.is_configured() assert await bot.account.get_config("bot") == "1" - bot.add_hook(callback, events.RawEvent(EventType.INFO)) - info_event = AttrDict(account=bot.account, type=EventType.INFO, msg="info") - warn_event = AttrDict(account=bot.account, type=EventType.WARNING, msg="warning") - await bot._on_event(info_event) - await bot._on_event(warn_event) - assert info_event in res - assert warn_event not in res - assert len(res) == 1 + hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG) + bot.add_hook(*hook) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="Hello!" + ) + mock.hook.assert_called_once_with(event.msg_id) + bot.remove_hook(*hook) - res = [] - bot.add_hook(callback, events.NewMessage(r"hello")) - snapshot1 = AttrDict(text="hello") - snapshot2 = AttrDict(text="hello, world") - snapshot3 = AttrDict(text="hey!") - for snapshot in [snapshot1, snapshot2, snapshot3]: - await bot._on_event(snapshot, events.NewMessage) - assert len(res) == 2 - assert snapshot1 in res - assert snapshot2 in res - assert snapshot3 not in res + track = lambda e: mock.hook(e.message_snapshot.id) + + mock.hook.reset_mock() + hook = track, events.NewMessage(r"hello") + bot.add_hook(*hook) + bot.add_hook(track, events.NewMessage(command="/help")) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="hello" + ) + mock.hook.assert_called_with(event.msg_id) + event = await acfactory.process_message( + from_account=user, to_client=bot, text="hello!" + ) + mock.hook.assert_called_with(event.msg_id) + await acfactory.process_message(from_account=user, to_client=bot, text="hey!") + assert len(mock.hook.mock_calls) == 2 + bot.remove_hook(*hook) + + mock.hook.reset_mock() + await acfactory.process_message(from_account=user, to_client=bot, text="hello") + event = await acfactory.process_message( + from_account=user, to_client=bot, text="/help" + ) + mock.hook.assert_called_once_with(event.msg_id) diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py new file mode 100644 index 000000000..1bc5f02c1 --- /dev/null +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -0,0 +1,50 @@ +import pytest + +from deltachat_rpc_client import EventType + + +@pytest.mark.asyncio +async def test_webxdc(acfactory) -> None: + alice, bob = await acfactory.get_online_accounts(2) + + bob_addr = await bob.get_config("addr") + alice_contact_bob = await alice.create_contact(bob_addr, "Bob") + alice_chat_bob = await alice_contact_bob.create_chat() + await alice_chat_bob.send_message( + text="Let's play chess!", file="../test-data/webxdc/chess.xdc" + ) + + while True: + event = await bob.wait_for_event() + if event.type == EventType.INCOMING_MSG: + bob_chat_alice = bob.get_chat_by_id(event.chat_id) + message = bob.get_message_by_id(event.msg_id) + break + + webxdc_info = await message.get_webxdc_info() + assert webxdc_info == { + "document": None, + "icon": "icon.png", + "internetAccess": False, + "name": "Chess Board", + "sourceCodeUrl": None, + "summary": None, + } + + status_updates = await message.get_webxdc_status_updates() + assert status_updates == [] + + await bob_chat_alice.accept() + await message.send_webxdc_status_update({"payload": 42}, "") + await message.send_webxdc_status_update({"payload": "Second update"}, "description") + + status_updates = await message.get_webxdc_status_updates() + assert status_updates == [ + {"payload": 42, "serial": 1, "max_serial": 2}, + {"payload": "Second update", "serial": 2, "max_serial": 2}, + ] + + status_updates = await message.get_webxdc_status_updates(1) + assert status_updates == [ + {"payload": "Second update", "serial": 2, "max_serial": 2}, + ] diff --git a/deltachat-rpc-client/tox.ini b/deltachat-rpc-client/tox.ini index 4aad8010b..bea3b4603 100644 --- a/deltachat-rpc-client/tox.ini +++ b/deltachat-rpc-client/tox.ini @@ -13,7 +13,6 @@ passenv = DCC_NEW_TMP_EMAIL deps = pytest - pytest-async pytest-asyncio aiohttp aiodns diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index bdb6a7168..753b99c81 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "deltachat-rpc-server" -version = "1.103.0" +version = "1.106.0" description = "DeltaChat JSON-RPC server" -authors = ["Delta Chat Developers (ML) "] edition = "2021" readme = "README.md" license = "MPL-2.0" @@ -20,7 +19,7 @@ anyhow = "1" env_logger = { version = "0.10.0" } futures-lite = "1.12.0" log = "0.4" -serde_json = "1.0.89" +serde_json = "1.0.91" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.22.0", features = ["io-std"] } +tokio = { version = "1.23.1", features = ["io-std"] } yerpc = { version = "0.3.1", features = ["anyhow_expose"] } diff --git a/deltachat_derive/Cargo.toml b/deltachat_derive/Cargo.toml index 616a71515..094796cda 100644 --- a/deltachat_derive/Cargo.toml +++ b/deltachat_derive/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "deltachat_derive" version = "2.0.0" -authors = ["Delta Chat Developers (ML) "] edition = "2018" license = "MPL-2.0" diff --git a/format-flowed/Cargo.toml b/format-flowed/Cargo.toml new file mode 100644 index 000000000..480704215 --- /dev/null +++ b/format-flowed/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "format-flowed" +version = "1.0.0" +description = "format=flowed support" +edition = "2021" +license = "MPL-2.0" + +keywords = ["email"] +categories = ["email"] + +[dependencies] diff --git a/src/format_flowed.rs b/format-flowed/src/lib.rs similarity index 67% rename from src/format_flowed.rs rename to format-flowed/src/lib.rs index cb4c5f045..10f9400b5 100644 --- a/src/format_flowed.rs +++ b/format-flowed/src/lib.rs @@ -24,10 +24,14 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { let mut result = String::new(); let mut buffer = prefix.to_string(); - let mut after_space = false; + let mut after_space = prefix.ends_with(' '); for c in line.chars() { if c == ' ' { + if buffer.is_empty() { + // Space stuffing, see RFC 3676 + buffer.push(' '); + } buffer.push(c); after_space = true; } else if c == '>' { @@ -51,14 +55,14 @@ fn format_line_flowed(line: &str, prefix: &str) -> String { result + &buffer } -/// Returns text formatted according to RFC 3767 (format=flowed). +/// Returns text formatted according to RFC 3676 (format=flowed). /// /// This function accepts text separated by LF, but returns text /// separated by CRLF. /// /// RFC 2646 technique is used to insert soft line breaks, so DelSp /// SHOULD be set to "no" when sending. -pub(crate) fn format_flowed(text: &str) -> String { +pub fn format_flowed(text: &str) -> String { let mut result = String::new(); for line in text.split('\n') { @@ -66,21 +70,20 @@ pub(crate) fn format_flowed(text: &str) -> String { result += "\r\n"; } - let line_no_prefix = line.strip_prefix('>'); - let is_quote = line_no_prefix.is_some(); - let line = line_no_prefix.unwrap_or(line).trim(); - let prefix = if is_quote { "> " } else { "" }; + let line = line.trim_end(); + let quote_depth = line.chars().take_while(|&c| c == '>').count(); + let (prefix, mut line) = line.split_at(quote_depth); - if prefix.len() + line.len() > 78 { - result += &format_line_flowed(line, prefix); - } else { - result += prefix; - if prefix.is_empty() && line.starts_with('>') { - // Space stuffing, see RFC 3676 - result.push(' '); + let mut prefix = prefix.to_string(); + + if quote_depth > 0 { + if let Some(s) = line.strip_prefix(' ') { + line = s; + prefix += " "; } - result += line; } + + result += &format_line_flowed(line, &prefix); } result @@ -105,9 +108,6 @@ pub fn format_flowed_quote(text: &str) -> String { /// /// Lines must be separated by single LF. /// -/// Quote processing is not supported, it is assumed that they are -/// deleted during simplification. -/// /// Signature separator line is not processed here, it is assumed to /// be stripped beforehand. pub fn unformat_flowed(text: &str, delsp: bool) -> String { @@ -115,6 +115,12 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { let mut skip_newline = true; for line in text.split('\n') { + let line = if !result.is_empty() && skip_newline { + line.trim_start_matches('>') + } else { + line + }; + // Revert space-stuffing let line = line.strip_prefix(' ').unwrap_or(line); @@ -141,12 +147,23 @@ pub fn unformat_flowed(text: &str, delsp: bool) -> String { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::TestContext; #[test] fn test_format_flowed() { + let text = ""; + assert_eq!(format_flowed(text), ""); + let text = "Foo bar baz"; - assert_eq!(format_flowed(text), "Foo bar baz"); + assert_eq!(format_flowed(text), text); + + let text = ">Foo bar"; + assert_eq!(format_flowed(text), text); + + let text = "> Foo bar"; + assert_eq!(format_flowed(text), text); + + let text = ">\n\nA"; + assert_eq!(format_flowed(text), ">\r\n\r\nA"); let text = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\ \n\ @@ -160,16 +177,45 @@ mod tests { let text = "> A quote"; assert_eq!(format_flowed(text), "> A quote"); + let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + assert_eq!( + format_flowed(text), + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \r\n> A" + ); + // Test space stuffing of wrapped lines let text = "> This is the Autocrypt Setup Message used to transfer your key between clients.\n\ > \n\ > To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; let expected = "> This is the Autocrypt Setup Message used to transfer your key between \r\n\ > clients.\r\n\ - > \r\n\ + >\r\n\ > To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ > client and enter the setup code presented on the generating device."; assert_eq!(format_flowed(text), expected); + + let text = ">> This is the Autocrypt Setup Message used to transfer your key between clients.\n\ + >> \n\ + >> To decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device."; + let expected = ">> This is the Autocrypt Setup Message used to transfer your key between \r\n\ + >> clients.\r\n\ + >>\r\n\ + >> To decrypt and use your key, open the message in an Autocrypt-compliant \r\n\ + >> client and enter the setup code presented on the generating device."; + assert_eq!(format_flowed(text), expected); + + // Test space stuffing of spaces. + let text = " Foo bar baz"; + assert_eq!(format_flowed(text), " Foo bar baz"); + + let text = " Foo bar baz"; + assert_eq!(format_flowed(text), " Foo bar baz"); + + let text = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAA"; + let expected = + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \r\nAAAAAA"; + assert_eq!(format_flowed(text), expected); } #[test] @@ -180,6 +226,16 @@ mod tests { "this is a very long message that should be wrapped using format=flowed and \ unwrapped on the receiver"; assert_eq!(unformat_flowed(text, false), expected); + + let text = " Foo bar"; + let expected = " Foo bar"; + assert_eq!(unformat_flowed(text, false), expected); + + let text = + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > \n> A"; + let expected = + "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + assert_eq!(unformat_flowed(text, false), expected); } #[test] @@ -202,25 +258,4 @@ mod tests { > unwrapped on the receiver"; assert_eq!(format_flowed_quote(quote), expected); } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_send_quotes() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat = alice.create_chat(&bob).await; - - let sent = alice.send_text(chat.id, "> First quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some("> First quote")); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - let sent = alice.send_text(chat.id, "> Second quote").await; - let received = bob.recv_msg(&sent).await; - assert_eq!(received.text.as_deref(), Some("> Second quote")); - assert!(received.quoted_text().is_none()); - assert!(received.quoted_message(&bob).await?.is_none()); - - Ok(()) - } } diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 000000000..46118154d --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,3458 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check 0.9.4", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-compression" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +dependencies = [ + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-imap" +version = "0.6.0" +source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e" +dependencies = [ + "async-channel", + "async-native-tls", + "base64 0.13.1", + "byte-pool", + "chrono", + "futures", + "imap-proto", + "log", + "nom 7.1.1", + "once_cell", + "ouroboros", + "pin-utils", + "stop-token", + "thiserror", + "tokio", +] + +[[package]] +name = "async-native-tls" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe" +dependencies = [ + "native-tls", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "async-smtp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da21e1dd19fbad3e095ad519fb1558ab77fd82e5c4778dca8f9be0464589e1e" +dependencies = [ + "async-native-tls", + "async-trait", + "base64 0.13.1", + "bufstream", + "fast-socks5", + "futures", + "hostname", + "log", + "nom 7.1.1", + "pin-project", + "pin-utils", + "thiserror", + "tokio", +] + +[[package]] +name = "async-trait" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_utilities" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b20cffc5590f4bf33f05f97a3ea587feba9c50d20325b401daa096b92ff7da0" +dependencies = [ + "tokio", +] + +[[package]] +name = "async_zip" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a36d43bdefc7215b2b3a97edd03b1553b7969ad76551025eedd3b913c645f6e" +dependencies = [ + "async-compression", + "async_io_utilities", + "chrono", + "crc32fast", + "thiserror", + "tokio", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bolero" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3387d308f66ed222bdbb19c6ba06b1517168c4e45dc64051c5f1b4845db2901c" +dependencies = [ + "bolero-afl", + "bolero-engine", + "bolero-generator", + "bolero-honggfuzz", + "bolero-kani", + "bolero-libfuzzer", + "cfg-if", + "rand 0.8.5", +] + +[[package]] +name = "bolero-afl" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973bc6341b6a865dee93f17b78de4a100551014a527798ff1d7265d3bc0f7d89" +dependencies = [ + "bolero-engine", + "cc", +] + +[[package]] +name = "bolero-engine" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c506a476cea9e95f58c264b343ee279c353d93ceaebe98cbfb16e74bfaee2e2" +dependencies = [ + "anyhow", + "backtrace", + "bolero-generator", + "lazy_static", + "pretty-hex", + "rand 0.8.5", +] + +[[package]] +name = "bolero-generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d52eca8714d110e581cf17eeacf0d1a0d409d38a9e9ce07efeda6125f7febb" +dependencies = [ + "bolero-generator-derive", + "either", + "rand_core 0.6.4", +] + +[[package]] +name = "bolero-generator-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3c57c2a0967ad1a09ba4c2bf8f1c6b6db2f71e8c0db4fa280c65a0f6c249c3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bolero-honggfuzz" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7996a3fa8d93652358b9b3b805233807168f49740a8bf91a531cd61e4da65355" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-kani" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206879993fffa1cf2c703b1ef93b0febfa76bae85a0a5d4ae0ee6d99a2e3b74e" +dependencies = [ + "bolero-engine", +] + +[[package]] +name = "bolero-libfuzzer" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc5547411b84703d9020914f15a7d709cfb738c72b5e0f5a499fe56b8465c98" +dependencies = [ + "bolero-engine", + "cc", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byte-pool" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca" +dependencies = [ + "crossbeam-queue", + "stable_deref_trait", +] + +[[package]] +name = "bytemuck" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "charset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46" +dependencies = [ + "base64 0.13.1", + "encoding_rs", +] + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec318a675afcb6a1ea1d4340e2d377e56e47c266f28043ceccbf4412ddfdd3b" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "cxx" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" + +[[package]] +name = "deltachat" +version = "1.104.0" +dependencies = [ + "anyhow", + "async-channel", + "async-imap", + "async-native-tls", + "async-smtp", + "async_zip", + "backtrace", + "base64 0.20.0", + "bitflags", + "chrono", + "deltachat_derive", + "email", + "encoded-words", + "escaper", + "fast-socks5", + "format-flowed", + "futures", + "futures-lite", + "hex", + "humansize", + "image", + "kamadak-exif", + "lettre_email", + "libc", + "mailparse 0.14.0", + "native-tls", + "num-derive", + "num-traits", + "num_cpus", + "once_cell", + "percent-encoding", + "pgp", + "qrcodegen", + "quick-xml", + "r2d2", + "r2d2_sqlite", + "rand 0.8.5", + "regex", + "reqwest", + "rusqlite", + "rust-hsluv", + "sanitize-filename", + "serde", + "serde_json", + "sha-1", + "sha2 0.10.6", + "smallvec", + "strum", + "strum_macros", + "tagger", + "textwrap", + "thiserror", + "tokio", + "tokio-io-timeout", + "tokio-stream", + "tokio-tar", + "toml", + "trust-dns-resolver", + "url", + "uuid 1.2.2", +] + +[[package]] +name = "deltachat-fuzz" +version = "0.0.0" +dependencies = [ + "bolero", + "deltachat", + "format-flowed", + "mailparse 0.13.8", +] + +[[package]] +name = "deltachat_derive" +version = "2.0.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "const-oid", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "email" +version = "0.0.21" +source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e" +dependencies = [ + "base64 0.11.0", + "chrono", + "encoded-words", + "encoding", + "lazy_static", + "rand 0.7.3", + "time", + "version_check 0.9.4", +] + +[[package]] +name = "encoded-words" +version = "0.2.0" +source = "git+https://github.com/async-email/encoded-words?branch=master#d55366b36f96e383f39c432aedce42ee8b43f796" +dependencies = [ + "base64 0.12.3", + "charset", + "encoding_rs", + "hex", + "lazy_static", + "regex", + "thiserror", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "entities" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "escaper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53eb97b7349ba1bdb31839eceafe9aaae8f1d8d944dc589b67fb0b26e1c1666" +dependencies = [ + "entities", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fast-socks5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2687b5a6108f18ba8621e0e618a3be1dcc2768632dad24b7cea1f87975375a9" +dependencies = [ + "anyhow", + "log", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.42.0", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "format-flowed" +version = "1.0.0" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check 0.9.4", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e682e2bd70ecbcce5209f11a992a4ba001fea8e60acf7860ce007629e6d2756" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "imap-proto" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73b1b63179418b20aa81002d616c5f21b4ba257da9bca6989ea64dc573933e0" +dependencies = [ + "nom 7.1.1", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipconfig" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "lettre" +version = "0.9.2" +source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" +dependencies = [ + "fast_chemail", + "log", +] + +[[package]] +name = "lettre_email" +version = "0.9.2" +source = "git+https://github.com/deltachat/lettre?branch=master#96555ec428ac114ecfca9934d2fda34c13737e54" +dependencies = [ + "base64 0.11.0", + "email", + "lazy_static", + "lettre", + "mime", + "regex", + "time", + "uuid 0.8.2", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "mailparse" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cae768a50835557749599277fc59f7c728118724eb34185e8feb633ef266a32" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + +[[package]] +name = "mailparse" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa" +dependencies = [ + "charset", + "data-encoding", + "quoted_printable", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.42.0", +] + +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239da7f290cfa979f43f85a8efeee9a8a76d0827c356d37f9d3d7254d6b537fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.24.0+1.1.1s" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ouroboros" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca" +dependencies = [ + "aliasable", + "ouroboros_macro", +] + +[[package]] +name = "ouroboros_macro" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pgp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "991e3f098483f52c454c7cb16720adc010c2966a8845d3c34aad589cb86d3196" +dependencies = [ + "aes", + "base64 0.13.1", + "bitfield", + "block-padding", + "blowfish", + "buf_redux", + "byteorder", + "cast5", + "cfb-mode", + "chrono", + "cipher", + "crc24", + "derive_builder", + "des", + "digest 0.10.6", + "ed25519-dalek", + "flate2", + "generic-array", + "hex", + "log", + "md-5", + "nom 4.2.3", + "num-bigint-dig", + "num-derive", + "num-traits", + "rand 0.8.5", + "ripemd", + "rsa", + "sha1", + "sha2 0.10.6", + "sha3", + "signature", + "smallvec", + "thiserror", + "twofish", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der", + "pkcs8", + "spki", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.4", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qrcodegen" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-xml" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quoted_printable" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb" + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + +[[package]] +name = "r2d2_sqlite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fdc8e4da70586127893be32b7adf21326a4c6b1aba907611edf467d13ffe895" +dependencies = [ + "r2d2", + "rusqlite", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +dependencies = [ + "base64 0.13.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest 0.10.6", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + +[[package]] +name = "rust-hsluv" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe2374f2385cdd8755a446f80b2a646de603c9d8539ca38734879b5c71e378b" + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustversion" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "sanitize-filename" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys 0.36.1", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.6", + "keccak", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stop-token" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af91f480ee899ab2d9f8435bfdfc14d08a5754bd9d3fef1f1a1c23336aad6c8b" +dependencies = [ + "async-channel", + "cfg-if", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tagger" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63" + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50188549787c32c1c3d9c8c71ad7e003ccf2f102489c5a96e385c84760477f4" +dependencies = [ + "filetime", + "futures-core", + "libc", + "redox_syscall", + "tokio", + "tokio-stream", + "xattr", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-linebreak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" +dependencies = [ + "hashbrown 0.12.3", + "regex", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna 0.3.0", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom 0.2.8", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "x25519-dalek" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..a3eba6256 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "deltachat-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[dev-dependencies] +bolero = "0.8" + +[dependencies] +mailparse = "0.13" +deltachat = { path = ".." } +format-flowed = { path = "../format-flowed" } + +[workspace] +members = ["."] + +[[test]] +name = "fuzz_dateparse" +path = "fuzz_targets/fuzz_dateparse.rs" +harness = false + +[[test]] +name = "fuzz_simplify" +path = "fuzz_targets/fuzz_simplify.rs" +harness = false + +[[test]] +name = "fuzz_mailparse" +path = "fuzz_targets/fuzz_mailparse.rs" +harness = false + +[[test]] +name = "fuzz_format_flowed" +path = "fuzz_targets/fuzz_format_flowed.rs" +harness = false diff --git a/fuzz/fuzz_targets/fuzz_dateparse.rs b/fuzz/fuzz_targets/fuzz_dateparse.rs new file mode 100644 index 000000000..e903363db --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_dateparse.rs @@ -0,0 +1,10 @@ +use bolero::check; + +fn main() { + check!().for_each(|data: &[u8]| match std::str::from_utf8(data) { + Ok(input) => { + mailparse::dateparse(input).ok(); + } + Err(_err) => {} + }); +} diff --git a/fuzz/fuzz_targets/fuzz_format_flowed.rs b/fuzz/fuzz_targets/fuzz_format_flowed.rs new file mode 100644 index 000000000..8f779a468 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_format_flowed.rs @@ -0,0 +1,22 @@ +use bolero::check; +use format_flowed::{format_flowed, unformat_flowed}; + +fn round_trip(input: &str) -> String { + let mut input = format_flowed(input); + input.retain(|c| c != '\r'); + unformat_flowed(&input, false) +} + +fn main() { + check!().for_each(|data: &[u8]| { + if let Ok(input) = std::str::from_utf8(data.into()) { + let input = input.trim().to_string(); + + // Only consider inputs that are the result of unformatting format=flowed text. + // At least this means that lines don't contain any trailing whitespace. + let input = round_trip(&input); + let output = round_trip(&input); + assert_eq!(input, output); + } + }); +} diff --git a/fuzz/fuzz_targets/fuzz_mailparse.rs b/fuzz/fuzz_targets/fuzz_mailparse.rs new file mode 100644 index 000000000..19f2fe097 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_mailparse.rs @@ -0,0 +1,7 @@ +use bolero::check; + +fn main() { + check!().for_each(|data: &[u8]| { + mailparse::parse_mail(data).ok(); + }); +} diff --git a/fuzz/fuzz_targets/fuzz_simplify.rs b/fuzz/fuzz_targets/fuzz_simplify.rs new file mode 100644 index 000000000..cd0a22352 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_simplify.rs @@ -0,0 +1,13 @@ +use bolero::check; + +use deltachat::fuzzing::simplify; + +fn main() { + check!().for_each(|data: &[u8]| match String::from_utf8(data.to_vec()) { + Ok(input) => { + simplify(input.clone(), true); + simplify(input, false); + } + Err(_err) => {} + }); +} diff --git a/node/test/test.js b/node/test/test.js index 2b4bc6d20..d76566b04 100644 --- a/node/test/test.js +++ b/node/test/test.js @@ -1,32 +1,29 @@ // @ts-check -import DeltaChat, { Message } from '../dist' -import binding from '../binding' +import DeltaChat from '../dist' -import { deepEqual, deepStrictEqual, strictEqual } from 'assert' +import { deepStrictEqual, strictEqual } from 'assert' import chai, { expect } from 'chai' import chaiAsPromised from 'chai-as-promised' import { EventId2EventName, C } from '../dist/constants' import { join } from 'path' -import { mkdtempSync, statSync } from 'fs' -import { tmpdir } from 'os' +import { statSync } from 'fs' import { Context } from '../dist/context' +import fetch from 'node-fetch' chai.use(chaiAsPromised) +chai.config.truncateThreshold = 0 // Do not truncate assertion errors. async function createTempUser(url) { - const fetch = require('node-fetch') - async function postData(url = '') { // Default options are marked with * const response = await fetch(url, { method: 'POST', // *GET, POST, PUT, DELETE, etc. - mode: 'cors', // no-cors, *cors, same-origin - cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached - credentials: 'same-origin', // include, *same-origin, omit headers: { 'cache-control': 'no-cache', }, - referrerPolicy: 'no-referrer', // no-referrer, *client }) + if (!response.ok) { + throw new Error('request failed: ' + response.body.read()) + } return response.json() // parses JSON response into native JavaScript objects } @@ -121,7 +118,7 @@ describe('JSON RPC', function () { const promises = {} dc.startJsonRpcHandler((msg) => { const response = JSON.parse(msg) - promises[response.id](response) + if (response.hasOwnProperty('id')) promises[response.id](response) delete promises[response.id] }) const call = (request) => { diff --git a/package.json b/package.json index fe638ee94..25771812e 100644 --- a/package.json +++ b/package.json @@ -60,5 +60,5 @@ "test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit" }, "types": "node/dist/index.d.ts", - "version": "1.103.0" + "version": "1.106.0" } \ No newline at end of file diff --git a/python/src/deltachat/contact.py b/python/src/deltachat/contact.py index 69fa809b6..75c44b8e5 100644 --- a/python/src/deltachat/contact.py +++ b/python/src/deltachat/contact.py @@ -75,6 +75,10 @@ class Contact(object): """Return True if the contact is verified.""" return lib.dc_contact_is_verified(self._dc_contact) + def get_verifier(self, contact): + """Return the address of the contact that verified the contact""" + return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact)) + def get_profile_image(self) -> Optional[str]: """Get contact profile image. diff --git a/python/src/deltachat/events.py b/python/src/deltachat/events.py index 72b84a44a..49ef23f96 100644 --- a/python/src/deltachat/events.py +++ b/python/src/deltachat/events.py @@ -30,7 +30,14 @@ class FFIEvent: self.data2 = data2 def __str__(self): - return "{name} data1={data1} data2={data2}".format(**self.__dict__) + if self.name == "DC_EVENT_INFO": + return "INFO {data2}".format(data2=self.data2) + elif self.name == "DC_EVENT_WARNING": + return "WARNING {data2}".format(data2=self.data2) + elif self.name == "DC_EVENT_ERROR": + return "ERROR {data2}".format(data2=self.data2) + else: + return "{name} data1={data1} data2={data2}".format(**self.__dict__) class FFIEventLogger: diff --git a/python/src/deltachat/testplugin.py b/python/src/deltachat/testplugin.py index 335e384a5..ed92917b6 100644 --- a/python/src/deltachat/testplugin.py +++ b/python/src/deltachat/testplugin.py @@ -176,7 +176,7 @@ class TestProcess: try: yield self._configlist[index] except IndexError: - res = requests.post(liveconfig_opt) + res = requests.post(liveconfig_opt, timeout=60) if res.status_code != 200: pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text)) d = res.json() diff --git a/python/tests/data/r b/python/tests/data/r new file mode 100644 index 000000000..3e23ae484 --- /dev/null +++ b/python/tests/data/r @@ -0,0 +1,2 @@ + +hello diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 335d07590..43e2b16c7 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -123,6 +123,7 @@ class TestGroupStressTests: def test_qr_verified_group_and_chatting(acfactory, lp): ac1, ac2, ac3 = acfactory.get_online_accounts(3) + ac1_addr = ac1.get_self_contact().addr lp.sec("ac1: create verified-group QR, ac2 scans and joins") chat1 = ac1.create_group_chat("hello", verified=True) assert chat1.is_protected() @@ -141,12 +142,17 @@ def test_qr_verified_group_and_chatting(acfactory, lp): msg_out = chat1.send_text("hello") assert msg_out.is_encrypted() - lp.sec("ac2: read message and check it's verified chat") + lp.sec("ac2: read message and check that it's a verified chat") msg = ac2._evtracker.wait_next_incoming_message() assert msg.text == "hello" assert msg.chat.is_protected() assert msg.is_encrypted() + lp.sec("ac2: Check that ac2 verified ac1") + # If we verified the contact ourselves then verifier addr == contact addr + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + lp.sec("ac2: send message and let ac1 read it") chat2.send_text("world") msg = ac1._evtracker.wait_next_incoming_message() @@ -168,6 +174,12 @@ def test_qr_verified_group_and_chatting(acfactory, lp): assert msg.is_system_message() assert not msg.error + lp.sec("ac2: Check that ac1 verified ac3 for ac2") + ac2_ac1_contact = ac2.get_contacts()[0] + assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr + ac2_ac3_contact = ac2.get_contacts()[1] + assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr + lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") # Skip system message about added member @@ -480,3 +492,48 @@ def test_multidevice_sync_seen(acfactory, lp): assert ac1_clone_message.is_in_seen # Test that the timer is started on the second device after synchronizing the seen status. assert "Expires: " in ac1_clone_message.get_message_info() + + +def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp): + """The test for the bug #3836: + - Alice has two devices, the second is offline. + - Alice creates a verified group and sends a QR invitation to Bob. + - Bob joins the group and sends a message there. Alice sees it. + - Alice's second devices goes online, but doesn't see Bob in the group. + """ + ac1, ac2 = acfactory.get_online_accounts(2) + ac2_addr = ac2.get_config("addr") + ac1_offl = acfactory.new_online_configuring_account(cloned_from=ac1) + for ac in [ac1, ac1_offl]: + ac.set_config("bcc_self", "1") + acfactory.bring_accounts_online() + dir = tmpdir.mkdir("exportdir") + ac1.export_self_keys(dir.strpath) + ac1_offl.import_self_keys(dir.strpath) + ac1_offl.stop_io() + + lp.sec("ac1: create verified-group QR, ac2 scans and joins") + chat = ac1.create_group_chat("hello", verified=True) + assert chat.is_protected() + qr = chat.get_join_qr() + lp.sec("ac2: start QR-code based join-group protocol") + chat2 = ac2.qr_join_chat(qr) + ac1._evtracker.wait_securejoin_inviter_progress(1000) + + lp.sec("ac2: sending message") + # Message can be sent only after a receipt of "vg-member-added" message. Just wait for + # "Member Me () added by ." message. + ac2._evtracker.wait_next_incoming_message() + msg_out = chat2.send_text("hello") + + lp.sec("ac1: receiving message") + msg_in = ac1._evtracker.wait_next_incoming_message() + assert msg_in.text == msg_out.text + assert msg_in.get_sender_contact().addr == ac2_addr + + lp.sec("ac1_offl: going online, receiving message") + ac1_offl.start_io() + ac1_offl._evtracker.wait_securejoin_inviter_progress(1000) + msg_in = ac1_offl._evtracker.wait_next_incoming_message() + assert msg_in.text == msg_out.text + assert msg_in.get_sender_contact().addr == ac2_addr diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index 07da89134..9634a470a 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -88,7 +88,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp): lp.indent(dir.strpath + os.sep + name) lp.sec("importing into existing account") ac2.import_self_keys(dir.strpath) - (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False) + (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*") assert key_id2 == key_id @@ -939,6 +939,20 @@ def test_dont_show_emails(acfactory, lp): ac1.get_config("configured_addr") ), ) + ac1.direct_imap.append( + "Spam", + """ + From: delta + Subject: subj + To: {} + Message-ID: + Content-Type: text/plain; charset=utf-8 + + Unknown & malformed message in Spam + """.format( + ac1.get_config("configured_addr") + ), + ) ac1.direct_imap.append( "Spam", """ diff --git a/python/tests/test_3_offline.py b/python/tests/test_3_offline.py index 3a09eebf4..98890287a 100644 --- a/python/tests/test_3_offline.py +++ b/python/tests/test_3_offline.py @@ -450,24 +450,25 @@ class TestOfflineChat: assert msg.filemime == "image/png" @pytest.mark.parametrize( - "typein,typeout", + "fn,typein,typeout", [ - (None, "application/octet-stream"), - ("text/plain", "text/plain"), - ("image/png", "image/png"), + ("r", None, "application/octet-stream"), + ("r.txt", None, "text/plain"), + ("r.txt", "text/plain", "text/plain"), + ("r.txt", "image/png", "image/png"), ], ) - def test_message_file(self, ac1, chat1, data, lp, typein, typeout): + def test_message_file(self, ac1, chat1, data, lp, fn, typein, typeout): lp.sec("sending file") - fn = data.get_path("r.txt") - msg = chat1.send_file(fn, typein) + fp = data.get_path(fn) + msg = chat1.send_file(fp, typein) assert msg assert msg.id > 0 assert msg.is_file() assert os.path.exists(msg.filename) assert msg.filename.endswith(msg.basename) assert msg.filemime == typeout - msg2 = chat1.send_file(fn, typein) + msg2 = chat1.send_file(fp, typein) assert msg2 != msg assert msg2.filename != msg.filename diff --git a/python/tox.ini b/python/tox.ini index f8304a9b3..b182c41a1 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -38,6 +38,8 @@ passenv = skipsdist = True deps = auditwheel passenv = + DCC_RS_DEV + DCC_RS_TARGET AUDITWHEEL_ARCH AUDITWHEEL_PLAT AUDITWHEEL_POLICY diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index 902c74186..000000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -1.65.0 diff --git a/scripts/concourse/docs_wheels.yml b/scripts/concourse/docs_wheels.yml index d902947bc..bd2074d4b 100644 --- a/scripts/concourse/docs_wheels.yml +++ b/scripts/concourse/docs_wheels.yml @@ -104,9 +104,6 @@ jobs: outputs: - name: py-docs path: ./python/doc/_build/ - # Source packages - - name: py-dist - path: ./python/.docker-tox/dist/ # Binary wheels - name: py-wheels path: ./python/.docker-tox/wheelhouse/ @@ -145,7 +142,6 @@ jobs: config: inputs: - name: py-wheels - - name: py-dist image_resource: type: registry-image source: @@ -162,7 +158,6 @@ jobs: devpi use https://m.devpi.net/dc/master devpi login ((devpi.login)) --password ((devpi.password)) devpi upload py-wheels/*manylinux201* - devpi upload py-dist/* - name: python-aarch64 plan: diff --git a/scripts/coredeps/install-rust.sh b/scripts/coredeps/install-rust.sh index dcbf30b77..b4a08f3f1 100755 --- a/scripts/coredeps/install-rust.sh +++ b/scripts/coredeps/install-rust.sh @@ -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.65.0 +RUST_VERSION=1.64.0 ARCH="$(uname -m)" test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu diff --git a/src/accounts.rs b/src/accounts.rs index 0ec5d1f40..8de0a7da8 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -1,7 +1,5 @@ //! # Account manager module. -#![warn(missing_docs)] - use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -66,7 +64,7 @@ impl Accounts { let events = Events::new(); let stockstrings = StockStrings::new(); let accounts = config - .load_accounts(&events, &stockstrings) + .load_accounts(&events, &stockstrings, &dir) .await .context("failed to load accounts")?; @@ -109,10 +107,11 @@ impl Accounts { /// /// Returns account ID. pub async fn add_account(&mut self) -> Result { - let account_config = self.config.new_account(&self.dir).await?; + let account_config = self.config.new_account().await?; + let dbfile = account_config.dbfile(&self.dir); let ctx = Context::new( - &account_config.dbfile(), + &dbfile, account_config.id, self.events.clone(), self.stockstrings.clone(), @@ -125,10 +124,10 @@ impl Accounts { /// Adds a new closed account. pub async fn add_closed_account(&mut self) -> Result { - let account_config = self.config.new_account(&self.dir).await?; + let account_config = self.config.new_account().await?; let ctx = Context::new_closed( - &account_config.dbfile(), + &account_config.dbfile(&self.dir), account_config.id, self.events.clone(), self.stockstrings.clone(), @@ -149,6 +148,8 @@ impl Accounts { drop(ctx); if let Some(cfg) = self.config.get_account(id) { + let account_path = self.dir.join(cfg.dir); + // Spend up to 1 minute trying to remove the files. // Files may remain locked up to 30 seconds due to r2d2 bug: // https://github.com/sfackler/r2d2/issues/99 @@ -156,7 +157,7 @@ impl Accounts { loop { counter += 1; - if let Err(err) = fs::remove_dir_all(&cfg.dir) + if let Err(err) = fs::remove_dir_all(&account_path) .await .context("failed to remove account data") { @@ -189,16 +190,16 @@ impl Accounts { // create new account let account_config = self .config - .new_account(&self.dir) + .new_account() .await .context("failed to create new account")?; - let new_dbfile = account_config.dbfile(); + let new_dbfile = account_config.dbfile(&self.dir); let new_blobdir = Context::derive_blobdir(&new_dbfile); let new_walfile = Context::derive_walfile(&new_dbfile); let res = { - fs::create_dir_all(&account_config.dir) + fs::create_dir_all(self.dir.join(&account_config.dir)) .await .context("failed to create dir")?; fs::rename(&dbfile, &new_dbfile) @@ -360,10 +361,26 @@ impl Config { /// Read a configuration from the given file into memory. pub async fn from_file(file: PathBuf) -> Result { + let dir = file.parent().context("can't get config file directory")?; let bytes = fs::read(&file).await.context("failed to read file")?; - let inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?; + let mut inner: InnerConfig = toml::from_slice(&bytes).context("failed to parse config")?; - Ok(Config { file, inner }) + // Previous versions of the core stored absolute paths in account config. + // Convert them to relative paths. + let mut modified = false; + for account in &mut inner.accounts { + if let Ok(new_dir) = account.dir.strip_prefix(dir) { + account.dir = new_dir.to_path_buf(); + modified = true; + } + } + + let config = Self { file, inner }; + if modified { + config.sync().await?; + } + + Ok(config) } /// Loads all accounts defined in the configuration file. @@ -374,12 +391,13 @@ impl Config { &self, events: &Events, stockstrings: &StockStrings, + dir: &Path, ) -> Result> { let mut accounts = BTreeMap::new(); for account_config in &self.inner.accounts { let ctx = Context::new( - &account_config.dbfile(), + &account_config.dbfile(dir), account_config.id, events.clone(), stockstrings.clone(), @@ -388,7 +406,7 @@ impl Config { .with_context(|| { format!( "failed to create context from file {:?}", - account_config.dbfile() + account_config.dbfile(dir) ) })?; @@ -398,12 +416,12 @@ impl Config { Ok(accounts) } - /// Create a new account in the given root directory. - async fn new_account(&mut self, dir: &Path) -> Result { + /// Creates a new account in the account manager directory. + async fn new_account(&mut self) -> Result { let id = { let id = self.inner.next_id; let uuid = Uuid::new_v4(); - let target_dir = dir.join(uuid.to_string()); + let target_dir = PathBuf::from(uuid.to_string()); self.inner.accounts.push(AccountConfig { id, @@ -475,14 +493,16 @@ struct AccountConfig { /// Unique id. pub id: u32, /// Root directory for all data for this account. + /// + /// The path is relative to the account manager directory. pub dir: std::path::PathBuf, pub uuid: Uuid, } impl AccountConfig { /// Get the canoncial dbfile name for this configuration. - pub fn dbfile(&self) -> std::path::PathBuf { - self.dir.join(DB_NAME) + pub fn dbfile(&self, accounts_dir: &Path) -> std::path::PathBuf { + accounts_dir.join(&self.dir).join(DB_NAME) } } diff --git a/src/aheader.rs b/src/aheader.rs index 9899c1978..ac9a54f60 100644 --- a/src/aheader.rs +++ b/src/aheader.rs @@ -4,11 +4,9 @@ use anyhow::{bail, Context as _, Error, Result}; use std::collections::BTreeMap; +use std::fmt; use std::str::FromStr; -use std::{fmt, str}; -use crate::contact::addr_cmp; -use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, SignedPublicKey}; /// Possible values for encryption preference @@ -36,7 +34,7 @@ impl fmt::Display for EncryptPreference { } } -impl str::FromStr for EncryptPreference { +impl FromStr for EncryptPreference { type Err = Error; fn from_str(s: &str) -> Result { @@ -69,29 +67,6 @@ impl Aheader { prefer_encrypt, } } - - /// Tries to parse Autocrypt header. - /// - /// If there is none, returns None. If the header is present but cannot be parsed, returns an - /// error. - pub fn from_headers( - wanted_from: &str, - headers: &[mailparse::MailHeader<'_>], - ) -> Result> { - if let Some(value) = headers.get_header_value(HeaderDef::Autocrypt) { - let header = Self::from_str(&value)?; - if !addr_cmp(&header.addr, wanted_from) { - bail!( - "Autocrypt header address {:?} is not {:?}", - header.addr, - wanted_from - ); - } - Ok(Some(header)) - } else { - Ok(None) - } - } } impl fmt::Display for Aheader { @@ -118,7 +93,7 @@ impl fmt::Display for Aheader { } } -impl str::FromStr for Aheader { +impl FromStr for Aheader { type Err = Error; fn from_str(s: &str) -> Result { diff --git a/src/blob.rs b/src/blob.rs index 6cbfdfa31..7b6109f2e 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -746,7 +746,7 @@ mod tests { assert!(file_size(&avatar_blob).await <= 3000); assert!(file_size(&avatar_blob).await > 2000); tokio::task::block_in_place(move || { - let img = image::open(&avatar_blob).unwrap(); + let img = image::open(avatar_blob).unwrap(); assert!(img.width() > 130); assert_eq!(img.width(), img.height()); }); diff --git a/src/chat.rs b/src/chat.rs index e92e6d574..0b54efc50 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,5 +1,7 @@ //! # Chat module. +#![allow(missing_docs)] + use std::collections::HashMap; use std::convert::{TryFrom, TryInto}; use std::fmt; @@ -766,7 +768,7 @@ impl ChatId { paramsv![self], ) .await?; - Ok(count as usize) + Ok(count) } pub async fn get_fresh_msg_cnt(self, context: &Context) -> Result { @@ -780,18 +782,36 @@ impl ChatId { // the times are average, no matter if there are fresh messages or not - // and have to be multiplied by the number of items shown at once on the chatlist, // so savings up to 2 seconds are possible on older devices - newer ones will feel "snappier" :) - let count = context - .sql - .count( - "SELECT COUNT(*) + let count = if self.is_archived_link() { + context + .sql + .count( + "SELECT COUNT(DISTINCT(m.chat_id)) + FROM msgs m + LEFT JOIN chats c ON m.chat_id=c.id + WHERE m.state=10 + and m.hidden=0 + AND m.chat_id>9 + AND c.blocked=0 + AND c.archived=1 + ", + paramsv![], + ) + .await? + } else { + context + .sql + .count( + "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, self], - ) - .await?; - Ok(count as usize) + paramsv![MessageState::InFresh, self], + ) + .await? + }; + Ok(count) } pub(crate) async fn get_param(self, context: &Context) -> Result { @@ -804,6 +824,19 @@ impl ChatId { .unwrap_or_default()) } + /// Returns true if the chat is not promoted. + pub(crate) async fn is_unpromoted(self, context: &Context) -> Result { + let param = self.get_param(context).await?; + let unpromoted = param.get_bool(Param::Unpromoted).unwrap_or_default(); + Ok(unpromoted) + } + + /// Returns true if the chat is promoted. + pub(crate) async fn is_promoted(self, context: &Context) -> Result { + let promoted = !self.is_unpromoted(context).await?; + Ok(promoted) + } + // Returns true if chat is a saved messages chat. pub async fn is_self_talk(self, context: &Context) -> Result { Ok(self.get_param(context).await?.exists(Param::Selftalk)) @@ -1096,7 +1129,7 @@ impl Chat { } } Err(err) => { - error!(context, "faild to load contacts for {}: {:?}", chat.id, err); + error!(context, "faild to load contacts for {}: {:#}", chat.id, err); } } chat.name = chat_name; @@ -1201,6 +1234,10 @@ impl Chat { if !image_rel.is_empty() { return Ok(Some(get_abs_path(context, image_rel))); } + } else if self.id.is_archived_link() { + if let Ok(image_rel) = get_archive_icon(context).await { + return Ok(Some(get_abs_path(context, image_rel))); + } } else if self.typ == Chattype::Single { let contacts = get_chat_contacts(context, self.id).await?; if let Some(contact_id) = contacts.first() { @@ -1275,7 +1312,7 @@ impl Chat { } pub fn is_unpromoted(&self) -> bool { - self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 + self.param.get_bool(Param::Unpromoted).unwrap_or_default() } pub fn is_promoted(&self) -> bool { @@ -1472,7 +1509,7 @@ impl Chat { new_rfc724_mid, self.id, ContactId::SELF, - to_id as i32, + to_id, timestamp, msg.viewtype, msg.state, @@ -1520,7 +1557,7 @@ impl Chat { new_rfc724_mid, self.id, ContactId::SELF, - to_id as i32, + to_id, timestamp, msg.viewtype, msg.state, @@ -1693,6 +1730,21 @@ pub(crate) async fn get_broadcast_icon(context: &Context) -> Result { Ok(icon) } +pub(crate) async fn get_archive_icon(context: &Context) -> Result { + if let Some(icon) = context.sql.get_raw_config("icon-archive").await? { + return Ok(icon); + } + + let icon = include_bytes!("../assets/icon-archive.png"); + let blob = BlobObject::create(context, "icon-archive.png", icon).await?; + let icon = blob.as_name().to_string(); + context + .sql + .set_raw_config("icon-archive", Some(&icon)) + .await?; + Ok(icon) +} + async fn update_special_chat_name( context: &Context, contact_id: ContactId, @@ -2094,7 +2146,7 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result attach_selfavatar, Err(err) => { - warn!(context, "job: cannot get selfavatar-state: {}", err); + warn!(context, "job: cannot get selfavatar-state: {:#}", err); false } }; @@ -2156,27 +2208,27 @@ async fn create_send_msg_job(context: &Context, msg_id: MsgId) -> Result Result<()> { // "WHERE" below uses the index `(state, hidden, chat_id)`, see get_fresh_msg_cnt() for reasoning // the additional SELECT statement may speed up things as no write-blocking is needed. - let exists = context - .sql - .exists( - "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", - paramsv![MessageState::InFresh, chat_id], - ) - .await?; - if !exists { - return Ok(()); - } + if chat_id.is_archived_link() { + let chat_ids_in_archive = context + .sql + .query_map( + "SELECT DISTINCT(m.chat_id) FROM msgs m + LEFT JOIN chats c ON m.chat_id=c.id + WHERE m.state=10 AND m.hidden=0 AND m.chat_id>9 AND c.blocked=0 AND c.archived=1", + paramsv![], + |row| row.get::<_, ChatId>(0), + |ids| ids.collect::, _>>().map_err(Into::into) + ) + .await?; + if chat_ids_in_archive.is_empty() { + return Ok(()); + } - context - .sql - .execute( - "UPDATE msgs - SET state=? - WHERE state=? - AND hidden=0 - AND chat_id=?;", - paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], - ) - .await?; + context + .sql + .execute( + &format!( + "UPDATE msgs SET state=13 WHERE state=10 AND hidden=0 AND chat_id IN ({});", + sql::repeat_vars(chat_ids_in_archive.len()) + ), + rusqlite::params_from_iter(&chat_ids_in_archive), + ) + .await?; + for chat_id_in_archive in chat_ids_in_archive { + context.emit_event(EventType::MsgsNoticed(chat_id_in_archive)); + } + } else { + let exists = context + .sql + .exists( + "SELECT COUNT(*) FROM msgs WHERE state=? AND hidden=0 AND chat_id=?;", + paramsv![MessageState::InFresh, chat_id], + ) + .await?; + if !exists { + return Ok(()); + } + + context + .sql + .execute( + "UPDATE msgs + SET state=? + WHERE state=? + AND hidden=0 + AND chat_id=?;", + paramsv![MessageState::InNoticed, MessageState::InFresh, chat_id], + ) + .await?; + } context.emit_event(EventType::MsgsNoticed(chat_id)); @@ -2818,14 +2902,12 @@ pub(crate) async fn add_contact_to_chat_ex( Ok(true) } +/// Returns true if an avatar should be attached in the given chat. +/// +/// This function does not check if the avatar is set. +/// If avatar is not set and this function returns `true`, +/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar. pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result { - // versions before 12/2019 already allowed to set selfavatar, however, it was never sent to others. - // to avoid sending out previously set selfavatars unexpectedly we added this additional check. - // it can be removed after some time. - if !context.sql.get_raw_config_bool("attach_selfavatar").await? { - return Ok(false); - } - let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60; let needs_attach = context .sql @@ -3037,7 +3119,11 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) - paramsv![new_name.to_string(), chat_id], ) .await?; - if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast { + if chat.is_promoted() + && !chat.is_mailing_list() + && chat.typ != Chattype::Broadcast + && improve_single_line_input(&chat.name) != new_name + { msg.viewtype = Viewtype::Text; msg.text = Some( stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await, @@ -3259,7 +3345,7 @@ pub(crate) async fn get_chat_cnt(context: &Context) -> Result { paramsv![], ) .await?; - Ok(count as usize) + Ok(count) } else { Ok(0) } @@ -3541,8 +3627,9 @@ mod tests { use crate::chatlist::{get_archived_cnt, Chatlist}; use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; - use crate::contact::Contact; + use crate::contact::{Contact, ContactAddress}; use crate::receive_imf::receive_imf; + use crate::test_utils::TestContext; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -4403,6 +4490,91 @@ mod tests { Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_archive_fresh_msgs() -> Result<()> { + let t = TestContext::new_alice().await; + + async fn msg_from(t: &TestContext, name: &str, num: u32) -> Result<()> { + receive_imf( + t, + format!( + "From: {}@example.net\n\ + To: alice@example.org\n\ + Message-ID: <{}@example.org>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2022 19:37:57 +0000\n\ + \n\ + hello\n", + name, num + ) + .as_bytes(), + false, + ) + .await?; + Ok(()) + } + + // receive some messages in archived+muted chats + msg_from(&t, "bob", 1).await?; + let bob_chat_id = t.get_last_msg().await.get_chat_id(); + bob_chat_id.accept(&t).await?; + set_muted(&t, bob_chat_id, MuteDuration::Forever).await?; + bob_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + + msg_from(&t, "bob", 2).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "bob", 3).await?; + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + msg_from(&t, "claire", 4).await?; + let claire_chat_id = t.get_last_msg().await.get_chat_id(); + claire_chat_id.accept(&t).await?; + set_muted(&t, claire_chat_id, MuteDuration::Forever).await?; + claire_chat_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + msg_from(&t, "claire", 5).await?; + msg_from(&t, "claire", 6).await?; + msg_from(&t, "claire", 7).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 3); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + + // mark one of the archived+muted chats as noticed: check that the archive-link counter is changed as well + marknoticed_chat(&t, claire_chat_id).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 1); + + // receive some more messages + msg_from(&t, "claire", 8).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 0); + + msg_from(&t, "dave", 9).await?; + let dave_chat_id = t.get_last_msg().await.get_chat_id(); + dave_chat_id.accept(&t).await?; + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 2); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + // mark the archived-link as noticed: check that the real chats are noticed as well + marknoticed_chat(&t, DC_CHAT_ID_ARCHIVED_LINK).await?; + assert_eq!(bob_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(claire_chat_id.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(dave_chat_id.get_fresh_msg_cnt(&t).await?, 1); + assert_eq!(DC_CHAT_ID_ARCHIVED_LINK.get_fresh_msg_cnt(&t).await?, 0); + assert_eq!(t.get_fresh_msgs().await?.len(), 1); + + Ok(()) + } + async fn get_chats_from_chat_list(ctx: &Context, listflags: usize) -> Vec { let chatlist = Chatlist::try_load(ctx, listflags, None, None) .await @@ -4518,15 +4690,21 @@ mod tests { let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); - let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::IncomingUnknownTo).await?; + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("foo@bar.org")?, + Origin::IncomingUnknownTo, + ) + .await?; add_contact_to_chat(&t, chat_id, contact_id).await?; - assert!(!shall_attach_selfavatar(&t, chat_id).await?); - t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending assert!(shall_attach_selfavatar(&t, chat_id).await?); chat_id.set_selfavatar_timestamp(&t, time()).await?; assert!(!shall_attach_selfavatar(&t, chat_id).await?); + + t.set_config(Config::Selfavatar, None).await?; // setting to None also forces re-sending + assert!(shall_attach_selfavatar(&t, chat_id).await?); Ok(()) } @@ -4765,8 +4943,8 @@ mod tests { alice.set_config(Config::ShowEmails, Some("2")).await?; bob.set_config(Config::ShowEmails, Some("2")).await?; - let (contact_id, _) = - Contact::add_or_lookup(&alice, "", "bob@example.net", Origin::ManuallyCreated).await?; + let alice_bob_contact = alice.add_or_lookup_contact(&bob).await; + let contact_id = alice_bob_contact.id; let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?; let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?; @@ -5028,7 +5206,7 @@ mod tests { assert_eq!(msg.get_filename(), Some(filename.to_string())); assert_eq!(msg.get_width(), w); assert_eq!(msg.get_height(), h); - assert!(msg.get_filebytes(&bob).await > 250); + assert!(msg.get_filebytes(&bob).await?.unwrap() > 250); Ok(()) } @@ -5476,8 +5654,13 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_create_for_contact_with_blocked() -> Result<()> { let t = TestContext::new().await; - let (contact_id, _) = - Contact::add_or_lookup(&t, "", "foo@bar.org", Origin::ManuallyCreated).await?; + let (contact_id, _) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("foo@bar.org")?, + Origin::ManuallyCreated, + ) + .await?; // create a blocked chat let chat_id_orig = diff --git a/src/chatlist.rs b/src/chatlist.rs index 8482213f2..df80b15e2 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -1,5 +1,7 @@ //! # Chat list module. +#![allow(missing_docs)] + use anyhow::{ensure, Context as _, Result}; use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility}; @@ -90,8 +92,6 @@ impl Chatlist { let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS; let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT; - let mut add_archived_link_item = false; - let process_row = |row: &rusqlite::Row| { let chat_id: ChatId = row.get(0)?; let msg_id: Option = row.get(1)?; @@ -121,7 +121,7 @@ impl Chatlist { // // The query shows messages from blocked contacts in // groups. Otherwise it would be hard to follow conversations. - let mut ids = if let Some(query_contact_id) = query_contact_id { + let ids = if let Some(query_contact_id) = query_contact_id { // show chats shared with a given contact context.sql.query_map( "SELECT c.id, m.id @@ -214,7 +214,7 @@ impl Chatlist { } else { ChatId::new(0) }; - let ids = context.sql.query_map( + let mut ids = context.sql.query_map( "SELECT c.id, m.id FROM chats c LEFT JOIN msgs m @@ -234,19 +234,15 @@ impl Chatlist { process_row, process_rows, ).await?; - if !flag_no_specials { - add_archived_link_item = true; + if !flag_no_specials && get_archived_cnt(context).await? > 0 { + if ids.is_empty() && flag_add_alldone_hint { + ids.push((DC_CHAT_ID_ALLDONE_HINT, None)); + } + ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None)); } ids }; - if add_archived_link_item && get_archived_cnt(context).await? > 0 { - if ids.is_empty() && flag_add_alldone_hint { - ids.push((DC_CHAT_ID_ALLDONE_HINT, None)); - } - ids.push((DC_CHAT_ID_ARCHIVED_LINK, None)); - } - Ok(Chatlist { ids }) } diff --git a/src/config.rs b/src/config.rs index 259c3e063..41b884fab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,7 @@ //! # Key-value configuration management. +#![allow(missing_docs)] + use anyhow::{ensure, Context as _, Result}; use strum::{EnumProperty as EnumPropertyTrait, IntoEnumIterator}; use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString}; @@ -202,7 +204,7 @@ impl Context { let value = match key { Config::Selfavatar => { let rel_path = self.sql.get_raw_config(key.as_ref()).await?; - rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned()) + rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned()) } Config::SysVersion => Some((*DC_VERSION_STR).clone()), Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)), @@ -290,9 +292,6 @@ impl Context { self.sql .execute("UPDATE contacts SET selfavatar_sent=0;", paramsv![]) .await?; - self.sql - .set_raw_config_bool("attach_selfavatar", true) - .await?; match value { Some(value) => { let mut blob = BlobObject::new_from_path(self, value.as_ref()).await?; diff --git a/src/configure.rs b/src/configure.rs index 63e85dcd6..82403a76a 100644 --- a/src/configure.rs +++ b/src/configure.rs @@ -579,10 +579,10 @@ async fn try_imap_one_param( let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) { Err(err) => { - info!(context, "failure: {}", err); + info!(context, "failure: {:#}", err); return Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }); } Ok(imap) => imap, @@ -590,10 +590,10 @@ async fn try_imap_one_param( match imap.connect(context).await { Err(err) => { - info!(context, "failure: {}", err); + info!(context, "failure: {:#}", err); Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }) } Ok(()) => { @@ -634,7 +634,7 @@ async fn try_smtp_one_param( info!(context, "failure: {}", err); Err(ConfigurationError { config: inf, - msg: err.to_string(), + msg: format!("{:#}", err), }) } else { info!(context, "success: {}", inf); diff --git a/src/configure/auto_mozilla.rs b/src/configure/auto_mozilla.rs index bc7b69712..2f5ca45b2 100644 --- a/src/configure/auto_mozilla.rs +++ b/src/configure/auto_mozilla.rs @@ -62,7 +62,7 @@ fn parse_server( reader: &mut quick_xml::Reader, server_event: &BytesStart, ) -> Result, quick_xml::Error> { - let end_tag = String::from_utf8_lossy(server_event.name()) + let end_tag = String::from_utf8_lossy(server_event.name().as_ref()) .trim() .to_lowercase(); @@ -70,12 +70,17 @@ fn parse_server( .attributes() .find(|attr| { attr.as_ref() - .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "type") + .map(|a| { + String::from_utf8_lossy(a.key.as_ref()) + .trim() + .to_lowercase() + == "type" + }) .unwrap_or_default() }) .map(|typ| { typ.unwrap() - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default() .to_lowercase() }) @@ -89,25 +94,23 @@ fn parse_server( let mut tag_config = MozConfigTag::Undefined; let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - tag_config = String::from_utf8_lossy(event.name()) + tag_config = String::from_utf8_lossy(event.name().as_ref()) .parse() .unwrap_or_default(); } Event::End(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == end_tag { break; } } Event::Text(ref event) => { - let val = event - .unescape_and_decode(reader) - .unwrap_or_default() - .trim() - .to_owned(); + let val = event.unescape().unwrap_or_default().trim().to_owned(); match tag_config { MozConfigTag::Hostname => hostname = Some(val), @@ -150,9 +153,11 @@ fn parse_xml_reader( let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "incomingserver" { if let Some(incoming_server) = parse_server(reader, event)? { diff --git a/src/configure/auto_outlook.rs b/src/configure/auto_outlook.rs index 0be2f520a..d374f7016 100644 --- a/src/configure/auto_outlook.rs +++ b/src/configure/auto_outlook.rs @@ -59,12 +59,18 @@ fn parse_protocol( let mut current_tag: Option = None; loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref event) => { - current_tag = Some(String::from_utf8_lossy(event.name()).trim().to_lowercase()); + current_tag = Some( + String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(), + ); } Event::End(ref event) => { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "protocol" { break; } @@ -73,7 +79,7 @@ fn parse_protocol( } } Event::Text(ref e) => { - let val = e.unescape_and_decode(reader).unwrap_or_default(); + let val = e.unescape().unwrap_or_default(); if let Some(ref tag) = current_tag { match tag.as_str() { @@ -115,9 +121,9 @@ fn parse_redirecturl( reader: &mut quick_xml::Reader, ) -> Result { let mut buf = Vec::new(); - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Text(ref e) => { - let val = e.unescape_and_decode(reader).unwrap_or_default(); + let val = e.unescape().unwrap_or_default(); Ok(val.trim().to_string()) } _ => Ok("".to_string()), @@ -131,9 +137,11 @@ fn parse_xml_reader( let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf)? { + match reader.read_event_into(&mut buf)? { Event::Start(ref e) => { - let tag = String::from_utf8_lossy(e.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(e.name().as_ref()) + .trim() + .to_lowercase(); if tag == "protocol" { if let Some(protocol) = parse_protocol(reader)? { diff --git a/src/configure/read_url.rs b/src/configure/read_url.rs index f275025db..b0cdd989f 100644 --- a/src/configure/read_url.rs +++ b/src/configure/read_url.rs @@ -16,7 +16,7 @@ pub async fn read_url(context: &Context, url: &str) -> anyhow::Result { } pub async fn read_url_inner(context: &Context, url: &str) -> anyhow::Result { - let client = reqwest::Client::new(); + let client = crate::http::get_client()?; let mut url = url.to_string(); // Follow up to 10 http-redirects diff --git a/src/constants.rs b/src/constants.rs index bbdad67f7..a7c60a8dd 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,7 @@ //! # Constants. + +#![allow(missing_docs)] + use deltachat_derive::{FromSql, ToSql}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -65,6 +68,7 @@ impl Default for MediaQuality { } } +/// Type of the key to generate. #[derive( Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql, )] @@ -115,13 +119,13 @@ pub const DC_GCL_VERIFIED_ONLY: u32 = 0x01; pub const DC_GCL_ADD_SELF: u32 = 0x02; // unchanged user avatars are resent to the recipients every some days -pub const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; +pub(crate) const DC_RESEND_USER_AVATAR_DAYS: i64 = 14; // warn about an outdated app after a given number of days. // as we use the "provider-db generation date" as reference (that might not be updated very often) // and as not all system get speedy updates, // do not use too small value that will annoy users checking for nonexistant updates. -pub const DC_OUTDATED_WARNING_DAYS: i64 = 365; +pub(crate) const DC_OUTDATED_WARNING_DAYS: i64 = 365; /// messages that should be deleted get this chat_id; the messages are deleted from the working thread later then. This is also needed as rfc724_mid should be preset as long as the message is not deleted on the server (otherwise it is downloaded again) pub const DC_CHAT_ID_TRASH: ChatId = ChatId::new(3); @@ -166,7 +170,7 @@ pub const DC_MSG_ID_DAYMARKER: u32 = 9; pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9; /// String that indicates that something is left out or truncated. -pub const DC_ELLIPSIS: &str = "[...]"; +pub(crate) const DC_ELLIPSIS: &str = "[...]"; // how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x) // (taken from "subjective" testing what looks ok) pub const DC_DESIRED_TEXT_LINES: usize = 38; @@ -183,11 +187,6 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100; /// `char`s), not Unicode Grapheme Clusters. pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES; -// Flags for empty server job - -pub const DC_EMPTY_MVBOX: u32 = 0x01; -pub const DC_EMPTY_INBOX: u32 = 0x02; - // Flags for configuring IMAP and SMTP servers. // These flags are optional // and may be set together with the username, password etc. @@ -217,21 +216,7 @@ pub const BALANCED_IMAGE_SIZE: u32 = 1280; pub const WORSE_IMAGE_SIZE: u32 = 640; // this value can be increased if the folder configuration is changed and must be redone on next program start -pub const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3; - -// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks. -// this does not affect MIME'e `To:` header. -// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db. -pub const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; - -pub const DC_JOB_DELETE_MSG_ON_IMAP: i32 = 110; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive)] -#[repr(u8)] -pub enum KeyType { - Public = 0, - Private = 1, -} +pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 3; #[cfg(test)] mod tests { @@ -259,13 +244,6 @@ mod tests { assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap()); } - #[test] - fn test_keytype_values() { - // values may be written to disk and must not change - assert_eq!(KeyType::Public, KeyType::from_i32(0).unwrap()); - assert_eq!(KeyType::Private, KeyType::from_i32(1).unwrap()); - } - #[test] fn test_showemails_values() { // values may be written to disk and must not change diff --git a/src/contact.rs b/src/contact.rs index ddaec2306..95a713b5b 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -4,6 +4,7 @@ use std::cmp::Reverse; use std::collections::BinaryHeap; use std::convert::{TryFrom, TryInto}; use std::fmt; +use std::ops::Deref; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; @@ -36,6 +37,51 @@ use crate::{chat, stock_str}; /// Time during which a contact is considered as seen recently. const SEEN_RECENTLY_SECONDS: i64 = 600; +/// Valid contact address. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ContactAddress<'a>(&'a str); + +impl Deref for ContactAddress<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl AsRef for ContactAddress<'_> { + fn as_ref(&self) -> &str { + self.0 + } +} + +impl fmt::Display for ContactAddress<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'a> ContactAddress<'a> { + /// Constructs a new contact address from string, + /// normalizing and validating it. + pub fn new(s: &'a str) -> Result { + let addr = addr_normalize(s); + if !may_be_valid_addr(addr) { + bail!("invalid address {:?}", s); + } + Ok(Self(addr)) + } +} + +/// Allow converting [`ContactAddress`] to an SQLite type. +impl rusqlite::types::ToSql for ContactAddress<'_> { + fn to_sql(&self) -> rusqlite::Result { + let val = rusqlite::types::Value::Text(self.0.to_string()); + let out = rusqlite::types::ToSqlOutput::Owned(val); + Ok(out) + } +} + /// Contact ID, including reserved IDs. /// /// Some contact IDs are reserved to identify special contacts. This @@ -46,12 +92,18 @@ const SEEN_RECENTLY_SECONDS: i64 = 600; pub struct ContactId(u32); impl ContactId { + /// Undefined contact. Used as a placeholder for trashed messages. pub const UNDEFINED: ContactId = ContactId::new(0); + /// The owner of the account. /// /// The email-address is set by `set_config` using "addr". pub const SELF: ContactId = ContactId::new(1); + + /// ID of the contact for info messages. pub const INFO: ContactId = ContactId::new(2); + + /// ID of the contact for device messages. pub const DEVICE: ContactId = ContactId::new(5); const LAST_SPECIAL: ContactId = ContactId::new(9); @@ -175,6 +227,8 @@ pub struct Contact { )] #[repr(u32)] pub enum Origin { + /// Unknown origin. Can be used as a minimum origin to specify that the caller does not care + /// about origin of the contact. Unknown = 0, /// The contact is a mailing list address, needed to unblock mailing lists @@ -255,12 +309,13 @@ pub(crate) enum Modifier { Created, } +/// Verification status of the contact. #[derive(Debug, PartialEq, Eq, Clone, Copy, FromPrimitive)] #[repr(u8)] pub enum VerifiedStatus { /// Contact is not verified. Unverified = 0, - // TODO: is this a thing? + /// SELF has verified the fingerprint of a contact. Currently unused. Verified = 1, /// SELF and contact have verified their fingerprints in both directions; in the UI typically checkmarks are shown. BidirectVerified = 2, @@ -273,6 +328,7 @@ impl Default for VerifiedStatus { } impl Contact { + /// Loads a contact snapshot from the database. pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result { let mut contact = context .sql @@ -366,12 +422,14 @@ impl Contact { /// May result in a `#DC_EVENT_CONTACTS_CHANGED` event. pub async fn create(context: &Context, name: &str, addr: &str) -> Result { let name = improve_single_line_input(name); - ensure!(!addr.is_empty(), "Cannot create contact with empty address"); let (name, addr) = sanitize_name_and_addr(&name, addr); + let addr = ContactAddress::new(&addr)?; let (contact_id, sth_modified) = - Contact::add_or_lookup(context, &name, &addr, Origin::ManuallyCreated).await?; + Contact::add_or_lookup(context, &name, addr, Origin::ManuallyCreated) + .await + .context("add_or_lookup")?; let blocked = Contact::is_blocked_load(context, contact_id).await?; match sth_modified { Modifier::None => {} @@ -456,10 +514,12 @@ impl Contact { /// Depending on the origin, both, "row_name" and "row_authname" are updated from "name". /// /// Returns the contact_id and a `Modifier` value indicating if a modification occurred. + /// + /// Returns None if the contact with such address cannot exist. pub(crate) async fn add_or_lookup( context: &Context, name: &str, - addr: &str, + addr: ContactAddress<'_>, mut origin: Origin, ) -> Result<(ContactId, Modifier)> { let mut sth_modified = Modifier::None; @@ -467,22 +527,10 @@ impl Contact { ensure!(!addr.is_empty(), "Can not add_or_lookup empty address"); ensure!(origin != Origin::Unknown, "Missing valid origin"); - let addr = addr_normalize(addr).to_string(); - if context.is_self_addr(&addr).await? { return Ok((ContactId::SELF, sth_modified)); } - if !may_be_valid_addr(&addr) { - warn!( - context, - "Bad address \"{}\" for contact \"{}\".", - addr, - if !name.is_empty() { name } else { "" }, - ); - bail!("Bad address supplied: {:?}", addr); - } - let mut name = name; #[allow(clippy::collapsible_if)] if origin <= Origin::OutgoingTo { @@ -541,7 +589,7 @@ impl Contact { || row_authname.is_empty()); row_id = u32::try_from(id)?; - if origin as i32 >= row_origin as i32 && addr != row_addr { + if origin >= row_origin && addr.as_ref() != row_addr { update_addr = true; } if update_name || update_authname || update_addr || origin > row_origin { @@ -669,18 +717,25 @@ impl Contact { for (name, addr) in split_address_book(addr_book).into_iter() { let (name, addr) = sanitize_name_and_addr(name, addr); let name = normalize_name(&name); - match Contact::add_or_lookup(context, &name, &addr, Origin::AddressBook).await { - Err(err) => { - warn!( - context, - "Failed to add address {} from address book: {}", addr, err - ); - } - Ok((_, modified)) => { - if modified != Modifier::None { - modify_cnt += 1 + match ContactAddress::new(&addr) { + Ok(addr) => { + match Contact::add_or_lookup(context, &name, addr, Origin::AddressBook).await { + Ok((_, modified)) => { + if modified != Modifier::None { + modify_cnt += 1 + } + } + Err(err) => { + warn!( + context, + "Failed to add address {} from address book: {}", addr, err + ); + } } } + Err(err) => { + warn!(context, "{:#}.", err); + } } } if modify_cnt > 0 { @@ -845,6 +900,7 @@ impl Contact { Ok(()) } + /// Returns number of blocked contacts. pub async fn get_blocked_cnt(context: &Context) -> Result { let count = context .sql @@ -853,7 +909,7 @@ impl Contact { paramsv![ContactId::LAST_SPECIAL], ) .await?; - Ok(count as usize) + Ok(count) } /// Get blocked contacts. @@ -1136,6 +1192,24 @@ impl Contact { Ok(VerifiedStatus::Unverified) } + /// Returns the address that verified the contact. + pub async fn get_verifier_addr(&self, context: &Context) -> Result> { + Ok(Peerstate::from_addr(context, self.get_addr()) + .await? + .and_then(|peerstate| peerstate.get_verifier().map(|addr| addr.to_owned()))) + } + + /// Returns the ContactId that verified the contact. + pub async fn get_verifier_id(&self, context: &Context) -> Result> { + let verifier_addr = self.get_verifier_addr(context).await?; + if let Some(addr) = verifier_addr { + Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?) + } else { + Ok(None) + } + } + + /// Returns the number of real (i.e. non-special) contacts in the database. pub async fn get_real_cnt(context: &Context) -> Result { if !context.sql.is_open().await { return Ok(0); @@ -1151,6 +1225,7 @@ impl Contact { Ok(count) } + /// Returns true if a contact with this ID exists. pub async fn real_exists_by_id(context: &Context, contact_id: ContactId) -> Result { if contact_id.is_special() { return Ok(false); @@ -1166,6 +1241,7 @@ impl Contact { Ok(exists) } + /// Updates the origin of the contact, but only if new origin is higher than the current one. pub async fn scaleup_origin_by_id( context: &Context, contact_id: ContactId, @@ -1377,6 +1453,7 @@ pub(crate) async fn update_last_seen( ) .await? > 0 + && timestamp > time() - SEEN_RECENTLY_SECONDS { context.interrupt_recently_seen(contact_id, timestamp).await; } @@ -1426,6 +1503,7 @@ fn cat_fingerprint( } } +/// Compares two email addresses, normalizing them beforehand. pub fn addr_cmp(addr1: &str, addr2: &str) -> bool { let norm1 = addr_normalize(addr1).to_lowercase(); let norm2 = addr_normalize(addr2).to_lowercase(); @@ -1653,7 +1731,7 @@ mod tests { let (id, _modified) = Contact::add_or_lookup( &context.ctx, "bob", - "user@example.org", + ContactAddress::new("user@example.org")?, Origin::IncomingReplyTo, ) .await?; @@ -1681,7 +1759,7 @@ mod tests { let (contact_bob_id, modified) = Contact::add_or_lookup( &context.ctx, "someone", - "user@example.org", + ContactAddress::new("user@example.org")?, Origin::ManuallyCreated, ) .await?; @@ -1716,6 +1794,18 @@ mod tests { Ok(()) } + #[test] + fn test_contact_address() -> Result<()> { + let alice_addr = "alice@example.org"; + let contact_address = ContactAddress::new(alice_addr)?; + assert_eq!(contact_address.as_ref(), alice_addr); + + let invalid_addr = "<> foobar"; + assert!(ContactAddress::new(invalid_addr).is_err()); + + Ok(()) + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_or_lookup() { // add some contacts, this also tests add_address_book() @@ -1731,10 +1821,14 @@ mod tests { assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4); // check first added contact, this modifies authname because it is empty - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bla foo", + ContactAddress::new("one@eins.org").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1746,10 +1840,14 @@ mod tests { assert_eq!(contact.get_name_n_addr(), "Name one (one@eins.org)"); // modify first added contact - let (contact_id_test, sth_modified) = - Contact::add_or_lookup(&t, "Real one", " one@eins.org ", Origin::ManuallyCreated) - .await - .unwrap(); + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "Real one", + ContactAddress::new(" one@eins.org ").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1758,10 +1856,14 @@ mod tests { assert!(!contact.is_blocked()); // check third added contact (contact without name) - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "", "three@drei.sam", Origin::IncomingUnknownTo) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("three@drei.sam").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1774,7 +1876,7 @@ mod tests { let (contact_id_test, sth_modified) = Contact::add_or_lookup( &t, "m. serious", - "three@drei.sam", + ContactAddress::new("three@drei.sam").unwrap(), Origin::IncomingUnknownFrom, ) .await @@ -1786,10 +1888,14 @@ mod tests { assert!(!contact.is_blocked()); // manually edit name of third contact (does not changed authorized name) - let (contact_id_test, sth_modified) = - Contact::add_or_lookup(&t, "schnucki", "three@drei.sam", Origin::ManuallyCreated) - .await - .unwrap(); + let (contact_id_test, sth_modified) = Contact::add_or_lookup( + &t, + "schnucki", + ContactAddress::new("three@drei.sam").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap(); assert_eq!(contact_id, contact_id_test); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1798,10 +1904,14 @@ mod tests { assert!(!contact.is_blocked()); // Fourth contact: - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "", "alice@w.de", Origin::IncomingUnknownTo) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("alice@w.de").unwrap(), + Origin::IncomingUnknownTo, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::None); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -1936,9 +2046,13 @@ mod tests { assert!(Contact::delete(&alice, ContactId::SELF).await.is_err()); // Create Bob contact - let (contact_id, _) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let chat = alice .create_chat_with_contact("Bob", "bob@example.net") .await; @@ -2011,10 +2125,14 @@ mod tests { let t = TestContext::new().await; // incoming mail `From: bob1 ` - this should init authname - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob1", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob1", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Created); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2023,10 +2141,14 @@ mod tests { assert_eq!(contact.get_display_name(), "bob1"); // incoming mail `From: bob2 ` - this should update authname - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob2", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob2", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2045,10 +2167,14 @@ mod tests { assert_eq!(contact.get_display_name(), "bob3"); // incoming mail `From: bob4 ` - this should update authname, manually given name is still "bob3" - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "bob4", "bob@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap(); + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "bob4", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); assert!(!contact_id.is_special()); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); @@ -2073,7 +2199,7 @@ mod tests { let (contact_id_same, sth_modified) = Contact::add_or_lookup( &t, "claire1", - "claire@example.org", + ContactAddress::new("claire@example.org").unwrap(), Origin::IncomingUnknownFrom, ) .await @@ -2089,7 +2215,7 @@ mod tests { let (contact_id_same, sth_modified) = Contact::add_or_lookup( &t, "claire2", - "claire@example.org", + ContactAddress::new("claire@example.org").unwrap(), Origin::IncomingUnknownFrom, ) .await @@ -2111,26 +2237,38 @@ mod tests { let t = TestContext::new().await; // Incoming message from Bob. - let (contact_id, sth_modified) = - Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await?; + let (contact_id, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; assert_eq!(sth_modified, Modifier::Created); let contact = Contact::load_from_db(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Bob"); // Incoming message from someone else with "Not Bob" in the "To:" field. - let (contact_id_same, sth_modified) = - Contact::add_or_lookup(&t, "Not Bob", "bob@example.org", Origin::IncomingUnknownTo) - .await?; + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Not Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownTo, + ) + .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); let contact = Contact::load_from_db(&t, contact_id).await?; assert_eq!(contact.get_display_name(), "Not Bob"); // Incoming message from Bob, changing the name back. - let (contact_id_same, sth_modified) = - Contact::add_or_lookup(&t, "Bob", "bob@example.org", Origin::IncomingUnknownFrom) - .await?; + let (contact_id_same, sth_modified) = Contact::add_or_lookup( + &t, + "Bob", + ContactAddress::new("bob@example.org")?, + Origin::IncomingUnknownFrom, + ) + .await?; assert_eq!(contact_id, contact_id_same); assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix let contact = Contact::load_from_db(&t, contact_id).await?; @@ -2153,9 +2291,14 @@ mod tests { assert_eq!(contact.get_display_name(), "dave1"); // incoming mail `From: dave2 ` - this should update authname - Contact::add_or_lookup(&t, "dave2", "dave@example.org", Origin::IncomingUnknownFrom) - .await - .unwrap(); + Contact::add_or_lookup( + &t, + "dave2", + ContactAddress::new("dave@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap(); let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); assert_eq!(contact.get_authname(), "dave2"); assert_eq!(contact.get_name(), "dave1"); @@ -2269,9 +2412,13 @@ mod tests { let encrinfo = Contact::get_encrinfo(&alice, ContactId::DEVICE).await; assert!(encrinfo.is_err()); - let (contact_bob_id, _modified) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + let (contact_bob_id, _modified) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?; assert_eq!(encrinfo, "No encryption"); @@ -2298,7 +2445,6 @@ bob@example.net: CCCB 5AA9 F6E1 141C 9431 65F1 DB18 B18C BCF7 0487" ); - Ok(()) } @@ -2429,9 +2575,13 @@ CCCB 5AA9 F6E1 141C 9431 async fn test_last_seen() -> Result<()> { let alice = TestContext::new_alice().await; - let (contact_id, _) = - Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated) - .await?; + let (contact_id, _) = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.net")?, + Origin::ManuallyCreated, + ) + .await?; let contact = Contact::load_from_db(&alice, contact_id).await?; assert_eq!(contact.last_seen(), 0); diff --git a/src/context.rs b/src/context.rs index d28774df9..c297ac409 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,5 +1,7 @@ //! Context module. +#![allow(missing_docs)] + use std::collections::{BTreeMap, HashMap}; use std::ffi::OsString; use std::ops::Deref; @@ -381,7 +383,7 @@ impl Context { let mut lock = self.inner.scheduler.write().await; if lock.is_none() { match Scheduler::start(self.clone()).await { - Err(err) => error!(self, "Failed to start IO: {}", err), + Err(err) => error!(self, "Failed to start IO: {:#}", err), Ok(scheduler) => *lock = Some(scheduler), } } @@ -497,7 +499,7 @@ impl Context { match &*s { RunningState::Running { cancel_sender } => { if let Err(err) = cancel_sender.send(()).await { - warn!(self, "could not cancel ongoing: {:?}", err); + warn!(self, "could not cancel ongoing: {:#}", err); } info!(self, "Signaling the ongoing process to stop ASAP.",); *s = RunningState::ShallStop; @@ -526,10 +528,10 @@ impl Context { let l2 = LoginParam::load_configured_params(self).await?; let secondary_addrs = self.get_secondary_self_addrs().await?.join(", "); let displayname = self.get_config(Config::Displayname).await?; - let chats = get_chat_cnt(self).await? as usize; - let unblocked_msgs = message::get_unblocked_msg_cnt(self).await as usize; - let request_msgs = message::get_request_msg_cnt(self).await as usize; - let contacts = Contact::get_real_cnt(self).await? as usize; + let chats = get_chat_cnt(self).await?; + let unblocked_msgs = message::get_unblocked_msg_cnt(self).await; + let request_msgs = message::get_request_msg_cnt(self).await; + let contacts = Contact::get_real_cnt(self).await?; let is_configured = self.get_config_int(Config::Configured).await?; let socks5_enabled = self.get_config_int(Config::Socks5Enabled).await?; let dbversion = self diff --git a/src/decrypt.rs b/src/decrypt.rs index b3413bb65..b7f831ab0 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -1,11 +1,12 @@ //! End-to-end decryption support. use std::collections::HashSet; +use std::str::FromStr; -use anyhow::{Context as _, Result}; +use anyhow::Result; use mailparse::ParsedMail; -use crate::aheader::{Aheader, EncryptPreference}; +use crate::aheader::Aheader; use crate::authres::handle_authres; use crate::authres::{self, DkimResults}; use crate::contact::addr_cmp; @@ -13,47 +14,35 @@ use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; use crate::keyring::Keyring; -use crate::log::LogExt; use crate::peerstate::Peerstate; use crate::pgp; -/// Tries to decrypt a message, but only if it is structured as an -/// Autocrypt message. +/// Tries to decrypt a message, but only if it is structured as an Autocrypt message. /// -/// Returns decrypted body and a set of valid signature fingerprints -/// if successful. +/// If successful and the message is encrypted, returns decrypted body and a set of valid +/// signature fingerprints. /// -/// If the message is wrongly signed, this will still return the decrypted -/// message but the HashSet will be empty. -pub async fn try_decrypt( +/// If the message is wrongly signed, HashSet will be empty. +pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, - decryption_info: &DecryptionInfo, + private_keyring: &Keyring, + public_keyring_for_validate: &Keyring, ) -> Result, HashSet)>> { - // Possibly perform decryption - let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate); - let encrypted_data_part = match get_autocrypt_mime(mail) .or_else(|| get_mixed_up_mime(mail)) .or_else(|| get_attachment_mime(mail)) { - None => { - // not an autocrypt mime message, abort and ignore - return Ok(None); - } + None => return Ok(None), Some(res) => res, }; info!(context, "Detected Autocrypt-mime message"); - let private_keyring: Keyring = Keyring::new_self(context) - .await - .context("failed to get own keyring")?; decrypt_part( encrypted_data_part, private_keyring, public_keyring_for_validate, ) - .await } pub(crate) async fn prepare_decryption( @@ -61,7 +50,6 @@ pub(crate) async fn prepare_decryption( mail: &ParsedMail<'_>, from: &str, message_time: i64, - is_thunderbird: bool, ) -> Result { if mail.headers.get_header(HeaderDef::ListPost).is_some() { if mail.headers.get_header(HeaderDef::Autocrypt).is_some() { @@ -84,15 +72,25 @@ pub(crate) async fn prepare_decryption( }); } - let mut autocrypt_header = Aheader::from_headers(from, &mail.headers) - .ok_or_log_msg(context, "Failed to parse Autocrypt header") - .flatten(); - - if is_thunderbird { - if let Some(autocrypt_header) = &mut autocrypt_header { - autocrypt_header.prefer_encrypt = EncryptPreference::Mutual; - } - } + let autocrypt_header = + if let Some(autocrypt_header_value) = mail.headers.get_header_value(HeaderDef::Autocrypt) { + match Aheader::from_str(&autocrypt_header_value) { + Ok(header) if addr_cmp(&header.addr, from) => Some(header), + Ok(header) => { + warn!( + context, + "Autocrypt header address {:?} is not {:?}.", header.addr, from + ); + None + } + Err(err) => { + warn!(context, "Failed to parse Autocrypt header: {:#}.", err); + None + } + } + } else { + None + }; let dkim_results = handle_authres(context, mail, from, message_time).await?; @@ -211,30 +209,17 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail } /// Returns Ok(None) if nothing encrypted was found. -async fn decrypt_part( +fn decrypt_part( mail: &ParsedMail<'_>, - private_keyring: Keyring, - public_keyring_for_validate: Keyring, + private_keyring: &Keyring, + public_keyring_for_validate: &Keyring, ) -> Result, HashSet)>> { let data = mail.get_body_raw()?; if has_decrypted_pgp_armor(&data) { let (plain, ret_valid_signatures) = - pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?; - - // Check for detached signatures. - // If decrypted part is a multipart/signed, then there is a detached signature. - let decrypted_part = mailparse::parse_mail(&plain)?; - if let Some((content, valid_detached_signatures)) = - validate_detached_signature(&decrypted_part, &public_keyring_for_validate)? - { - return Ok(Some((content, valid_detached_signatures))); - } else { - // If the message was wrongly or not signed, still return the plain text. - // The caller has to check if the signatures set is empty then. - - return Ok(Some((plain, ret_valid_signatures))); - } + pgp::pk_decrypt(data, private_keyring, public_keyring_for_validate)?; + return Ok(Some((plain, ret_valid_signatures))); } Ok(None) @@ -256,32 +241,35 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// Validates signatures of Multipart/Signed message part, as defined in RFC 1847. /// -/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key +/// Returns the signed part and the set of key /// fingerprints for which there is a valid signature. -fn validate_detached_signature( - mail: &ParsedMail<'_>, +/// +/// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts. +pub(crate) fn validate_detached_signature<'a, 'b>( + mail: &'a ParsedMail<'b>, public_keyring_for_validate: &Keyring, -) -> Result, HashSet)>> { +) -> Option<(&'a ParsedMail<'b>, HashSet)> { if mail.ctype.mimetype != "multipart/signed" { - return Ok(None); + return None; } if let [first_part, second_part] = &mail.subparts[..] { // First part is the content, second part is the signature. let content = first_part.raw_bytes; - let signature = second_part.get_body_raw()?; - let ret_valid_signatures = - pgp::pk_validate(content, &signature, public_keyring_for_validate)?; - - Ok(Some((content.to_vec(), ret_valid_signatures))) + let ret_valid_signatures = match second_part.get_body_raw() { + Ok(signature) => pgp::pk_validate(content, &signature, public_keyring_for_validate) + .unwrap_or_default(), + Err(_) => Default::default(), + }; + Some((first_part, ret_valid_signatures)) } else { - Ok(None) + None } } -fn keyring_from_peerstate(peerstate: &Option) -> Keyring { +pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring { let mut public_keyring_for_validate: Keyring = Keyring::new(); - if let Some(ref peerstate) = *peerstate { + if let Some(peerstate) = peerstate { if let Some(key) = &peerstate.public_key { public_keyring_for_validate.add(key.clone()); } else if let Some(key) = &peerstate.gossip_key { diff --git a/src/dehtml.rs b/src/dehtml.rs index 3d39080c6..30c96e366 100644 --- a/src/dehtml.rs +++ b/src/dehtml.rs @@ -20,7 +20,7 @@ struct Dehtml { /// increased at each `
` and decreased at each `
`. This way we know when the quote ends. /// If this is > `0`, then we are inside a `
` divs_since_quote_div: u32, - /// Everything between
and
is usually metadata + /// Everything between `
` and `
` is usually metadata /// If this is > `0`, then we are inside a `
`. divs_since_quoted_content_div: u32, /// All-Inkl just puts the quote into `
`. This count is @@ -42,7 +42,7 @@ impl Dehtml { } fn get_add_text(&self) -> AddText { if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 { - AddText::No // Everything between
and
is metadata which we don't want + AddText::No // Everything between `
` and `
` is metadata which we don't want } else { self.add_text } @@ -88,18 +88,30 @@ fn dehtml_quick_xml(buf: &str) -> String { let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(quick_xml::events::Event::Start(ref e)) => { dehtml_starttag_cb(e, &mut dehtml, &reader) } Ok(quick_xml::events::Event::End(ref e)) => dehtml_endtag_cb(e, &mut dehtml), Ok(quick_xml::events::Event::Text(ref e)) => dehtml_text_cb(e, &mut dehtml), - Ok(quick_xml::events::Event::CData(e)) => dehtml_text_cb(&e.escape(), &mut dehtml), + Ok(quick_xml::events::Event::CData(e)) => match e.escape() { + Ok(e) => dehtml_text_cb(&e, &mut dehtml), + Err(e) => { + eprintln!( + "CDATA escape error at position {}: {:?}", + reader.buffer_position(), + e, + ); + } + }, Ok(quick_xml::events::Event::Empty(ref e)) => { // Handle empty tags as a start tag immediately followed by end tag. // For example, `

` is treated as `

`. dehtml_starttag_cb(e, &mut dehtml, &reader); - dehtml_endtag_cb(&BytesEnd::borrowed(e.name()), &mut dehtml); + dehtml_endtag_cb( + &BytesEnd::new(String::from_utf8_lossy(e.name().as_ref())), + &mut dehtml, + ); } Err(e) => { eprintln!( @@ -121,7 +133,7 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) { if dehtml.get_add_text() == AddText::YesPreserveLineEnds || dehtml.get_add_text() == AddText::YesRemoveLineEnds { - let last_added = escaper::decode_html_buf_sloppy(event.escaped()).unwrap_or_default(); + let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default(); if dehtml.get_add_text() == AddText::YesRemoveLineEnds { dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref(); @@ -135,7 +147,9 @@ fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) { } fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); match tag.as_str() { "p" | "table" | "td" | "style" | "script" | "title" | "pre" => { @@ -176,7 +190,9 @@ fn dehtml_starttag_cb( dehtml: &mut Dehtml, reader: &quick_xml::Reader, ) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); match tag.as_str() { "p" | "table" | "td" => { @@ -206,10 +222,15 @@ fn dehtml_starttag_cb( if let Some(href) = event .html_attributes() .filter_map(|attr| attr.ok()) - .find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "href") + .find(|attr| { + String::from_utf8_lossy(attr.key.as_ref()) + .trim() + .to_lowercase() + == "href" + }) { let href = href - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default() .to_lowercase(); @@ -258,7 +279,7 @@ fn maybe_push_tag( fn tag_contains_attr(event: &BytesStart, reader: &Reader, name: &str) -> bool { event.attributes().any(|r| { r.map(|a| { - a.unescape_and_decode_value(reader) + a.decode_and_unescape_value(reader) .map(|v| v == name) .unwrap_or(false) }) diff --git a/src/download.rs b/src/download.rs index f9fc9711b..015eb56f1 100644 --- a/src/download.rs +++ b/src/download.rs @@ -31,6 +31,7 @@ const MIN_DOWNLOAD_LIMIT: u32 = 32768; /// `MIN_DELETE_SERVER_AFTER` increases the timeout in this case. pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; +/// Download state of the message. #[derive( Debug, Display, @@ -47,9 +48,16 @@ pub(crate) const MIN_DELETE_SERVER_AFTER: i64 = 48 * 60 * 60; )] #[repr(u32)] pub enum DownloadState { + /// Message is fully downloaded. Done = 0, + + /// Message is partially downloaded and can be fully downloaded at request. Available = 10, + + /// Failed to fully download the message. Failure = 20, + + /// Full download of the message is in progress. InProgress = 1000, } @@ -124,7 +132,7 @@ impl Job { /// Called in response to `Action::DownloadMsg`. pub(crate) async fn download_msg(&self, context: &Context, imap: &mut Imap) -> Status { if let Err(err) = imap.prepare(context).await { - warn!(context, "download: could not connect: {:?}", err); + warn!(context, "download: could not connect: {:#}", err); return Status::RetryNow; } diff --git a/src/e2ee.rs b/src/e2ee.rs index 311894a04..62f699fdf 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -297,6 +297,7 @@ Sent with my Delta Chat Messenger: https://delta.chat"; verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; vec![(Some(peerstate), addr)] } diff --git a/src/ephemeral.rs b/src/ephemeral.rs index a65e1636c..1118ef077 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -62,6 +62,8 @@ //! the database entries which are expired either according to their //! ephemeral message timers or global `delete_server_after` setting. +#![allow(missing_docs)] + use std::convert::{TryFrom, TryInto}; use std::num::ParseIntError; use std::str::FromStr; @@ -203,14 +205,17 @@ impl ChatId { return Ok(()); } self.inner_set_ephemeral_timer(context, timer).await?; - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await); - msg.param.set_cmd(SystemMessage::EphemeralTimerChanged); - if let Err(err) = send_msg(context, self, &mut msg).await { - error!( - context, - "Failed to send a message about ephemeral message timer change: {:?}", err - ); + + if self.is_promoted(context).await? { + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await); + msg.param.set_cmd(SystemMessage::EphemeralTimerChanged); + if let Err(err) = send_msg(context, self, &mut msg).await { + error!( + context, + "Failed to send a message about ephemeral message timer change: {:?}", err + ); + } } Ok(()) } @@ -628,7 +633,7 @@ mod tests { use crate::test_utils::TestContext; use crate::tools::MAX_SECONDS_TO_LEND_FROM_FUTURE; use crate::{ - chat::{self, Chat, ChatItem}, + chat::{self, create_group_chat, send_text_msg, Chat, ChatItem, ProtectionStatus}, tools::IsNoneOrEmpty, }; @@ -793,6 +798,42 @@ mod tests { Ok(()) } + /// Test that enabling ephemeral timer in unpromoted group does not send a message. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_ephemeral_unpromoted() -> Result<()> { + let alice = TestContext::new_alice().await; + + let chat_id = + create_group_chat(&alice, ProtectionStatus::Unprotected, "Group name").await?; + + // Group is unpromoted, the timer can be changed without sending a message. + assert!(chat_id.is_unpromoted(&alice).await?); + chat_id + .set_ephemeral_timer(&alice, Timer::Enabled { duration: 60 }) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_none()); + assert_eq!( + chat_id.get_ephemeral_timer(&alice).await?, + Timer::Enabled { duration: 60 } + ); + + // Promote the group. + send_text_msg(&alice, chat_id, "hi!".to_string()).await?; + assert!(chat_id.is_promoted(&alice).await?); + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + + chat_id + .set_ephemeral_timer(&alice.ctx, Timer::Disabled) + .await?; + let sent = alice.pop_sent_msg_opt(Duration::from_secs(1)).await; + assert!(sent.is_some()); + assert_eq!(chat_id.get_ephemeral_timer(&alice).await?, Timer::Disabled); + + Ok(()) + } + /// Test that timer is enabled even if the message explicitly enabling the timer is lost. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ephemeral_enable_lost() -> Result<()> { diff --git a/src/events.rs b/src/events.rs index 2b3c4e996..1341b637b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,5 +1,7 @@ //! # Events specification. +#![allow(missing_docs)] + use std::path::PathBuf; use async_channel::{self as channel, Receiver, Sender, TrySendError}; @@ -281,7 +283,7 @@ pub enum EventType { /// @param data2 (int) Progress as: /// 300=vg-/vc-request received, typically shown as "bob@addr joins". /// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified". - /// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. + /// 800=contact added to chat, shown as "bob@addr securely joined GROUP". Only for the verified-group-protocol. /// 1000=Protocol finished for this contact. SecurejoinInviterProgress { contact_id: ContactId, diff --git a/src/fuzzing.rs b/src/fuzzing.rs new file mode 100644 index 000000000..ad920b9ab --- /dev/null +++ b/src/fuzzing.rs @@ -0,0 +1,12 @@ +//! # Fuzzing module. +//! +//! This module exposes private APIs for fuzzing. + +/// Fuzzing target for simplify(). +/// +/// Calls simplify() and panics if simplify() panics. +/// Does not return any vaule to avoid exposing internal crate types. +#[cfg(fuzzing)] +pub fn simplify(input: String, is_chat_message: bool) { + crate::simplify::simplify(input, is_chat_message); +} diff --git a/src/headerdef.rs b/src/headerdef.rs index 231bc3987..30d01222b 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -1,5 +1,7 @@ //! # List of email headers. +#![allow(missing_docs)] + use mailparse::{MailHeader, MailHeaderMap}; #[derive(Debug, Display, Clone, PartialEq, Eq, EnumVariantNames, IntoStaticStr)] diff --git a/src/html.rs b/src/html.rs index b112fcd2f..279995292 100644 --- a/src/html.rs +++ b/src/html.rs @@ -234,7 +234,7 @@ impl HtmlMsgParser { /// Convert a mime part to a data: url as defined in [RFC 2397](https://tools.ietf.org/html/rfc2397). fn mimepart_to_data_url(mail: &mailparse::ParsedMail<'_>) -> Result { let data = mail.get_body_raw()?; - let data = base64::encode(&data); + let data = base64::encode(data); Ok(format!("data:{};base64,{}", mail.ctype.mimetype, data)) } @@ -250,7 +250,7 @@ impl MsgId { if !rawmime.is_empty() { match HtmlMsgParser::from_bytes(context, &rawmime).await { Err(err) => { - warn!(context, "get_html: parser error: {}", err); + warn!(context, "get_html: parser error: {:#}", err); Ok(None) } Ok(parser) => Ok(Some(parser.html)), diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 000000000..4f0c3fb35 --- /dev/null +++ b/src/http.rs @@ -0,0 +1,12 @@ +//! # HTTP module. + +use anyhow::Result; +use std::time::Duration; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(30); + +pub(crate) fn get_client() -> Result { + Ok(reqwest::ClientBuilder::new() + .timeout(HTTP_TIMEOUT) + .build()?) +} diff --git a/src/imap.rs b/src/imap.rs index 64dd59976..f8b75574f 100644 --- a/src/imap.rs +++ b/src/imap.rs @@ -22,7 +22,7 @@ use crate::config::Config; use crate::constants::{ Blocked, Chattype, ShowEmails, DC_FETCH_EXISTING_MSGS_COUNT, DC_FOLDERS_CONFIGURED_VERSION, }; -use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin}; +use crate::contact::{normalize_name, Contact, ContactAddress, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; @@ -297,6 +297,7 @@ impl Imap { let oauth2 = self.config.lp.oauth2; + info!(context, "Connecting to IMAP server"); let connection_res: Result = if self.config.lp.security == Socket::Starttls || self.config.lp.security == Socket::Plain { @@ -304,22 +305,23 @@ impl Imap { let imap_server: &str = config.lp.server.as_ref(); let imap_port = config.lp.port; - let connection = if let Some(socks5_config) = &config.socks5_config { - Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone()) + if let Some(socks5_config) = &config.socks5_config { + if config.lp.security == Socket::Starttls { + Client::connect_starttls_socks5( + imap_server, + imap_port, + socks5_config.clone(), + config.strict_tls, + ) .await + } else { + Client::connect_insecure_socks5((imap_server, imap_port), socks5_config.clone()) + .await + } + } else if config.lp.security == Socket::Starttls { + Client::connect_starttls(imap_server, imap_port, config.strict_tls).await } else { Client::connect_insecure((imap_server, imap_port)).await - }; - - match connection { - Ok(client) => { - if config.lp.security == Socket::Starttls { - client.secure(imap_server, config.strict_tls).await - } else { - Ok(client) - } - } - Err(err) => Err(err), } } else { let config = &self.config; @@ -328,8 +330,8 @@ impl Imap { if let Some(socks5_config) = &config.socks5_config { Client::connect_secure_socks5( - (imap_server, imap_port), imap_server, + imap_port, config.strict_tls, socks5_config.clone(), ) @@ -345,6 +347,7 @@ impl Imap { let imap_pw: &str = config.lp.password.as_ref(); let login_res = if oauth2 { + info!(context, "Logging into IMAP server with OAuth 2"); let addr: &str = config.addr.as_ref(); let token = get_oauth2_access_token(context, addr, imap_pw, true) @@ -356,6 +359,7 @@ impl Imap { }; client.authenticate("XOAUTH2", auth).await } else { + info!(context, "Logging into IMAP server with LOGIN"); client.login(imap_user, imap_pw).await }; @@ -371,6 +375,7 @@ impl Imap { "IMAP-LOGIN as {}", self.config.lp.user ))); + info!(context, "Successfully logged into IMAP server"); Ok(()) } @@ -378,7 +383,7 @@ impl Imap { let imap_user = self.config.lp.user.to_owned(); let message = stock_str::cannot_login(context, &imap_user).await; - warn!(context, "{} ({})", message, err); + warn!(context, "{} ({:#})", message, err); let lock = context.wrong_pw_warning_mutex.lock().await; if self.login_failed_once @@ -386,7 +391,7 @@ impl Imap { && context.get_config_bool(Config::NotifyAboutWrongPw).await? { if let Err(e) = context.set_config(Config::NotifyAboutWrongPw, None).await { - warn!(context, "{}", e); + warn!(context, "{:#}", e); } drop(lock); @@ -396,13 +401,13 @@ impl Imap { chat::add_device_msg_with_importance(context, None, Some(&mut msg), true) .await { - warn!(context, "{}", e); + warn!(context, "{:#}", e); } } else { self.login_failed_once = true; } - Err(format_err!("{}\n\n{}", message, err)) + Err(format_err!("{}\n\n{:#}", message, err)) } } } @@ -549,7 +554,10 @@ impl Imap { folder: &str, ) -> Result { let session = self.session.as_mut().context("no session")?; - let newly_selected = session.select_or_create_folder(context, folder).await?; + let newly_selected = session + .select_or_create_folder(context, folder) + .await + .with_context(|| format!("failed to select or create folder {}", folder))?; let mailbox = session .selected_mailbox .as_mut() @@ -559,8 +567,12 @@ impl Imap { .uid_validity .with_context(|| format!("No UIDVALIDITY for folder {}", folder))?; - let old_uid_validity = get_uidvalidity(context, folder).await?; - let old_uid_next = get_uid_next(context, folder).await?; + let old_uid_validity = get_uidvalidity(context, folder) + .await + .with_context(|| format!("failed to get old UID validity for folder {}", folder))?; + let old_uid_next = get_uid_next(context, folder) + .await + .with_context(|| format!("failed to get old UID NEXT for folder {}", folder))?; if new_uid_validity == old_uid_validity { let new_emails = if newly_selected == NewlySelected::No { @@ -672,7 +684,10 @@ impl Imap { return Ok(false); } - let new_emails = self.select_with_uidvalidity(context, folder).await?; + let new_emails = self + .select_with_uidvalidity(context, folder) + .await + .with_context(|| format!("failed to select folder {}", folder))?; if !new_emails && !fetch_existing_msgs { info!(context, "No new emails in folder {}", folder); @@ -683,9 +698,11 @@ impl Imap { let old_uid_next = get_uid_next(context, folder).await?; let msgs = if fetch_existing_msgs { - self.prefetch_existing_msgs().await? + self.prefetch_existing_msgs() + .await + .context("prefetch_existing_msgs")? } else { - self.prefetch(old_uid_next).await? + self.prefetch(old_uid_next).await.context("prefetch")? }; let read_cnt = msgs.len(); @@ -748,7 +765,7 @@ impl Imap { fetch_response.flags(), show_emails, ) - .await? + .await.context("prefetch_should_download")? { match download_limit { Some(download_limit) => uids_fetch.push(( @@ -784,7 +801,8 @@ impl Imap { fetch_partially, fetch_existing_msgs, ) - .await?; + .await + .context("fetch_many_msgs")?; received_msgs.extend(received_msgs_in_batch); largest_uid_fetched = max( largest_uid_fetched, @@ -810,11 +828,13 @@ impl Imap { info!(context, "{} mails read from \"{}\".", read_cnt, folder); - let msg_ids = received_msgs + let msg_ids: Vec = received_msgs .iter() .flat_map(|m| m.msg_ids.clone()) .collect(); - context.emit_event(EventType::IncomingMsgBunch { msg_ids }); + if !msg_ids.is_empty() { + context.emit_event(EventType::IncomingMsgBunch { msg_ids }); + } chat::mark_old_messages_as_noticed(context, received_msgs).await?; @@ -832,9 +852,15 @@ impl Imap { } self.prepare(context).await.context("could not connect")?; - add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder).await; - add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder).await; - add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder).await; + add_all_recipients_as_contacts(context, self, Config::ConfiguredSentboxFolder) + .await + .context("failed to get recipients from the sentbox")?; + add_all_recipients_as_contacts(context, self, Config::ConfiguredMvboxFolder) + .await + .context("failed to ge recipients from the movebox")?; + add_all_recipients_as_contacts(context, self, Config::ConfiguredInboxFolder) + .await + .context("failed to get recipients from the inbox")?; if context.get_config_bool(Config::FetchExistingMsgs).await? { for config in &[ @@ -843,17 +869,18 @@ impl Imap { Config::ConfiguredSentboxFolder, ] { if let Some(folder) = context.get_config(*config).await? { + info!( + context, + "Fetching existing messages from folder \"{}\"", folder + ); self.fetch_new_messages(context, &folder, false, true) .await - .context("could not fetch messages")?; + .context("could not fetch existing messages")?; } } } info!(context, "Done fetching existing messages."); - context - .set_config_bool(Config::FetchedExistingMsgs, true) - .await?; Ok(()) } } @@ -1202,8 +1229,10 @@ impl Imap { /// Prefetch all messages greater than or equal to `uid_next`. Returns a list of fetch results /// in the order of ascending delivery time to the server (INTERNALDATE). async fn prefetch(&mut self, uid_next: u32) -> Result> { - let session = self.session.as_mut(); - let session = session.context("fetch_after(): IMAP No Connection established")?; + let session = self + .session + .as_mut() + .context("no IMAP connection established")?; // fetch messages with larger UID than the last one seen let set = format!("{}:*", uid_next); @@ -1228,7 +1257,6 @@ impl Imap { } } } - drop(list); Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect()) } @@ -1709,7 +1737,19 @@ async fn should_move_out_of_spam( }; // No chat found. let (from_id, blocked_contact, _origin) = - from_field_to_contact_id(context, &from, true).await?; + match from_field_to_contact_id(context, &from, true) + .await + .context("from_field_to_contact_id")? + { + Some(res) => res, + None => { + warn!( + context, + "Contact with From address {:?} cannot exist, not moving out of spam", from + ); + return Ok(false); + } + }; if blocked_contact { // Contact is blocked, leave the message in spam. return Ok(false); @@ -1999,7 +2039,10 @@ pub(crate) async fn prefetch_should_download( None => return Ok(false), }; let (_from_id, blocked_contact, origin) = - from_field_to_contact_id(context, &from, true).await?; + match from_field_to_contact_id(context, &from, true).await? { + Some(res) => res, + None => return Ok(false), + }; // prevent_rename=true as this might be a mailing list message and in this case it would be bad if we rename the contact. // (prevent_rename is the last argument of from_field_to_contact_id()) @@ -2301,49 +2344,66 @@ impl std::fmt::Display for UidRange { } } } -async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, folder: Config) { - let mailbox = if let Ok(Some(m)) = context.get_config(folder).await { +async fn add_all_recipients_as_contacts( + context: &Context, + imap: &mut Imap, + folder: Config, +) -> Result<()> { + let mailbox = if let Some(m) = context.get_config(folder).await? { m } else { - return; + info!( + context, + "Folder {} is not configured, skipping fetching contacts from it.", folder + ); + return Ok(()); }; - if let Err(e) = imap.select_with_uidvalidity(context, &mailbox).await { - // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}: - warn!(context, "Could not select {}: {:#}", mailbox, e); - return; - } - match imap.get_all_recipients(context).await { - Ok(contacts) => { - let mut any_modified = false; - for contact in contacts { - let display_name_normalized = contact - .display_name - .as_ref() - .map(|s| normalize_name(s)) - .unwrap_or_default(); + imap.select_with_uidvalidity(context, &mailbox) + .await + .with_context(|| format!("could not select {}", mailbox))?; - match Contact::add_or_lookup( + let recipients = imap + .get_all_recipients(context) + .await + .context("could not get recipients")?; + + let mut any_modified = false; + for recipient in recipients { + let display_name_normalized = recipient + .display_name + .as_ref() + .map(|s| normalize_name(s)) + .unwrap_or_default(); + + let recipient_addr = match ContactAddress::new(&recipient.addr) { + Err(err) => { + warn!( context, - &display_name_normalized, - &contact.addr, - Origin::OutgoingTo, - ) - .await - { - Ok((_, modified)) => { - if modified != Modifier::None { - any_modified = true; - } - } - Err(e) => warn!(context, "Could not add recipient: {}", e), - } - } - if any_modified { - context.emit_event(EventType::ContactsChanged(None)); + "Could not add contact for recipient with address {:?}: {:#}", + recipient.addr, + err + ); + continue; } + Ok(recipient_addr) => recipient_addr, + }; + + let (_, modified) = Contact::add_or_lookup( + context, + &display_name_normalized, + recipient_addr, + Origin::OutgoingTo, + ) + .await?; + if modified != Modifier::None { + any_modified = true; } - Err(e) => warn!(context, "Could not add recipients: {}", e), - }; + } + if any_modified { + context.emit_event(EventType::ContactsChanged(None)); + } + + Ok(()) } #[cfg(test)] diff --git a/src/imap/client.rs b/src/imap/client.rs index eef6e7929..74624a119 100644 --- a/src/imap/client.rs +++ b/src/imap/client.rs @@ -8,13 +8,13 @@ use anyhow::{Context as _, Result}; use async_imap::Client as ImapClient; use async_imap::Session as ImapSession; -use tokio::net::{self, TcpStream}; -use tokio::time::timeout; -use tokio_io_timeout::TimeoutStream; +use tokio::io::BufWriter; +use tokio::net::ToSocketAddrs; use super::capabilities::Capabilities; use super::session::Session; use crate::login_param::build_tls; +use crate::net::connect_tcp; use crate::socks::Socks5Config; use super::session::SessionStream; @@ -24,7 +24,6 @@ pub(crate) const IMAP_TIMEOUT: Duration = Duration::from_secs(30); #[derive(Debug)] pub(crate) struct Client { - is_secure: bool, inner: ImapClient>, } @@ -93,108 +92,131 @@ impl Client { } pub async fn connect_secure(hostname: &str, port: u16, strict_tls: bool) -> Result { - let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect((hostname, port))).await??; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT)); - timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT)); - let timeout_stream = Box::pin(timeout_stream); - + let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?; let tls = build_tls(strict_tls); - let tls_stream: Box = - Box::new(tls.connect(hostname, timeout_stream).await?); - let mut client = ImapClient::new(tls_stream); + let tls_stream = tls.connect(hostname, tcp_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; - Ok(Client { - is_secure: true, - inner: client, - }) + Ok(Client { inner: client }) } - pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result { - let tcp_stream = timeout(IMAP_TIMEOUT, TcpStream::connect(addr)).await??; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(IMAP_TIMEOUT)); - timeout_stream.set_read_timeout(Some(IMAP_TIMEOUT)); - let timeout_stream = Box::pin(timeout_stream); - let stream: Box = Box::new(timeout_stream); - - let mut client = ImapClient::new(stream); + pub async fn connect_insecure(addr: impl ToSocketAddrs) -> Result { + let tcp_stream = connect_tcp(addr, IMAP_TIMEOUT).await?; + let buffered_stream = BufWriter::new(tcp_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; - Ok(Client { - is_secure: false, - inner: client, - }) + Ok(Client { inner: client }) + } + + pub async fn connect_starttls(hostname: &str, port: u16, strict_tls: bool) -> Result { + let tcp_stream = connect_tcp((hostname, port), IMAP_TIMEOUT).await?; + + // Run STARTTLS command and convert the client back into a stream. + let session_stream: Box = Box::new(tcp_stream); + let mut client = ImapClient::new(session_stream); + let _greeting = client + .read_response() + .await + .context("failed to read greeting")??; + client + .run_command_and_check_ok("STARTTLS", None) + .await + .context("STARTTLS command failed")?; + let tcp_stream = client.into_inner(); + + let tls = build_tls(strict_tls); + let tls_stream = tls + .connect(hostname, tcp_stream) + .await + .context("STARTTLS upgrade failed")?; + + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let client = ImapClient::new(session_stream); + + Ok(Client { inner: client }) } pub async fn connect_secure_socks5( - target_addr: impl net::ToSocketAddrs, domain: &str, + port: u16, strict_tls: bool, socks5_config: Socks5Config, ) -> Result { - let socks5_stream: Box = - Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?); - + let socks5_stream = socks5_config.connect((domain, port), IMAP_TIMEOUT).await?; let tls = build_tls(strict_tls); - let tls_stream: Box = - Box::new(tls.connect(domain, socks5_stream).await?); - let mut client = ImapClient::new(tls_stream); - + let tls_stream = tls.connect(domain, socks5_stream).await?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; - Ok(Client { - is_secure: true, - inner: client, - }) + Ok(Client { inner: client }) } pub async fn connect_insecure_socks5( - target_addr: impl net::ToSocketAddrs, + target_addr: impl ToSocketAddrs, socks5_config: Socks5Config, ) -> Result { - let socks5_stream: Box = - Box::new(socks5_config.connect(target_addr, IMAP_TIMEOUT).await?); - - let mut client = ImapClient::new(socks5_stream); + let socks5_stream = socks5_config.connect(target_addr, IMAP_TIMEOUT).await?; + let buffered_stream = BufWriter::new(socks5_stream); + let session_stream: Box = Box::new(buffered_stream); + let mut client = ImapClient::new(session_stream); let _greeting = client .read_response() .await - .context("failed to read greeting")?; + .context("failed to read greeting")??; - Ok(Client { - is_secure: false, - inner: client, - }) + Ok(Client { inner: client }) } - pub async fn secure(self, domain: &str, strict_tls: bool) -> Result { - if self.is_secure { - Ok(self) - } else { - let Client { mut inner, .. } = self; - let tls = build_tls(strict_tls); - inner.run_command_and_check_ok("STARTTLS", None).await?; + pub async fn connect_starttls_socks5( + hostname: &str, + port: u16, + socks5_config: Socks5Config, + strict_tls: bool, + ) -> Result { + let socks5_stream = socks5_config + .connect((hostname, port), IMAP_TIMEOUT) + .await?; - let stream = inner.into_inner(); - let ssl_stream = tls.connect(domain, stream).await?; - let boxed: Box = Box::new(ssl_stream); + // Run STARTTLS command and convert the client back into a stream. + let session_stream: Box = Box::new(socks5_stream); + let mut client = ImapClient::new(session_stream); + let _greeting = client + .read_response() + .await + .context("failed to read greeting")??; + client + .run_command_and_check_ok("STARTTLS", None) + .await + .context("STARTTLS command failed")?; + let socks5_stream = client.into_inner(); - Ok(Client { - is_secure: true, - inner: ImapClient::new(boxed), - }) - } + let tls = build_tls(strict_tls); + let tls_stream = tls + .connect(hostname, socks5_stream) + .await + .context("STARTTLS upgrade failed")?; + let buffered_stream = BufWriter::new(tls_stream); + let session_stream: Box = Box::new(buffered_stream); + let client = ImapClient::new(session_stream); + + Ok(Client { inner: client }) } } diff --git a/src/imap/select_folder.rs b/src/imap/select_folder.rs index 3f04f41a2..facf04ffc 100644 --- a/src/imap/select_folder.rs +++ b/src/imap/select_folder.rs @@ -109,13 +109,14 @@ impl ImapSession { Ok(newly_selected) => Ok(newly_selected), Err(err) => match err { Error::NoFolder(..) => { + info!(context, "Failed to select folder {} because it does not exist, trying to create it.", folder); self.create(folder).await.with_context(|| { format!("Couldn't select folder ('{}'), then create() failed", err) })?; - Ok(self.select_folder(context, Some(folder)).await?) + Ok(self.select_folder(context, Some(folder)).await.with_context(|| format!("failed to select newely created folder {}", folder))?) } - _ => Err(err.into()), + _ => Err(err).with_context(|| format!("failed to select folder {} with error other than NO, not trying to create it", folder)), }, } } diff --git a/src/imap/session.rs b/src/imap/session.rs index a66dd4852..f3dd00a86 100644 --- a/src/imap/session.rs +++ b/src/imap/session.rs @@ -6,6 +6,7 @@ use async_imap::types::Mailbox; use async_imap::Session as ImapSession; use async_native_tls::TlsStream; use fast_socks5::client::Socks5Stream; +use tokio::io::BufWriter; use tokio::net::TcpStream; use tokio_io_timeout::TimeoutStream; @@ -33,12 +34,17 @@ pub(crate) trait SessionStream: fn set_read_timeout(&mut self, timeout: Option); } -impl SessionStream for TlsStream> { +impl SessionStream for Box { + fn set_read_timeout(&mut self, timeout: Option) { + self.as_mut().set_read_timeout(timeout); + } +} +impl SessionStream for TlsStream { fn set_read_timeout(&mut self, timeout: Option) { self.get_mut().set_read_timeout(timeout); } } -impl SessionStream for TlsStream>>> { +impl SessionStream for BufWriter { fn set_read_timeout(&mut self, timeout: Option) { self.get_mut().set_read_timeout(timeout); } @@ -48,7 +54,7 @@ impl SessionStream for Pin>> { self.as_mut().set_read_timeout_pinned(timeout); } } -impl SessionStream for Socks5Stream>>> { +impl SessionStream for Socks5Stream { fn set_read_timeout(&mut self, timeout: Option) { self.get_socket_mut().set_read_timeout(timeout) } diff --git a/src/imex.rs b/src/imex.rs index 809c7958e..e265e2f6b 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -1,5 +1,7 @@ //! # Import/export module. +#![allow(missing_docs)] + use std::any::Any; use std::ffi::OsStr; use std::path::{Path, PathBuf}; @@ -348,7 +350,7 @@ async fn decrypt_setup_file( fn normalize_setup_code(s: &str) -> String { let mut out = String::new(); for c in s.chars() { - if ('0'..='9').contains(&c) { + if c.is_ascii_digit() { out.push(c); if let 4 | 9 | 14 | 19 | 24 | 29 | 34 | 39 = out.len() { out += "-" @@ -654,7 +656,7 @@ async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> { Ok(buf) => { let armored = std::string::String::from_utf8_lossy(&buf); if let Err(err) = set_self_key(context, &armored, set_default, false).await { - error!(context, "set_self_key: {}", err); + info!(context, "set_self_key: {}", err); continue; } } diff --git a/src/job.rs b/src/job.rs index 438cbbab5..689a7cade 100644 --- a/src/job.rs +++ b/src/job.rs @@ -2,6 +2,9 @@ //! //! This module implements a job queue maintained in the SQLite database //! and job types. + +#![allow(missing_docs)] + use std::fmt; use anyhow::{Context as _, Result}; @@ -154,7 +157,7 @@ impl Job { /// Synchronizes UIDs for all folders. async fn resync_folders(&mut self, context: &Context, imap: &mut Imap) -> Status { if let Err(err) = imap.prepare(context).await { - warn!(context, "could not connect: {:?}", err); + warn!(context, "could not connect: {:#}", err); return Status::RetryLater; } @@ -238,12 +241,12 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ info!( context, "job #{} not succeeded on try #{}, retry in {} seconds.", - job.job_id as u32, + job.job_id, tries, time_offset ); job.save(context).await.unwrap_or_else(|err| { - error!(context, "failed to save job: {}", err); + error!(context, "failed to save job: {:#}", err); }); } else { info!( @@ -251,7 +254,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ "remove job {} as it exhausted {} retries", job, JOB_RETRIES ); job.delete(context).await.unwrap_or_else(|err| { - error!(context, "failed to delete job: {}", err); + error!(context, "failed to delete job: {:#}", err); }); } } @@ -266,7 +269,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_ } job.delete(context).await.unwrap_or_else(|err| { - error!(context, "failed to delete job: {}", err); + error!(context, "failed to delete job: {:#}", err); }); } } @@ -400,7 +403,7 @@ LIMIT 1; Ok(job) => return Ok(job), Err(err) => { // Remove invalid job from the DB - info!(context, "cleaning up job, because of {}", err); + info!(context, "cleaning up job, because of {:#}", err); // TODO: improve by only doing a single query let id = context diff --git a/src/key.rs b/src/key.rs index 7c385a313..82ea25667 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,5 +1,7 @@ //! Cryptographic key module. +#![allow(missing_docs)] + use std::collections::BTreeMap; use std::fmt; use std::io::Cursor; @@ -28,17 +30,13 @@ pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; /// [SignedSecretKey] types and makes working with them a little /// easier in the deltachat world. pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { - type KeyType: Serialize + Deserializable + KeyTrait + Clone; - /// Create a key from some bytes. - fn from_slice(bytes: &[u8]) -> Result { - Ok(::from_bytes(Cursor::new( - bytes, - ))?) + fn from_slice(bytes: &[u8]) -> Result { + Ok(::from_bytes(Cursor::new(bytes))?) } /// Create a key from a base64 string. - fn from_base64(data: &str) -> Result { + fn from_base64(data: &str) -> Result { // strip newlines and other whitespace let cleaned: String = data.split_whitespace().collect(); let bytes = base64::decode(cleaned.as_bytes())?; @@ -49,15 +47,15 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { /// /// Returns the key and a map of any headers which might have been set in /// the ASCII-armored representation. - fn from_asc(data: &str) -> Result<(Self::KeyType, BTreeMap)> { + fn from_asc(data: &str) -> Result<(Self, BTreeMap)> { let bytes = data.as_bytes(); - Self::KeyType::from_armor_single(Cursor::new(bytes)).context("rPGP error") + Self::from_armor_single(Cursor::new(bytes)).context("rPGP error") } /// Load the users' default key from the database. fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>>; + ) -> Pin> + 'a + Send>>; /// Serialise the key as bytes. fn to_bytes(&self) -> Vec { @@ -72,7 +70,7 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { /// Serialise the key to a base64 string. fn to_base64(&self) -> String { - base64::encode(&DcKey::to_bytes(self)) + base64::encode(DcKey::to_bytes(self)) } /// Serialise the key to ASCII-armored representation. @@ -90,11 +88,9 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { } impl DcKey for SignedPublicKey { - type KeyType = SignedPublicKey; - fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>> { + ) -> Pin> + 'a + Send>> { Box::pin(async move { let addr = context.get_primary_self_addr().await?; match context @@ -141,11 +137,9 @@ impl DcKey for SignedPublicKey { } impl DcKey for SignedSecretKey { - type KeyType = SignedSecretKey; - fn load_self<'a>( context: &'a Context, - ) -> Pin> + 'a + Send>> { + ) -> Pin> + 'a + Send>> { Box::pin(async move { match context .sql @@ -385,7 +379,7 @@ impl std::str::FromStr for Fingerprint { let hex_repr: String = input .to_uppercase() .chars() - .filter(|&c| ('0'..='9').contains(&c) || ('A'..='F').contains(&c)) + .filter(|&c| c.is_ascii_hexdigit()) .collect(); let v: Vec = hex::decode(&hex_repr)?; ensure!(v.len() == 20, "wrong fingerprint length: {}", hex_repr); diff --git a/src/keyring.rs b/src/keyring.rs index 192b8de80..fa5e9b5f4 100644 --- a/src/keyring.rs +++ b/src/keyring.rs @@ -19,7 +19,7 @@ where impl Keyring where - T: DcKey, + T: DcKey, { /// New empty keyring. pub fn new() -> Keyring { diff --git a/src/lib.rs b/src/lib.rs index f3925a306..831f6609e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ unused, clippy::correctness, missing_debug_implementations, + missing_docs, clippy::all, clippy::indexing_slicing, clippy::wildcard_imports, @@ -14,15 +15,13 @@ clippy::unused_async )] #![allow( + clippy::uninlined_format_args, clippy::match_bool, clippy::mixed_read_write_in_expression, clippy::bool_assert_comparison, clippy::manual_split_once, clippy::format_push_string, - clippy::bool_to_int_with_if, - // This lint can be re-enabled once we don't target - // Rust 1.56 anymore: - clippy::collapsible_str_replace + clippy::bool_to_int_with_if )] #[macro_use] @@ -34,6 +33,7 @@ extern crate rusqlite; #[macro_use] extern crate strum_macros; +#[allow(missing_docs)] pub trait ToSql: rusqlite::ToSql + Send + Sync {} impl ToSql for T {} @@ -66,12 +66,12 @@ mod decrypt; pub mod download; mod e2ee; pub mod ephemeral; +mod http; mod imap; pub mod imex; mod scheduler; #[macro_use] mod job; -mod format_flowed; pub mod key; mod keyring; pub mod location; @@ -101,6 +101,7 @@ mod dehtml; mod authres; mod color; pub mod html; +mod net; pub mod plaintext; mod ratelimit; pub mod summary; @@ -118,3 +119,6 @@ pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG"; mod test_utils; #[cfg(test)] mod tests; + +#[cfg(fuzzing)] +pub mod fuzzing; diff --git a/src/location.rs b/src/location.rs index f99838d9e..dc6bf3af6 100644 --- a/src/location.rs +++ b/src/location.rs @@ -1,4 +1,5 @@ //! Location handling. + use std::convert::TryFrom; use std::time::Duration; @@ -17,32 +18,63 @@ use crate::mimeparser::SystemMessage; use crate::stock_str; use crate::tools::{duration_to_str, time}; -/// Location record +/// Location record. #[derive(Debug, Clone, Default)] pub struct Location { + /// Row ID of the location. pub location_id: u32, + + /// Location latitude. pub latitude: f64, + + /// Location longitude. pub longitude: f64, + + /// Nonstandard `accuracy` attribute of the `coordinates` tag. pub accuracy: f64, + + /// Location timestamp in seconds. pub timestamp: i64, + + /// Contact ID. pub contact_id: ContactId, + + /// Message ID. pub msg_id: u32, + + /// Chat ID. pub chat_id: ChatId, + + /// A marker string, such as an emoji, to be displayed on top of the location. pub marker: Option, + + /// Whether location is independent, i.e. not part of the path. pub independent: u32, } impl Location { + /// Creates a new empty location. pub fn new() -> Self { Default::default() } } +/// KML document. +/// +/// See for the standard and +/// for documentation. #[derive(Debug, Clone, Default)] pub struct Kml { + /// Nonstandard `addr` attribute of the `Document` tag storing the user email address. pub addr: Option, + + /// Placemarks. pub locations: Vec, + + /// Currently parsed XML tag. tag: KmlTag, + + /// Currently parsed placemark. pub curr: Location, } @@ -59,10 +91,12 @@ bitflags! { } impl Kml { + /// Creates a new empty KML document. pub fn new() -> Self { Default::default() } + /// Parses a KML document. pub fn parse(to_parse: &[u8]) -> Result { ensure!(to_parse.len() <= 1024 * 1024, "kml-file is too large"); @@ -75,7 +109,7 @@ impl Kml { let mut buf = Vec::new(); loop { - match reader.read_event(&mut buf).with_context(|| { + match reader.read_event_into(&mut buf).with_context(|| { format!( "location parsing error at position {}", reader.buffer_position() @@ -83,7 +117,7 @@ impl Kml { })? { quick_xml::events::Event::Start(ref e) => kml.starttag_cb(e, &reader), quick_xml::events::Event::End(ref e) => kml.endtag_cb(e), - quick_xml::events::Event::Text(ref e) => kml.text_cb(e, &reader), + quick_xml::events::Event::Text(ref e) => kml.text_cb(e), quick_xml::events::Event::Eof => break, _ => (), } @@ -93,15 +127,11 @@ impl Kml { Ok(kml) } - fn text_cb(&mut self, event: &BytesText, reader: &quick_xml::Reader) { + fn text_cb(&mut self, event: &BytesText) { if self.tag.contains(KmlTag::WHEN) || self.tag.contains(KmlTag::COORDINATES) { - let val = event.unescape_and_decode(reader).unwrap_or_default(); + let val = event.unescape().unwrap_or_default(); - let val = val - .replace('\n', "") - .replace('\r', "") - .replace('\t', "") - .replace(' ', ""); + let val = val.replace(['\n', '\r', '\t', ' '], ""); if self.tag.contains(KmlTag::WHEN) && val.len() >= 19 { // YYYY-MM-DDTHH:MM:SSZ @@ -128,7 +158,9 @@ impl Kml { } fn endtag_cb(&mut self, event: &BytesEnd) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "placemark" { if self.tag.contains(KmlTag::PLACEMARK) @@ -148,14 +180,20 @@ impl Kml { event: &BytesStart, reader: &quick_xml::Reader, ) { - let tag = String::from_utf8_lossy(event.name()).trim().to_lowercase(); + let tag = String::from_utf8_lossy(event.name().as_ref()) + .trim() + .to_lowercase(); if tag == "document" { - if let Some(addr) = event - .attributes() - .filter_map(|a| a.ok()) - .find(|attr| String::from_utf8_lossy(attr.key).trim().to_lowercase() == "addr") - { - self.addr = addr.unescape_and_decode_value(reader).ok(); + if let Some(addr) = event.attributes().filter_map(|a| a.ok()).find(|attr| { + String::from_utf8_lossy(attr.key.as_ref()) + .trim() + .to_lowercase() + == "addr" + }) { + self.addr = addr + .decode_and_unescape_value(reader) + .ok() + .map(|a| a.into_owned()); } } else if tag == "placemark" { self.tag = KmlTag::PLACEMARK; @@ -173,12 +211,17 @@ impl Kml { self.tag = KmlTag::PLACEMARK | KmlTag::POINT | KmlTag::COORDINATES; if let Some(acc) = event.attributes().find(|attr| { attr.as_ref() - .map(|a| String::from_utf8_lossy(a.key).trim().to_lowercase() == "accuracy") + .map(|a| { + String::from_utf8_lossy(a.key.as_ref()) + .trim() + .to_lowercase() + == "accuracy" + }) .unwrap_or_default() }) { let v = acc .unwrap() - .unescape_and_decode_value(reader) + .decode_and_unescape_value(reader) .unwrap_or_default(); self.curr.accuracy = v.trim().parse().unwrap_or_default(); @@ -260,6 +303,7 @@ pub async fn is_sending_locations_to_chat( Ok(exists) } +/// Sets current location of the user device. pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool { if latitude == 0.0 && longitude == 0.0 { return true; @@ -293,7 +337,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 ContactId::SELF, ] ).await { - warn!(context, "failed to store location {:?}", err); + warn!(context, "failed to store location {:#}", err); } else { info!(context, "stored location for chat {}", chat_id); continue_streaming = true; @@ -307,6 +351,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64 continue_streaming } +/// Searches for locations in the given time range, optionally filtering by chat and contact IDs. pub async fn get_range( context: &Context, chat_id: Option, @@ -397,6 +442,7 @@ pub async fn delete_all(context: &Context) -> Result<()> { Ok(()) } +/// Returns `location.kml` contents. pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> { let mut last_added_location_id = 0; @@ -482,6 +528,7 @@ fn get_kml_timestamp(utc: i64) -> String { .to_string() } +/// Returns a KML document containing a single location with the given timestamp and coordinates. pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String { format!( "\n\ @@ -499,6 +546,7 @@ pub fn get_message_kml(timestamp: i64, latitude: f64, longitude: f64) -> String ) } +/// Sets the timestamp of the last time location was sent in the chat. pub async fn set_kml_sent_timestamp( context: &Context, chat_id: ChatId, @@ -514,6 +562,7 @@ pub async fn set_kml_sent_timestamp( Ok(()) } +/// Sets the location of the message. pub async fn set_msg_location_id(context: &Context, msg_id: MsgId, location_id: u32) -> Result<()> { context .sql @@ -589,7 +638,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive loop { let next_event = match maybe_send_locations(context).await { Err(err) => { - warn!(context, "maybe_send_locations failed: {}", err); + warn!(context, "maybe_send_locations failed: {:#}", err); Some(60) // Retry one minute later. } Ok(next_event) => next_event, diff --git a/src/log.rs b/src/log.rs index 4ae368514..51ada7480 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,5 +1,7 @@ //! # Logging. +#![allow(missing_docs)] + use crate::context::Context; #[macro_export] diff --git a/src/message.rs b/src/message.rs index 600bbc8f1..48819a218 100644 --- a/src/message.rs +++ b/src/message.rs @@ -235,11 +235,18 @@ impl Default for MessengerMessage { /// If you want an update, you have to recreate the object. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Message { + /// Message ID. pub(crate) id: MsgId, + + /// `From:` contact ID. pub(crate) from_id: ContactId, + + /// ID of the first contact in the `To:` header. pub(crate) to_id: ContactId, pub(crate) chat_id: ChatId, pub(crate) viewtype: Viewtype, + + /// State of the message. pub(crate) state: MessageState, pub(crate) download_state: DownloadState, pub(crate) hidden: bool, @@ -261,6 +268,7 @@ pub struct Message { } impl Message { + /// Creates a new message with given view type. pub fn new(viewtype: Viewtype) -> Self { Message { viewtype, @@ -268,6 +276,7 @@ impl Message { } } + /// Loads message with given ID from the database. pub async fn load_from_db(context: &Context, id: MsgId) -> Result { ensure!( !id.is_special(), @@ -364,6 +373,12 @@ impl Message { Ok(msg) } + /// Returns the MIME type of an attached file if it exists. + /// + /// If the MIME type is not known, the function guesses the MIME type + /// from the extension. `application/octet-stream` is used as a fallback + /// if MIME type is not known, but `None` is only returned if no file + /// is attached. pub fn get_filemime(&self) -> Option { if let Some(m) = self.param.get(Param::MimeType) { return Some(m.to_string()); @@ -378,11 +393,12 @@ impl Message { None } + /// Returns the full path to the file associated with a message. pub fn get_file(&self, context: &Context) -> Option { self.param.get_path(Param::File, context).unwrap_or(None) } - pub async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { + pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { if self.viewtype.has_file() { let file_param = self.param.get_path(Param::File, context)?; if let Some(path_and_filename) = file_param { @@ -440,6 +456,8 @@ impl Message { self.param.set_float(Param::SetLongitude, longitude); } + /// Returns the message timestamp for display in the UI + /// as a unix timestamp in seconds. pub fn get_timestamp(&self) -> i64 { if 0 != self.timestamp_sent { self.timestamp_sent @@ -448,10 +466,12 @@ impl Message { } } + /// Returns the message ID. pub fn get_id(&self) -> MsgId { self.id } + /// Returns the ID of the contact who wrote the message. pub fn get_from_id(&self) -> ContactId { self.from_id } @@ -461,30 +481,40 @@ impl Message { self.chat_id } + /// Returns the type of the message. pub fn get_viewtype(&self) -> Viewtype { self.viewtype } + /// Returns the state of the message. pub fn get_state(&self) -> MessageState { self.state } + /// Returns the message receive time as a unix timestamp in seconds. pub fn get_received_timestamp(&self) -> i64 { self.timestamp_rcvd } + /// Returns the timestamp of the message for sorting. pub fn get_sort_timestamp(&self) -> i64 { self.timestamp_sort } + /// Returns the text of the message. pub fn get_text(&self) -> Option { self.text.as_ref().map(|s| s.to_string()) } + /// Returns message subject. pub fn get_subject(&self) -> &str { &self.subject } + /// Returns base file name without the path. + /// The base file name includes the extension. + /// + /// To get the full path, use [`Self::get_file()`]. pub fn get_filename(&self) -> Option { self.param .get(Param::File) @@ -492,26 +522,31 @@ impl Message { .map(|name| name.to_string_lossy().to_string()) } - pub async fn get_filebytes(&self, context: &Context) -> u64 { - match self.param.get_path(Param::File, context) { - Ok(Some(path)) => get_filebytes(context, &path).await, - Ok(None) => 0, - Err(_) => 0, + /// Returns the size of the file in bytes, if applicable. + pub async fn get_filebytes(&self, context: &Context) -> Result> { + if let Some(path) = self.param.get_path(Param::File, context)? { + Ok(Some(get_filebytes(context, &path).await?)) + } else { + Ok(None) } } + /// Returns width of associated image or video file. pub fn get_width(&self) -> i32 { self.param.get_int(Param::Width).unwrap_or_default() } + /// Returns height of associated image or video file. pub fn get_height(&self) -> i32 { self.param.get_int(Param::Height).unwrap_or_default() } + /// Returns duration of associated audio or video file. pub fn get_duration(&self) -> i32 { self.param.get_int(Param::Duration).unwrap_or_default() } + /// Returns true if padlock indicating message encryption should be displayed in the UI. pub fn get_showpadlock(&self) -> bool { self.param.get_int(Param::GuaranteeE2ee).unwrap_or_default() != 0 } @@ -521,10 +556,12 @@ impl Message { self.param.get_bool(Param::Bot).unwrap_or_default() } + /// Return the ephemeral timer duration for a message. pub fn get_ephemeral_timer(&self) -> EphemeralTimer { self.ephemeral_timer } + /// Returns the timestamp of the epehemeral message removal. pub fn get_ephemeral_timestamp(&self) -> i64 { self.ephemeral_timestamp } @@ -562,6 +599,7 @@ impl Message { // C-data in the Java code (i.e. a `long` storing a C pointer) // - We can't make a param `SenderDisplayname` for messages as sometimes the display name of a contact changes, and we want to show // the same display name over all messages from the same sender. + /// Returns the name that should be shown over the message instead of the contact display ame. pub fn get_override_sender_name(&self) -> Option { self.param .get(Param::OverrideSenderDisplayname) @@ -570,27 +608,35 @@ impl Message { // Exposing this function over the ffi instead of get_override_sender_name() would mean that at least Android Java code has // to handle raw C-data (as it is done for msg_get_summary()) - pub fn get_sender_name(&self, contact: &Contact) -> String { + pub(crate) fn get_sender_name(&self, contact: &Contact) -> String { self.get_override_sender_name() .unwrap_or_else(|| contact.get_display_name().to_string()) } + /// Returns true if a message has a deviating timestamp. + /// + /// A message has a deviating timestamp when it is sent on + /// another day as received/sorted by. pub fn has_deviating_timestamp(&self) -> bool { let cnv_to_local = gm2local_offset(); - let sort_timestamp = self.get_sort_timestamp() as i64 + cnv_to_local; - let send_timestamp = self.get_timestamp() as i64 + cnv_to_local; + let sort_timestamp = self.get_sort_timestamp() + cnv_to_local; + let send_timestamp = self.get_timestamp() + cnv_to_local; sort_timestamp / 86400 != send_timestamp / 86400 } + /// Returns true if the message was successfully delivered to the outgoing server or even + /// received a read receipt. pub fn is_sent(&self) -> bool { - self.state as i32 >= MessageState::OutDelivered as i32 + self.state >= MessageState::OutDelivered } + /// Returns true if the message is a forwarded message. pub fn is_forwarded(&self) -> bool { 0 != self.param.get_int(Param::Forwarded).unwrap_or_default() } + /// Returns true if the message is an informational message. pub fn is_info(&self) -> bool { let cmd = self.param.get_cmd(); self.from_id == ContactId::INFO @@ -598,10 +644,12 @@ impl Message { || cmd != SystemMessage::Unknown && cmd != SystemMessage::AutocryptSetupMessage } + /// Returns the type of an informational message. pub fn get_info_type(&self) -> SystemMessage { self.param.get_cmd() } + /// Returns true if the message is a system message. pub fn is_system_message(&self) -> bool { let cmd = self.param.get_cmd(); cmd != SystemMessage::Unknown @@ -619,6 +667,7 @@ impl Message { self.viewtype.has_file() && self.state == MessageState::OutPreparing } + /// Returns true if the message is an Autocrypt Setup Message. pub fn is_setupmessage(&self) -> bool { if self.viewtype != Viewtype::File { return false; @@ -627,6 +676,9 @@ impl Message { self.param.get_cmd() == SystemMessage::AutocryptSetupMessage } + /// Returns the first characters of the setup code. + /// + /// This is used to pre-fill the first entry field of the setup code. pub async fn get_setupcodebegin(&self, context: &Context) -> Option { if !self.is_setupmessage() { return None; @@ -647,7 +699,7 @@ impl Message { // add room to a webrtc_instance as defined by the corresponding config-value; // the result may still be prefixed by the type - pub fn create_webrtc_instance(instance: &str, room: &str) -> String { + pub(crate) fn create_webrtc_instance(instance: &str, room: &str) -> String { let (videochat_type, mut url) = Message::parse_webrtc_instance(instance); // make sure, there is a scheme in the url @@ -704,6 +756,7 @@ impl Message { } } + /// Returns videochat URL if the message is a videochat invitation. pub fn get_videochat_url(&self) -> Option { if self.viewtype == Viewtype::VideochatInvitation { if let Some(instance) = self.param.get(Param::WebrtcRoom) { @@ -713,6 +766,7 @@ impl Message { None } + /// Returns videochat type if the message is a videochat invitation. pub fn get_videochat_type(&self) -> Option { if self.viewtype == Viewtype::VideochatInvitation { if let Some(instance) = self.param.get(Param::WebrtcRoom) { @@ -722,10 +776,16 @@ impl Message { None } + /// Sets or unsets message text. pub fn set_text(&mut self, text: Option) { self.text = text; } + /// Sets the file associated with a message. + /// + /// This function does not use the file or check if it exists, + /// the file will only be used when the message is prepared + /// for sending. pub fn set_file(&mut self, file: impl ToString, filemime: Option<&str>) { self.param.set(Param::File, file); if let Some(filemime) = filemime { @@ -743,11 +803,13 @@ impl Message { } } + /// Sets the dimensions of associated image or video file. pub fn set_dimension(&mut self, width: i32, height: i32) { self.param.set_int(Param::Width, width); self.param.set_int(Param::Height, height); } + /// Sets the duration of associated audio or video file. pub fn set_duration(&mut self, duration: i32) { self.param.set_int(Param::Duration, duration); } @@ -757,6 +819,8 @@ impl Message { self.param.set_int(Param::Reaction, 1); } + /// Changes the message width, height or duration, + /// and stores it into the database. pub async fn latefiling_mediasize( &mut self, context: &Context, @@ -821,10 +885,12 @@ impl Message { Ok(()) } + /// Returns quoted message text, if any. pub fn quoted_text(&self) -> Option { self.param.get(Param::Quote).map(|s| s.to_string()) } + /// Returns quoted message, if any. pub async fn quoted_message(&self, context: &Context) -> Result> { if self.param.get(Param::Quote).is_some() && !self.is_forwarded() { return self.parent(context).await; @@ -832,6 +898,10 @@ impl Message { Ok(None) } + /// Returns parent message according to the `In-Reply-To` header + /// if it exists in the database and is not trashed. + /// + /// `References` header is not taken into account. pub async fn parent(&self, context: &Context) -> Result> { if let Some(in_reply_to) = &self.in_reply_to { if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? { @@ -852,6 +922,7 @@ impl Message { self.param.set_int(Param::ForcePlaintext, 1); } + /// Updates `param` column of the message in the database without changing other columns. pub async fn update_param(&self, context: &Context) -> Result<()> { context .sql @@ -891,12 +962,17 @@ impl Message { } } +/// State of the message. +/// For incoming messages, stores the information on whether the message was read or not. +/// For outgoing message, the message could be pending, already delivered or confirmed. #[derive( Debug, Clone, Copy, PartialEq, Eq, + PartialOrd, + Ord, FromPrimitive, ToPrimitive, ToSql, @@ -906,6 +982,7 @@ impl Message { )] #[repr(u32)] pub enum MessageState { + /// Undefined message state. Undefined = 0, /// Incoming *fresh* message. Fresh messages are neither noticed @@ -976,6 +1053,7 @@ impl std::fmt::Display for MessageState { } impl MessageState { + /// Returns true if the message can transition to `OutFailed` state from the current state. pub fn can_fail(self) -> bool { use MessageState::*; matches!( @@ -983,6 +1061,8 @@ impl MessageState { OutPreparing | OutPending | OutDelivered | OutMdnRcvd // OutMdnRcvd can still fail because it could be a group message and only some recipients failed. ) } + + /// Returns true for any outgoing message states. pub fn is_outgoing(self) -> bool { use MessageState::*; matches!( @@ -992,6 +1072,7 @@ impl MessageState { } } +/// Returns detailed message information in a multi-line text form. pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { let msg = Message::load_from_db(context, msg_id).await?; let rawtxt: Option = context @@ -1098,8 +1179,8 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { } if let Some(path) = msg.get_file(context) { - let bytes = get_filebytes(context, &path).await; - ret += &format!("\nFile: {}, {}, bytes\n", path.display(), bytes); + let bytes = get_filebytes(context, &path).await?; + ret += &format!("\nFile: {}, {} bytes\n", path.display(), bytes); } if msg.viewtype != Viewtype::Text { @@ -1156,7 +1237,7 @@ pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { Ok(ret) } -pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { +pub(crate) fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { let extension: &str = &path.extension()?.to_str()?.to_lowercase(); let info = match extension { // before using viewtype other than Viewtype::File, @@ -1166,6 +1247,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "3gp" => (Viewtype::Video, "video/3gpp"), "aac" => (Viewtype::Audio, "audio/aac"), "avi" => (Viewtype::Video, "video/x-msvideo"), + "avif" => (Viewtype::File, "image/avif"), // supported since Android 12 / iOS 16 "doc" => (Viewtype::File, "application/msword"), "docx" => ( Viewtype::File, @@ -1174,6 +1256,8 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "epub" => (Viewtype::File, "application/epub+zip"), "flac" => (Viewtype::Audio, "audio/flac"), "gif" => (Viewtype::Gif, "image/gif"), + "heic" => (Viewtype::File, "image/heic"), // supported since Android 10 / iOS 11 + "heif" => (Viewtype::File, "image/heif"), // supported since Android 10 / iOS 11 "html" => (Viewtype::File, "text/html"), "htm" => (Viewtype::File, "text/html"), "ico" => (Viewtype::File, "image/vnd.microsoft.icon"), @@ -1198,10 +1282,15 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "oga" => (Viewtype::Audio, "audio/ogg"), "ogg" => (Viewtype::Audio, "audio/ogg"), "ogv" => (Viewtype::File, "video/ogg"), - "opus" => (Viewtype::File, "audio/ogg"), // not supported eg. on Android 4 + "opus" => (Viewtype::File, "audio/ogg"), // supported since Android 10 "otf" => (Viewtype::File, "font/otf"), "pdf" => (Viewtype::File, "application/pdf"), "png" => (Viewtype::Image, "image/png"), + "ppt" => (Viewtype::File, "application/vnd.ms-powerpoint"), + "pptx" => ( + Viewtype::File, + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ), "rar" => (Viewtype::File, "application/vnd.rar"), "rtf" => (Viewtype::File, "application/rtf"), "spx" => (Viewtype::File, "audio/ogg"), // Ogg Speex Profile @@ -1210,6 +1299,7 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "tiff" => (Viewtype::File, "image/tiff"), "tif" => (Viewtype::File, "image/tiff"), "ttf" => (Viewtype::File, "font/ttf"), + "txt" => (Viewtype::File, "text/plain"), "vcard" => (Viewtype::File, "text/vcard"), "vcf" => (Viewtype::File, "text/vcard"), "wav" => (Viewtype::File, "audio/wav"), @@ -1219,11 +1309,12 @@ pub fn guess_msgtype_from_suffix(path: &Path) -> Option<(Viewtype, &str)> { "wmv" => (Viewtype::Video, "video/x-ms-wmv"), "xdc" => (Viewtype::Webxdc, "application/webxdc+zip"), "xhtml" => (Viewtype::File, "application/xhtml+xml"), + "xls" => (Viewtype::File, "application/vnd.ms-excel"), "xlsx" => ( Viewtype::File, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ), - "xml" => (Viewtype::File, "application/vnd.ms-excel"), + "xml" => (Viewtype::File, "application/xml"), "zip" => (Viewtype::File, "application/zip"), _ => { return None; @@ -1259,6 +1350,9 @@ pub async fn get_mime_headers(context: &Context, msg_id: MsgId) -> Result Result<()> { for msg_id in msg_ids.iter() { let msg = Message::load_from_db(context, *msg_id).await?; @@ -1306,6 +1400,7 @@ async fn delete_poi_location(context: &Context, location_id: u32) -> Result<()> Ok(()) } +/// Marks requested messages as seen. pub async fn markseen_msgs(context: &Context, msg_ids: Vec) -> Result<()> { if msg_ids.is_empty() { return Ok(()); @@ -1438,7 +1533,8 @@ pub(crate) async fn update_msg_state( // Context functions to work with messages -pub async fn exists(context: &Context, msg_id: MsgId) -> Result { +/// Returns true if given message ID exists in the database and is not trashed. +pub(crate) async fn exists(context: &Context, msg_id: MsgId) -> Result { if msg_id.is_special() { return Ok(false); } @@ -1455,7 +1551,7 @@ pub async fn exists(context: &Context, msg_id: MsgId) -> Result { } } -pub async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) { +pub(crate) async fn set_msg_failed(context: &Context, msg_id: MsgId, error: &str) { if let Ok(mut msg) = Message::load_from_db(context, msg_id).await { if msg.state.can_fail() { msg.state = MessageState::OutFailed; @@ -1675,7 +1771,7 @@ pub async fn get_unblocked_msg_cnt(context: &Context) -> usize { { Ok(res) => res, Err(err) => { - error!(context, "get_unblocked_msg_cnt() failed. {}", err); + error!(context, "get_unblocked_msg_cnt() failed. {:#}", err); 0 } } @@ -1695,12 +1791,26 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize { { Ok(res) => res, Err(err) => { - error!(context, "get_request_msg_cnt() failed. {}", err); + error!(context, "get_request_msg_cnt() failed. {:#}", err); 0 } } } +/// Estimates the number of messages that will be deleted +/// by the options `delete_device_after` or `delete_server_after`. +/// This is typically used to show the estimated impact to the user +/// before actually enabling deletion of old messages. +/// +/// If `from_server` is true, +/// estimate deletion count for server, +/// otherwise estimate deletion count for device. +/// +/// Count messages older than the given number of `seconds`. +/// +/// Returns the number of messages that are older than the given number of seconds. +/// This includes e-mails downloaded due to the `show_emails` option. +/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically. pub async fn estimate_deletion_cnt( context: &Context, from_server: bool, @@ -1789,6 +1899,7 @@ pub(crate) async fn rfc724_mid_exists( )] #[repr(u32)] pub enum Viewtype { + /// Unknown message type. Unknown = 0, /// Text message. @@ -1875,7 +1986,7 @@ mod tests { use crate::chatlist::Chatlist; use crate::receive_imf::receive_imf; use crate::test_utils as test; - use crate::test_utils::TestContext; + use crate::test_utils::{TestContext, TestContextManager}; use super::*; @@ -2338,4 +2449,57 @@ mod tests { ); assert_eq!(Viewtype::Webxdc, Viewtype::from_i32(80).unwrap()); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_send_quotes() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat = alice.create_chat(&bob).await; + + let sent = alice.send_text(chat.id, "> First quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some("> First quote")); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + let sent = alice.send_text(chat.id, "> Second quote").await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some("> Second quote")); + assert!(received.quoted_text().is_none()); + assert!(received.quoted_message(&bob).await?.is_none()); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_format_flowed_round_trip() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let chat = alice.create_chat(&bob).await; + + let text = " Foo bar"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let text = "Foo bar baz"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let text = "> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx > A"; + let sent = alice.send_text(chat.id, text).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(text)); + + let python_program = "\ +def hello(): + return 'Hello, world!'"; + let sent = alice.send_text(chat.id, python_program).await; + let received = bob.recv_msg(&sent).await; + assert_eq!(received.text.as_deref(), Some(python_program)); + + Ok(()) + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 5f6f33adb..bd198329d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -4,6 +4,7 @@ use std::convert::TryInto; use anyhow::{bail, ensure, Context as _, Result}; use chrono::TimeZone; +use format_flowed::{format_flowed, format_flowed_quote}; use lettre_email::{mime, Address, Header, MimeMultipartType, PartBuilder}; use tokio::fs; @@ -15,7 +16,6 @@ use crate::contact::Contact; use crate::context::{get_version_str, Context}; use crate::e2ee::EncryptHelper; use crate::ephemeral::Timer as EphemeralTimer; -use crate::format_flowed::{format_flowed, format_flowed_quote}; use crate::html::new_html_mimepart; use crate::location; use crate::message::{self, Message, MsgId, Viewtype}; @@ -76,6 +76,7 @@ pub struct MimeFactory<'a> { /// and must be deleted if the message is actually queued for sending. sync_ids_to_delete: Option, + /// True if the avatar should be attached. attach_selfavatar: bool, } @@ -689,7 +690,9 @@ impl<'a> MimeFactory<'a> { .fold(message, |message, header| message.header(header)); // Add gossip headers in chats with multiple recipients - if peerstates.len() > 1 && self.should_do_gossip(context).await? { + if (peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?) + && self.should_do_gossip(context).await? + { for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) { if peerstate.peek_key(min_verified).is_some() { if let Some(header) = peerstate.render_gossip_header(min_verified) { @@ -722,9 +725,11 @@ impl<'a> MimeFactory<'a> { )); if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "mimefactory: outgoing message mime:"); - let raw_message = message.clone().build().as_string(); - println!("{}", raw_message); + info!( + context, + "mimefactory: unencrypted message mime-body:\n{}", + message.clone().build().as_string(), + ); } let encrypted = encrypt_helper @@ -782,6 +787,14 @@ impl<'a> MimeFactory<'a> { .into_iter() .fold(outer_message, |message, header| message.header(header)); + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!( + context, + "mimefactory: outgoing message mime-body:\n{}", + outer_message.clone().build().as_string(), + ); + } + let MimeFactory { last_added_location_id, .. @@ -799,6 +812,7 @@ impl<'a> MimeFactory<'a> { }) } + /// Returns MIME part with a `message.kml` attachment. fn get_message_kml_part(&self) -> Option { let latitude = self.msg.param.get_float(Param::SetLatitude)?; let longitude = self.msg.param.get_float(Param::SetLongitude)?; @@ -818,6 +832,7 @@ impl<'a> MimeFactory<'a> { Some(part) } + /// Returns MIME part with a `location.kml` attachment. async fn get_location_kml_part(&mut self, context: &Context) -> Result { let (kml_content, last_added_location_id) = location::get_kml(context, self.msg.chat_id).await?; @@ -903,6 +918,17 @@ impl<'a> MimeFactory<'a> { "Secure-Join".to_string(), "vg-member-added".to_string(), )); + // FIXME: Old clients require Secure-Join-Fingerprint header. Remove this + // eventually. + let fingerprint = Peerstate::from_addr(context, email_to_add) + .await? + .context("No peerstate found in db")? + .public_key_fingerprint + .context("No public key fingerprint in db for the member to add")?; + headers.protected.push(Header::new( + "Secure-Join-Fingerprint".into(), + fingerprint.hex(), + )); } } SystemMessage::GroupNameChanged => { @@ -1430,7 +1456,7 @@ fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool async fn is_file_size_okay(context: &Context, msg: &Message) -> Result { match msg.param.get_path(Param::File, context)? { Some(path) => { - let bytes = get_filebytes(context, &path).await; + let bytes = get_filebytes(context, &path).await?; Ok(bytes <= UPPER_LIMIT_FILE_SIZE) } None => Ok(false), @@ -1488,7 +1514,7 @@ mod tests { ProtectionStatus, }; use crate::chatlist::Chatlist; - use crate::contact::Origin; + use crate::contact::{ContactAddress, Origin}; use crate::mimeparser::MimeMessage; use crate::receive_imf::receive_imf; use crate::test_utils::{get_chat_msg, TestContext}; @@ -1815,11 +1841,15 @@ mod tests { } async fn first_subject_str(t: TestContext) -> String { - let contact_id = - Contact::add_or_lookup(&t, "Dave", "dave@example.com", Origin::ManuallyCreated) - .await - .unwrap() - .0; + let contact_id = Contact::add_or_lookup( + &t, + "Dave", + ContactAddress::new("dave@example.com").unwrap(), + Origin::ManuallyCreated, + ) + .await + .unwrap() + .0; let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 2e811ebb9..34318a143 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -1,26 +1,33 @@ //! # MIME message parsing module. +#![allow(missing_docs)] + use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; +use std::str; use anyhow::{bail, Context as _, Result}; use deltachat_derive::{FromSql, ToSql}; +use format_flowed::unformat_flowed; use lettre_email::mime::{self, Mime}; use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo}; use once_cell::sync::Lazy; -use crate::aheader::Aheader; +use crate::aheader::{Aheader, EncryptPreference}; use crate::blob::BlobObject; use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN}; use crate::contact::{addr_cmp, addr_normalize, ContactId}; use crate::context::Context; -use crate::decrypt::{prepare_decryption, try_decrypt}; +use crate::decrypt::{ + keyring_from_peerstate, prepare_decryption, try_decrypt, validate_detached_signature, + DecryptionInfo, +}; use crate::dehtml::dehtml; use crate::events::EventType; -use crate::format_flowed::unformat_flowed; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::key::Fingerprint; +use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; +use crate::keyring::Keyring; use crate::message::{self, Viewtype}; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; @@ -52,6 +59,7 @@ pub struct MimeMessage { pub from_is_signed: bool, pub list_post: Option, pub chat_disposition_notification_to: Option, + pub decryption_info: DecryptionInfo, pub decrypting_failed: bool, /// Set of valid signature fingerprints if a message is an @@ -60,7 +68,6 @@ pub struct MimeMessage { /// If a message is not encrypted or the signature is not valid, /// this set is empty. pub signatures: HashSet, - /// The set of mail recipient addresses for which gossip headers were applied, regardless of /// whether they modified any peerstates. pub gossiped_addr: HashSet, @@ -216,16 +223,12 @@ impl MimeMessage { headers.remove("secure-join-fingerprint"); headers.remove("chat-verified"); - let is_thunderbird = headers - .get("user-agent") - .map_or(false, |user_agent| user_agent.contains("Thunderbird")); - if is_thunderbird { - info!(context, "Detected Thunderbird"); - } - let from = from.context("No from in message")?; + let private_keyring: Keyring = Keyring::new_self(context) + .await + .context("failed to get own keyring")?; let mut decryption_info = - prepare_decryption(context, &mail, &from.addr, message_time, is_thunderbird).await?; + prepare_decryption(context, &mail, &from.addr, message_time).await?; // Memory location for a possible decrypted message. let mut mail_raw = Vec::new(); @@ -234,92 +237,103 @@ impl MimeMessage { hop_info += "\n\n"; hop_info += &decryption_info.dkim_results.to_string(); - // `signatures` is non-empty exactly if the message was encrypted and correctly signed. - let (mail, signatures, warn_empty_signature) = - match try_decrypt(context, &mail, &decryption_info).await { + let public_keyring = keyring_from_peerstate(decryption_info.peerstate.as_ref()); + let (mail, mut signatures, encrypted) = + match try_decrypt(context, &mail, &private_keyring, &public_keyring) { Ok(Some((raw, signatures))) => { - // Encrypted, but maybe unsigned message. Only if - // `signatures` set is non-empty, it is a valid - // autocrypt message. - mail_raw = raw; let decrypted_mail = mailparse::parse_mail(&mail_raw)?; if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { - info!(context, "decrypted message mime-body:"); - println!("{}", String::from_utf8_lossy(&mail_raw)); + info!( + context, + "decrypted message mime-body:\n{}", + String::from_utf8_lossy(&mail_raw), + ); } - - // Handle any gossip headers if the mail was encrypted. See section - // "3.6 Key Gossip" of - // but only if the mail was correctly signed: - if !signatures.is_empty() { - let gossip_headers = - decrypted_mail.headers.get_all_values("Autocrypt-Gossip"); - gossiped_addr = - update_gossip_peerstates(context, message_time, &mail, gossip_headers) - .await?; - } - - // let known protected headers from the decrypted - // part override the unencrypted top-level - - // Signature was checked for original From, so we - // do not allow overriding it. - let mut signed_from = None; - - // We do not want to allow unencrypted subject in encrypted emails because the user might falsely think that the subject is safe. - // See . - headers.remove("subject"); - - MimeMessage::merge_headers( - context, - &mut headers, - &mut recipients, - &mut signed_from, - &mut list_post, - &mut chat_disposition_notification_to, - &decrypted_mail.headers, - ); - if let Some(signed_from) = signed_from { - if addr_cmp(&signed_from.addr, &from.addr) { - from_is_signed = true; - } else { - // There is a From: header in the encrypted & - // signed part, but it doesn't match the outer one. - // This _might_ be because the sender's mail server - // replaced the sending address, e.g. in a mailing list. - // Or it's because someone is doing some replay attack - // - OTOH, I can't come up with an attack scenario - // where this would be useful. - warn!( - context, - "From header in signed part does't match the outer one" - ); - } - } - (Ok(decrypted_mail), signatures, true) } - Ok(None) => { - // Message was not encrypted. - // If it is not a read receipt, degrade encryption. - if let Some(peerstate) = &mut decryption_info.peerstate { - if message_time > peerstate.last_seen_autocrypt - && mail.ctype.mimetype != "multipart/report" - // Disallowing keychanges is disabled for now: - // && decryption_info.dkim_results.allow_keychange - { - peerstate.degrade_encryption(message_time); - peerstate.save_to_db(&context.sql).await?; - } - } - (Ok(mail), HashSet::new(), false) - } + Ok(None) => (Ok(mail), HashSet::new(), false), Err(err) => { - warn!(context, "decryption failed: {}", err); - (Err(err), HashSet::new(), true) + warn!(context, "decryption failed: {:#}", err); + (Err(err), HashSet::new(), false) } }; + let mail = mail.as_ref().map(|mail| { + let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring) + .unwrap_or((mail, Default::default())); + signatures.extend(signatures_detached); + content + }); + if let (Ok(mail), true) = (mail, encrypted) { + // Handle any gossip headers if the mail was encrypted. See section + // "3.6 Key Gossip" of + // but only if the mail was correctly signed: + if !signatures.is_empty() { + let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip"); + gossiped_addr = update_gossip_peerstates( + context, + message_time, + &from.addr, + &recipients, + gossip_headers, + ) + .await?; + } + + // let known protected headers from the decrypted + // part override the unencrypted top-level + + // Signature was checked for original From, so we + // do not allow overriding it. + let mut signed_from = None; + + // We do not want to allow unencrypted subject in encrypted emails because the + // user might falsely think that the subject is safe. + // See . + headers.remove("subject"); + + MimeMessage::merge_headers( + context, + &mut headers, + &mut recipients, + &mut signed_from, + &mut list_post, + &mut chat_disposition_notification_to, + &mail.headers, + ); + if let Some(signed_from) = signed_from { + if addr_cmp(&signed_from.addr, &from.addr) { + from_is_signed = true; + } else { + // There is a From: header in the encrypted & + // signed part, but it doesn't match the outer one. + // This _might_ be because the sender's mail server + // replaced the sending address, e.g. in a mailing list. + // Or it's because someone is doing some replay attack + // - OTOH, I can't come up with an attack scenario + // where this would be useful. + warn!( + context, + "From header in signed part does't match the outer one", + ); + } + } + } + if signatures.is_empty() { + // If it is not a read receipt, degrade encryption. + if let (Some(peerstate), Ok(mail)) = (&mut decryption_info.peerstate, mail) { + if message_time > peerstate.last_seen_autocrypt + && mail.ctype.mimetype != "multipart/report" + // Disallowing keychanges is disabled for now: + // && decryption_info.dkim_results.allow_keychange + { + peerstate.degrade_encryption(message_time); + } + } + } + if !encrypted { + signatures.clear(); + } let mut parser = MimeMessage { parts: Vec::new(), @@ -329,6 +343,7 @@ impl MimeMessage { from, from_is_signed, chat_disposition_notification_to, + decryption_info, decrypting_failed: mail.is_err(), // only non-empty if it was a valid autocrypt message @@ -358,7 +373,7 @@ impl MimeMessage { } None => match mail { Ok(mail) => { - parser.parse_mime_recursive(context, &mail, false).await?; + parser.parse_mime_recursive(context, mail, false).await?; } Err(err) => { let msg_body = stock_str::cant_decrypt_msg_body(context).await; @@ -368,7 +383,7 @@ impl MimeMessage { typ: Viewtype::Text, msg_raw: Some(txt.clone()), msg: txt, - error: Some(format!("Decrypting failed: {}", err)), + error: Some(format!("Decrypting failed: {:#}", err)), ..Default::default() }; parser.parts.push(part); @@ -387,21 +402,23 @@ impl MimeMessage { // part.error = Some("Seems like DKIM failed, this either is an attack or (more likely) a bug in Authentication-Results checking. Please tell us about this at https://support.delta.chat.".to_string()); // } // } - if warn_empty_signature && parser.signatures.is_empty() { - for part in parser.parts.iter_mut() { - part.error = Some("No valid signature".to_string()); - } - } if parser.is_mime_modified { parser.decoded_data = mail_raw; } - crate::peerstate::maybe_do_aeap_transition(context, &mut decryption_info, &parser).await?; - if let Some(peerstate) = decryption_info.peerstate { + crate::peerstate::maybe_do_aeap_transition(context, &mut parser).await?; + if let Some(peerstate) = &parser.decryption_info.peerstate { peerstate .handle_fingerprint_change(context, message_time) .await?; + // When peerstate is set to Mutual, it's saved immediately to not lose that fact in case + // of an error. Otherwise we don't save peerstate until get here to reduce the number of + // calls to save_to_db() and not to degrade encryption if a mail wasn't parsed + // successfully. + if peerstate.prefer_encrypt != EncryptPreference::Mutual { + peerstate.save_to_db(&context.sql).await?; + } } Ok(parser) @@ -666,7 +683,7 @@ impl MimeMessage { Err(err) => { warn!( context, - "Could not save decoded avatar to blob file: {}", err + "Could not save decoded avatar to blob file: {:#}", err ); None } @@ -957,7 +974,7 @@ impl MimeMessage { &filename, is_related, ) - .await; + .await?; } None => { match mime_type.type_() { @@ -973,7 +990,7 @@ impl MimeMessage { let decoded_data = match mail.get_body() { Ok(decoded_data) => decoded_data, Err(err) => { - warn!(context, "Invalid body parsed {:?}", err); + warn!(context, "Invalid body parsed {:#}", err); // Note that it's not always an error - might be no data return Ok(false); } @@ -993,7 +1010,7 @@ impl MimeMessage { let decoded_data = match mail.get_body() { Ok(decoded_data) => decoded_data, Err(err) => { - warn!(context, "Invalid body parsed {:?}", err); + warn!(context, "Invalid body parsed {:#}", err); // Note that it's not always an error - might be no data return Ok(false); } @@ -1100,9 +1117,18 @@ impl MimeMessage { decoded_data: &[u8], filename: &str, is_related: bool, - ) { + ) -> Result<()> { if decoded_data.is_empty() { - return; + return Ok(()); + } + if let Some(peerstate) = &mut self.decryption_info.peerstate { + if peerstate.prefer_encrypt != EncryptPreference::Mutual + && mime_type.type_() == mime::APPLICATION + && mime_type.subtype().as_str() == "pgp-keys" + && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await? + { + return Ok(()); + } } let msg_type = if context .is_webxdc_file(filename, decoded_data) @@ -1116,7 +1142,7 @@ impl MimeMessage { if filename.starts_with("location") || filename.starts_with("message") { let parsed = location::Kml::parse(decoded_data) .map_err(|err| { - warn!(context, "failed to parse kml part: {}", err); + warn!(context, "failed to parse kml part: {:#}", err); }) .ok(); if filename.starts_with("location") { @@ -1124,7 +1150,7 @@ impl MimeMessage { } else { self.message_kml = parsed; } - return; + return Ok(()); } msg_type } else if filename == "multi-device-sync.json" { @@ -1134,16 +1160,16 @@ impl MimeMessage { self.sync_items = context .parse_sync_items(serialized) .map_err(|err| { - warn!(context, "failed to parse sync data: {}", err); + warn!(context, "failed to parse sync data: {:#}", err); }) .ok(); - return; + return Ok(()); } else if filename == "status-update.json" { let serialized = String::from_utf8_lossy(decoded_data) .parse() .unwrap_or_default(); self.webxdc_status_update = Some(serialized); - return; + return Ok(()); } else { msg_type }; @@ -1156,9 +1182,9 @@ impl MimeMessage { Err(err) => { error!( context, - "Could not add blob for mime part {}, error {}", filename, err + "Could not add blob for mime part {}, error {:#}", filename, err ); - return; + return Ok(()); } }; info!(context, "added blobfile: {:?}", blob.as_name()); @@ -1181,6 +1207,66 @@ impl MimeMessage { part.is_related = is_related; self.do_add_single_part(part); + Ok(()) + } + + /// Returns whether a key from the attachment was set as peer's pubkey. + async fn try_set_peer_key_from_file_part( + context: &Context, + peerstate: &mut Peerstate, + decoded_data: &[u8], + ) -> Result { + let key = match str::from_utf8(decoded_data) { + Err(err) => { + warn!(context, "PGP key attachment is not a UTF-8 file: {}", err); + return Ok(false); + } + Ok(key) => key, + }; + let key = match SignedPublicKey::from_asc(key) { + Err(err) => { + warn!( + context, + "PGP key attachment is not an ASCII-armored file: {:#}", err + ); + return Ok(false); + } + Ok((key, _)) => key, + }; + if let Err(err) = key.verify() { + warn!(context, "attached PGP key verification failed: {}", err); + return Ok(false); + } + if !key.details.users.iter().any(|user| { + user.id + .id() + .ends_with(&(String::from("<") + &peerstate.addr + ">")) + }) { + return Ok(false); + } + if let Some(curr_key) = &peerstate.public_key { + if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset { + // We don't want to break the existing Autocrypt setup. Yes, it's unlikely that a + // user have an Autocrypt-capable MUA and also attaches a key, but if that's the + // case, let 'em first disable Autocrypt and then change the key by attaching it. + warn!( + context, + "not using attached PGP key for peer '{}' because another one is already set \ + with prefer-encrypt={}", + peerstate.addr, + peerstate.prefer_encrypt, + ); + return Ok(false); + } + } + peerstate.public_key = Some(key); + info!( + context, + "using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr, + ); + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await?; + Ok(true) } fn do_add_single_part(&mut self, mut part: Part) { @@ -1552,12 +1638,15 @@ impl MimeMessage { } /// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates. +/// Params: +/// from: The address which sent the message currently beeing parsed /// /// Returns the set of mail recipient addresses for which valid gossip headers were found. async fn update_gossip_peerstates( context: &Context, message_time: i64, - mail: &mailparse::ParsedMail<'_>, + from: &str, + recipients: &[SingleInfo], gossip_headers: Vec, ) -> Result> { // XXX split the parsing from the modification part @@ -1572,9 +1661,9 @@ async fn update_gossip_peerstates( } }; - if !get_recipients(&mail.headers) + if !recipients .iter() - .any(|info| info.addr == header.addr.to_lowercase()) + .any(|info| addr_cmp(&info.addr, &header.addr)) { warn!( context, @@ -1582,6 +1671,14 @@ async fn update_gossip_peerstates( ); continue; } + if addr_cmp(from, &header.addr) { + // Non-standard, but anyway we can't update the cached peerstate here. + warn!( + context, + "Ignoring gossiped \"{}\" as it equals the From address", &header.addr, + ); + continue; + } let peerstate; if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? { @@ -1936,7 +2033,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_crash() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -1948,7 +2045,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_rfc724_mid_exists() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/mail_with_message_id.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -1962,7 +2059,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_rfc724_mid_not_exists() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/issue_523.txt"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -2168,7 +2265,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_get_parent_timestamp() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"From: foo@example.org\n\ Content-Type: text/plain\n\ Chat-Version: 1.0\n\ @@ -2201,7 +2298,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_context() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"From: hello@example.org\n\ Content-Type: multipart/mixed; boundary=\"==break==\";\n\ Subject: outer-subject\n\ @@ -2253,7 +2350,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_avatars() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/mail_attach_txt.eml"); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap(); @@ -2294,7 +2391,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_with_videochat() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/videochat_invitation.eml"); let mimeparser = MimeMessage::from_bytes(&t, &raw[..]).await.unwrap(); @@ -2316,7 +2413,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mimeparser_message_kml() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Chat-Version: 1.0\n\ From: foo \n\ To: bar \n\ @@ -2361,7 +2458,7 @@ Content-Disposition: attachment; filename=\"message.kml\"\n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_mdn() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2411,7 +2508,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ /// multipart MIME messages. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_multiple_mdns() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2487,7 +2584,7 @@ Disposition: manual-action/MDN-sent-automatically; displayed\n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_mdn_with_additional_message_ids() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = b"Subject: =?utf-8?q?Chat=3A_Message_opened?=\n\ Date: Mon, 10 Jan 2020 00:00:00 +0000\n\ Chat-Version: 1.0\n\ @@ -2542,7 +2639,7 @@ Additional-Message-IDs: \n\ #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_parse_inline_attachment() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) From: sender@example.com To: receiver@example.com @@ -2582,7 +2679,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_hide_html_without_content() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = br#"Date: Thu, 13 Feb 2020 22:41:20 +0000 (UTC) From: sender@example.com To: receiver@example.com @@ -2631,7 +2728,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg== #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_inline_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"Message-ID: From: foo Subject: example @@ -2677,7 +2774,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_thunderbird_html_embedded_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br#"To: Alice From: Bob Subject: Test subject @@ -2750,7 +2847,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= // Outlook specifies filename in the "name" attribute of Content-Type #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_outlook_html_embedded_image() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"From: Anonymous To: Anonymous Subject: Delta Chat is great stuff! @@ -2889,7 +2986,7 @@ CWt6wx7fiLp0qS9RrX75g6Gqw7nfCs6EcBERcIPt7DTe8VStJwf3LWqVwxl4gQl46yhfoqwEO+I= #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_format_flowed_quote() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: swipe-to-reply MIME-Version: 1.0 @@ -2925,7 +3022,7 @@ Reply #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_quote_without_reply() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: swipe-to-reply MIME-Version: 1.0 @@ -2957,7 +3054,7 @@ From: alice #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn parse_quote_top_posting() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = br##"Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no Subject: Re: top posting MIME-Version: 1.0 @@ -2988,7 +3085,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_attachment_quote() { - let context = TestContext::new().await; + let context = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/quote_attach.eml"); let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) .await @@ -3006,7 +3103,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_quote_div() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/gmx-quote.eml"); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap(); assert_eq!(mimeparser.parts[0].msg, "YIPPEEEEEE\n\nMulti-line"); @@ -3016,7 +3113,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_allinkl_blockquote() { // all-inkl.com puts quotes into `
`. - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/allinkl-quote.eml"); let mimeparser = MimeMessage::from_bytes(&t, raw).await.unwrap(); assert!(mimeparser.parts[0].msg.starts_with("It's 1.0.")); @@ -3051,7 +3148,7 @@ On 2020-10-25, Bob wrote: assert_eq!(msg.is_dc_message, MessengerMessage::No); assert_eq!(msg.chat_blocked, Blocked::Request); assert_eq!(msg.state, MessageState::InFresh); - assert_eq!(msg.get_filebytes(&t).await, 2115); + assert_eq!(msg.get_filebytes(&t).await.unwrap().unwrap(), 2115); assert!(msg.get_file(&t).is_some()); assert_eq!(msg.get_filename().unwrap(), "avatar64x64.png"); assert_eq!(msg.get_width(), 64); @@ -3061,7 +3158,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_plain_unspecified.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(!mimeparser.is_mime_modified); @@ -3073,7 +3170,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_plain_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3085,7 +3182,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_plain.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(!mimeparser.is_mime_modified); @@ -3100,7 +3197,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_alt_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_alt_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3112,7 +3209,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_html() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let raw = include_bytes!("../test-data/message/text_html.eml"); let mimeparser = MimeMessage::from_bytes(&t.ctx, raw).await.unwrap(); assert!(mimeparser.is_mime_modified); @@ -3124,7 +3221,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_mime_modified_large_plain() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; static REPEAT_TXT: &str = "this text with 42 chars is just repeated.\n"; static REPEAT_CNT: usize = 2000; // results in a text of 84k, should be more than DC_DESIRED_TEXT_LEN @@ -3145,7 +3242,7 @@ On 2020-10-25, Bob wrote: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_x_microsoft_original_message_id() { - let t = TestContext::new().await; + let t = TestContext::new_alice().await; let message = MimeMessage::from_bytes(&t, b"Date: Wed, 17 Feb 2021 15:45:15 +0000\n\ Chat-Version: 1.0\n\ Message-ID: \n\ diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 000000000..6c0c7dd0b --- /dev/null +++ b/src/net.rs @@ -0,0 +1,33 @@ +///! # Common network utilities. +use std::pin::Pin; +use std::time::Duration; + +use anyhow::{Context as _, Result}; +use tokio::net::{TcpStream, ToSocketAddrs}; +use tokio::time::timeout; +use tokio_io_timeout::TimeoutStream; + +/// Returns a TCP connection stream with read/write timeouts set +/// and Nagle's algorithm disabled with `TCP_NODELAY`. +/// +/// `TCP_NODELAY` ensures writing to the stream always results in immediate sending of the packet +/// to the network, which is important to reduce the latency of interactive protocols such as IMAP. +pub(crate) async fn connect_tcp( + addr: impl ToSocketAddrs, + timeout_val: Duration, +) -> Result>>> { + let tcp_stream = timeout(timeout_val, TcpStream::connect(addr)) + .await + .context("connection timeout")? + .context("connection failure")?; + + // Disable Nagle's algorithm. + tcp_stream.set_nodelay(true)?; + + let mut timeout_stream = TimeoutStream::new(tcp_stream); + timeout_stream.set_write_timeout(Some(timeout_val)); + timeout_stream.set_read_timeout(Some(timeout_val)); + let pinned_stream = Box::pin(timeout_stream); + + Ok(pinned_stream) +} diff --git a/src/oauth2.rs b/src/oauth2.rs index 1d0296716..ab0b4eef5 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -1,5 +1,7 @@ //! OAuth 2 module. +#![allow(missing_docs)] + use std::collections::HashMap; use anyhow::Result; @@ -156,7 +158,7 @@ pub async fn get_oauth2_access_token( } // ... and POST - let client = reqwest::Client::new(); + let client = crate::http::get_client()?; let response: Response = match client.post(post_url).form(&post_param).send().await { Ok(resp) => match resp.json().await { @@ -282,7 +284,14 @@ impl Oauth2 { // "verified_email": true, // "picture": "https://lh4.googleusercontent.com/-Gj5jh_9R0BY/AAAAAAAAAAI/AAAAAAAAAAA/IAjtjfjtjNA/photo.jpg" // } - let response = match reqwest::get(userinfo_url).await { + let client = match crate::http::get_client() { + Ok(cl) => cl, + Err(err) => { + warn!(context, "failed to get HTTP client: {}", err); + return None; + } + }; + let response = match client.get(userinfo_url).send().await { Ok(response) => response, Err(err) => { warn!(context, "failed to get userinfo: {}", err); diff --git a/src/param.rs b/src/param.rs index 981a1f0dd..69e587e18 100644 --- a/src/param.rs +++ b/src/param.rs @@ -131,7 +131,7 @@ pub enum Param { /// For Chats Selftalk = b'K', - /// For Chats: On sending a new message we set the subject to "Re: ". + /// For Chats: On sending a new message we set the subject to `Re: `. /// Usually we just use the subject of the parent message, but if the parent message /// is deleted, we use the LastSubject of the chat. LastSubject = b't', diff --git a/src/peerstate.rs b/src/peerstate.rs index e9591c789..10c561287 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -1,5 +1,7 @@ //! # [Autocrypt Peer State](https://autocrypt.org/level1.html#peer-state-management) module. +#![allow(missing_docs)] + use std::collections::HashSet; use std::fmt; @@ -7,16 +9,15 @@ use crate::aheader::{Aheader, EncryptPreference}; use crate::chat::{self, Chat}; use crate::chatlist::Chatlist; use crate::constants::Chattype; -use crate::contact::{addr_cmp, Contact, Origin}; +use crate::contact::{addr_cmp, Contact, ContactAddress, Origin}; use crate::context::Context; -use crate::decrypt::DecryptionInfo; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::message::Message; use crate::mimeparser::SystemMessage; use crate::sql::Sql; use crate::stock_str; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Error, Result}; use num_traits::FromPrimitive; #[derive(Debug)] @@ -47,6 +48,8 @@ pub struct Peerstate { pub verified_key: Option, pub verified_key_fingerprint: Option, pub fingerprint_changed: bool, + /// The address that verified this contact + pub verifier: Option, } impl PartialEq for Peerstate { @@ -102,9 +105,11 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } + /// Create peerstate from gossip pub fn from_gossip(gossip_header: &Aheader, message_time: i64) -> Self { Peerstate { addr: gossip_header.addr.clone(), @@ -118,7 +123,6 @@ impl Peerstate { // learn encryption preferences of other members immediately and don't send unencrypted // messages to a group where everyone prefers encryption. prefer_encrypt: gossip_header.prefer_encrypt, - public_key: None, public_key_fingerprint: None, gossip_key: Some(gossip_header.public_key.clone()), @@ -127,13 +131,14 @@ impl Peerstate { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, } } pub async fn from_addr(context: &Context, addr: &str) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE addr=? COLLATE NOCASE LIMIT 1;"; Self::from_stmt(context, query, paramsv![addr]).await @@ -145,7 +150,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE public_key_fingerprint=? \ OR gossip_key_fingerprint=? \ @@ -161,7 +166,7 @@ impl Peerstate { ) -> Result> { let query = "SELECT addr, last_seen, last_seen_autocrypt, prefer_encrypted, public_key, \ gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \ - verified_key, verified_key_fingerprint \ + verified_key, verified_key_fingerprint, verifier \ FROM acpeerstates \ WHERE verified_key_fingerprint=? \ OR addr=? COLLATE NOCASE \ @@ -218,6 +223,7 @@ impl Peerstate { .transpose() .unwrap_or_default(), fingerprint_changed: false, + verifier: row.get("verifier")?, }; Ok(res) @@ -357,39 +363,54 @@ impl Peerstate { } } + /// Set this peerstate to verified + /// Make sure to call `self.save_to_db` to save these changes + /// Params: + /// verifier: + /// The address which verifies the given contact + /// If we are verifying the contact, use that contacts address pub fn set_verified( &mut self, which_key: PeerstateKeyType, - fingerprint: &Fingerprint, + fingerprint: Fingerprint, verified: PeerstateVerifiedStatus, - ) -> bool { + verifier: String, + ) -> Result<()> { if verified == PeerstateVerifiedStatus::BidirectVerified { match which_key { PeerstateKeyType::PublicKey => { if self.public_key_fingerprint.is_some() - && self.public_key_fingerprint.as_ref().unwrap() == fingerprint + && self.public_key_fingerprint.as_ref().unwrap() == &fingerprint { self.verified_key = self.public_key.clone(); - self.verified_key_fingerprint = self.public_key_fingerprint.clone(); - true + self.verified_key_fingerprint = Some(fingerprint); + self.verifier = Some(verifier); + Ok(()) } else { - false + Err(Error::msg(format!( + "{} is not peer's public key fingerprint", + fingerprint, + ))) } } PeerstateKeyType::GossipKey => { if self.gossip_key_fingerprint.is_some() - && self.gossip_key_fingerprint.as_ref().unwrap() == fingerprint + && self.gossip_key_fingerprint.as_ref().unwrap() == &fingerprint { self.verified_key = self.gossip_key.clone(); - self.verified_key_fingerprint = self.gossip_key_fingerprint.clone(); - true + self.verified_key_fingerprint = Some(fingerprint); + self.verifier = Some(verifier); + Ok(()) } else { - false + Err(Error::msg(format!( + "{} is not peer's gossip key fingerprint", + fingerprint, + ))) } } } } else { - false + Err(Error::msg("BidirectVerified required")) } } @@ -406,8 +427,9 @@ impl Peerstate { gossip_key_fingerprint, verified_key, verified_key_fingerprint, - addr) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + addr, + verifier) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ON CONFLICT (addr) DO UPDATE SET last_seen = excluded.last_seen, @@ -419,7 +441,8 @@ impl Peerstate { public_key_fingerprint = excluded.public_key_fingerprint, gossip_key_fingerprint = excluded.gossip_key_fingerprint, verified_key = excluded.verified_key, - verified_key_fingerprint = excluded.verified_key_fingerprint", + verified_key_fingerprint = excluded.verified_key_fingerprint, + verifier = excluded.verifier", paramsv![ self.last_seen, self.last_seen_autocrypt, @@ -432,6 +455,7 @@ impl Peerstate { self.verified_key.as_ref().map(|k| k.to_bytes()), self.verified_key_fingerprint.as_ref().map(|fp| fp.hex()), self.addr, + self.verifier, ], ) .await?; @@ -446,6 +470,11 @@ impl Peerstate { } } + /// Returns the address that verified the contact + pub fn get_verifier(&self) -> Option<&str> { + self.verifier.as_deref() + } + /// Add an info message to all the chats with this contact, informing about /// a [`PeerstateChange`]. /// @@ -518,14 +547,31 @@ impl Peerstate { if (chat.typ == Chattype::Group && chat.is_protected()) || chat.typ == Chattype::Broadcast { - chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?; - - let (new_contact_id, _) = - Contact::add_or_lookup(context, "", new_addr, Origin::IncomingUnknownFrom) + match ContactAddress::new(new_addr) { + Ok(new_addr) => { + let (new_contact_id, _) = Contact::add_or_lookup( + context, + "", + new_addr, + Origin::IncomingUnknownFrom, + ) .await?; - chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]).await?; + chat::remove_from_chat_contacts_table(context, *chat_id, contact_id) + .await?; + chat::add_to_chat_contacts_table(context, *chat_id, &[new_contact_id]) + .await?; - context.emit_event(EventType::ChatModified(*chat_id)); + context.emit_event(EventType::ChatModified(*chat_id)); + } + Err(err) => { + warn!( + context, + "New address {:?} is not vaild, not doing AEAP: {:#}.", + new_addr, + err + ) + } + } } } @@ -565,10 +611,10 @@ impl Peerstate { /// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP. pub async fn maybe_do_aeap_transition( context: &Context, - info: &mut DecryptionInfo, - mime_parser: &crate::mimeparser::MimeMessage, + mime_parser: &mut crate::mimeparser::MimeMessage, ) -> Result<()> { - if let Some(peerstate) = &mut info.peerstate { + let info = &mime_parser.decryption_info; + if let Some(peerstate) = &info.peerstate { // If the from addr is different from the peerstate address we know, // we may want to do an AEAP transition. if !addr_cmp(&peerstate.addr, &mime_parser.from.addr) @@ -588,6 +634,8 @@ pub async fn maybe_do_aeap_transition( && mime_parser.from_is_signed && info.message_time > peerstate.last_seen { + let info = &mut mime_parser.decryption_info; + let peerstate = info.peerstate.as_mut().context("no peerstate??")?; // Add info messages to chats with this (verified) contact // peerstate @@ -669,6 +717,7 @@ mod tests { verified_key: Some(pub_key.clone()), verified_key_fingerprint: Some(pub_key.fingerprint()), fingerprint_changed: false, + verifier: None, }; assert!( @@ -708,6 +757,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -740,6 +790,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( @@ -802,8 +853,8 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::NoPreference); peerstate.apply_header(&header, 100); assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); diff --git a/src/pgp.rs b/src/pgp.rs index e9b9e7c84..93762e68a 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -1,5 +1,7 @@ //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). +#![allow(missing_docs)] + use std::collections::{BTreeMap, HashSet}; use std::io; use std::io::Cursor; @@ -263,23 +265,20 @@ pub async fn pk_encrypt( /// of all keys from the `public_keys_for_validation` keyring that /// have valid signatures there. #[allow(clippy::implicit_hasher)] -pub async fn pk_decrypt( +pub fn pk_decrypt( ctext: Vec, - private_keys_for_decryption: Keyring, + private_keys_for_decryption: &Keyring, public_keys_for_validation: &Keyring, ) -> Result<(Vec, HashSet)> { let mut ret_signature_fingerprints: HashSet = Default::default(); - let msgs = tokio::task::spawn_blocking(move || { - let cursor = Cursor::new(ctext); - let (msg, _) = Message::from_armor_single(cursor)?; + let cursor = Cursor::new(ctext); + let (msg, _) = Message::from_armor_single(cursor)?; - let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect(); + let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect(); - let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; - decryptor.collect::>>() - }) - .await??; + let (decryptor, _) = msg.decrypt(|| "".into(), || "".into(), &skeys[..])?; + let msgs = decryptor.collect::>>()?; if let Some(msg) = msgs.into_iter().next() { // get_content() will decompress the message if needed, @@ -512,10 +511,9 @@ mod tests { sig_check_keyring.add(KEYS.alice_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); @@ -527,10 +525,9 @@ mod tests { sig_check_keyring.add(KEYS.alice_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); @@ -543,10 +540,9 @@ mod tests { let empty_keyring = Keyring::new(); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - keyring, + &keyring, &empty_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); @@ -561,10 +557,9 @@ mod tests { sig_check_keyring.add(KEYS.bob_public.clone()); let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); @@ -577,10 +572,9 @@ mod tests { let sig_check_keyring = Keyring::new(); let (plain, valid_signatures) = pk_decrypt( ctext_unsigned().await.as_bytes().to_vec(), - decrypt_keyring, + &decrypt_keyring, &sig_check_keyring, ) - .await .unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); diff --git a/src/plaintext.rs b/src/plaintext.rs index fd3ba1675..5bdeffed4 100644 --- a/src/plaintext.rs +++ b/src/plaintext.rs @@ -1,5 +1,7 @@ //! Handle plain text together with some attributes. +#![allow(missing_docs)] + use crate::simplify::split_lines; use once_cell::sync::Lazy; diff --git a/src/provider.rs b/src/provider.rs index fb36982a6..4d8ee74d8 100644 --- a/src/provider.rs +++ b/src/provider.rs @@ -1,5 +1,7 @@ //! [Provider database](https://providers.delta.chat/) module. +#![allow(missing_docs)] + mod data; use crate::config::Config; diff --git a/src/provider/data.rs b/src/provider/data.rs index 3b8b4b642..8c9516272 100644 --- a/src/provider/data.rs +++ b/src/provider/data.rs @@ -526,7 +526,7 @@ static P_GMX_NET: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); -// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, hermes.radio +// hermes.radio.md: ac.hermes.radio, ac1.hermes.radio, ac2.hermes.radio, ac3.hermes.radio, ac4.hermes.radio, ac5.hermes.radio, ac6.hermes.radio, ac7.hermes.radio, ac8.hermes.radio, ac9.hermes.radio, ac10.hermes.radio, ac11.hermes.radio, ac12.hermes.radio, ac13.hermes.radio, ac14.hermes.radio, ac15.hermes.radio, ka.hermes.radio, ka1.hermes.radio, ka2.hermes.radio, ka3.hermes.radio, ka4.hermes.radio, ka5.hermes.radio, ka6.hermes.radio, ka7.hermes.radio, ka8.hermes.radio, ka9.hermes.radio, ka10.hermes.radio, ka11.hermes.radio, ka12.hermes.radio, ka13.hermes.radio, ka14.hermes.radio, ka15.hermes.radio, ec.hermes.radio, ec1.hermes.radio, ec2.hermes.radio, ec3.hermes.radio, ec4.hermes.radio, ec5.hermes.radio, ec6.hermes.radio, ec7.hermes.radio, ec8.hermes.radio, ec9.hermes.radio, ec10.hermes.radio, ec11.hermes.radio, ec12.hermes.radio, ec13.hermes.radio, ec14.hermes.radio, ec15.hermes.radio, hermes.radio static P_HERMES_RADIO: Lazy = Lazy::new(|| Provider { id: "hermes.radio", status: Status::Ok, @@ -902,6 +902,35 @@ static P_NAVER: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); +// nubo.coop.md: nubo.coop +static P_NUBO_COOP: Lazy = Lazy::new(|| Provider { + id: "nubo.coop", + status: Status::Ok, + before_login_hint: "", + after_login_hint: "", + overview_page: "https://providers.delta.chat/nubo-coop", + server: vec![ + Server { + protocol: Imap, + socket: Ssl, + hostname: "mail.nubo.coop", + port: 993, + username_pattern: Email, + }, + Server { + protocol: Smtp, + socket: Ssl, + hostname: "mail.nubo.coop", + port: 465, + username_pattern: Email, + }, + ], + config_defaults: None, + strict_tls: true, + max_smtp_rcpt_to: None, + oauth2_authorizer: None, +}); + // outlook.com.md: hotmail.com, outlook.com, office365.com, outlook.com.tr, live.com, outlook.de static P_OUTLOOK_COM: Lazy = Lazy::new(|| Provider { id: "outlook.com", @@ -931,6 +960,35 @@ static P_OUTLOOK_COM: Lazy = Lazy::new(|| Provider { oauth2_authorizer: None, }); +// ouvaton.coop.md: ouvaton.org +static P_OUVATON_COOP: Lazy = Lazy::new(|| Provider { + id: "ouvaton.coop", + status: Status::Ok, + before_login_hint: "", + after_login_hint: "", + overview_page: "https://providers.delta.chat/ouvaton-coop", + server: vec![ + Server { + protocol: Imap, + socket: Ssl, + hostname: "imap.ouvaton.coop", + port: 993, + username_pattern: Email, + }, + Server { + protocol: Smtp, + socket: Ssl, + hostname: "smtp.ouvaton.coop", + port: 465, + username_pattern: Email, + }, + ], + config_defaults: None, + strict_tls: true, + max_smtp_rcpt_to: None, + oauth2_authorizer: None, +}); + // posteo.md: posteo.de, posteo.af, posteo.at, posteo.be, posteo.ca, posteo.ch, posteo.cl, posteo.co, posteo.co.uk, posteo.com.br, posteo.cr, posteo.cz, posteo.dk, posteo.ee, posteo.es, posteo.eu, posteo.fi, posteo.gl, posteo.gr, posteo.hn, posteo.hr, posteo.hu, posteo.ie, posteo.in, posteo.is, posteo.it, posteo.jp, posteo.la, posteo.li, posteo.lt, posteo.lu, posteo.me, posteo.mx, posteo.my, posteo.net, posteo.nl, posteo.no, posteo.nz, posteo.org, posteo.pe, posteo.pl, posteo.pm, posteo.pt, posteo.ro, posteo.ru, posteo.se, posteo.sg, posteo.si, posteo.tn, posteo.uk, posteo.us static P_POSTEO: Lazy = Lazy::new(|| Provider { id: "posteo", @@ -1659,6 +1717,22 @@ pub(crate) static PROVIDER_DATA: Lazy> ("ka13.hermes.radio", &*P_HERMES_RADIO), ("ka14.hermes.radio", &*P_HERMES_RADIO), ("ka15.hermes.radio", &*P_HERMES_RADIO), + ("ec.hermes.radio", &*P_HERMES_RADIO), + ("ec1.hermes.radio", &*P_HERMES_RADIO), + ("ec2.hermes.radio", &*P_HERMES_RADIO), + ("ec3.hermes.radio", &*P_HERMES_RADIO), + ("ec4.hermes.radio", &*P_HERMES_RADIO), + ("ec5.hermes.radio", &*P_HERMES_RADIO), + ("ec6.hermes.radio", &*P_HERMES_RADIO), + ("ec7.hermes.radio", &*P_HERMES_RADIO), + ("ec8.hermes.radio", &*P_HERMES_RADIO), + ("ec9.hermes.radio", &*P_HERMES_RADIO), + ("ec10.hermes.radio", &*P_HERMES_RADIO), + ("ec11.hermes.radio", &*P_HERMES_RADIO), + ("ec12.hermes.radio", &*P_HERMES_RADIO), + ("ec13.hermes.radio", &*P_HERMES_RADIO), + ("ec14.hermes.radio", &*P_HERMES_RADIO), + ("ec15.hermes.radio", &*P_HERMES_RADIO), ("hermes.radio", &*P_HERMES_RADIO), ("hey.com", &*P_HEY_COM), ("i.ua", &*P_I_UA), @@ -1681,12 +1755,14 @@ pub(crate) static PROVIDER_DATA: Lazy> ("mailo.com", &*P_MAILO_COM), ("nauta.cu", &*P_NAUTA_CU), ("naver.com", &*P_NAVER), + ("nubo.coop", &*P_NUBO_COOP), ("hotmail.com", &*P_OUTLOOK_COM), ("outlook.com", &*P_OUTLOOK_COM), ("office365.com", &*P_OUTLOOK_COM), ("outlook.com.tr", &*P_OUTLOOK_COM), ("live.com", &*P_OUTLOOK_COM), ("outlook.de", &*P_OUTLOOK_COM), + ("ouvaton.org", &*P_OUVATON_COOP), ("posteo.de", &*P_POSTEO), ("posteo.af", &*P_POSTEO), ("posteo.at", &*P_POSTEO), @@ -1861,7 +1937,9 @@ pub(crate) static PROVIDER_IDS: Lazy> = ("mailo.com", &*P_MAILO_COM), ("nauta.cu", &*P_NAUTA_CU), ("naver", &*P_NAVER), + ("nubo.coop", &*P_NUBO_COOP), ("outlook.com", &*P_OUTLOOK_COM), + ("ouvaton.coop", &*P_OUVATON_COOP), ("posteo", &*P_POSTEO), ("protonmail", &*P_PROTONMAIL), ("qq", &*P_QQ), @@ -1891,4 +1969,4 @@ pub(crate) static PROVIDER_IDS: Lazy> = }); pub static PROVIDER_UPDATED: Lazy = - Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2022, 7, 5).unwrap()); + Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2023, 1, 6).unwrap()); diff --git a/src/provider/update.py b/src/provider/update.py index 9031e85b4..b67e9fb59 100755 --- a/src/provider/update.py +++ b/src/provider/update.py @@ -190,6 +190,6 @@ if __name__ == "__main__": now = datetime.datetime.utcnow() out_all += "pub static PROVIDER_UPDATED: Lazy = "\ - "Lazy::new(|| chrono::NaiveDate::from_ymd("+str(now.year)+", "+str(now.month)+", "+str(now.day)+"));\n" + "Lazy::new(|| chrono::NaiveDate::from_ymd_opt("+str(now.year)+", "+str(now.month)+", "+str(now.day)+").unwrap());\n" print(out_all) diff --git a/src/qr.rs b/src/qr.rs index c12ce8045..c6b5e18b7 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -1,5 +1,7 @@ //! # QR code module. +#![allow(missing_docs)] + mod dclogin_scheme; pub use dclogin_scheme::LoginOptions; @@ -12,7 +14,9 @@ use std::collections::BTreeMap; use crate::chat::{self, get_chat_id_by_grpid, ChatIdBlocked}; use crate::config::Config; use crate::constants::Blocked; -use crate::contact::{addr_normalize, may_be_valid_addr, Contact, ContactId, Origin}; +use crate::contact::{ + addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin, +}; use crate::context::Context; use crate::key::Fingerprint; use crate::message::Message; @@ -219,14 +223,14 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { .context("Can't load peerstate")?; if let (Some(addr), Some(invitenumber), Some(authcode)) = (&addr, invitenumber, authcode) { - let contact_id = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan) + let addr = ContactAddress::new(addr)?; + let (contact_id, _) = Contact::add_or_lookup(context, &name, addr, Origin::UnhandledQrScan) .await - .map(|(id, _)| id) .with_context(|| format!("failed to add or lookup contact for address {:?}", addr))?; if let (Some(grpid), Some(grpname)) = (grpid, grpname) { if context - .is_self_addr(addr) + .is_self_addr(&addr) .await .with_context(|| format!("can't check if address {:?} is our address", addr))? { @@ -259,7 +263,7 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { authcode, }) } - } else if context.is_self_addr(addr).await? { + } else if context.is_self_addr(&addr).await? { if token::exists(context, token::Namespace::InviteNumber, &invitenumber).await { Ok(Qr::WithdrawVerifyContact { contact_id, @@ -285,10 +289,11 @@ async fn decode_openpgp(context: &Context, qr: &str) -> Result { } } else if let Some(addr) = addr { if let Some(peerstate) = peerstate { - let contact_id = - Contact::add_or_lookup(context, &name, &peerstate.addr, Origin::UnhandledQrScan) + let peerstate_addr = ContactAddress::new(&peerstate.addr)?; + let (contact_id, _) = + Contact::add_or_lookup(context, &name, peerstate_addr, Origin::UnhandledQrScan) .await - .map(|(id, _)| id)?; + .context("add_or_lookup")?; let chat = ChatIdBlocked::get_for_contact(context, contact_id, Blocked::Request) .await .context("Failed to create (new) chat for contact")?; @@ -371,7 +376,7 @@ struct CreateAccountErrorResponse { #[allow(clippy::indexing_slicing)] async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> { let url_str = &qr[DCACCOUNT_SCHEME.len()..]; - let response = reqwest::Client::new().post(url_str).send().await?; + let response = crate::http::get_client()?.post(url_str).send().await?; let response_status = response.status(); let response_text = response.text().await.with_context(|| { format!( @@ -528,11 +533,11 @@ async fn decode_mailto(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); + let name = ""; Qr::from_address( context, name, - addr, + &addr, if draft.is_empty() { None } else { Some(draft) }, ) .await @@ -552,8 +557,8 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); - Qr::from_address(context, name, addr, None).await + let name = ""; + Qr::from_address(context, name, &addr, None).await } /// Extract address for the matmsg scheme. @@ -577,8 +582,8 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result { }; let addr = normalize_address(addr)?; - let name = "".to_string(); - Qr::from_address(context, name, addr, None).await + let name = ""; + Qr::from_address(context, name, &addr, None).await } static VCARD_NAME_RE: Lazy = @@ -607,18 +612,19 @@ async fn decode_vcard(context: &Context, qr: &str) -> Result { bail!("Bad e-mail address"); }; - Qr::from_address(context, name, addr, None).await + Qr::from_address(context, &name, &addr, None).await } impl Qr { pub async fn from_address( context: &Context, - name: String, - addr: String, + name: &str, + addr: &str, draft: Option, ) -> Result { + let addr = ContactAddress::new(addr)?; let (contact_id, _) = - Contact::add_or_lookup(context, &name, &addr, Origin::UnhandledQrScan).await?; + Contact::add_or_lookup(context, name, addr, Origin::UnhandledQrScan).await?; Ok(Qr::Addr { contact_id, draft }) } } @@ -894,6 +900,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; assert!( peerstate.save_to_db(&ctx.ctx.sql).await.is_ok(), diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index b3b4aef11..6dbdfc249 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use anyhow::Result; use qrcodegen::{QrCode, QrCodeEcc}; diff --git a/src/quota.rs b/src/quota.rs index 1053cd81a..9b233a349 100644 --- a/src/quota.rs +++ b/src/quota.rs @@ -1,5 +1,7 @@ //! # Support for IMAP QUOTA extension. +#![allow(missing_docs)] + use anyhow::{anyhow, Context as _, Result}; use async_imap::types::{Quota, QuotaResource}; use std::collections::BTreeMap; @@ -132,7 +134,7 @@ impl Context { /// Called in response to `Action::UpdateRecentQuota`. pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result { if let Err(err) = imap.prepare(self).await { - warn!(self, "could not connect: {:?}", err); + warn!(self, "could not connect: {:#}", err); return Ok(Status::RetryNow); } @@ -160,7 +162,7 @@ impl Context { self.set_config(Config::QuotaExceeding, None).await?; } } - Err(err) => warn!(self, "cannot get highest quota usage: {:?}", err), + Err(err) => warn!(self, "cannot get highest quota usage: {:#}", err), } } diff --git a/src/reaction.rs b/src/reaction.rs index 72a096030..f858f6172 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -286,11 +286,11 @@ pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result Result> { info!(context, "Receiving message, seen={}...", seen); - if std::env::var(crate::DCC_MIME_DEBUG).unwrap_or_default() == "2" { - info!(context, "receive_imf: incoming message mime-body:"); - println!("{}", String::from_utf8_lossy(imf_raw)); + if std::env::var(crate::DCC_MIME_DEBUG).is_ok() { + info!( + context, + "receive_imf: incoming message mime-body:\n{}", + String::from_utf8_lossy(imf_raw), + ); } let mut mime_parser = match MimeMessage::from_bytes_with_partial(context, imf_raw, is_partial_download).await { Err(err) => { - warn!(context, "receive_imf: can't parse MIME: {}", err); + warn!(context, "receive_imf: can't parse MIME: {:#}", err); let msg_ids; if !rfc724_mid.starts_with(GENERATED_PREFIX) { let row_id = context @@ -168,7 +173,16 @@ pub(crate) async fn receive_imf_inner( // If this is a mailing list email (i.e. list_id_header is some), don't change the displayname because in // a mailing list the sender displayname sometimes does not belong to the sender email address. let (from_id, _from_id_blocked, incoming_origin) = - from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await?; + match from_field_to_contact_id(context, &mime_parser.from, prevent_rename).await? { + Some(contact_id_res) => contact_id_res, + None => { + warn!( + context, + "receive_imf: From field does not contain an acceptable address" + ); + return Ok(None); + } + }; let incoming = from_id != ContactId::SELF; @@ -251,7 +265,7 @@ pub(crate) async fn receive_imf_inner( if from_id == ContactId::SELF { if mime_parser.was_encrypted() { if let Err(err) = context.execute_sync_items(sync_items).await { - warn!(context, "receive_imf cannot execute sync items: {}", err); + warn!(context, "receive_imf cannot execute sync items: {:#}", err); } } else { warn!(context, "sync items are not encrypted."); @@ -266,7 +280,7 @@ pub(crate) async fn receive_imf_inner( .receive_status_update(from_id, insert_msg_id, status_update) .await { - warn!(context, "receive_imf cannot update status: {}", err); + warn!(context, "receive_imf cannot update status: {:#}", err); } } @@ -276,7 +290,7 @@ pub(crate) async fn receive_imf_inner( .update_contacts_timestamp(from_id, Param::AvatarTimestamp, sent_timestamp) .await? { - match contact::set_profile_image( + if let Err(err) = contact::set_profile_image( context, from_id, avatar_action, @@ -284,12 +298,10 @@ pub(crate) async fn receive_imf_inner( ) .await { - Ok(()) => { - context.emit_event(EventType::ChatModified(chat_id)); - } - Err(err) => { - warn!(context, "receive_imf cannot update profile image: {}", err); - } + warn!( + context, + "receive_imf cannot update profile image: {:#}", err + ); }; } } @@ -315,7 +327,7 @@ pub(crate) async fn receive_imf_inner( ) .await { - warn!(context, "cannot update contact status: {}", err); + warn!(context, "cannot update contact status: {:#}", err); } } @@ -364,26 +376,39 @@ pub(crate) async fn receive_imf_inner( /// Also returns whether it is blocked or not and its origin. /// /// * `prevent_rename`: passed through to `add_or_lookup_contacts_by_address_list()` +/// +/// Returns `None` if From field does not contain a valid contact address. pub async fn from_field_to_contact_id( context: &Context, from: &SingleInfo, prevent_rename: bool, -) -> Result<(ContactId, bool, Origin)> { +) -> Result> { let display_name = if prevent_rename { Some("") } else { from.display_name.as_deref() }; + let from_addr = match ContactAddress::new(&from.addr) { + Ok(from_addr) => from_addr, + Err(err) => { + warn!( + context, + "Cannot create a contact for the given From field: {:#}.", err + ); + return Ok(None); + } + }; + let from_id = add_or_lookup_contact_by_addr( context, display_name, - &from.addr, + from_addr, Origin::IncomingUnknownFrom, ) .await?; if from_id == ContactId::SELF { - Ok((ContactId::SELF, false, Origin::OutgoingBcc)) + Ok(Some((ContactId::SELF, false, Origin::OutgoingBcc))) } else { let mut from_id_blocked = false; let mut incoming_origin = Origin::Unknown; @@ -391,7 +416,7 @@ pub async fn from_field_to_contact_id( from_id_blocked = contact.blocked; incoming_origin = contact.origin; } - Ok((from_id, from_id_blocked, incoming_origin)) + Ok(Some((from_id, from_id_blocked, incoming_origin))) } } @@ -493,7 +518,7 @@ async fn add_parts( securejoin_seen = false; } Err(err) => { - warn!(context, "Error in Secure-Join message handling: {}", err); + warn!(context, "Error in Secure-Join message handling: {:#}", err); chat_id = Some(DC_CHAT_ID_TRASH); securejoin_seen = true; } @@ -728,7 +753,7 @@ async fn add_parts( chat_id = None; } Err(err) => { - warn!(context, "Error in Secure-Join watching: {}", err); + warn!(context, "Error in Secure-Join watching: {:#}", err); chat_id = Some(DC_CHAT_ID_TRASH); } } @@ -868,7 +893,7 @@ async fn add_parts( Err(err) => { warn!( context, - "can't parse ephemeral timer \"{}\": {}", value, err + "can't parse ephemeral timer \"{}\": {:#}", value, err ); EphemeralTimer::Disabled } @@ -924,7 +949,7 @@ async fn add_parts( { warn!( context, - "failed to modify timer for chat {}: {}", chat_id, err + "failed to modify timer for chat {}: {:#}", chat_id, err ); } else { info!( @@ -973,7 +998,7 @@ async fn add_parts( if chat.is_protected() || new_status.is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } else { @@ -1485,7 +1510,7 @@ async fn create_or_lookup_group( let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } @@ -1683,7 +1708,7 @@ async fn apply_group_changes( if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "verification problem: {}", err); + warn!(context, "verification problem: {:#}", err); let s = format!("{}. See 'Info' for more details", err); mime_parser.repl_msg_by_error(&s); } @@ -1923,6 +1948,13 @@ async fn apply_mailinglist_changes( } let listid = &chat.grpid; + let list_post = match ContactAddress::new(list_post) { + Ok(list_post) => list_post, + Err(err) => { + warn!(context, "Invalid List-Post: {:#}.", err); + return Ok(()); + } + }; let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?; let mut contact = Contact::load_from_db(context, contact_id).await?; @@ -1932,7 +1964,7 @@ async fn apply_mailinglist_changes( } if let Some(old_list_post) = chat.param.get(Param::ListPost) { - if list_post != old_list_post { + if list_post.as_ref() != old_list_post { // Apparently the mailing list is using a different List-Post header in each message. // Make the mailing list read-only because we would't know which message the user wants to reply to. chat.param.remove(Param::ListPost); @@ -2136,14 +2168,15 @@ async fn check_verified_properties( || peerstate.verified_key_fingerprint != peerstate.public_key_fingerprint && peerstate.verified_key_fingerprint != peerstate.gossip_key_fingerprint { - info!(context, "{} has verified {}.", contact.get_addr(), to_addr,); + info!(context, "{} has verified {}.", contact.get_addr(), to_addr); let fp = peerstate.gossip_key_fingerprint.clone(); if let Some(fp) = fp { peerstate.set_verified( PeerstateKeyType::GossipKey, - &fp, + fp, PeerstateVerifiedStatus::BidirectVerified, - ); + contact.get_addr().to_owned(), + )?; peerstate.save_to_db(&context.sql).await?; is_verified = true; } @@ -2262,8 +2295,13 @@ async fn add_or_lookup_contacts_by_address_list( continue; } let display_name = info.display_name.as_deref(); - contact_ids - .insert(add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?); + if let Ok(addr) = ContactAddress::new(addr) { + let contact_id = + add_or_lookup_contact_by_addr(context, display_name, addr, origin).await?; + contact_ids.insert(contact_id); + } else { + warn!(context, "Contact with address {:?} cannot exist.", addr); + } } Ok(contact_ids.into_iter().collect::>()) @@ -2273,3224 +2311,18 @@ async fn add_or_lookup_contacts_by_address_list( async fn add_or_lookup_contact_by_addr( context: &Context, display_name: Option<&str>, - addr: &str, + addr: ContactAddress<'_>, origin: Origin, ) -> Result { - if context.is_self_addr(addr).await? { + if context.is_self_addr(&addr).await? { return Ok(ContactId::SELF); } let display_name_normalized = display_name.map(normalize_name).unwrap_or_default(); - let (row_id, _modified) = + let (contact_id, _modified) = Contact::add_or_lookup(context, &display_name_normalized, addr, origin).await?; - Ok(row_id) + Ok(contact_id) } #[cfg(test)] -mod tests { - use tokio::fs; - - use super::*; - - use crate::aheader::EncryptPreference; - use crate::chat::get_chat_contacts; - use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; - use crate::chatlist::Chatlist; - use crate::constants::DC_GCL_NO_SPECIALS; - use crate::imap::prefetch_should_download; - use crate::message::Message; - use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_grpid_simple() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello@example.org\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) - .await - .unwrap(); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); - let grpid = Some("HcxyMARjyJy"); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_bad_from() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; - assert!(mimeparser.is_err()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_grpid_from_multiple() { - let context = TestContext::new().await; - let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: hello@example.org\n\ - Subject: outer-subject\n\ - In-Reply-To: \n\ - References: , \n\ - \n\ - hello\x00"; - let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) - .await - .unwrap(); - let grpid = Some("HcxyMARjyJy"); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid); - assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); - } - - static MSGRMSG: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Chat-Version: 1.0\n\ - Subject: Chat: hello\n\ - Message-ID: \n\ - Date: Sun, 22 Mar 2020 22:37:55 +0000\n\ - \n\ - hello\n"; - - static ONETOONE_NOREPLY_MAIL: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: alice@example.org\n\ - Subject: Chat: hello\n\ - Message-ID: <2222@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ - \n\ - hello\n"; - - static GRP_MAIL: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org, claire@example.com\n\ - Subject: group with Alice, Bob and Claire\n\ - Message-ID: <3333@example.com>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_chats_only() { - let t = TestContext::new_alice().await; - assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - receive_imf(&t, MSGRMSG, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - - receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_unknown() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_known() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - Contact::create(&t, "Bob", "bob@example.com").await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts - // (and existent chat is required) - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_accepted_contact_accepted() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); - - // accept Bob by accepting a delta-message from Bob - receive_imf(&t, MSGRMSG, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - assert!(!chat_id.is_special()); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(chat.name, "Bob"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1); - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1); - - // receive a non-delta-message from Bob, shows up because of the show_emails setting - receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); - - assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2); - - // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_adhoc_group_show_all() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - receive_imf(&t, GRP_MAIL, false).await.unwrap(); - - // adhoc-group with unknown contacts with show_emails=all will show up in a single chat - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(chat.name, "group with Alice, Bob and Claire"); - assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_read_receipt_and_unarchive() -> Result<()> { - // create alice's account - let t = TestContext::new_alice().await; - - let bob_id = Contact::create(&t, "bob", "bob@example.com").await?; - let one2one_id = ChatId::create_for_contact(&t, bob_id).await?; - one2one_id - .set_visibility(&t, ChatVisibility::Archived) - .await - .unwrap(); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); - - // create a group with bob, archive group - let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - chat::add_contact_to_chat(&t, group_id, bob_id).await?; - assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await.unwrap().len(), 0); - group_id - .set_visibility(&t, ChatVisibility::Archived) - .await?; - let group = Chat::load_from_db(&t, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Archived); - - // everything archived, chatlist should be empty - assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 0 - ); - - // send a message to group with bob - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: {}\n\ - Chat-Group-Name: foo\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - group.grpid, group.grpid - ) - .as_bytes(), - false, - ) - .await?; - let msg = get_chat_msg(&t, group_id, 0, 1).await; - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.state, MessageState::OutDelivered); - let group = Chat::load_from_db(&t, group_id).await?; - assert!(group.get_visibility() == ChatVisibility::Normal); - - // bob sends a read receipt to the group - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: bob@example.com\n\ - To: alice@example.org\n\ - Subject: message opened\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - Chat-Version: 1.0\n\ - Message-ID: \n\ - Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: text/plain; charset=utf-8\n\ - \n\ - Read receipts do not guarantee sth. was read.\n\ - \n\ - \n\ - --SNIPP\n\ - Content-Type: message/disposition-notification\n\ - \n\ - Reporting-UA: Delta Chat 1.28.0\n\ - Original-Recipient: rfc822;bob@example.com\n\ - Final-Recipient: rfc822;bob@example.com\n\ - Original-Message-ID: \n\ - Disposition: manual-action/MDN-sent-automatically; displayed\n\ - \n\ - \n\ - --SNIPP--", - group.grpid - ) - .as_bytes(), - false, - ) - .await?; - assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await?.len(), 1); - let msg = message::Message::load_from_db(&t, msg.id).await?; - assert_eq!(msg.state, MessageState::OutMdnRcvd); - - // check, the read-receipt has not unarchived the one2one - assert_eq!( - Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) - .await? - .len(), - 1 - ); - let one2one = Chat::load_from_db(&t, one2one_id).await?; - assert!(one2one.get_visibility() == ChatVisibility::Archived); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_from() { - // if there is no from given, from_id stays 0 which is just fine. These messages - // are very rare, however, we have to add them to the database - // to avoid a re-download from the server. - - let t = TestContext::new_alice().await; - let context = &t; - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert!(chats.get_msg_id(0).is_err()); - - receive_imf( - context, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: <3924@example.com>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - // Check that the message is not shown to the user: - assert!(chats.is_empty()); - - // Check that the message was added to the db: - assert!(message::rfc724_mid_exists(context, "3924@example.com") - .await - .unwrap() - .is_some()); - } - - /// If there is no Message-Id header, we generate a random id. - /// But there is no point in adding a trash entry in the database - /// if the email is malformed (e.g. because `From` is missing) - /// with this random id we just generated. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_message_id_header() { - let t = TestContext::new_alice().await; - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert!(chats.get_msg_id(0).is_err()); - - let received = receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - dbg!(&received); - assert!(received.is_none()); - - assert!(!t - .sql - .exists( - "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", - paramsv![DC_CHAT_ID_TRASH], - ) - .await - .unwrap()); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - // Check that the message is not shown to the user: - assert!(chats.is_empty()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_escaped_from() { - let t = TestContext::new_alice().await; - let contact_id = Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); - receive_imf( - &t, - b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ - To: alice@example.org\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ).await.unwrap(); - assert_eq!( - Contact::load_from_db(&t, contact_id) - .await - .unwrap() - .get_authname(), - "Имя, Фамилия", - ); - let msg = get_chat_msg(&t, chat_id, 0, 1).await; - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_escaped_recipients() { - let t = TestContext::new_alice().await; - Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - - let carl_contact_id = - Contact::add_or_lookup(&t, "Carl", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .0; - - receive_imf( - &t, - b"From: Foobar \n\ - To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\ - Cc: =?utf-8?q?=3Ch2=3E?= \n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "h2"); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap()) - .await - .unwrap(); - assert_eq!(msg.is_dc_message, MessengerMessage::Yes); - assert_eq!(msg.text.unwrap(), "hello"); - assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_cc_to_contact() { - let t = TestContext::new_alice().await; - Contact::create(&t, "foobar", "foobar@example.com") - .await - .unwrap(); - - let carl_contact_id = - Contact::add_or_lookup(&t, "garabage", "carl@host.tld", Origin::IncomingUnknownFrom) - .await - .unwrap() - .0; - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Foobar \n\ - To: alice@example.org\n\ - Cc: Carl \n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Disposition-Notification-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); - assert_eq!(contact.get_name(), ""); - assert_eq!(contact.get_display_name(), "Carl"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_tiscali() { - test_parse_ndn( - "alice@tiscali.it", - "shenauithz@testrun.org", - "Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it", - include_bytes!("../test-data/message/tiscali_ndn.eml"), - Some("Delivery status notification – This is an automatically generated Delivery Status Notification. \n\nDelivery to the following recipients was aborted after 2 second(s):\n\n * shenauithz@testrun.org"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_testrun() { - test_parse_ndn( - "alice@testrun.org", - "hcksocnsofoejx@five.chat", - "Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org", - include_bytes!("../test-data/message/testrun_ndn.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n : Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_yahoo() { - test_parse_ndn( - "alice@yahoo.com", - "haeclirth.sinoenrat@yahoo.com", - "1680295672.3657931.1591783872936@mail.yahoo.com", - include_bytes!("../test-data/message/yahoo_ndn.eml"), - Some("Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_gmail() { - test_parse_ndn( - "alice@gmail.com", - "assidhfaaspocwaeofi@gmail.com", - "CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com", - include_bytes!("../test-data/message/gmail_ndn.eml"), - Some("Delivery Status Notification (Failure) – ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_gmx() { - test_parse_ndn( - "alice@gmx.com", - "snaerituhaeirns@gmail.com", - "9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de", - include_bytes!("../test-data/message/gmx_ndn.eml"), - Some("Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_posteo() { - test_parse_ndn( - "alice@posteo.org", - "hanerthaertidiuea@gmx.de", - "04422840-f884-3e37-5778-8192fe22d8e1@posteo.de", - include_bytes!("../test-data/message/posteo_ndn.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)"), - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_testrun_2() { - test_parse_ndn( - "alice@example.org", - "bob@example.org", - "Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org", - include_bytes!("../test-data/message/testrun_ndn_2.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: Host or domain name not found. Name service error for\n name=echedelyr.tk type=AAAA: Host not found"), - ) - .await; - } - - /// Tests that text part is not squashed into OpenPGP attachment. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_with_attachment() { - test_parse_ndn( - "alice@example.org", - "bob@example.net", - "Mr.I6Da6dXcTel.TroC5J3uSDH@example.org", - include_bytes!("../test-data/message/ndn_with_attachment.eml"), - Some("Undelivered Mail Returned to Sender – This is the mail system at host relay01.example.org.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx2.example.net[80.241.60.215] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota exceeded (in\n reply to RCPT TO command)\n\n: host mx1.example.net[80.241.60.212] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota\n exceeded (in reply to RCPT TO command)") - ) - .await; - } - - /// Test that DSN is not treated as NDN if Action: is not "failed" - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_dsn_relayed() { - test_parse_ndn( - "anon_1@posteo.de", - "anon_2@gmx.at", - "8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de", - include_bytes!("../test-data/message/dsn_relayed.eml"), - None, - ) - .await; - } - - // ndn = Non Delivery Notification - async fn test_parse_ndn( - self_addr: &str, - foreign_addr: &str, - rfc724_mid_outgoing: &str, - raw_ndn: &[u8], - error_msg: Option<&str>, - ) { - let t = TestContext::new().await; - t.configure_addr(self_addr).await; - - receive_imf( - &t, - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: {}\n\ - To: {}\n\ - Subject: foo\n\ - Message-ID: <{}>\n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - self_addr, foreign_addr, rfc724_mid_outgoing - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(0).unwrap().unwrap(); - - // Check that the ndn would be downloaded: - let headers = mailparse::parse_mail(raw_ndn).unwrap().headers; - assert!(prefetch_should_download( - &t, - &headers, - "some-other-message-id", - std::iter::empty(), - ShowEmails::Off, - ) - .await - .unwrap()); - - receive_imf(&t, raw_ndn, false).await.unwrap(); - let msg = Message::load_from_db(&t, msg_id).await.unwrap(); - - assert_eq!( - msg.state, - if error_msg.is_some() { - MessageState::OutFailed - } else { - MessageState::OutDelivered - } - ); - - assert_eq!(msg.error(), error_msg.map(|error| error.to_string())); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_parse_ndn_group_msg() -> Result<()> { - let t = TestContext::new().await; - t.configure_addr("alice@gmail.com").await; - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@gmail.com\n\ - To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: abcde\n\ - Chat-Group-Name: foo\n\ - Chat-Disposition-Notification-To: alice@example.org\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await?; - - let chats = Chatlist::try_load(&t, 0, None, None).await?; - let msg_id = chats.get_msg_id(0)?.unwrap(); - - let raw = include_bytes!("../test-data/message/gmail_ndn_group.eml"); - receive_imf(&t, raw, false).await?; - - let msg = Message::load_from_db(&t, msg_id).await?; - - assert_eq!(msg.state, MessageState::OutFailed); - - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await?; - let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { - msg_id - } else { - panic!("Wrong item type"); - }; - let last_msg = Message::load_from_db(&t, *msg_id).await?; - - assert_eq!( - last_msg.text, - Some(stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await,) - ); - assert_eq!(last_msg.from_id, ContactId::INFO); - Ok(()) - } - - async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message { - context - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(context, imf_raw, false).await.unwrap(); - let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); - let msg_id = chats.get_msg_id(0).unwrap().unwrap(); - Message::load_from_db(context, msg_id).await.unwrap() - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_html_only_mail() { - let t = TestContext::new_alice().await; - let msg = load_imf_email(&t, include_bytes!("../test-data/message/wrong-html.eml")).await; - assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]"); - } - - static GH_MAILINGLIST: &[u8] = - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Max Mustermann \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: Let's put some [brackets here that] have nothing to do with the topic\n\ - Message-ID: <3333@example.org>\n\ - List-ID: deltachat/deltachat-core-rust \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n"; - - static GH_MAILINGLIST2: &str = - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Github \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [deltachat/deltachat-core-rust] PR run failed\n\ - Message-ID: <3334@example.org>\n\ - List-ID: deltachat/deltachat-core-rust \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello back\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_github_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.ctx.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf(&t.ctx, GH_MAILINGLIST, false).await?; - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; - assert_eq!(chats.len(), 1); - - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(&t).await.unwrap(); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - - assert!(chat.is_mailing_list()); - assert!(chat.can_send(&t.ctx).await?); - assert_eq!( - chat.get_mailinglist_addr(), - Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com") - ); - assert_eq!(chat.name, "deltachat/deltachat-core-rust"); - assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1); - - receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?; - - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - assert!(!chat.can_send(&t.ctx).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; - assert_eq!(chats.len(), 1); - let contacts = Contact::get_all(&t.ctx, 0, None).await?; - assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts" - - let msg1 = get_chat_msg(&t, chat_id, 0, 2).await; - let contact1 = Contact::load_from_db(&t.ctx, msg1.from_id).await?; - assert_eq!(contact1.get_addr(), "notifications@github.com"); - assert_eq!(contact1.get_display_name(), "notifications@github.com"); // Make sure this is not "Max Mustermann" or somethinng - - let msg2 = get_chat_msg(&t, chat_id, 1, 2).await; - let contact2 = Contact::load_from_db(&t.ctx, msg2.from_id).await?; - assert_eq!(contact2.get_addr(), "notifications@github.com"); - - assert_eq!(msg1.get_override_sender_name().unwrap(), "Max Mustermann"); - assert_eq!(msg2.get_override_sender_name().unwrap(), "Github"); - Ok(()) - } - - static DC_MAILINGLIST: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Bob \n\ - To: delta@codespeak.net\n\ - Subject: Re: [delta-dev] What's up?\n\ - Message-ID: <38942@posteo.org>\n\ - List-ID: \"discussions about and around https://delta.chat developments\" \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body\n"; - - static DC_MAILINGLIST2: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Charlie \n\ - To: delta@codespeak.net\n\ - Subject: Re: [delta-dev] DC is nice!\n\ - Message-ID: <38943@posteo.org>\n\ - List-ID: \"discussions about and around https://delta.chat developments\" \n\ - List-Post: \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body 4\n"; - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_classic_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - let chat_id = chats.get_chat_id(0).unwrap(); - chat_id.accept(&t).await.unwrap(); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert_eq!(chat.name, "delta-dev"); - assert!(chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net")); - - let msg = get_chat_msg(&t, chat_id, 0, 1).await; - let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap(); - assert_eq!(contact1.get_addr(), "bob@posteo.org"); - - let sent = t.send_text(chat.id, "Hello mailinglist!").await; - let mime = sent.payload(); - - println!("Sent mime message is:\n\n{}\n\n", mime); - assert!( - mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n") - ); - assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); - assert!(mime.contains("MIME-Version: 1.0\r\n")); - assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); - assert!(mime.contains("Chat-Version: 1.0\r\n")); - assert!(mime.contains("To: \r\n")); - assert!(mime.contains("From: \r\n")); - assert!(mime.contains( - "\r\n\ -\r\n\ -Hello mailinglist!\r\n" - )); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?; - - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; - assert!(chat.can_send(&t.ctx).await?); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_other_device_writes_to_mailinglist() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); - let first_msg = t.get_last_msg().await; - let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?; - assert_eq!( - first_chat.param.get(Param::ListPost).unwrap(), - "delta@codespeak.net" - ); - - let list_post_contact_id = - Contact::lookup_id_by_addr(&t, "delta@codespeak.net", Origin::Unknown) - .await? - .unwrap(); - let list_post_contact = Contact::load_from_db(&t, list_post_contact_id).await?; - assert_eq!( - list_post_contact.param.get(Param::ListId).unwrap(), - "delta.codespeak.net" - ); - assert_eq!( - chat::get_chat_id_by_grpid(&t, "delta.codespeak.net") - .await? - .unwrap(), - (first_chat.id, false, Blocked::Request) - ); - - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: Alice \n\ - To: delta@codespeak.net\n\ - Subject: [delta-dev] Subject\n\ - Message-ID: <0476@example.org>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - body 4\n", - false, - ) - .await - .unwrap(); - - let second_msg = t.get_last_msg().await; - - assert_eq!(first_msg.chat_id, second_msg.chat_id); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_block_mailing_list() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - t.evtracker.wait_next_incoming_message().await; - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - - // Block the contact request. - chat_id.block(&t).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message disappeared - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - // Check that no notification is displayed for blocked mailing list message. - while let Ok(event) = t.evtracker.try_recv() { - assert!(!matches!(event.typ, EventType::IncomingMsg { .. })); - } - - // Test that the mailing list stays disappeared - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message is not shown - - // Both messages are in the same blocked chat. - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_block_then_unblock() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 0); - - // Block the contact request, this should add one blocked contact. - let msg = t.get_last_msg().await; - msg.chat_id.block(&t).await.unwrap(); - - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 1); - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); // Test that the message is not shown - - // Unblock contact and check if the next message arrives in a chat - Contact::unblock(&t, *blocked.first().unwrap()) - .await - .unwrap(); - let blocked = Contact::get_all_blocked(&t).await.unwrap(); - assert_eq!(blocked.len(), 0); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - let msg = t.get_last_msg().await; - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_not_now() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - - // Open the chat and go back - chat::marknoticed_chat(&t.ctx, chat_id).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that chat is still in the chatlist - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 1); // ...and contains 1 message - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.is_contact_request()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_decide_accept() { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); - - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - chat_id.accept(&t).await.unwrap(); - - let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); // Test that the message is shown - assert!(!chat_id.is_special()); - - receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); - - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 2); - let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); - assert!(chat.can_send(&t.ctx).await.unwrap()); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_multiple_names_in_subject() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - receive_imf( - &t, - b"From: Foo Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola list] [foo][bar] just a subject\n\ - Message-ID: <3333@example.org>\n\ - List-ID: \"looong description of 'ola list', with foo, bar\" \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - let chat = Chat::load_from_db(&t, chat_id).await?; - assert_eq!(chat.name, "ola list [foo][bar]"); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_majordomo_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - // test mailing lists not having a `ListId:`-header - receive_imf( - &t, - b"From: Foo Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola] just a subject\n\ - Message-ID: <3333@example.org>\n\ - Sender: My list \n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat_id = msg.get_chat_id(); - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "mylist@bar.org"); - assert_eq!(chat.name, "ola"); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - // receive another message with no sender name but the same address, - // make sure this lands in the same chat - receive_imf( - &t, - b"From: Nu Bar \n\ - To: deltachat/deltachat-core-rust \n\ - Subject: [ola] Re: just a subject\n\ - Message-ID: <4444@example.org>\n\ - Sender: mylist@bar.org\n\ - Precedence: list\n\ - Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailchimp_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - b"To: alice \n\ - Subject: =?utf-8?Q?How=20early=20megacities=20emerged=20from=20Cambodia=E2=80=99s=20jungles?=\n\ - From: =?utf-8?Q?Atlas=20Obscura?= \n\ - List-ID: 399fc0402f1b154b67965632emc list <399fc0402f1b154b67965632e.100761.list-id.mcsv.net>\n\ - Message-ID: <555@example.org>\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!( - chat.grpid, - "399fc0402f1b154b67965632e.100761.list-id.mcsv.net" - ); - assert_eq!(chat.name, "Atlas Obscura"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dhl_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_dhl.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("Ihr Paket ist in der Packstation 123 – bla bla".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de"); - assert_eq!(chat.name, "DHL Paket"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dpd_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_dpd.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("Bald ist Ihr DPD Paket da – bla bla".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de"); - assert_eq!(chat.name, "DPD"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_xt_local_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xt_local_microsoft.eml"), - false, - ) - .await?; - let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "96540.xt.local"); - assert_eq!(chat.name, "Microsoft Store"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xt_local_spiegel.eml"), - false, - ) - .await?; - let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "121231234.xt.local"); - assert_eq!(chat.name, "DER SPIEGEL Kundenservice"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_xing_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_xing.eml"), - false, - ) - .await?; - let msg = t.get_last_msg().await; - assert_eq!(msg.subject, "Kennst Du Dr. Mabuse?"); - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com"); - assert_eq!(chat.name, "xing.com"); - assert!(!chat.can_send(&t).await?); - assert_eq!(chat.get_mailinglist_addr(), None); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ttline_mailing_list() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_ttline.eml"), - false, - ) - .await?; - let msg = t.get_last_msg().await; - assert_eq!(msg.subject, "Unsere Sommerangebote an Bord ⚓"); - let chat = Chat::load_from_db(&t, msg.chat_id).await?; - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.grpid, "39123123-1BBQXPY.t.ttline.com"); - assert_eq!(chat.name, "TT-Line - Die Schwedenfähren"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_with_mimepart_footer() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - // the mailing list message contains two top-level texts. - // the second text is a footer that is added by some mailing list software - // if the user-edited text contains html. - // this footer should not become a text-message in delta chat - // (otherwise every second mail might be the same footer) - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_with_mimepart_footer.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!( - msg.text, - Some("[Intern] important stuff – Hi mr ... [text part]".to_string()) - ); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "intern.lists.abc.de"); - assert_eq!(chat.name, "Intern"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_with_mimepart_footer_signed() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_with_mimepart_footer_signed.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); - let text = msg.text.clone().unwrap(); - assert!(text.contains("content text")); - assert!(!text.contains("footer text")); - assert!(msg.has_html()); - let html = msg.get_id().get_html(&t).await.unwrap().unwrap(); - assert!(html.contains("content text")); - assert!(!html.contains("footer text")); - } - - /// Test that the changes from apply_mailinglist_changes() are also applied - /// if the message is assigned to the chat by In-Reply-To - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_apply_mailinglist_changes_assigned_by_reply() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf(&t, GH_MAILINGLIST, false).await.unwrap(); - - let chat_id = t.get_last_msg().await.chat_id; - chat_id.accept(&t).await.unwrap(); - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(chat.can_send(&t).await.unwrap()); - - let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2); - receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap(); - - assert_eq!( - t.get_last_msg().await.in_reply_to.unwrap(), - "3333@example.org" - ); - // `Assigning message to Chat#... as it's a reply to 3333@example.org` - t.evtracker - .get_info_contains("as it's a reply to 3333@example.org") - .await; - - let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); - assert!(!chat.can_send(&t).await.unwrap()); - - let contact_id = Contact::lookup_id_by_addr( - &t, - "reply+EGELITBABIHXSITUZIEPAKYONASITEPUANERGRUSHE@reply.github.com", - Origin::Hidden, - ) - .await - .unwrap() - .unwrap(); - let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); - assert_eq!( - contact.param.get(Param::ListId).unwrap(), - "deltachat-core-rust.deltachat.github.com" - ) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_chat_message() { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_chat_message.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string())); - assert!(!msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Mailinglist); - assert_eq!(chat.blocked, Blocked::Request); - assert_eq!(chat.grpid, "test1.example.net"); - assert_eq!(chat.name, "Test1"); - } - - /// Tests that bots automatically accept mailing lists. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mailing_list_bot() { - let t = TestContext::new_alice().await; - t.set_config(Config::Bot, Some("1")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/mailinglist_chat_message.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.blocked, Blocked::Not); - - // Bot should see the message as fresh and process it. - assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_tokens_in_contacts_list() { - check_dont_show_in_contacts_list( - "reply+OGHVYCLVBEGATYBICAXBIRQATABUOTUCERABERAHNO@reply.github.com", - ) - .await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_noreply_in_contacts_list() { - check_dont_show_in_contacts_list("noreply@github.com").await; - } - - async fn check_dont_show_in_contacts_list(addr: &str) { - let t = TestContext::new_alice().await; - t.ctx - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf( - &t, - format!( - "Subject: Re: [deltachat/deltachat-core-rust] DC is the best repo on GitHub! -To: {} -References: - -From: alice@example.org -Message-ID: -Date: Tue, 16 Jun 2020 12:04:20 +0200 -MIME-Version: 1.0 -Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: 7bit - -YEAAAAAA!. -", - addr - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - let contacts = Contact::get_all(&t, 0, None as Option<&str>).await.unwrap(); - assert!(contacts.is_empty()); // The contact should not have been added to the db - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pdf_filename_simple() { - let t = TestContext::new_alice().await; - let msg = load_imf_email( - &t, - include_bytes!("../test-data/message/pdf_filename_simple.eml"), - ) - .await; - assert_eq!(msg.viewtype, Viewtype::File); - assert_eq!(msg.text.unwrap(), "mail body"); - assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_pdf_filename_continuation() { - // test filenames split across multiple header lines, see rfc 2231 - let t = TestContext::new_alice().await; - let msg = load_imf_email( - &t, - include_bytes!("../test-data/message/pdf_filename_continuation.eml"), - ) - .await; - assert_eq!(msg.viewtype, Viewtype::File); - assert_eq!(msg.text.unwrap(), "mail body"); - assert_eq!( - msg.param.get(Param::File).unwrap(), - "$BLOBDIR/test pdf äöüß.pdf" - ); - } - - /// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting, - /// twitter/facebook/whatever logos and so on. - /// that may easily be 50 and more images, one would not have these images in a chat. - /// - /// fortunately, if we remove them, they are accessible by get_msg_html() now. - /// - /// unfortunately, these images are not that easy to detect as they may also be on purpose, - /// or mua may use multipart/related not correctly - - /// so this test is in competition with parse_thunderbird_html_embedded_image() - /// that wants the image to be kept in the chat. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_many_images() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - include_bytes!("../test-data/message/many_images_amazon_via_apple_mail.eml"), - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert_eq!(msg.viewtype, Viewtype::Image); - assert!(msg.has_html()); - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); - } - - /// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To` - /// header. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_in_reply_to() { - let t = TestContext::new().await; - t.configure_addr("bob@example.com").await; - - // Receive message from Alice about group "foo". - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com, charlie@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello foo\n", - false, - ) - .await - .unwrap(); - - // Receive reply from Charlie without group ID but with In-Reply-To header. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: charlie@example.net\n\ - To: alice@example.org, bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - reply foo\n", - false, - ) - .await - .unwrap(); - - let msg = t.get_last_msg().await; - assert_eq!(msg.get_text().unwrap(), "reply foo"); - - // Load the first message from the same chat. - let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); - let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { - msg_id - } else { - panic!("Wrong item type"); - }; - - let reply_msg = Message::load_from_db(&t, *msg_id).await.unwrap(); - assert_eq!(reply_msg.get_text().unwrap(), "hello foo"); - - // Check that reply got into the same chat as the original message. - assert_eq!(msg.chat_id, reply_msg.chat_id); - - // Make sure we looked at real chat ID and do not just - // test that both messages got into the same virtual chat. - assert!(!msg.chat_id.is_special()); - } - - /// Test that classical MUA messages are assigned to group chats - /// based on the `In-Reply-To` header for two-member groups. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_in_reply_to_two_member_group() { - let t = TestContext::new().await; - t.configure_addr("bob@example.com").await; - - // Receive message from Alice about group "foo". - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello foo\n", - false, - ) - .await - .unwrap(); - - // Receive a classic MUA reply from Alice. - // It is assigned to the group chat. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - classic reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text().unwrap(), "classic reply"); - - // Receive a Delta Chat reply from Alice. - // It is assigned to group chat, because it has a group ID. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - chat reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to group chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(msg.get_text().unwrap(), "chat reply"); - - // Receive a private Delta Chat reply from Alice. - // It is assigned to 1:1 chat, because it has no group ID, - // which means it was created using "reply privately" feature. - // Normally it contains a quote, but it should not matter. - receive_imf( - &t, - b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: Re: foo\n\ - Message-ID: \n\ - In-Reply-To: \n\ - Chat-Version: 1.0\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - private reply\n", - false, - ) - .await - .unwrap(); - - // Ensure message is assigned to a 1:1 chat. - let msg = t.get_last_msg().await; - let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - assert_eq!(msg.get_text().unwrap(), "private reply"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_mime_headers_off() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let chat_alice = alice.create_chat(&bob).await; - chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("hi!".to_string())); - assert!(!msg.get_showpadlock()); - let mime = message::get_mime_headers(&bob, msg.id).await?; - assert!(mime.is_empty()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_save_mime_headers_on() -> anyhow::Result<()> { - let alice = TestContext::new_alice().await; - alice.set_config_bool(Config::SaveMimeHeaders, true).await?; - let bob = TestContext::new_bob().await; - bob.set_config_bool(Config::SaveMimeHeaders, true).await?; - - // alice sends a message to bob, bob sees full mime - let chat_alice = alice.create_chat(&bob).await; - chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; - - let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("hi!".to_string())); - assert!(!msg.get_showpadlock()); - let mime = message::get_mime_headers(&bob, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - - // another one, from bob to alice, that gets encrypted - let chat_bob = bob.create_chat(&alice).await; - chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; - let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; - assert_eq!(msg.get_text(), Some("ho!".to_string())); - assert!(msg.get_showpadlock()); - let mime = message::get_mime_headers(&alice, msg.id).await?; - let mime_str = String::from_utf8_lossy(&mime); - assert!(mime_str.contains("Message-ID:")); - assert!(mime_str.contains("From:")); - Ok(()) - } - - async fn create_test_alias( - chat_request: bool, - group_request: bool, - ) -> (TestContext, TestContext) { - // Claire, a customer, sends a support request - // to the alias address from a classic MUA. - // The alias expands to the supporters Alice and Bob. - // Check that Alice receives the message in a group chat. - let claire_request = if group_request { - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: support@example.org, ceo@example.org\n\ - From: claire@example.org\n\ - Subject: i have a question\n\ - Message-ID: \n\ - {}\ - Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ - Content-Type: text/plain\n\ - \n\ - hi support! what is the current version?", - if chat_request { - "Chat-Group-ID: 8ud29aridt29arid\n\ - Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n" - } else { - "" - } - ) - } else { - format!( - "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ - To: support@example.org\n\ - From: claire@example.org\n\ - Subject: i have a question\n\ - Message-ID: \n\ - {}\ - Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ - Content-Type: text/plain\n\ - \n\ - hi support! what is the current version?", - if chat_request { - "Chat-Version: 1.0\n" - } else { - "" - } - ) - }; - - let alice = TestContext::new_alice().await; - alice - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&alice, claire_request.as_bytes(), false) - .await - .unwrap(); - - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_subject(), "i have a question"); - assert!(msg.get_text().unwrap().contains("hi support!")); - let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Group); - assert_eq!(get_chat_msgs(&alice, chat.id, 0).await.unwrap().len(), 1); - if group_request { - assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 4); - } else { - assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 3); - } - assert_eq!(msg.get_override_sender_name(), None); - - let claire = TestContext::new().await; - claire.configure_addr("claire@example.org").await; - claire - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - receive_imf(&claire, claire_request.as_bytes(), false) - .await - .unwrap(); - - let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org") - .await - .unwrap() - .unwrap(); - - let msg = Message::load_from_db(&claire, msg_id).await.unwrap(); - msg.chat_id.accept(&claire).await.unwrap(); - assert_eq!(msg.get_subject(), "i have a question"); - assert!(msg.get_text().unwrap().contains("hi support!")); - let chat = Chat::load_from_db(&claire, msg.chat_id).await.unwrap(); - if group_request { - assert_eq!(chat.typ, Chattype::Group); - } else { - assert_eq!(chat.typ, Chattype::Single); - } - assert_eq!(get_chat_msgs(&claire, chat.id, 0).await.unwrap().len(), 1); - assert_eq!(msg.get_override_sender_name(), None); - - (claire, alice) - } - - async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) { - let (claire, alice) = create_test_alias(chat_request, group_request).await; - - // Check that Alice gets the message in the same chat. - let request = alice.get_last_msg().await; - receive_imf(&alice, reply, false).await.unwrap(); - let answer = alice.get_last_msg().await; - assert_eq!(answer.get_subject(), "Re: i have a question"); - assert!(answer.get_text().unwrap().contains("the version is 1.0")); - assert_eq!(answer.chat_id, request.chat_id); - let chat_contacts = get_chat_contacts(&alice, answer.chat_id) - .await - .unwrap() - .len(); - if group_request { - // Claire, Support, CEO and Alice (Bob is not added) - assert_eq!(chat_contacts, 4); - } else { - // Claire, Support and Alice - assert_eq!(chat_contacts, 3); - } - assert_eq!( - answer.get_override_sender_name().unwrap(), - "bob@example.net" - ); // Bob is not part of the group, so override-sender-name should be set - - // Check that Claire also gets the message in the same chat. - let request = claire.get_last_msg().await; - receive_imf(&claire, reply, false).await.unwrap(); - let answer = claire.get_last_msg().await; - assert_eq!(answer.get_subject(), "Re: i have a question"); - assert!(answer.get_text().unwrap().contains("the version is 1.0")); - assert_eq!(answer.chat_id, request.chat_id); - assert_eq!( - answer.get_override_sender_name().unwrap(), - "bob@example.net" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_alias_support_answer_from_nondc() { - // Bob, the other supporter, answers with a classic MUA. - let bob_answer = b"To: support@example.org, claire@example.org\n\ - From: bob@example.net\n\ - Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ - References: \n\ - In-Reply-To: \n\ - Message-ID: \n\ - Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ - Content-Type: text/plain\n\ - \n\ - hi claire, the version is 1.0, cheers bob"; - - check_alias_reply(bob_answer, true, true).await; - check_alias_reply(bob_answer, false, true).await; - check_alias_reply(bob_answer, true, false).await; - check_alias_reply(bob_answer, false, false).await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_alias_answer_from_dc() { - // Bob, the other supporter, answers with Delta Chat. - let bob_answer = b"To: support@example.org, claire@example.org\n\ - From: bob@example.net\n\ - Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ - References: \n\ - In-Reply-To: \n\ - Message-ID: \n\ - Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: af9e810c9b592927\n\ - Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\ - Chat-Disposition-Notification-To: bob@example.net\n\ - Content-Type: text/plain\n\ - \n\ - hi claire, the version is 1.0, cheers bob"; - - check_alias_reply(bob_answer, true, true).await; - check_alias_reply(bob_answer, false, true).await; - check_alias_reply(bob_answer, true, false).await; - check_alias_reply(bob_answer, false, false).await; - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_assign_to_trash_by_parent() { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - println!("\n========= Receive a message =========="); - receive_imf( - &t, - b"From: Nu Bar \n\ - To: alice@example.org, bob@example.org\n\ - Subject: Hi\n\ - Message-ID: <4444@example.org>\n\ - \n\ - hello\n", - false, - ) - .await - .unwrap(); - let chat_id = t.get_last_msg().await.chat_id; - chat_id.accept(&t).await.unwrap(); - let msg = get_chat_msg(&t, chat_id, 0, 1).await; // Make sure that the message is actually in the chat - assert!(!msg.chat_id.is_special()); - assert_eq!(msg.text.unwrap(), "Hi – hello"); - - println!("\n========= Delete the message =========="); - msg.id.trash(&t).await.unwrap(); - - let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); - assert_eq!(msgs.len(), 0); - - println!("\n========= Receive a message that is a reply to the deleted message =========="); - receive_imf( - &t, - b"From: Nu Bar \n\ - To: alice@example.org, bob@example.org\n\ - Subject: Re: Hi\n\ - Message-ID: <5555@example.org>\n\ - In-Reply-To: <4444@example.org\n\ - \n\ - Reply\n", - false, - ) - .await - .unwrap(); - let msg = t.get_last_msg().await; - assert!(!msg.chat_id.is_special()); // Esp. check that the chat_id is not TRASH - assert_eq!(msg.text.unwrap(), "Reply"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_dont_show_all_outgoing_msgs_in_self_chat() { - // Regression test for : - // Some servers add a `Bcc: ` header, which caused all outgoing messages to - // be shown in the self-chat. - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"Bcc: alice@example.org -Received: from [127.0.0.1] -Subject: s -Chat-Version: 1.0 -Message-ID: -To: -From: - -Message content", - false, - ) - .await - .unwrap(); - - let msg = t.get_last_msg().await; - assert_ne!(msg.chat_id, t.get_self_chat().await.id); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_outgoing_classic_mail_creates_chat() { - let alice = TestContext::new_alice().await; - - // Alice enables classic emails. - alice - .set_config(Config::ShowEmails, Some("2")) - .await - .unwrap(); - - // Alice downloads outgoing classic email. - receive_imf( - &alice, - b"Received: from [127.0.0.1] -Subject: Subj -Message-ID: -To: -From: - -Message content", - false, - ) - .await - .unwrap(); - - // Outgoing email should create a chat. - let msg = alice.get_last_msg().await; - assert_eq!(msg.get_text().unwrap(), "Subj – Message content"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_duplicate_message() -> Result<()> { - // Test that duplicate messages are ignored based on the Message-ID - let alice = TestContext::new_alice().await; - - let bob_contact_id = Contact::add_or_lookup( - &alice, - "Bob", - "bob@example.org", - Origin::IncomingUnknownFrom, - ) - .await? - .0; - - let first_message = b"Received: from [127.0.0.1] -Subject: First message -Message-ID: -To: Alice -From: Bob1 -Chat-Version: 1.0 - -Message content - --- -First signature"; - - let second_message = b"Received: from [127.0.0.1] -Subject: Second message -Message-ID: -To: Alice -From: Bob2 -Chat-Version: 1.0 - -Message content - --- -Second signature"; - - receive_imf(&alice, first_message, false).await?; - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "First signature"); - assert_eq!(contact.get_display_name(), "Bob1"); - - receive_imf(&alice, second_message, false).await?; - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "Second signature"); - assert_eq!(contact.get_display_name(), "Bob2"); - - // Duplicate message, should be ignored - receive_imf(&alice, first_message, false).await?; - - // No change because last message is duplicate of the first. - let contact = Contact::load_from_db(&alice, bob_contact_id).await?; - assert_eq!(contact.get_status(), "Second signature"); - assert_eq!(contact.get_display_name(), "Bob2"); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - let bob_id = Contact::add_or_lookup(&t, "", "bob@example.net", Origin::IncomingUnknownCc) - .await? - .0; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), ""); - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); - - receive_imf( - &t, - b"From: Bob -To: Alice -Message-ID: <1@example.org> -Subject: first message - -body 1 - --- -Original signature", - false, - ) - .await?; - let one2one_chat_id = t.get_last_msg().await.chat_id; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature"); - - receive_imf( - &t, - b"From: Bob -Sender: ml@example.net -To: Alice -Message-ID: <2@example.net> -Precedence: list -Subject: second message - -body 2 - --- -The modified signature --- -Tap here to unsubscribe ...", - false, - ) - .await?; - let ml_chat_id = t.get_last_msg().await.chat_id; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature"); - - receive_imf( - &t, - b"From: Bob -To: Alice -Message-ID: <3@example.org> -Subject: third message - -body 3 - --- -Original signature updated", - false, - ) - .await?; - let bob = Contact::load_from_db(&t, bob_id).await?; - assert_eq!(bob.get_status(), "Original signature updated"); - assert_eq!(get_chat_msgs(&t, one2one_chat_id, 0).await?.len(), 2); - assert_eq!(get_chat_msgs(&t, ml_chat_id, 0).await?.len(), 1); - assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 2); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_private_classical_reply() { - for outgoing_is_classical in &[true, false] { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -Date: Fri, 28 May 2021 10:15:05 +0000 -To: Bob , -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - receive_imf( - &t, - format!( - r#"Subject: Re: single reply-to -To: "Alice" -References: <{0}> - <{0}> -From: Bob -Message-ID: <028674eb-77f9-4ad1-1c30-e93e18b891c8@testrun.org> -Date: Fri, 28 May 2021 12:17:03 +0200 -User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 - Thunderbird/78.10.2 -MIME-Version: 1.0 -In-Reply-To: <{0}> - -Private reply"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let private_msg = t.get_last_msg().await; - assert_eq!(private_msg.text.unwrap(), "Private reply"); - let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); - assert_eq!(private_chat.typ, Chattype::Single); - assert_ne!(private_msg.chat_id, group_msg.chat_id); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_private_chat_reply() { - for (outgoing_is_classical, outgoing_has_multiple_recipients) in - &[(true, true), (false, true), (false, false)] - { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -Date: Fri, 28 May 2021 10:15:05 +0000 -To: Bob {} -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - }, - if *outgoing_has_multiple_recipients { - ", " - } else { - "" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - receive_imf( - &t, - format!( - r#"Subject: =?utf-8?q?Re=3A_single_reply-to?= -MIME-Version: 1.0 -In-Reply-To: <{0}> -Date: Sat, 03 Jul 2021 20:00:26 +0000 -Chat-Version: 1.0 -Message-ID: -To: -From: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -> Hello, I've just created the group "single reply-to" for us. - -Private reply - -=2D- -Sent with my Delta Chat Messenger: https://delta.chat - -"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let private_msg = t.get_last_msg().await; - assert_eq!(private_msg.text.unwrap(), "Private reply"); - let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); - assert_eq!(private_chat.typ, Chattype::Single); - assert_ne!(private_msg.chat_id, group_msg.chat_id); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_nonprivate_classical_reply() { - for outgoing_is_classical in &[true, false] { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); - - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: =?utf-8?q?single_reply-to?= -{} -To: Bob , -From: Alice -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no -Content-Transfer-Encoding: quoted-printable - -Hello, I've just created the group "single reply-to" for us."#, - if *outgoing_is_classical { - r"Message-ID: abcd@gmx.de" - } else { - r"Chat-Group-ID: eJ_llQIXf0K -Chat-Group-Name: =?utf-8?q?single_reply-to?= -References: -Chat-Version: 1.0 -Message-ID: " - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let group_msg = t.get_last_msg().await; - assert_eq!( - group_msg.text.unwrap(), - if *outgoing_is_classical { - "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." - } else { - "Hello, I've just created the group \"single reply-to\" for us." - } - ); - let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!(group_chat.name, "single reply-to"); - - // =============== Receive another outgoing message and check that it is put into the same chat =============== - receive_imf( - &t, - format!( - r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: Out subj -To: "Bob" , "Claire" -From: Alice -Message-ID: -MIME-Version: 1.0 -In-Reply-To: <{0}> - -Outgoing reply to all"#, - if *outgoing_is_classical { - "abcd@gmx.de" - } else { - "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" - } - ) - .as_bytes(), - false, - ) - .await - .unwrap(); - - let reply = t.get_last_msg().await; - assert_eq!(reply.text.unwrap(), "Out subj – Outgoing reply to all"); - let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); - assert_eq!(reply_chat.typ, Chattype::Group); - assert_eq!(reply.chat_id, group_msg.chat_id); - - // =============== Receive an incoming message and check that it is put into the same chat =============== - receive_imf( - &t, - br#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) -Subject: In subj -To: "Bob" , "Claire" -From: alice -Message-ID: -MIME-Version: 1.0 -In-Reply-To: - -Reply to all"#, - false, - ) - .await - .unwrap(); - - let reply = t.get_last_msg().await; - assert_eq!(reply.text.unwrap(), "In subj – Reply to all"); - let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); - assert_eq!(reply_chat.typ, Chattype::Group); - assert_eq!(reply.chat_id, group_msg.chat_id); - } - } - - /// Tests that replies to similar ad hoc groups are correctly assigned to chats. - /// - /// The difficutly here is that ad hoc groups don't have unique group IDs, because both - /// messages have the same recipient lists and only differ in the subject and message contents. - /// The messages can be properly assigned to chats only using the In-Reply-To or References - /// headers. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_chat_assignment_adhoc() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice.set_config(Config::ShowEmails, Some("2")).await?; - bob.set_config(Config::ShowEmails, Some("2")).await?; - - let first_thread_mime = br#"Subject: First thread -Message-ID: first@example.org -To: Alice , Bob -From: Claire -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First thread."#; - let second_thread_mime = br#"Subject: Second thread -Message-ID: second@example.org -To: Alice , Bob -From: Claire -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Second thread."#; - - // Alice receives two classic emails from Claire. - receive_imf(&alice, first_thread_mime, false).await?; - let alice_first_msg = alice.get_last_msg().await; - receive_imf(&alice, second_thread_mime, false).await?; - let alice_second_msg = alice.get_last_msg().await; - - // Bob receives the same two emails. - receive_imf(&bob, first_thread_mime, false).await?; - let bob_first_msg = bob.get_last_msg().await; - receive_imf(&bob, second_thread_mime, false).await?; - let bob_second_msg = bob.get_last_msg().await; - - // Messages go to separate chats both for Alice and Bob. - assert!(alice_first_msg.chat_id != alice_second_msg.chat_id); - assert!(bob_first_msg.chat_id != bob_second_msg.chat_id); - - // Alice replies to both chats. Bob receives two messages and assigns them to corresponding - // chats. - alice_first_msg.chat_id.accept(&alice).await?; - let alice_first_reply = alice - .send_text(alice_first_msg.chat_id, "First reply") - .await; - let bob_first_reply = bob.recv_msg(&alice_first_reply).await; - assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id); - - alice_second_msg.chat_id.accept(&alice).await?; - let alice_second_reply = alice - .send_text(alice_second_msg.chat_id, "Second reply") - .await; - let bob_second_reply = bob.recv_msg(&alice_second_reply).await; - assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); - - // Alice adds Fiona to both ad hoc groups. - let fiona = TestContext::new_fiona().await; - let (alice_fiona_contact_id, _) = Contact::add_or_lookup( - &alice, - "Fiona", - "fiona@example.net", - Origin::IncomingUnknownTo, - ) - .await?; - - chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; - let alice_first_invite = alice.pop_sent_msg().await; - let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await; - - chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?; - let alice_second_invite = alice.pop_sent_msg().await; - let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await; - - // Fiona was added to two separate chats and should see two separate chats, even though they - // don't have different group IDs to distinguish them. - assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id); - - Ok(()) - } - - /// Test that read receipts don't create chats. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_read_receipts_dont_create_chats() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - let alice_chat = alice.create_chat(&bob).await; - - // Alice sends a message to Bob. - assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); - bob.recv_msg(&alice.send_text(alice_chat.id, "Message").await) - .await; - let received_msg = bob.get_last_msg().await; - - // Alice deletes the chat. - alice_chat.id.delete(&alice).await?; - let chats = Chatlist::try_load(&alice, 0, None, None).await?; - assert_eq!(chats.len(), 0); - - // Bob sends a read receipt. - let mdn_mimefactory = - crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?; - let rendered_mdn = mdn_mimefactory.render(&bob).await?; - let mdn_body = rendered_mdn.message; - - // Alice receives the read receipt. - receive_imf(&alice, mdn_body.as_bytes(), false).await?; - - // Chat should not pop up in the chatlist. - let chats = Chatlist::try_load(&alice, 0, None, None).await?; - assert_eq!(chats.len(), 0); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_gmx_forwarded_msg() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - receive_imf( - &t, - include_bytes!("../test-data/message/gmx-forward.eml"), - false, - ) - .await?; - - let msg = t.get_last_msg().await; - assert!(msg.has_html()); - assert_eq!(msg.id.get_html(&t).await?.unwrap().replace("\r\n", "\n"), "
 
\n\n
 \n
 \n
\n
Gesendet: Donnerstag, 12. August 2021 um 15:52 Uhr
\nVon: "Claire" <claire@example.org>
\nAn: alice@example.org
\nBetreff: subject
\n\n
bodytext
\n
\n
\n
\n\n"); - - Ok(()) - } - - /// Tests that user is notified about new incoming contact requests. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_incoming_contact_request() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf(&t, MSGRMSG, false).await?; - let msg = t.get_last_msg().await; - let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; - assert!(chat.is_contact_request()); - - loop { - let event = t - .evtracker - .get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. })) - .await; - match event { - EventType::IncomingMsg { chat_id, msg_id } => { - assert_eq!(msg.chat_id, chat_id); - assert_eq!(msg.id, msg_id); - return Ok(()); - } - _ => unreachable!(), - } - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_get_parent_message() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - let mime = br#"Subject: First -Message-ID: first@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let first = t.get_last_msg().await; - let mime = br#"Subject: Second -Message-ID: second@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let second = t.get_last_msg().await; - let mime = br#"Subject: Third -Message-ID: third@example.net -To: Alice -From: Bob -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -First."#; - receive_imf(&t, mime, false).await?; - let third = t.get_last_msg().await; - - let mime = br#"Subject: Message with references. -Message-ID: second@example.net -To: Alice -From: Bob -In-Reply-To: -References: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Message with references."#; - let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?; - - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, first.id); - - message::delete_msgs(&t, &[first.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, second.id); - - message::delete_msgs(&t, &[second.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); - assert_eq!(parent.id, third.id); - - message::delete_msgs(&t, &[third.id]).await?; - let parent = get_parent_message(&t, &mime_parser).await?; - assert!(parent.is_none()); - - Ok(()) - } - - /// Test a message with RFC 1847 encapsulation as created by Thunderbird. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_rfc1847_encapsulation() -> Result<()> { - let alice = TestContext::new_alice().await; - let bob = TestContext::new_bob().await; - alice.configure_addr("alice@example.org").await; - - // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. - let chat_alice = alice.create_chat(&bob).await; - let first_msg = alice - .send_text(chat_alice.id, "Sending Alice key to Bob.") - .await; - bob.recv_msg(&first_msg).await; - message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?; - - bob.set_config(Config::ShowEmails, Some("2")).await?; - - // Alice sends a message to Bob using Thunderbird. - let raw = include_bytes!("../test-data/message/rfc1847_encapsulation.eml"); - receive_imf(&bob, raw, false).await?; - - let msg = bob.get_last_msg().await; - assert!(msg.get_showpadlock()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_invalid_to_address() -> Result<()> { - let alice = TestContext::new_alice().await; - - let mime = include_bytes!("../test-data/message/invalid_email_to.eml"); - - // receive_imf should not fail on this mail with invalid To: field - receive_imf(&alice, mime, false).await?; - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_reply_from_different_addr() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - // Alice creates a 2-person-group with Bob - receive_imf( - &t, - br#"Subject: =?utf-8?q?Januar_13-19?= -Chat-Group-ID: qetqsutor7a -Chat-Group-Name: =?utf-8?q?Januar_13-19?= -MIME-Version: 1.0 -References: -Date: Mon, 20 Dec 2021 12:15:01 +0000 -Chat-Version: 1.0 -Message-ID: -To: -From: -Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no - -Hi, I created a group"#, - false, - ) - .await?; - let msg_out = t.get_last_msg().await; - assert_eq!(msg_out.from_id, ContactId::SELF); - assert_eq!(msg_out.text.unwrap(), "Hi, I created a group"); - assert_eq!(msg_out.in_reply_to, None); - - // Bob replies from a different address - receive_imf( - &t, - b"Content-Type: text/plain; charset=utf-8 -Content-Transfer-Encoding: quoted-printable -From: -Mime-Version: 1.0 (1.0) -Subject: Re: Januar 13-19 -Date: Mon, 20 Dec 2021 13:54:55 +0100 -Message-Id: -References: -In-Reply-To: -To: holger - -Reply from different address -", - false, - ) - .await?; - let msg_in = t.get_last_msg().await; - assert_eq!(msg_in.to_id, ContactId::SELF); - assert_eq!(msg_in.text.unwrap(), "Reply from different address"); - assert_eq!( - msg_in.in_reply_to.unwrap(), - "Gr.qetqsutor7a.Aresxresy-4@deltachat.de" - ); - assert_eq!( - msg_in.param.get(Param::OverrideSenderDisplayname), - Some("bob-alias@example.com") - ); - - assert_eq!(msg_in.chat_id, msg_out.chat_id); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_long_filenames() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - for filename_sent in &[ - "foo.bar very long file name test baz.tar.gz", - "foobarabababababababbababababverylongfilenametestbaz.tar.gz", - "fooo...tar.gz", - "foo. .tar.gz", - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz", - "a.tar.gz", - "a.a..a.a.a.a.tar.gz", - ] { - let attachment = alice.blobdir.join(filename_sent); - let content = format!("File content of {}", filename_sent); - tokio::fs::write(&attachment, content.as_bytes()).await?; - - let mut msg_alice = Message::new(Viewtype::File); - msg_alice.set_file(attachment.to_str().unwrap(), None); - let alice_chat = alice.create_chat(&bob).await; - let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await; - println!("{}", sent.payload()); - - let msg_bob = bob.recv_msg(&sent).await; - - async fn check_message(msg: &Message, t: &TestContext, content: &str) { - assert_eq!(msg.get_viewtype(), Viewtype::File); - let resulting_filename = msg.get_filename().unwrap(); - let path = msg.get_file(t).unwrap(); - assert!( - resulting_filename.ends_with(".tar.gz"), - "{:?} doesn't end with .tar.gz, path: {:?}", - resulting_filename, - path - ); - assert!( - path.to_str().unwrap().ends_with(".tar.gz"), - "path {:?} doesn't end with .tar.gz", - path - ); - assert_eq!(fs::read_to_string(path).await.unwrap(), content); - } - check_message(&msg_alice, &alice, &content).await; - check_message(&msg_bob, &bob, &content).await; - } - - Ok(()) - } - - /// Tests that contact request is accepted automatically on outgoing message. - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_accept_outgoing() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice1 = tcm.alice().await; - let alice2 = tcm.alice().await; - let bob1 = tcm.bob().await; - let bob2 = tcm.bob().await; - - let bob1_chat = bob1.create_chat(&alice1).await; - let sent = bob1.send_text(bob1_chat.id, "Hello!").await; - - alice1.recv_msg(&sent).await; - alice2.recv_msg(&sent).await; - let alice1_msg = bob2.recv_msg(&sent).await; - assert_eq!(alice1_msg.text.unwrap(), "Hello!"); - let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?; - assert!(alice1_chat.is_contact_request()); - - let alice2_msg = alice2.get_last_msg().await; - assert_eq!(alice2_msg.text.unwrap(), "Hello!"); - let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; - assert!(alice2_chat.is_contact_request()); - - let bob1_msg = bob1.get_last_msg().await; - assert_eq!(bob1_msg.text.unwrap(), "Hello!"); - let bob1_chat = chat::Chat::load_from_db(&bob1, bob1_msg.chat_id).await?; - assert!(!bob1_chat.is_contact_request()); - - let bob2_msg = bob2.get_last_msg().await; - assert_eq!(bob2_msg.text.unwrap(), "Hello!"); - let bob2_chat = chat::Chat::load_from_db(&bob2, bob2_msg.chat_id).await?; - assert!(!bob2_chat.is_contact_request()); - - // Alice sends reply. - alice1_msg.chat_id.accept(&alice1).await.unwrap(); - let sent = alice1.send_text(alice1_chat.id, "Hi!").await; - alice2.recv_msg(&sent).await; - - // Second device automatically accepts the contact request. - let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; - assert!(!alice2_chat.is_contact_request()); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_outgoing_private_reply_multidevice() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice1 = tcm.alice().await; - let alice2 = tcm.alice().await; - let bob = tcm.bob().await; - - // =============== Bob creates a group =============== - let group_id = - chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; - chat::add_to_chat_contacts_table( - &bob, - group_id, - &[ - bob.add_or_lookup_contact(&alice1).await.id, - Contact::create(&bob, "", "charlie@example.org").await?, - ], - ) - .await?; - - // =============== Bob sends the first message to the group =============== - let sent = bob.send_text(group_id, "Hello all!").await; - alice1.recv_msg(&sent).await; - alice2.recv_msg(&sent).await; - - // =============== Alice answers privately with device 1 =============== - let received = alice1.get_last_msg().await; - let alice1_bob_contact = alice1.add_or_lookup_contact(&bob).await; - assert_eq!(received.from_id, alice1_bob_contact.id); - assert_eq!(received.to_id, ContactId::SELF); - assert!(!received.hidden); - assert_eq!(received.text, Some("Hello all!".to_string())); - assert_eq!(received.in_reply_to, None); - assert_eq!(received.chat_blocked, Blocked::Request); - - let received_group = Chat::load_from_db(&alice1, received.chat_id).await?; - assert_eq!(received_group.typ, Chattype::Group); - assert_eq!(received_group.name, "Group"); - assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request - - let mut msg_out = Message::new(Viewtype::Text); - msg_out.set_text(Some("Private reply".to_string())); - - assert_eq!(received_group.blocked, Blocked::Request); - msg_out.set_quote(&alice1, Some(&received)).await?; - let alice1_bob_chat = alice1.create_chat(&bob).await; - let sent2 = alice1.send_msg(alice1_bob_chat.id, &mut msg_out).await; - alice2.recv_msg(&sent2).await; - - // =============== Alice's second device receives the message =============== - let received = alice2.get_last_msg().await; - - // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: - assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id); - - let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; - assert_eq!(received.from_id, ContactId::SELF); - assert_eq!(received.to_id, alice2_bob_contact.id); - assert!(!received.hidden); - assert_eq!(received.text, Some("Private reply".to_string())); - assert_eq!( - received.parent(&alice2).await?.unwrap().text, - Some("Hello all!".to_string()) - ); - assert_eq!(received.chat_blocked, Blocked::Not); - - let received_chat = Chat::load_from_db(&alice2, received.chat_id).await?; - assert_eq!(received_chat.typ, Chattype::Single); - assert_eq!(received_chat.name, "bob@example.net"); - assert_eq!(received_chat.can_send(&alice2).await?, true); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_auto_accept_for_bots() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config(Config::Bot, Some("1")).await.unwrap(); - receive_imf(&t, MSGRMSG, false).await?; - let msg = t.get_last_msg().await; - let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; - assert!(!chat.is_contact_request()); - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_no_private_reply_to_blocked_account() -> Result<()> { - let mut tcm = TestContextManager::new(); - let alice = tcm.alice().await; - let bob = tcm.bob().await; - - // =============== Bob creates a group =============== - let group_id = - chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; - chat::add_to_chat_contacts_table( - &bob, - group_id, - &[bob.add_or_lookup_contact(&alice).await.id], - ) - .await?; - - // =============== Bob sends the first message to the group =============== - let sent = bob.send_text(group_id, "Hello all!").await; - alice.recv_msg(&sent).await; - - let chats = Chatlist::try_load(&bob, 0, None, None).await?; - assert_eq!(chats.len(), 1); - - // =============== Bob blocks Alice ================ - Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; - - // =============== Alice replies private to Bob ============== - let received = alice.get_last_msg().await; - assert_eq!(received.text, Some("Hello all!".to_string())); - - let received_group = Chat::load_from_db(&alice, received.chat_id).await?; - assert_eq!(received_group.typ, Chattype::Group); - - let mut msg_out = Message::new(Viewtype::Text); - msg_out.set_text(Some("Private reply".to_string())); - msg_out.set_quote(&alice, Some(&received)).await?; - - let alice_bob_chat = alice.create_chat(&bob).await; - let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await; - bob.recv_msg(&sent2).await; - - // ========= check that no contact request was created ============ - let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 1); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - - // since only chat is a group, no new open chat has been created - assert_eq!(chat.typ, Chattype::Group); - let received = bob.get_last_msg().await; - assert_eq!(received.text, Some("Hello all!".to_string())); - - // =============== Bob unblocks Alice ================ - // test if the blocked chat is restored correctly - Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; - let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - let chat_id = chats.get_chat_id(0).unwrap(); - let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); - assert_eq!(chat.typ, Chattype::Single); - let received = bob.get_last_msg().await; - assert_eq!(received.text, Some("Private reply".to_string())); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_thunderbird_autocrypt() -> Result<()> { - let t = TestContext::new_bob().await; - t.set_config(Config::ShowEmails, Some("2")).await?; - - let raw = include_bytes!("../test-data/message/thunderbird_with_autocrypt.eml"); - receive_imf(&t, raw, false).await?; - - let peerstate = Peerstate::from_addr(&t, "alice@example.org") - .await? - .unwrap(); - assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mua_user_adds_member() -> Result<()> { - let t = TestContext::new_alice().await; - - receive_imf( - &t, - b"From: alice@example.org\n\ - To: bob@example.com\n\ - Subject: foo\n\ - Message-ID: \n\ - Chat-Version: 1.0\n\ - Chat-Group-ID: gggroupiddd\n\ - Chat-Group-Name: foo\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await? - .unwrap(); - - receive_imf( - &t, - b"From: bob@example.com\n\ - To: alice@example.org, fiona@example.net\n\ - Subject: foo\n\ - Message-ID: \n\ - In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ - Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ - \n\ - hello\n", - false, - ) - .await? - .unwrap(); - - let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd") - .await? - .unwrap(); - let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?; - actual_chat_contacts.sort(); - let mut expected_chat_contacts = vec![ - Contact::create(&t, "", "bob@example.com").await?, - Contact::create(&t, "", "fiona@example.net").await?, - ContactId::SELF, - ]; - expected_chat_contacts.sort(); - assert_eq!(actual_chat_contacts, expected_chat_contacts); - - Ok(()) - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> { - let alice = TestContext::new_alice().await; - - // Alice sends a 1:1 message to Bob, creating a 1:1 chat. - let msg = receive_imf( - &alice, - b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ - From: alice@example.org\r\n\ - To: \r\n\ - Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\ - Message-ID: \r\n\ - Chat-Version: 1.0\r\n\ - \r\n\ - tst\r\n", - false, - ) - .await? - .unwrap(); - let single_chat = Chat::load_from_db(&alice, msg.chat_id).await?; - assert_eq!(single_chat.typ, Chattype::Single); - - // Bob uses a classical MUA to answer in the 1:1 chat. - let msg2 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: \r\n\ - Date: Mon, 12 Dec 2022 14:31:39 +0000\r\n\ - Message-ID: \r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_eq!(msg2.chat_id, single_chat.id); - - // Bob uses a classical MUA to answer again, this time adding a recipient. - // This message should go to a newly created ad-hoc group. - let msg3 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: , \r\n\ - Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\ - Message-ID: \r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_ne!(msg3.chat_id, single_chat.id); - let group_chat = Chat::load_from_db(&alice, msg3.chat_id).await?; - assert_eq!(group_chat.typ, Chattype::Group); - assert_eq!( - chat::get_chat_contacts(&alice, group_chat.id).await?.len(), - 3 - ); - - // Bob uses a classical MUA to answer once more, adding another recipient. - // This new recipient should also be added to the group. - let msg4 = receive_imf( - &alice, - b"Subject: Re: Message from alice\r\n\ - From: \r\n\ - To: , , \r\n\ - Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ - Message-ID: <69573857-542f-0fx3-55da-1289be5e0efe@example.net>\r\n\ - In-Reply-To: \r\n\ - \r\n\ - Hi back!\r\n", - false, - ) - .await? - .unwrap(); - assert_eq!(msg4.chat_id, group_chat.id); - assert_eq!( - chat::get_chat_contacts(&alice, group_chat.id).await?.len(), - 4 - ); - let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo) - .await? - .unwrap(); - assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?); - - Ok(()) - } -} +mod tests; diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs new file mode 100644 index 000000000..cd5dcd30e --- /dev/null +++ b/src/receive_imf/tests.rs @@ -0,0 +1,3248 @@ +use tokio::fs; + +use super::*; + +use crate::aheader::EncryptPreference; +use crate::chat::get_chat_contacts; +use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility}; +use crate::chatlist::Chatlist; +use crate::constants::DC_GCL_NO_SPECIALS; +use crate::imap::prefetch_should_download; +use crate::message::Message; +use crate::test_utils::{get_chat_msg, TestContext, TestContextManager}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_grpid_simple() { + let context = TestContext::new_alice().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello@example.org\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), None); + let grpid = Some("HcxyMARjyJy"); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_bad_from() { + let context = TestContext::new_alice().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes_with_partial(&context.ctx, &raw[..], None).await; + assert!(mimeparser.is_err()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_grpid_from_multiple() { + let context = TestContext::new_alice().await; + let raw = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: hello@example.org\n\ + Subject: outer-subject\n\ + In-Reply-To: \n\ + References: , \n\ + \n\ + hello\x00"; + let mimeparser = MimeMessage::from_bytes(&context.ctx, &raw[..]) + .await + .unwrap(); + let grpid = Some("HcxyMARjyJy"); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::InReplyTo), grpid); + assert_eq!(extract_grpid(&mimeparser, HeaderDef::References), grpid); +} + +static MSGRMSG: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Chat-Version: 1.0\n\ + Subject: Chat: hello\n\ + Message-ID: \n\ + Date: Sun, 22 Mar 2020 22:37:55 +0000\n\ + \n\ + hello\n"; + +static ONETOONE_NOREPLY_MAIL: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: alice@example.org\n\ + Subject: Chat: hello\n\ + Message-ID: <2222@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:56 +0000\n\ + \n\ + hello\n"; + +static GRP_MAIL: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org, claire@example.com\n\ + Subject: group with Alice, Bob and Claire\n\ + Message-ID: <3333@example.com>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_chats_only() { + let t = TestContext::new_alice().await; + assert_eq!(t.get_config_int(Config::ShowEmails).await.unwrap(), 0); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + receive_imf(&t, MSGRMSG, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + + receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_unknown() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with unknown contacts with show_emails=accepted is ignored for unknown contacts + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_known() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + Contact::create(&t, "Bob", "bob@example.com").await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with known contacts with show_emails=accepted is still ignored for known contacts + // (and existent chat is required) + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_accepted_contact_accepted() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("1")).await.unwrap(); + + // accept Bob by accepting a delta-message from Bob + receive_imf(&t, MSGRMSG, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + assert!(!chat_id.is_special()); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + assert_eq!(chat.name, "Bob"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1); + assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1); + + // receive a non-delta-message from Bob, shows up because of the show_emails setting + receive_imf(&t, ONETOONE_NOREPLY_MAIL, false).await.unwrap(); + + assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2); + + // let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.name, "group with Alice, Bob and Claire"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_adhoc_group_show_all() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + receive_imf(&t, GRP_MAIL, false).await.unwrap(); + + // adhoc-group with unknown contacts with show_emails=all will show up in a single chat + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(chat.name, "group with Alice, Bob and Claire"); + assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 3); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_read_receipt_and_unarchive() -> Result<()> { + // create alice's account + let t = TestContext::new_alice().await; + + let bob_id = Contact::create(&t, "bob", "bob@example.com").await?; + let one2one_id = ChatId::create_for_contact(&t, bob_id).await?; + one2one_id + .set_visibility(&t, ChatVisibility::Archived) + .await + .unwrap(); + let one2one = Chat::load_from_db(&t, one2one_id).await?; + assert!(one2one.get_visibility() == ChatVisibility::Archived); + + // create a group with bob, archive group + let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; + chat::add_contact_to_chat(&t, group_id, bob_id).await?; + assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await.unwrap().len(), 0); + group_id + .set_visibility(&t, ChatVisibility::Archived) + .await?; + let group = Chat::load_from_db(&t, group_id).await?; + assert!(group.get_visibility() == ChatVisibility::Archived); + + // everything archived, chatlist should be empty + assert_eq!( + Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + .await? + .len(), + 0 + ); + + // send a message to group with bob + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: {}\n\ + Chat-Group-Name: foo\n\ + Chat-Disposition-Notification-To: alice@example.org\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + group.grpid, group.grpid + ) + .as_bytes(), + false, + ) + .await?; + let msg = get_chat_msg(&t, group_id, 0, 1).await; + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.state, MessageState::OutDelivered); + let group = Chat::load_from_db(&t, group_id).await?; + assert!(group.get_visibility() == ChatVisibility::Normal); + + // bob sends a read receipt to the group + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: bob@example.com\n\ + To: alice@example.org\n\ + Subject: message opened\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + Chat-Version: 1.0\n\ + Message-ID: \n\ + Content-Type: multipart/report; report-type=disposition-notification; boundary=\"SNIPP\"\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: text/plain; charset=utf-8\n\ + \n\ + Read receipts do not guarantee sth. was read.\n\ + \n\ + \n\ + --SNIPP\n\ + Content-Type: message/disposition-notification\n\ + \n\ + Reporting-UA: Delta Chat 1.28.0\n\ + Original-Recipient: rfc822;bob@example.com\n\ + Final-Recipient: rfc822;bob@example.com\n\ + Original-Message-ID: \n\ + Disposition: manual-action/MDN-sent-automatically; displayed\n\ + \n\ + \n\ + --SNIPP--", + group.grpid + ) + .as_bytes(), + false, + ) + .await?; + assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await?.len(), 1); + let msg = message::Message::load_from_db(&t, msg.id).await?; + assert_eq!(msg.state, MessageState::OutMdnRcvd); + + // check, the read-receipt has not unarchived the one2one + assert_eq!( + Chatlist::try_load(&t, DC_GCL_NO_SPECIALS, None, None) + .await? + .len(), + 1 + ); + let one2one = Chat::load_from_db(&t, one2one_id).await?; + assert!(one2one.get_visibility() == ChatVisibility::Archived); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_from() { + // if there is no from given, from_id stays 0 which is just fine. These messages + // are very rare, however, we have to add them to the database + // to avoid a re-download from the server. + + let t = TestContext::new_alice().await; + let context = &t; + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert!(chats.get_msg_id(0).is_err()); + + receive_imf( + context, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: <3924@example.com>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + // Check that the message is not shown to the user: + assert!(chats.is_empty()); + + // Check that the message was added to the db: + assert!(message::rfc724_mid_exists(context, "3924@example.com") + .await + .unwrap() + .is_some()); +} + +/// If there is no Message-Id header, we generate a random id. +/// But there is no point in adding a trash entry in the database +/// if the email is malformed (e.g. because `From` is missing) +/// with this random id we just generated. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_message_id_header() { + let t = TestContext::new_alice().await; + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert!(chats.get_msg_id(0).is_err()); + + let received = receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + dbg!(&received); + assert!(received.is_none()); + + assert!(!t + .sql + .exists( + "SELECT COUNT(*) FROM msgs WHERE chat_id=?;", + paramsv![DC_CHAT_ID_TRASH], + ) + .await + .unwrap()); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + // Check that the message is not shown to the user: + assert!(chats.is_empty()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_escaped_from() { + let t = TestContext::new_alice().await; + let contact_id = Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + let chat_id = ChatId::create_for_contact(&t, contact_id).await.unwrap(); + receive_imf( + &t, + b"From: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ + To: alice@example.org\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ).await.unwrap(); + assert_eq!( + Contact::load_from_db(&t, contact_id) + .await + .unwrap() + .get_authname(), + "Имя, Фамилия", + ); + let msg = get_chat_msg(&t, chat_id, 0, 1).await; + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_escaped_recipients() { + let t = TestContext::new_alice().await; + Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + + let carl_contact_id = Contact::add_or_lookup( + &t, + "Carl", + ContactAddress::new("carl@host.tld").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap() + .0; + + receive_imf( + &t, + b"From: Foobar \n\ + To: =?UTF-8?B?0JjQvNGPLCDQpNCw0LzQuNC70LjRjw==?= alice@example.org\n\ + Cc: =?utf-8?q?=3Ch2=3E?= \n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "h2"); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let msg = Message::load_from_db(&t, chats.get_msg_id(0).unwrap().unwrap()) + .await + .unwrap(); + assert_eq!(msg.is_dc_message, MessengerMessage::Yes); + assert_eq!(msg.text.unwrap(), "hello"); + assert_eq!(msg.param.get_int(Param::WantsMdn).unwrap(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_cc_to_contact() { + let t = TestContext::new_alice().await; + Contact::create(&t, "foobar", "foobar@example.com") + .await + .unwrap(); + + let carl_contact_id = Contact::add_or_lookup( + &t, + "garabage", + ContactAddress::new("carl@host.tld").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await + .unwrap() + .0; + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Foobar \n\ + To: alice@example.org\n\ + Cc: Carl \n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Disposition-Notification-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let contact = Contact::load_from_db(&t, carl_contact_id).await.unwrap(); + assert_eq!(contact.get_name(), ""); + assert_eq!(contact.get_display_name(), "Carl"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_tiscali() { + test_parse_ndn( + "alice@tiscali.it", + "shenauithz@testrun.org", + "Mr.un2NYERi1RM.lbQ5F9q-QyJ@tiscali.it", + include_bytes!("../../test-data/message/tiscali_ndn.eml"), + Some("Delivery status notification – This is an automatically generated Delivery Status Notification. \n\nDelivery to the following recipients was aborted after 2 second(s):\n\n * shenauithz@testrun.org"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_testrun() { + test_parse_ndn( + "alice@testrun.org", + "hcksocnsofoejx@five.chat", + "Mr.A7pTA5IgrUA.q4bP41vAJOp@testrun.org", + include_bytes!("../../test-data/message/testrun_ndn.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mail.five.chat[195.62.125.103] said: 550 5.1.1\n : Recipient address rejected: User unknown in\n virtual mailbox table (in reply to RCPT TO command)"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_yahoo() { + test_parse_ndn( + "alice@yahoo.com", + "haeclirth.sinoenrat@yahoo.com", + "1680295672.3657931.1591783872936@mail.yahoo.com", + include_bytes!("../../test-data/message/yahoo_ndn.eml"), + Some("Failure Notice – Sorry, we were unable to deliver your message to the following address.\n\n:\n554: delivery error: dd Not a valid recipient - atlas117.free.mail.ne1.yahoo.com [...]"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_gmail() { + test_parse_ndn( + "alice@gmail.com", + "assidhfaaspocwaeofi@gmail.com", + "CABXKi8zruXJc_6e4Dr087H5wE7sLp+u250o0N2q5DdjF_r-8wg@mail.gmail.com", + include_bytes!("../../test-data/message/gmail_ndn.eml"), + Some("Delivery Status Notification (Failure) – ** Die Adresse wurde nicht gefunden **\n\nIhre Nachricht wurde nicht an assidhfaaspocwaeofi@gmail.com zugestellt, weil die Adresse nicht gefunden wurde oder keine E-Mails empfangen kann.\n\nHier erfahren Sie mehr: https://support.google.com/mail/?p=NoSuchUser\n\nAntwort:\n\n550 5.1.1 The email account that you tried to reach does not exist. Please try double-checking the recipient\'s email address for typos or unnecessary spaces. Learn more at https://support.google.com/mail/?p=NoSuchUser i18sor6261697wrs.38 - gsmtp"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_gmx() { + test_parse_ndn( + "alice@gmx.com", + "snaerituhaeirns@gmail.com", + "9c9c2a32-056b-3592-c372-d7e8f0bd4bc2@gmx.de", + include_bytes!("../../test-data/message/gmx_ndn.eml"), + Some("Mail delivery failed: returning message to sender – This message was created automatically by mail delivery software.\n\nA message that you sent could not be delivered to one or more of\nits recipients. This is a permanent error. The following address(es)\nfailed:\n\nsnaerituhaeirns@gmail.com:\nSMTP error from remote server for RCPT TO command, host: gmail-smtp-in.l.google.com (66.102.1.27) reason: 550-5.1.1 The email account that you tried to reach does not exist. Please\n try\n550-5.1.1 double-checking the recipient\'s email address for typos or\n550-5.1.1 unnecessary spaces. Learn more at\n550 5.1.1 https://support.google.com/mail/?p=NoSuchUser f6si2517766wmc.21\n9 - gsmtp [...]"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_posteo() { + test_parse_ndn( + "alice@posteo.org", + "hanerthaertidiuea@gmx.de", + "04422840-f884-3e37-5778-8192fe22d8e1@posteo.de", + include_bytes!("../../test-data/message/posteo_ndn.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host mout01.posteo.de.\n\nI\'m sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It\'s attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx01.emig.gmx.net[212.227.17.5] said: 550\n Requested action not taken: mailbox unavailable (in reply to RCPT TO\n command)"), + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_testrun_2() { + test_parse_ndn( + "alice@example.org", + "bob@example.org", + "Mr.5xqflwt0YFv.IXDFfHauvWx@testrun.org", + include_bytes!("../../test-data/message/testrun_ndn_2.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host hq5.merlinux.eu.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: Host or domain name not found. Name service error for\n name=echedelyr.tk type=AAAA: Host not found"), + ) + .await; +} + +/// Tests that text part is not squashed into OpenPGP attachment. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_with_attachment() { + test_parse_ndn( + "alice@example.org", + "bob@example.net", + "Mr.I6Da6dXcTel.TroC5J3uSDH@example.org", + include_bytes!("../../test-data/message/ndn_with_attachment.eml"), + Some("Undelivered Mail Returned to Sender – This is the mail system at host relay01.example.org.\n\nI'm sorry to have to inform you that your message could not\nbe delivered to one or more recipients. It's attached below.\n\nFor further assistance, please send mail to postmaster.\n\nIf you do so, please include this problem report. You can\ndelete your own text from the attached returned message.\n\n The mail system\n\n: host mx2.example.net[80.241.60.215] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota exceeded (in\n reply to RCPT TO command)\n\n: host mx1.example.net[80.241.60.212] said: 552 5.2.2\n : Recipient address rejected: Mailbox quota\n exceeded (in reply to RCPT TO command)") + ) + .await; +} + +/// Test that DSN is not treated as NDN if Action: is not "failed" +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_dsn_relayed() { + test_parse_ndn( + "anon_1@posteo.de", + "anon_2@gmx.at", + "8b7b1a9d0c8cc588c7bcac47f5687634@posteo.de", + include_bytes!("../../test-data/message/dsn_relayed.eml"), + None, + ) + .await; +} + +// ndn = Non Delivery Notification +async fn test_parse_ndn( + self_addr: &str, + foreign_addr: &str, + rfc724_mid_outgoing: &str, + raw_ndn: &[u8], + error_msg: Option<&str>, +) { + let t = TestContext::new().await; + t.configure_addr(self_addr).await; + + receive_imf( + &t, + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: {}\n\ + To: {}\n\ + Subject: foo\n\ + Message-ID: <{}>\n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + self_addr, foreign_addr, rfc724_mid_outgoing + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + let msg_id = chats.get_msg_id(0).unwrap().unwrap(); + + // Check that the ndn would be downloaded: + let headers = mailparse::parse_mail(raw_ndn).unwrap().headers; + assert!(prefetch_should_download( + &t, + &headers, + "some-other-message-id", + std::iter::empty(), + ShowEmails::Off, + ) + .await + .unwrap()); + + receive_imf(&t, raw_ndn, false).await.unwrap(); + let msg = Message::load_from_db(&t, msg_id).await.unwrap(); + + assert_eq!( + msg.state, + if error_msg.is_some() { + MessageState::OutFailed + } else { + MessageState::OutDelivered + } + ); + + assert_eq!(msg.error(), error_msg.map(|error| error.to_string())); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_parse_ndn_group_msg() -> Result<()> { + let t = TestContext::new().await; + t.configure_addr("alice@gmail.com").await; + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@gmail.com\n\ + To: bob@example.com, assidhfaaspocwaeofi@gmail.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: abcde\n\ + Chat-Group-Name: foo\n\ + Chat-Disposition-Notification-To: alice@example.org\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await?; + + let chats = Chatlist::try_load(&t, 0, None, None).await?; + let msg_id = chats.get_msg_id(0)?.unwrap(); + + let raw = include_bytes!("../../test-data/message/gmail_ndn_group.eml"); + receive_imf(&t, raw, false).await?; + + let msg = Message::load_from_db(&t, msg_id).await?; + + assert_eq!(msg.state, MessageState::OutFailed); + + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await?; + let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() { + msg_id + } else { + panic!("Wrong item type"); + }; + let last_msg = Message::load_from_db(&t, *msg_id).await?; + + assert_eq!( + last_msg.text, + Some(stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com").await,) + ); + assert_eq!(last_msg.from_id, ContactId::INFO); + Ok(()) +} + +async fn load_imf_email(context: &Context, imf_raw: &[u8]) -> Message { + context + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(context, imf_raw, false).await.unwrap(); + let chats = Chatlist::try_load(context, 0, None, None).await.unwrap(); + let msg_id = chats.get_msg_id(0).unwrap().unwrap(); + Message::load_from_db(context, msg_id).await.unwrap() +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_html_only_mail() { + let t = TestContext::new_alice().await; + let msg = load_imf_email(&t, include_bytes!("../../test-data/message/wrong-html.eml")).await; + assert_eq!(msg.text.unwrap(), " Guten Abend, \n\n Lots of text \n\n text with Umlaut ä... \n\n MfG [...]"); +} + +static GH_MAILINGLIST: &[u8] = + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Max Mustermann \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: Let's put some [brackets here that] have nothing to do with the topic\n\ + Message-ID: <3333@example.org>\n\ + List-ID: deltachat/deltachat-core-rust \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n"; + +static GH_MAILINGLIST2: &str = + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Github \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [deltachat/deltachat-core-rust] PR run failed\n\ + Message-ID: <3334@example.org>\n\ + List-ID: deltachat/deltachat-core-rust \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello back\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_github_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.ctx.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf(&t.ctx, GH_MAILINGLIST, false).await?; + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; + assert_eq!(chats.len(), 1); + + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(&t).await.unwrap(); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + + assert!(chat.is_mailing_list()); + assert!(chat.can_send(&t.ctx).await?); + assert_eq!( + chat.get_mailinglist_addr(), + Some("reply+elernshsetushoyseshetihseusaferuhsedtisneu@reply.github.com") + ); + assert_eq!(chat.name, "deltachat/deltachat-core-rust"); + assert_eq!(chat::get_chat_contacts(&t.ctx, chat_id).await?.len(), 1); + + receive_imf(&t.ctx, GH_MAILINGLIST2.as_bytes(), false).await?; + + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + assert!(!chat.can_send(&t.ctx).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?; + assert_eq!(chats.len(), 1); + let contacts = Contact::get_all(&t.ctx, 0, None).await?; + assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts" + + let msg1 = get_chat_msg(&t, chat_id, 0, 2).await; + let contact1 = Contact::load_from_db(&t.ctx, msg1.from_id).await?; + assert_eq!(contact1.get_addr(), "notifications@github.com"); + assert_eq!(contact1.get_display_name(), "notifications@github.com"); // Make sure this is not "Max Mustermann" or somethinng + + let msg2 = get_chat_msg(&t, chat_id, 1, 2).await; + let contact2 = Contact::load_from_db(&t.ctx, msg2.from_id).await?; + assert_eq!(contact2.get_addr(), "notifications@github.com"); + + assert_eq!(msg1.get_override_sender_name().unwrap(), "Max Mustermann"); + assert_eq!(msg2.get_override_sender_name().unwrap(), "Github"); + Ok(()) +} + +static DC_MAILINGLIST: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Bob \n\ + To: delta@codespeak.net\n\ + Subject: Re: [delta-dev] What's up?\n\ + Message-ID: <38942@posteo.org>\n\ + List-ID: \"discussions about and around https://delta.chat developments\" \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body\n"; + +static DC_MAILINGLIST2: &[u8] = b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Charlie \n\ + To: delta@codespeak.net\n\ + Subject: Re: [delta-dev] DC is nice!\n\ + Message-ID: <38943@posteo.org>\n\ + List-ID: \"discussions about and around https://delta.chat developments\" \n\ + List-Post: \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body 4\n"; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_classic_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + let chat_id = chats.get_chat_id(0).unwrap(); + chat_id.accept(&t).await.unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert_eq!(chat.name, "delta-dev"); + assert!(chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), Some("delta@codespeak.net")); + + let msg = get_chat_msg(&t, chat_id, 0, 1).await; + let contact1 = Contact::load_from_db(&t.ctx, msg.from_id).await.unwrap(); + assert_eq!(contact1.get_addr(), "bob@posteo.org"); + + let sent = t.send_text(chat.id, "Hello mailinglist!").await; + let mime = sent.payload(); + + println!("Sent mime message is:\n\n{}\n\n", mime); + assert!(mime.contains("Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no\r\n")); + assert!(mime.contains("Subject: =?utf-8?q?Re=3A_=5Bdelta-dev=5D_What=27s_up=3F?=\r\n")); + assert!(mime.contains("MIME-Version: 1.0\r\n")); + assert!(mime.contains("In-Reply-To: <38942@posteo.org>\r\n")); + assert!(mime.contains("Chat-Version: 1.0\r\n")); + assert!(mime.contains("To: \r\n")); + assert!(mime.contains("From: \r\n")); + assert!(mime.contains( + "\r\n\ +\r\n\ +Hello mailinglist!\r\n" + )); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await?; + + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await?; + assert!(chat.can_send(&t.ctx).await?); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_other_device_writes_to_mailinglist() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); + let first_msg = t.get_last_msg().await; + let first_chat = Chat::load_from_db(&t, first_msg.chat_id).await?; + assert_eq!( + first_chat.param.get(Param::ListPost).unwrap(), + "delta@codespeak.net" + ); + + let list_post_contact_id = + Contact::lookup_id_by_addr(&t, "delta@codespeak.net", Origin::Unknown) + .await? + .unwrap(); + let list_post_contact = Contact::load_from_db(&t, list_post_contact_id).await?; + assert_eq!( + list_post_contact.param.get(Param::ListId).unwrap(), + "delta.codespeak.net" + ); + assert_eq!( + chat::get_chat_id_by_grpid(&t, "delta.codespeak.net") + .await? + .unwrap(), + (first_chat.id, false, Blocked::Request) + ); + + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: Alice \n\ + To: delta@codespeak.net\n\ + Subject: [delta-dev] Subject\n\ + Message-ID: <0476@example.org>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + body 4\n", + false, + ) + .await + .unwrap(); + + let second_msg = t.get_last_msg().await; + + assert_eq!(first_msg.chat_id, second_msg.chat_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_block_mailing_list() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + t.evtracker.wait_next_incoming_message().await; + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); + + // Block the contact request. + chat_id.block(&t).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message disappeared + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + // Check that no notification is displayed for blocked mailing list message. + while let Ok(event) = t.evtracker.try_recv() { + assert!(!matches!(event.typ, EventType::IncomingMsg { .. })); + } + + // Test that the mailing list stays disappeared + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message is not shown + + // Both messages are in the same blocked chat. + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_block_then_unblock() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf(&t, DC_MAILINGLIST, false).await.unwrap(); + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 0); + + // Block the contact request, this should add one blocked contact. + let msg = t.get_last_msg().await; + msg.chat_id.block(&t).await.unwrap(); + + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 1); + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); // Test that the message is not shown + + // Unblock contact and check if the next message arrives in a chat + Contact::unblock(&t, *blocked.first().unwrap()) + .await + .unwrap(); + let blocked = Contact::get_all_blocked(&t).await.unwrap(); + assert_eq!(blocked.len(), 0); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + let msg = t.get_last_msg().await; + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_not_now() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + + // Open the chat and go back + chat::marknoticed_chat(&t.ctx, chat_id).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that chat is still in the chatlist + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 1); // ...and contains 1 message + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); + let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.is_contact_request()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_decide_accept() { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + receive_imf(&t.ctx, DC_MAILINGLIST, false).await.unwrap(); + + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + chat_id.accept(&t).await.unwrap(); + + let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); // Test that the message is shown + assert!(!chat_id.is_special()); + + receive_imf(&t.ctx, DC_MAILINGLIST2, false).await.unwrap(); + + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 2); + let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap(); + assert!(chat.can_send(&t.ctx).await.unwrap()); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_multiple_names_in_subject() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + receive_imf( + &t, + b"From: Foo Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola list] [foo][bar] just a subject\n\ + Message-ID: <3333@example.org>\n\ + List-ID: \"looong description of 'ola list', with foo, bar\" \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + let chat = Chat::load_from_db(&t, chat_id).await?; + assert_eq!(chat.name, "ola list [foo][bar]"); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_majordomo_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + // test mailing lists not having a `ListId:`-header + receive_imf( + &t, + b"From: Foo Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola] just a subject\n\ + Message-ID: <3333@example.org>\n\ + Sender: My list \n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat_id = msg.get_chat_id(); + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "mylist@bar.org"); + assert_eq!(chat.name, "ola"); + assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + // receive another message with no sender name but the same address, + // make sure this lands in the same chat + receive_imf( + &t, + b"From: Nu Bar \n\ + To: deltachat/deltachat-core-rust \n\ + Subject: [ola] Re: just a subject\n\ + Message-ID: <4444@example.org>\n\ + Sender: mylist@bar.org\n\ + Precedence: list\n\ + Date: Sun, 22 Mar 2020 23:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailchimp_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + b"To: alice \n\ + Subject: =?utf-8?Q?How=20early=20megacities=20emerged=20from=20Cambodia=E2=80=99s=20jungles?=\n\ + From: =?utf-8?Q?Atlas=20Obscura?= \n\ + List-ID: 399fc0402f1b154b67965632emc list <399fc0402f1b154b67965632e.100761.list-id.mcsv.net>\n\ + Message-ID: <555@example.org>\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!( + chat.grpid, + "399fc0402f1b154b67965632e.100761.list-id.mcsv.net" + ); + assert_eq!(chat.name, "Atlas Obscura"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dhl_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_dhl.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("Ihr Paket ist in der Packstation 123 – bla bla".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "1234ABCD-123LMNO.mailing.dhl.de"); + assert_eq!(chat.name, "DHL Paket"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dpd_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_dpd.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("Bald ist Ihr DPD Paket da – bla bla".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "dpdde.mxmail.service.dpd.de"); + assert_eq!(chat.name, "DPD"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_xt_local_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xt_local_microsoft.eml"), + false, + ) + .await?; + let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "96540.xt.local"); + assert_eq!(chat.name, "Microsoft Store"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xt_local_spiegel.eml"), + false, + ) + .await?; + let chat = Chat::load_from_db(&t, t.get_last_msg().await.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "121231234.xt.local"); + assert_eq!(chat.name, "DER SPIEGEL Kundenservice"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_xing_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_xing.eml"), + false, + ) + .await?; + let msg = t.get_last_msg().await; + assert_eq!(msg.subject, "Kennst Du Dr. Mabuse?"); + let chat = Chat::load_from_db(&t, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "51231231231231231231231232869f58.xing.com"); + assert_eq!(chat.name, "xing.com"); + assert!(!chat.can_send(&t).await?); + assert_eq!(chat.get_mailinglist_addr(), None); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ttline_mailing_list() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_ttline.eml"), + false, + ) + .await?; + let msg = t.get_last_msg().await; + assert_eq!(msg.subject, "Unsere Sommerangebote an Bord ⚓"); + let chat = Chat::load_from_db(&t, msg.chat_id).await?; + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.grpid, "39123123-1BBQXPY.t.ttline.com"); + assert_eq!(chat.name, "TT-Line - Die Schwedenfähren"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_with_mimepart_footer() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + // the mailing list message contains two top-level texts. + // the second text is a footer that is added by some mailing list software + // if the user-edited text contains html. + // this footer should not become a text-message in delta chat + // (otherwise every second mail might be the same footer) + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_with_mimepart_footer.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!( + msg.text, + Some("[Intern] important stuff – Hi mr ... [text part]".to_string()) + ); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "intern.lists.abc.de"); + assert_eq!(chat.name, "Intern"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_with_mimepart_footer_signed() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_with_mimepart_footer_signed.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1); + let text = msg.text.clone().unwrap(); + assert!(text.contains("content text")); + assert!(!text.contains("footer text")); + assert!(msg.has_html()); + let html = msg.get_id().get_html(&t).await.unwrap().unwrap(); + assert!(html.contains("content text")); + assert!(!html.contains("footer text")); +} + +/// Test that the changes from apply_mailinglist_changes() are also applied +/// if the message is assigned to the chat by In-Reply-To +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_apply_mailinglist_changes_assigned_by_reply() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf(&t, GH_MAILINGLIST, false).await.unwrap(); + + let chat_id = t.get_last_msg().await.chat_id; + chat_id.accept(&t).await.unwrap(); + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(chat.can_send(&t).await.unwrap()); + + let imf_raw = format!("In-Reply-To: 3333@example.org\n{}", GH_MAILINGLIST2); + receive_imf(&t, imf_raw.as_bytes(), false).await.unwrap(); + + assert_eq!( + t.get_last_msg().await.in_reply_to.unwrap(), + "3333@example.org" + ); + // `Assigning message to Chat#... as it's a reply to 3333@example.org` + t.evtracker + .get_info_contains("as it's a reply to 3333@example.org") + .await; + + let chat = Chat::load_from_db(&t, chat_id).await.unwrap(); + assert!(!chat.can_send(&t).await.unwrap()); + + let contact_id = Contact::lookup_id_by_addr( + &t, + "reply+EGELITBABIHXSITUZIEPAKYONASITEPUANERGRUSHE@reply.github.com", + Origin::Hidden, + ) + .await + .unwrap() + .unwrap(); + let contact = Contact::load_from_db(&t, contact_id).await.unwrap(); + assert_eq!( + contact.param.get(Param::ListId).unwrap(), + "deltachat-core-rust.deltachat.github.com" + ) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_chat_message() { + let t = TestContext::new_alice().await; + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_chat_message.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(msg.text, Some("hello, this is a test 👋\n\n_______________________________________________\nTest1 mailing list -- test1@example.net\nTo unsubscribe send an email to test1-leave@example.net".to_string())); + assert!(!msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Mailinglist); + assert_eq!(chat.blocked, Blocked::Request); + assert_eq!(chat.grpid, "test1.example.net"); + assert_eq!(chat.name, "Test1"); +} + +/// Tests that bots automatically accept mailing lists. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mailing_list_bot() { + let t = TestContext::new_alice().await; + t.set_config(Config::Bot, Some("1")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/mailinglist_chat_message.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.blocked, Blocked::Not); + + // Bot should see the message as fresh and process it. + assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_tokens_in_contacts_list() { + check_dont_show_in_contacts_list( + "reply+OGHVYCLVBEGATYBICAXBIRQATABUOTUCERABERAHNO@reply.github.com", + ) + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_noreply_in_contacts_list() { + check_dont_show_in_contacts_list("noreply@github.com").await; +} + +async fn check_dont_show_in_contacts_list(addr: &str) { + let t = TestContext::new_alice().await; + t.ctx + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf( + &t, + format!( + "Subject: Re: [deltachat/deltachat-core-rust] DC is the best repo on GitHub! +To: {} +References: + +From: alice@example.org +Message-ID: +Date: Tue, 16 Jun 2020 12:04:20 +0200 +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 7bit + +YEAAAAAA!. +", + addr + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + let contacts = Contact::get_all(&t, 0, None as Option<&str>).await.unwrap(); + assert!(contacts.is_empty()); // The contact should not have been added to the db +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pdf_filename_simple() { + let t = TestContext::new_alice().await; + let msg = load_imf_email( + &t, + include_bytes!("../../test-data/message/pdf_filename_simple.eml"), + ) + .await; + assert_eq!(msg.viewtype, Viewtype::File); + assert_eq!(msg.text.unwrap(), "mail body"); + assert_eq!(msg.param.get(Param::File).unwrap(), "$BLOBDIR/simple.pdf"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_pdf_filename_continuation() { + // test filenames split across multiple header lines, see rfc 2231 + let t = TestContext::new_alice().await; + let msg = load_imf_email( + &t, + include_bytes!("../../test-data/message/pdf_filename_continuation.eml"), + ) + .await; + assert_eq!(msg.viewtype, Viewtype::File); + assert_eq!(msg.text.unwrap(), "mail body"); + assert_eq!( + msg.param.get(Param::File).unwrap(), + "$BLOBDIR/test pdf äöüß.pdf" + ); +} + +/// HTML-images may come with many embedded images, eg. tiny icons, corners for formatting, +/// twitter/facebook/whatever logos and so on. +/// that may easily be 50 and more images, one would not have these images in a chat. +/// +/// fortunately, if we remove them, they are accessible by get_msg_html() now. +/// +/// unfortunately, these images are not that easy to detect as they may also be on purpose, +/// or mua may use multipart/related not correctly - +/// so this test is in competition with parse_thunderbird_html_embedded_image() +/// that wants the image to be kept in the chat. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_many_images() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + include_bytes!("../../test-data/message/many_images_amazon_via_apple_mail.eml"), + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert_eq!(msg.viewtype, Viewtype::Image); + assert!(msg.has_html()); + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1); +} + +/// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To` +/// header. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_in_reply_to() { + let t = TestContext::new().await; + t.configure_addr("bob@example.com").await; + + // Receive message from Alice about group "foo". + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com, charlie@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello foo\n", + false, + ) + .await + .unwrap(); + + // Receive reply from Charlie without group ID but with In-Reply-To header. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: charlie@example.net\n\ + To: alice@example.org, bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + reply foo\n", + false, + ) + .await + .unwrap(); + + let msg = t.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "reply foo"); + + // Load the first message from the same chat. + let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap(); + let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() { + msg_id + } else { + panic!("Wrong item type"); + }; + + let reply_msg = Message::load_from_db(&t, *msg_id).await.unwrap(); + assert_eq!(reply_msg.get_text().unwrap(), "hello foo"); + + // Check that reply got into the same chat as the original message. + assert_eq!(msg.chat_id, reply_msg.chat_id); + + // Make sure we looked at real chat ID and do not just + // test that both messages got into the same virtual chat. + assert!(!msg.chat_id.is_special()); +} + +/// Test that classical MUA messages are assigned to group chats +/// based on the `In-Reply-To` header for two-member groups. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_in_reply_to_two_member_group() { + let t = TestContext::new().await; + t.configure_addr("bob@example.com").await; + + // Receive message from Alice about group "foo". + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello foo\n", + false, + ) + .await + .unwrap(); + + // Receive a classic MUA reply from Alice. + // It is assigned to the group chat. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + classic reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to group chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(msg.get_text().unwrap(), "classic reply"); + + // Receive a Delta Chat reply from Alice. + // It is assigned to group chat, because it has a group ID. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + chat reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to group chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(msg.get_text().unwrap(), "chat reply"); + + // Receive a private Delta Chat reply from Alice. + // It is assigned to 1:1 chat, because it has no group ID, + // which means it was created using "reply privately" feature. + // Normally it contains a quote, but it should not matter. + receive_imf( + &t, + b"Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: Re: foo\n\ + Message-ID: \n\ + In-Reply-To: \n\ + Chat-Version: 1.0\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + private reply\n", + false, + ) + .await + .unwrap(); + + // Ensure message is assigned to a 1:1 chat. + let msg = t.get_last_msg().await; + let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + assert_eq!(msg.get_text().unwrap(), "private reply"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_mime_headers_off() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + assert!(mime.is_empty()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_save_mime_headers_on() -> anyhow::Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config_bool(Config::SaveMimeHeaders, true).await?; + let bob = TestContext::new_bob().await; + bob.set_config_bool(Config::SaveMimeHeaders, true).await?; + + // alice sends a message to bob, bob sees full mime + let chat_alice = alice.create_chat(&bob).await; + chat::send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?; + + let msg = bob.recv_msg(&alice.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("hi!".to_string())); + assert!(!msg.get_showpadlock()); + let mime = message::get_mime_headers(&bob, msg.id).await?; + let mime_str = String::from_utf8_lossy(&mime); + assert!(mime_str.contains("Message-ID:")); + assert!(mime_str.contains("From:")); + + // another one, from bob to alice, that gets encrypted + let chat_bob = bob.create_chat(&alice).await; + chat::send_text_msg(&bob, chat_bob.id, "ho!".to_string()).await?; + let msg = alice.recv_msg(&bob.pop_sent_msg().await).await; + assert_eq!(msg.get_text(), Some("ho!".to_string())); + assert!(msg.get_showpadlock()); + let mime = message::get_mime_headers(&alice, msg.id).await?; + let mime_str = String::from_utf8_lossy(&mime); + assert!(mime_str.contains("Message-ID:")); + assert!(mime_str.contains("From:")); + Ok(()) +} + +async fn create_test_alias(chat_request: bool, group_request: bool) -> (TestContext, TestContext) { + // Claire, a customer, sends a support request + // to the alias address from a classic MUA. + // The alias expands to the supporters Alice and Bob. + // Check that Alice receives the message in a group chat. + let claire_request = if group_request { + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: support@example.org, ceo@example.org\n\ + From: claire@example.org\n\ + Subject: i have a question\n\ + Message-ID: \n\ + {}\ + Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ + Content-Type: text/plain\n\ + \n\ + hi support! what is the current version?", + if chat_request { + "Chat-Group-ID: 8ud29aridt29arid\n\ + Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n" + } else { + "" + } + ) + } else { + format!( + "Received: (Postfix, from userid 1000); Mon, 4 Dec 2006 14:51:39 +0100 (CET)\n\ + To: support@example.org\n\ + From: claire@example.org\n\ + Subject: i have a question\n\ + Message-ID: \n\ + {}\ + Date: Sun, 14 Mar 2021 17:04:36 +0100\n\ + Content-Type: text/plain\n\ + \n\ + hi support! what is the current version?", + if chat_request { + "Chat-Version: 1.0\n" + } else { + "" + } + ) + }; + + let alice = TestContext::new_alice().await; + alice + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&alice, claire_request.as_bytes(), false) + .await + .unwrap(); + + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_subject(), "i have a question"); + assert!(msg.get_text().unwrap().contains("hi support!")); + let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Group); + assert_eq!(get_chat_msgs(&alice, chat.id, 0).await.unwrap().len(), 1); + if group_request { + assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 4); + } else { + assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 3); + } + assert_eq!(msg.get_override_sender_name(), None); + + let claire = TestContext::new().await; + claire.configure_addr("claire@example.org").await; + claire + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + receive_imf(&claire, claire_request.as_bytes(), false) + .await + .unwrap(); + + let msg_id = rfc724_mid_exists(&claire, "non-dc-1@example.org") + .await + .unwrap() + .unwrap(); + + let msg = Message::load_from_db(&claire, msg_id).await.unwrap(); + msg.chat_id.accept(&claire).await.unwrap(); + assert_eq!(msg.get_subject(), "i have a question"); + assert!(msg.get_text().unwrap().contains("hi support!")); + let chat = Chat::load_from_db(&claire, msg.chat_id).await.unwrap(); + if group_request { + assert_eq!(chat.typ, Chattype::Group); + } else { + assert_eq!(chat.typ, Chattype::Single); + } + assert_eq!(get_chat_msgs(&claire, chat.id, 0).await.unwrap().len(), 1); + assert_eq!(msg.get_override_sender_name(), None); + + (claire, alice) +} + +async fn check_alias_reply(reply: &[u8], chat_request: bool, group_request: bool) { + let (claire, alice) = create_test_alias(chat_request, group_request).await; + + // Check that Alice gets the message in the same chat. + let request = alice.get_last_msg().await; + receive_imf(&alice, reply, false).await.unwrap(); + let answer = alice.get_last_msg().await; + assert_eq!(answer.get_subject(), "Re: i have a question"); + assert!(answer.get_text().unwrap().contains("the version is 1.0")); + assert_eq!(answer.chat_id, request.chat_id); + let chat_contacts = get_chat_contacts(&alice, answer.chat_id) + .await + .unwrap() + .len(); + if group_request { + // Claire, Support, CEO and Alice (Bob is not added) + assert_eq!(chat_contacts, 4); + } else { + // Claire, Support and Alice + assert_eq!(chat_contacts, 3); + } + assert_eq!( + answer.get_override_sender_name().unwrap(), + "bob@example.net" + ); // Bob is not part of the group, so override-sender-name should be set + + // Check that Claire also gets the message in the same chat. + let request = claire.get_last_msg().await; + receive_imf(&claire, reply, false).await.unwrap(); + let answer = claire.get_last_msg().await; + assert_eq!(answer.get_subject(), "Re: i have a question"); + assert!(answer.get_text().unwrap().contains("the version is 1.0")); + assert_eq!(answer.chat_id, request.chat_id); + assert_eq!( + answer.get_override_sender_name().unwrap(), + "bob@example.net" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_alias_support_answer_from_nondc() { + // Bob, the other supporter, answers with a classic MUA. + let bob_answer = b"To: support@example.org, claire@example.org\n\ + From: bob@example.net\n\ + Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ + References: \n\ + In-Reply-To: \n\ + Message-ID: \n\ + Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ + Content-Type: text/plain\n\ + \n\ + hi claire, the version is 1.0, cheers bob"; + + check_alias_reply(bob_answer, true, true).await; + check_alias_reply(bob_answer, false, true).await; + check_alias_reply(bob_answer, true, false).await; + check_alias_reply(bob_answer, false, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_alias_answer_from_dc() { + // Bob, the other supporter, answers with Delta Chat. + let bob_answer = b"To: support@example.org, claire@example.org\n\ + From: bob@example.net\n\ + Subject: =?utf-8?q?Re=3A_i_have_a_question?=\n\ + References: \n\ + In-Reply-To: \n\ + Message-ID: \n\ + Date: Sun, 14 Mar 2021 16:04:57 +0000\n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: af9e810c9b592927\n\ + Chat-Group-Name: =?utf-8?q?i_have_a_question?=\n\ + Chat-Disposition-Notification-To: bob@example.net\n\ + Content-Type: text/plain\n\ + \n\ + hi claire, the version is 1.0, cheers bob"; + + check_alias_reply(bob_answer, true, true).await; + check_alias_reply(bob_answer, false, true).await; + check_alias_reply(bob_answer, true, false).await; + check_alias_reply(bob_answer, false, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_assign_to_trash_by_parent() { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + println!("\n========= Receive a message =========="); + receive_imf( + &t, + b"From: Nu Bar \n\ + To: alice@example.org, bob@example.org\n\ + Subject: Hi\n\ + Message-ID: <4444@example.org>\n\ + \n\ + hello\n", + false, + ) + .await + .unwrap(); + let chat_id = t.get_last_msg().await.chat_id; + chat_id.accept(&t).await.unwrap(); + let msg = get_chat_msg(&t, chat_id, 0, 1).await; // Make sure that the message is actually in the chat + assert!(!msg.chat_id.is_special()); + assert_eq!(msg.text.unwrap(), "Hi – hello"); + + println!("\n========= Delete the message =========="); + msg.id.trash(&t).await.unwrap(); + + let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap(); + assert_eq!(msgs.len(), 0); + + println!("\n========= Receive a message that is a reply to the deleted message =========="); + receive_imf( + &t, + b"From: Nu Bar \n\ + To: alice@example.org, bob@example.org\n\ + Subject: Re: Hi\n\ + Message-ID: <5555@example.org>\n\ + In-Reply-To: <4444@example.org\n\ + \n\ + Reply\n", + false, + ) + .await + .unwrap(); + let msg = t.get_last_msg().await; + assert!(!msg.chat_id.is_special()); // Esp. check that the chat_id is not TRASH + assert_eq!(msg.text.unwrap(), "Reply"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_dont_show_all_outgoing_msgs_in_self_chat() { + // Regression test for : + // Some servers add a `Bcc: ` header, which caused all outgoing messages to + // be shown in the self-chat. + let t = TestContext::new_alice().await; + + receive_imf( + &t, + b"Bcc: alice@example.org +Received: from [127.0.0.1] +Subject: s +Chat-Version: 1.0 +Message-ID: +To: +From: + +Message content", + false, + ) + .await + .unwrap(); + + let msg = t.get_last_msg().await; + assert_ne!(msg.chat_id, t.get_self_chat().await.id); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_classic_mail_creates_chat() { + let alice = TestContext::new_alice().await; + + // Alice enables classic emails. + alice + .set_config(Config::ShowEmails, Some("2")) + .await + .unwrap(); + + // Alice downloads outgoing classic email. + receive_imf( + &alice, + b"Received: from [127.0.0.1] +Subject: Subj +Message-ID: +To: +From: + +Message content", + false, + ) + .await + .unwrap(); + + // Outgoing email should create a chat. + let msg = alice.get_last_msg().await; + assert_eq!(msg.get_text().unwrap(), "Subj – Message content"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_duplicate_message() -> Result<()> { + // Test that duplicate messages are ignored based on the Message-ID + let alice = TestContext::new_alice().await; + + let bob_contact_id = Contact::add_or_lookup( + &alice, + "Bob", + ContactAddress::new("bob@example.org").unwrap(), + Origin::IncomingUnknownFrom, + ) + .await? + .0; + + let first_message = b"Received: from [127.0.0.1] +Subject: First message +Message-ID: +To: Alice +From: Bob1 +Chat-Version: 1.0 + +Message content + +-- +First signature"; + + let second_message = b"Received: from [127.0.0.1] +Subject: Second message +Message-ID: +To: Alice +From: Bob2 +Chat-Version: 1.0 + +Message content + +-- +Second signature"; + + receive_imf(&alice, first_message, false).await?; + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "First signature"); + assert_eq!(contact.get_display_name(), "Bob1"); + + receive_imf(&alice, second_message, false).await?; + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "Second signature"); + assert_eq!(contact.get_display_name(), "Bob2"); + + // Duplicate message, should be ignored + receive_imf(&alice, first_message, false).await?; + + // No change because last message is duplicate of the first. + let contact = Contact::load_from_db(&alice, bob_contact_id).await?; + assert_eq!(contact.get_status(), "Second signature"); + assert_eq!(contact.get_display_name(), "Bob2"); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_ignore_footer_status_from_mailinglist() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + let bob_id = Contact::add_or_lookup( + &t, + "", + ContactAddress::new("bob@example.net").unwrap(), + Origin::IncomingUnknownCc, + ) + .await? + .0; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), ""); + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0); + + receive_imf( + &t, + b"From: Bob +To: Alice +Message-ID: <1@example.org> +Subject: first message + +body 1 + +-- +Original signature", + false, + ) + .await?; + let one2one_chat_id = t.get_last_msg().await.chat_id; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature"); + + receive_imf( + &t, + b"From: Bob +Sender: ml@example.net +To: Alice +Message-ID: <2@example.net> +Precedence: list +Subject: second message + +body 2 + +-- +The modified signature +-- +Tap here to unsubscribe ...", + false, + ) + .await?; + let ml_chat_id = t.get_last_msg().await.chat_id; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature"); + + receive_imf( + &t, + b"From: Bob +To: Alice +Message-ID: <3@example.org> +Subject: third message + +body 3 + +-- +Original signature updated", + false, + ) + .await?; + let bob = Contact::load_from_db(&t, bob_id).await?; + assert_eq!(bob.get_status(), "Original signature updated"); + assert_eq!(get_chat_msgs(&t, one2one_chat_id, 0).await?.len(), 2); + assert_eq!(get_chat_msgs(&t, ml_chat_id, 0).await?.len(), 1); + assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 2); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_private_classical_reply() { + for outgoing_is_classical in &[true, false] { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +Date: Fri, 28 May 2021 10:15:05 +0000 +To: Bob , +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + receive_imf( + &t, + format!( + r#"Subject: Re: single reply-to +To: "Alice" +References: <{0}> + <{0}> +From: Bob +Message-ID: <028674eb-77f9-4ad1-1c30-e93e18b891c8@testrun.org> +Date: Fri, 28 May 2021 12:17:03 +0200 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 + Thunderbird/78.10.2 +MIME-Version: 1.0 +In-Reply-To: <{0}> + +Private reply"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let private_msg = t.get_last_msg().await; + assert_eq!(private_msg.text.unwrap(), "Private reply"); + let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); + assert_eq!(private_chat.typ, Chattype::Single); + assert_ne!(private_msg.chat_id, group_msg.chat_id); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_private_chat_reply() { + for (outgoing_is_classical, outgoing_has_multiple_recipients) in + &[(true, true), (false, true), (false, false)] + { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +Date: Fri, 28 May 2021 10:15:05 +0000 +To: Bob {} +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + }, + if *outgoing_has_multiple_recipients { + ", " + } else { + "" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + receive_imf( + &t, + format!( + r#"Subject: =?utf-8?q?Re=3A_single_reply-to?= +MIME-Version: 1.0 +In-Reply-To: <{0}> +Date: Sat, 03 Jul 2021 20:00:26 +0000 +Chat-Version: 1.0 +Message-ID: +To: +From: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +> Hello, I've just created the group "single reply-to" for us. + +Private reply + +=2D- +Sent with my Delta Chat Messenger: https://delta.chat + +"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.iy1KCE2y65_.mH2TM52miv9@testrun.org" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let private_msg = t.get_last_msg().await; + assert_eq!(private_msg.text.unwrap(), "Private reply"); + let private_chat = Chat::load_from_db(&t, private_msg.chat_id).await.unwrap(); + assert_eq!(private_chat.typ, Chattype::Single); + assert_ne!(private_msg.chat_id, group_msg.chat_id); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_nonprivate_classical_reply() { + for outgoing_is_classical in &[true, false] { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await.unwrap(); + + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: =?utf-8?q?single_reply-to?= +{} +To: Bob , +From: Alice +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no +Content-Transfer-Encoding: quoted-printable + +Hello, I've just created the group "single reply-to" for us."#, + if *outgoing_is_classical { + r"Message-ID: abcd@gmx.de" + } else { + r"Chat-Group-ID: eJ_llQIXf0K +Chat-Group-Name: =?utf-8?q?single_reply-to?= +References: +Chat-Version: 1.0 +Message-ID: " + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let group_msg = t.get_last_msg().await; + assert_eq!( + group_msg.text.unwrap(), + if *outgoing_is_classical { + "single reply-to – Hello, I\'ve just created the group \"single reply-to\" for us." + } else { + "Hello, I've just created the group \"single reply-to\" for us." + } + ); + let group_chat = Chat::load_from_db(&t, group_msg.chat_id).await.unwrap(); + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!(group_chat.name, "single reply-to"); + + // =============== Receive another outgoing message and check that it is put into the same chat =============== + receive_imf( + &t, + format!( + r#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: Out subj +To: "Bob" , "Claire" +From: Alice +Message-ID: +MIME-Version: 1.0 +In-Reply-To: <{0}> + +Outgoing reply to all"#, + if *outgoing_is_classical { + "abcd@gmx.de" + } else { + "Gr.eJ_llQIXf0K.buxmrnMmG0Y@gmx.de" + } + ) + .as_bytes(), + false, + ) + .await + .unwrap(); + + let reply = t.get_last_msg().await; + assert_eq!(reply.text.unwrap(), "Out subj – Outgoing reply to all"); + let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); + assert_eq!(reply_chat.typ, Chattype::Group); + assert_eq!(reply.chat_id, group_msg.chat_id); + + // =============== Receive an incoming message and check that it is put into the same chat =============== + receive_imf( + &t, + br#"Received: from mout.gmx.net (mout.gmx.net [212.227.17.22]) +Subject: In subj +To: "Bob" , "Claire" +From: alice +Message-ID: +MIME-Version: 1.0 +In-Reply-To: + +Reply to all"#, + false, + ) + .await + .unwrap(); + + let reply = t.get_last_msg().await; + assert_eq!(reply.text.unwrap(), "In subj – Reply to all"); + let reply_chat = Chat::load_from_db(&t, reply.chat_id).await.unwrap(); + assert_eq!(reply_chat.typ, Chattype::Group); + assert_eq!(reply.chat_id, group_msg.chat_id); + } +} + +/// Tests that replies to similar ad hoc groups are correctly assigned to chats. +/// +/// The difficutly here is that ad hoc groups don't have unique group IDs, because both +/// messages have the same recipient lists and only differ in the subject and message contents. +/// The messages can be properly assigned to chats only using the In-Reply-To or References +/// headers. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_chat_assignment_adhoc() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice.set_config(Config::ShowEmails, Some("2")).await?; + bob.set_config(Config::ShowEmails, Some("2")).await?; + + let first_thread_mime = br#"Subject: First thread +Message-ID: first@example.org +To: Alice , Bob +From: Claire +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First thread."#; + let second_thread_mime = br#"Subject: Second thread +Message-ID: second@example.org +To: Alice , Bob +From: Claire +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Second thread."#; + + // Alice receives two classic emails from Claire. + receive_imf(&alice, first_thread_mime, false).await?; + let alice_first_msg = alice.get_last_msg().await; + receive_imf(&alice, second_thread_mime, false).await?; + let alice_second_msg = alice.get_last_msg().await; + + // Bob receives the same two emails. + receive_imf(&bob, first_thread_mime, false).await?; + let bob_first_msg = bob.get_last_msg().await; + receive_imf(&bob, second_thread_mime, false).await?; + let bob_second_msg = bob.get_last_msg().await; + + // Messages go to separate chats both for Alice and Bob. + assert!(alice_first_msg.chat_id != alice_second_msg.chat_id); + assert!(bob_first_msg.chat_id != bob_second_msg.chat_id); + + // Alice replies to both chats. Bob receives two messages and assigns them to corresponding + // chats. + alice_first_msg.chat_id.accept(&alice).await?; + let alice_first_reply = alice + .send_text(alice_first_msg.chat_id, "First reply") + .await; + let bob_first_reply = bob.recv_msg(&alice_first_reply).await; + assert_eq!(bob_first_reply.chat_id, bob_first_msg.chat_id); + + alice_second_msg.chat_id.accept(&alice).await?; + let alice_second_reply = alice + .send_text(alice_second_msg.chat_id, "Second reply") + .await; + let bob_second_reply = bob.recv_msg(&alice_second_reply).await; + assert_eq!(bob_second_reply.chat_id, bob_second_msg.chat_id); + + // Alice adds Fiona to both ad hoc groups. + let fiona = TestContext::new_fiona().await; + let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await; + let alice_fiona_contact_id = alice_fiona_contact.id; + + chat::add_contact_to_chat(&alice, alice_first_msg.chat_id, alice_fiona_contact_id).await?; + let alice_first_invite = alice.pop_sent_msg().await; + let fiona_first_invite = fiona.recv_msg(&alice_first_invite).await; + + chat::add_contact_to_chat(&alice, alice_second_msg.chat_id, alice_fiona_contact_id).await?; + let alice_second_invite = alice.pop_sent_msg().await; + let fiona_second_invite = fiona.recv_msg(&alice_second_invite).await; + + // Fiona was added to two separate chats and should see two separate chats, even though they + // don't have different group IDs to distinguish them. + assert!(fiona_first_invite.chat_id != fiona_second_invite.chat_id); + + Ok(()) +} + +/// Test that read receipts don't create chats. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_read_receipts_dont_create_chats() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + // Alice sends a message to Bob. + assert_eq!(Chatlist::try_load(&bob, 0, None, None).await?.len(), 0); + bob.recv_msg(&alice.send_text(alice_chat.id, "Message").await) + .await; + let received_msg = bob.get_last_msg().await; + + // Alice deletes the chat. + alice_chat.id.delete(&alice).await?; + let chats = Chatlist::try_load(&alice, 0, None, None).await?; + assert_eq!(chats.len(), 0); + + // Bob sends a read receipt. + let mdn_mimefactory = + crate::mimefactory::MimeFactory::from_mdn(&bob, &received_msg, vec![]).await?; + let rendered_mdn = mdn_mimefactory.render(&bob).await?; + let mdn_body = rendered_mdn.message; + + // Alice receives the read receipt. + receive_imf(&alice, mdn_body.as_bytes(), false).await?; + + // Chat should not pop up in the chatlist. + let chats = Chatlist::try_load(&alice, 0, None, None).await?; + assert_eq!(chats.len(), 0); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_gmx_forwarded_msg() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + receive_imf( + &t, + include_bytes!("../../test-data/message/gmx-forward.eml"), + false, + ) + .await?; + + let msg = t.get_last_msg().await; + assert!(msg.has_html()); + assert_eq!(msg.id.get_html(&t).await?.unwrap().replace("\r\n", "\n"), "
 
\n\n
 \n
 \n
\n
Gesendet: Donnerstag, 12. August 2021 um 15:52 Uhr
\nVon: "Claire" <claire@example.org>
\nAn: alice@example.org
\nBetreff: subject
\n\n
bodytext
\n
\n
\n
\n\n"); + + Ok(()) +} + +/// Tests that user is notified about new incoming contact requests. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_incoming_contact_request() -> Result<()> { + let t = TestContext::new_alice().await; + + receive_imf(&t, MSGRMSG, false).await?; + let msg = t.get_last_msg().await; + let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; + assert!(chat.is_contact_request()); + + loop { + let event = t + .evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingMsg { .. })) + .await; + match event { + EventType::IncomingMsg { chat_id, msg_id } => { + assert_eq!(msg.chat_id, chat_id); + assert_eq!(msg.id, msg_id); + return Ok(()); + } + _ => unreachable!(), + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_get_parent_message() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let mime = br#"Subject: First +Message-ID: first@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let first = t.get_last_msg().await; + let mime = br#"Subject: Second +Message-ID: second@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let second = t.get_last_msg().await; + let mime = br#"Subject: Third +Message-ID: third@example.net +To: Alice +From: Bob +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +First."#; + receive_imf(&t, mime, false).await?; + let third = t.get_last_msg().await; + + let mime = br#"Subject: Message with references. +Message-ID: second@example.net +To: Alice +From: Bob +In-Reply-To: +References: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Message with references."#; + let mime_parser = MimeMessage::from_bytes(&t, &mime[..]).await?; + + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, first.id); + + message::delete_msgs(&t, &[first.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, second.id); + + message::delete_msgs(&t, &[second.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?.unwrap(); + assert_eq!(parent.id, third.id); + + message::delete_msgs(&t, &[third.id]).await?; + let parent = get_parent_message(&t, &mime_parser).await?; + assert!(parent.is_none()); + + Ok(()) +} + +/// Test a message with RFC 1847 encapsulation as created by Thunderbird. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_rfc1847_encapsulation() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + alice.configure_addr("alice@example.org").await; + + // Alice sends an Autocrypt message to Bob so Bob gets Alice's key. + let chat_alice = alice.create_chat(&bob).await; + let first_msg = alice + .send_text(chat_alice.id, "Sending Alice key to Bob.") + .await; + bob.recv_msg(&first_msg).await; + message::delete_msgs(&bob, &[bob.get_last_msg().await.id]).await?; + + bob.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice sends a message to Bob using Thunderbird. + let raw = include_bytes!("../../test-data/message/rfc1847_encapsulation.eml"); + receive_imf(&bob, raw, false).await?; + + let msg = bob.get_last_msg().await; + assert!(msg.get_showpadlock()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_invalid_to_address() -> Result<()> { + let alice = TestContext::new_alice().await; + + let mime = include_bytes!("../../test-data/message/invalid_email_to.eml"); + + // receive_imf should not fail on this mail with invalid To: field + receive_imf(&alice, mime, false).await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_reply_from_different_addr() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice creates a 2-person-group with Bob + receive_imf( + &t, + br#"Subject: =?utf-8?q?Januar_13-19?= +Chat-Group-ID: qetqsutor7a +Chat-Group-Name: =?utf-8?q?Januar_13-19?= +MIME-Version: 1.0 +References: +Date: Mon, 20 Dec 2021 12:15:01 +0000 +Chat-Version: 1.0 +Message-ID: +To: +From: +Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no + +Hi, I created a group"#, + false, + ) + .await?; + let msg_out = t.get_last_msg().await; + assert_eq!(msg_out.from_id, ContactId::SELF); + assert_eq!(msg_out.text.unwrap(), "Hi, I created a group"); + assert_eq!(msg_out.in_reply_to, None); + + // Bob replies from a different address + receive_imf( + &t, + b"Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable +From: +Mime-Version: 1.0 (1.0) +Subject: Re: Januar 13-19 +Date: Mon, 20 Dec 2021 13:54:55 +0100 +Message-Id: +References: +In-Reply-To: +To: holger + +Reply from different address +", + false, + ) + .await?; + let msg_in = t.get_last_msg().await; + assert_eq!(msg_in.to_id, ContactId::SELF); + assert_eq!(msg_in.text.unwrap(), "Reply from different address"); + assert_eq!( + msg_in.in_reply_to.unwrap(), + "Gr.qetqsutor7a.Aresxresy-4@deltachat.de" + ); + assert_eq!( + msg_in.param.get(Param::OverrideSenderDisplayname), + Some("bob-alias@example.com") + ); + + assert_eq!(msg_in.chat_id, msg_out.chat_id); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_long_filenames() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + for filename_sent in &[ + "foo.bar very long file name test baz.tar.gz", + "foobarabababababababbababababverylongfilenametestbaz.tar.gz", + "fooo...tar.gz", + "foo. .tar.gz", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz", + "a.tar.gz", + "a.a..a.a.a.a.tar.gz", + ] { + let attachment = alice.blobdir.join(filename_sent); + let content = format!("File content of {}", filename_sent); + tokio::fs::write(&attachment, content.as_bytes()).await?; + + let mut msg_alice = Message::new(Viewtype::File); + msg_alice.set_file(attachment.to_str().unwrap(), None); + let alice_chat = alice.create_chat(&bob).await; + let sent = alice.send_msg(alice_chat.id, &mut msg_alice).await; + println!("{}", sent.payload()); + + let msg_bob = bob.recv_msg(&sent).await; + + async fn check_message(msg: &Message, t: &TestContext, content: &str) { + assert_eq!(msg.get_viewtype(), Viewtype::File); + let resulting_filename = msg.get_filename().unwrap(); + let path = msg.get_file(t).unwrap(); + assert!( + resulting_filename.ends_with(".tar.gz"), + "{:?} doesn't end with .tar.gz, path: {:?}", + resulting_filename, + path + ); + assert!( + path.to_str().unwrap().ends_with(".tar.gz"), + "path {:?} doesn't end with .tar.gz", + path + ); + assert_eq!(fs::read_to_string(path).await.unwrap(), content); + } + check_message(&msg_alice, &alice, &content).await; + check_message(&msg_bob, &bob, &content).await; + } + + Ok(()) +} + +/// Tests that contact request is accepted automatically on outgoing message. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_accept_outgoing() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = tcm.alice().await; + let alice2 = tcm.alice().await; + let bob1 = tcm.bob().await; + let bob2 = tcm.bob().await; + + let bob1_chat = bob1.create_chat(&alice1).await; + let sent = bob1.send_text(bob1_chat.id, "Hello!").await; + + alice1.recv_msg(&sent).await; + alice2.recv_msg(&sent).await; + let alice1_msg = bob2.recv_msg(&sent).await; + assert_eq!(alice1_msg.text.unwrap(), "Hello!"); + let alice1_chat = chat::Chat::load_from_db(&alice1, alice1_msg.chat_id).await?; + assert!(alice1_chat.is_contact_request()); + + let alice2_msg = alice2.get_last_msg().await; + assert_eq!(alice2_msg.text.unwrap(), "Hello!"); + let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; + assert!(alice2_chat.is_contact_request()); + + let bob1_msg = bob1.get_last_msg().await; + assert_eq!(bob1_msg.text.unwrap(), "Hello!"); + let bob1_chat = chat::Chat::load_from_db(&bob1, bob1_msg.chat_id).await?; + assert!(!bob1_chat.is_contact_request()); + + let bob2_msg = bob2.get_last_msg().await; + assert_eq!(bob2_msg.text.unwrap(), "Hello!"); + let bob2_chat = chat::Chat::load_from_db(&bob2, bob2_msg.chat_id).await?; + assert!(!bob2_chat.is_contact_request()); + + // Alice sends reply. + alice1_msg.chat_id.accept(&alice1).await.unwrap(); + let sent = alice1.send_text(alice1_chat.id, "Hi!").await; + alice2.recv_msg(&sent).await; + + // Second device automatically accepts the contact request. + let alice2_chat = chat::Chat::load_from_db(&alice2, alice2_msg.chat_id).await?; + assert!(!alice2_chat.is_contact_request()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_private_reply_multidevice() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice1 = tcm.alice().await; + let alice2 = tcm.alice().await; + let bob = tcm.bob().await; + + // =============== Bob creates a group =============== + let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + chat::add_to_chat_contacts_table( + &bob, + group_id, + &[ + bob.add_or_lookup_contact(&alice1).await.id, + Contact::create(&bob, "", "charlie@example.org").await?, + ], + ) + .await?; + + // =============== Bob sends the first message to the group =============== + let sent = bob.send_text(group_id, "Hello all!").await; + alice1.recv_msg(&sent).await; + alice2.recv_msg(&sent).await; + + // =============== Alice answers privately with device 1 =============== + let received = alice1.get_last_msg().await; + let alice1_bob_contact = alice1.add_or_lookup_contact(&bob).await; + assert_eq!(received.from_id, alice1_bob_contact.id); + assert_eq!(received.to_id, ContactId::SELF); + assert!(!received.hidden); + assert_eq!(received.text, Some("Hello all!".to_string())); + assert_eq!(received.in_reply_to, None); + assert_eq!(received.chat_blocked, Blocked::Request); + + let received_group = Chat::load_from_db(&alice1, received.chat_id).await?; + assert_eq!(received_group.typ, Chattype::Group); + assert_eq!(received_group.name, "Group"); + assert_eq!(received_group.can_send(&alice1).await?, false); // Can't send because it's Blocked::Request + + let mut msg_out = Message::new(Viewtype::Text); + msg_out.set_text(Some("Private reply".to_string())); + + assert_eq!(received_group.blocked, Blocked::Request); + msg_out.set_quote(&alice1, Some(&received)).await?; + let alice1_bob_chat = alice1.create_chat(&bob).await; + let sent2 = alice1.send_msg(alice1_bob_chat.id, &mut msg_out).await; + alice2.recv_msg(&sent2).await; + + // =============== Alice's second device receives the message =============== + let received = alice2.get_last_msg().await; + + // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: + assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id); + + let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; + assert_eq!(received.from_id, ContactId::SELF); + assert_eq!(received.to_id, alice2_bob_contact.id); + assert!(!received.hidden); + assert_eq!(received.text, Some("Private reply".to_string())); + assert_eq!( + received.parent(&alice2).await?.unwrap().text, + Some("Hello all!".to_string()) + ); + assert_eq!(received.chat_blocked, Blocked::Not); + + let received_chat = Chat::load_from_db(&alice2, received.chat_id).await?; + assert_eq!(received_chat.typ, Chattype::Single); + assert_eq!(received_chat.name, "bob@example.net"); + assert_eq!(received_chat.can_send(&alice2).await?, true); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_auto_accept_for_bots() -> Result<()> { + let t = TestContext::new_alice().await; + t.set_config(Config::Bot, Some("1")).await.unwrap(); + receive_imf(&t, MSGRMSG, false).await?; + let msg = t.get_last_msg().await; + let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?; + assert!(!chat.is_contact_request()); + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_no_private_reply_to_blocked_account() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + // =============== Bob creates a group =============== + let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?; + chat::add_to_chat_contacts_table( + &bob, + group_id, + &[bob.add_or_lookup_contact(&alice).await.id], + ) + .await?; + + // =============== Bob sends the first message to the group =============== + let sent = bob.send_text(group_id, "Hello all!").await; + alice.recv_msg(&sent).await; + + let chats = Chatlist::try_load(&bob, 0, None, None).await?; + assert_eq!(chats.len(), 1); + + // =============== Bob blocks Alice ================ + Contact::block(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; + + // =============== Alice replies private to Bob ============== + let received = alice.get_last_msg().await; + assert_eq!(received.text, Some("Hello all!".to_string())); + + let received_group = Chat::load_from_db(&alice, received.chat_id).await?; + assert_eq!(received_group.typ, Chattype::Group); + + let mut msg_out = Message::new(Viewtype::Text); + msg_out.set_text(Some("Private reply".to_string())); + msg_out.set_quote(&alice, Some(&received)).await?; + + let alice_bob_chat = alice.create_chat(&bob).await; + let sent2 = alice.send_msg(alice_bob_chat.id, &mut msg_out).await; + bob.recv_msg(&sent2).await; + + // ========= check that no contact request was created ============ + let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 1); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); + + // since only chat is a group, no new open chat has been created + assert_eq!(chat.typ, Chattype::Group); + let received = bob.get_last_msg().await; + assert_eq!(received.text, Some("Hello all!".to_string())); + + // =============== Bob unblocks Alice ================ + // test if the blocked chat is restored correctly + Contact::unblock(&bob, bob.add_or_lookup_contact(&alice).await.id).await?; + let chats = Chatlist::try_load(&bob, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + let chat_id = chats.get_chat_id(0).unwrap(); + let chat = Chat::load_from_db(&bob, chat_id).await.unwrap(); + assert_eq!(chat.typ, Chattype::Single); + let received = bob.get_last_msg().await; + assert_eq!(received.text, Some("Private reply".to_string())); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_autocrypt() -> Result<()> { + let t = TestContext::new_bob().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt.eml"); + receive_imf(&t, raw, false).await?; + + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_autocrypt_unencrypted() -> Result<()> { + let t = TestContext::new_bob().await; + t.set_config(Config::ShowEmails, Some("2")).await?; + + let raw = include_bytes!("../../test-data/message/thunderbird_with_autocrypt_unencrypted.eml"); + receive_imf(&t, raw, false).await?; + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + let raw = include_bytes!("../../test-data/message/thunderbird_signed_unencrypted.eml"); + receive_imf(&t, raw, false).await?; + let peerstate = Peerstate::from_addr(&t, "alice@example.org") + .await? + .unwrap(); + assert_eq!(peerstate.prefer_encrypt, EncryptPreference::Mutual); + + Ok(()) +} + +/// Alice receives an encrypted, but unsigned message. +/// +/// Test that the message is displayed without any errors, +/// but also without a padlock. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_thunderbird_unsigned() -> Result<()> { + let alice = TestContext::new_alice().await; + alice.set_config(Config::ShowEmails, Some("2")).await?; + + // Alice receives an unsigned message from Bob. + let raw = include_bytes!("../../test-data/message/thunderbird_encrypted_unsigned.eml"); + receive_imf(&alice, raw, false).await?; + + let msg = alice.get_last_msg().await; + assert!(!msg.get_showpadlock()); + assert!(msg.error().is_none()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mua_user_adds_member() -> Result<()> { + let t = TestContext::new_alice().await; + + receive_imf( + &t, + b"From: alice@example.org\n\ + To: bob@example.com\n\ + Subject: foo\n\ + Message-ID: \n\ + Chat-Version: 1.0\n\ + Chat-Group-ID: gggroupiddd\n\ + Chat-Group-Name: foo\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await? + .unwrap(); + + receive_imf( + &t, + b"From: bob@example.com\n\ + To: alice@example.org, fiona@example.net\n\ + Subject: foo\n\ + Message-ID: \n\ + In-Reply-To: Gr.gggroupiddd.12345678901@example.com\n\ + Date: Sun, 22 Mar 2020 22:37:57 +0000\n\ + \n\ + hello\n", + false, + ) + .await? + .unwrap(); + + let (chat_id, _, _) = chat::get_chat_id_by_grpid(&t, "gggroupiddd") + .await? + .unwrap(); + let mut actual_chat_contacts = chat::get_chat_contacts(&t, chat_id).await?; + actual_chat_contacts.sort(); + let mut expected_chat_contacts = vec![ + Contact::create(&t, "", "bob@example.com").await?, + Contact::create(&t, "", "fiona@example.net").await?, + ContactId::SELF, + ]; + expected_chat_contacts.sort(); + assert_eq!(actual_chat_contacts, expected_chat_contacts); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mua_user_adds_recipient_to_single_chat() -> Result<()> { + let alice = TestContext::new_alice().await; + + // Alice sends a 1:1 message to Bob, creating a 1:1 chat. + let msg = receive_imf( + &alice, + b"Subject: =?utf-8?q?Message_from_alice=40example=2Eorg?=\r\n\ + From: alice@example.org\r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:30:39 +0000\r\n\ + Message-ID: \r\n\ + Chat-Version: 1.0\r\n\ + \r\n\ + tst\r\n", + false, + ) + .await? + .unwrap(); + let single_chat = Chat::load_from_db(&alice, msg.chat_id).await?; + assert_eq!(single_chat.typ, Chattype::Single); + + // Bob uses a classical MUA to answer in the 1:1 chat. + let msg2 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:31:39 +0000\r\n\ + Message-ID: \r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_eq!(msg2.chat_id, single_chat.id); + + // Bob uses a classical MUA to answer again, this time adding a recipient. + // This message should go to a newly created ad-hoc group. + let msg3 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: , \r\n\ + Date: Mon, 12 Dec 2022 14:32:39 +0000\r\n\ + Message-ID: \r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_ne!(msg3.chat_id, single_chat.id); + let group_chat = Chat::load_from_db(&alice, msg3.chat_id).await?; + assert_eq!(group_chat.typ, Chattype::Group); + assert_eq!( + chat::get_chat_contacts(&alice, group_chat.id).await?.len(), + 3 + ); + + // Bob uses a classical MUA to answer once more, adding another recipient. + // This new recipient should also be added to the group. + let msg4 = receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: , , \r\n\ + Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ + Message-ID: <69573857-542f-0fx3-55da-1289be5e0efe@example.net>\r\n\ + In-Reply-To: \r\n\ + \r\n\ + Hi back!\r\n", + false, + ) + .await? + .unwrap(); + assert_eq!(msg4.chat_id, group_chat.id); + assert_eq!( + chat::get_chat_contacts(&alice, group_chat.id).await?.len(), + 4 + ); + let fiona = Contact::lookup_id_by_addr(&alice, "fiona@example.net", Origin::IncomingTo) + .await? + .unwrap(); + assert!(chat::is_contact_in_chat(&alice, group_chat.id, fiona).await?); + + Ok(()) +} diff --git a/src/scheduler.rs b/src/scheduler.rs index 7a3dcc5ff..27a729cc4 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -141,8 +141,19 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne match ctx.get_config_bool(Config::FetchedExistingMsgs).await { Ok(fetched_existing_msgs) => { if !fetched_existing_msgs { + // Consider it done even if we fail. + // + // This operation is not critical enough to retry, + // especially if the error is persistent. + if let Err(err) = + ctx.set_config_bool(Config::FetchedExistingMsgs, true).await + { + warn!(ctx, "Can't set Config::FetchedExistingMsgs: {:#}", err); + } + if let Err(err) = connection.fetch_existing_msgs(&ctx).await { warn!(ctx, "Failed to fetch existing messages: {:#}", err); + connection.trigger_reconnect(&ctx); } } } @@ -198,8 +209,8 @@ async fn fetch_idle(ctx: &Context, connection: &mut Imap, folder_config: Config) .await .context("prepare IMAP connection") { - connection.trigger_reconnect(ctx); warn!(ctx, "{:#}", err); + connection.trigger_reconnect(ctx); return connection.fake_idle(ctx, Some(watch_folder)).await; } diff --git a/src/scheduler/connectivity.rs b/src/scheduler/connectivity.rs index 9ca9fb263..630053864 100644 --- a/src/scheduler/connectivity.rs +++ b/src/scheduler/connectivity.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use core::fmt; use std::{ops::Deref, sync::Arc}; diff --git a/src/securejoin.rs b/src/securejoin.rs index 08822a5a1..4da710ac4 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -1,5 +1,7 @@ //! Verified contact protocol implementation as [specified by countermitm project](https://countermitm.readthedocs.io/en/stable/new.html#setup-contact-protocol). +#![allow(missing_docs)] + use std::convert::TryFrom; use anyhow::{bail, Context as _, Error, Result}; @@ -409,7 +411,14 @@ pub(crate) async fn handle_securejoin_handshake( .await?; return Ok(HandshakeMessage::Ignore); } - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + let contact_addr = Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(); + if mark_peer_as_verified(context, fingerprint.clone(), contact_addr) + .await + .is_err() + { could_not_establish_secure_connection( context, contact_id, @@ -446,6 +455,8 @@ pub(crate) async fn handle_securejoin_handshake( } None => bail!("Chat {} not found", &field_grpid), } + inviter_progress!(context, contact_id, 800); + inviter_progress!(context, contact_id, 1000); } else { // Alice -> Bob secure_connection_established( @@ -494,9 +505,6 @@ pub(crate) async fn handle_securejoin_handshake( return Ok(HandshakeMessage::Ignore); } if join_vg { - // Responsible for showing "$Bob securely joined $group" message - inviter_progress!(context, contact_id, 800); - inviter_progress!(context, contact_id, 1000); let field_grpid = mime_message .get_header(HeaderDef::SecureJoinGroup) .map(|s| s.as_str()) @@ -529,7 +537,7 @@ pub(crate) async fn handle_securejoin_handshake( /// /// - if we see the self-sent-message vg-member-added/vc-contact-confirm, /// we know that we're an inviter-observer. -/// the inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth +/// The inviting device has marked a peer as verified on vg-request-with-auth/vc-request-with-auth /// before sending vg-member-added/vc-contact-confirm - so, if we observe vg-member-added/vc-contact-confirm, /// we can mark the peer as verified as well. /// @@ -570,30 +578,103 @@ pub(crate) async fn observe_securejoin_on_other_device( .await?; return Ok(HandshakeMessage::Ignore); } - let fingerprint: Fingerprint = - match mime_message.get_header(HeaderDef::SecureJoinFingerprint) { - Some(fp) => fp.parse()?, + let addr = Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_string(); + if mime_message.gossiped_addr.contains(&addr) { + let mut peerstate = match Peerstate::from_addr(context, &addr).await? { + Some(p) => p, None => { could_not_establish_secure_connection( - context, - contact_id, - info_chat_id(context, contact_id).await?, - "Fingerprint not provided, please update Delta Chat on all your devices.", - ) - .await?; + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!("No peerstate in db for '{}' at step {}", &addr, step), + ) + .await?; return Ok(HandshakeMessage::Ignore); } }; - if mark_peer_as_verified(context, &fingerprint).await.is_err() { + let fingerprint = match peerstate.gossip_key_fingerprint.clone() { + Some(fp) => fp, + None => { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!( + "No gossip key fingerprint in db for '{}' at step {}", + &addr, step, + ), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + }; + if let Err(err) = peerstate.set_verified( + PeerstateKeyType::GossipKey, + fingerprint, + PeerstateVerifiedStatus::BidirectVerified, + addr, + ) { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + &format!("Could not mark peer as verified at step {}: {}", step, err), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await.unwrap_or_default(); + } else if let Some(fingerprint) = + mime_message.get_header(HeaderDef::SecureJoinFingerprint) + { + // FIXME: Old versions of DC send this header instead of gossips. Remove this + // eventually. + let fingerprint = fingerprint.parse()?; + if mark_peer_as_verified( + context, + fingerprint, + Contact::load_from_db(context, contact_id) + .await? + .get_addr() + .to_owned(), + ) + .await + .is_err() + { + could_not_establish_secure_connection( + context, + contact_id, + info_chat_id(context, contact_id).await?, + format!("Fingerprint mismatch on observing {}.", step).as_ref(), + ) + .await?; + return Ok(HandshakeMessage::Ignore); + } + } else { could_not_establish_secure_connection( context, contact_id, info_chat_id(context, contact_id).await?, - format!("Fingerprint mismatch on observing {}.", step).as_ref(), + &format!( + "No gossip header for '{}' at step {}, please update Delta Chat on all \ + your devices.", + &addr, step, + ), ) .await?; return Ok(HandshakeMessage::Ignore); } + if step.as_str() == "vg-member-added" { + inviter_progress!(context, contact_id, 800); + } + if step.as_str() == "vg-member-added" || step.as_str() == "vc-contact-confirm" { + inviter_progress!(context, contact_id, 1000); + } Ok(if step.as_str() == "vg-member-added" { HandshakeMessage::Propagate } else { @@ -632,22 +713,27 @@ async fn could_not_establish_secure_connection( Ok(()) } -async fn mark_peer_as_verified(context: &Context, fingerprint: &Fingerprint) -> Result<(), Error> { - if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, fingerprint).await? { - if peerstate.set_verified( +async fn mark_peer_as_verified( + context: &Context, + fingerprint: Fingerprint, + verifier: String, +) -> Result<(), Error> { + if let Some(ref mut peerstate) = Peerstate::from_fingerprint(context, &fingerprint).await? { + if let Err(err) = peerstate.set_verified( PeerstateKeyType::PublicKey, fingerprint, PeerstateVerifiedStatus::BidirectVerified, + verifier, ) { - peerstate.prefer_encrypt = EncryptPreference::Mutual; - peerstate.save_to_db(&context.sql).await.unwrap_or_default(); - return Ok(()); + error!(context, "Could not mark peer as verified: {}", err); + return Err(err); } + peerstate.prefer_encrypt = EncryptPreference::Mutual; + peerstate.save_to_db(&context.sql).await.unwrap_or_default(); + Ok(()) + } else { + bail!("no peerstate in db for fingerprint {}", fingerprint.hex()); } - bail!( - "could not mark peer as verified for fingerprint {}", - fingerprint.hex() - ); } /* ****************************************************************************** @@ -686,6 +772,8 @@ mod tests { use crate::chat::ProtectionStatus; use crate::chatlist::Chatlist; use crate::constants::{Chattype, DC_GCM_ADDDAYMARKER}; + use crate::contact::ContactAddress; + use crate::contact::VerifiedStatus; use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::test_utils::{TestContext, TestContextManager}; @@ -929,6 +1017,7 @@ mod tests { verified_key: None, verified_key_fingerprint: None, fingerprint_changed: false, + verifier: None, }; peerstate.save_to_db(&bob.ctx.sql).await?; @@ -978,7 +1067,7 @@ mod tests { let (contact_bob_id, _modified) = Contact::add_or_lookup( &alice.ctx, "Bob", - "bob@example.net", + ContactAddress::new("bob@example.net")?, Origin::ManuallyCreated, ) .await?; diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index f5e340622..36a43e02d 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -326,7 +326,7 @@ impl BobState { /// /// This deviates from the protocol by also sending a confirmation message in response /// to the *vc-contact-confirm* message. This has no specific value to the protocol and - /// is only done out of symmerty with *vg-member-added* handling. + /// is only done out of symmetry with *vg-member-added* handling. async fn step_contact_confirm( &mut self, context: &Context, @@ -366,7 +366,12 @@ impl BobState { "Contact confirm message not encrypted", ))); } - mark_peer_as_verified(context, self.invite.fingerprint()).await?; + mark_peer_as_verified( + context, + self.invite.fingerprint().clone(), + mime_message.from.addr.to_string(), + ) + .await?; Contact::scaleup_origin_by_id(context, self.invite.contact_id(), Origin::SecurejoinJoined) .await?; context.emit_event(EventType::ContactsChanged(None)); diff --git a/src/simplify.rs b/src/simplify.rs index 36daa333b..1c30bc2ef 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -1,13 +1,13 @@ //! # Simplify incoming plaintext. -// protect lines starting with `--` against being treated as a footer. -// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B); -// this should be invisible on most systems and there is no need to unescape it again -// (which won't be done by non-deltas anyway) -// -// this escapes a bit more than actually needed by delta (eg. also lines as "-- footer"), -// but for non-delta-compatibility, that seems to be better. -// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced) +/// Protects lines starting with `--` against being treated as a footer. +/// for that, we insert a ZERO WIDTH SPACE (ZWSP, 0x200B); +/// this should be invisible on most systems and there is no need to unescape it again +/// (which won't be done by non-deltas anyway). +/// +/// This escapes a bit more than actually needed by delta (e.g. also lines as "-- footer"), +/// but for non-delta-compatibility, that seems to be better. +/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced) pub fn escape_message_footer_marks(text: &str) -> String { if let Some(text) = text.strip_prefix("--") { "-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-") @@ -74,6 +74,7 @@ pub(crate) fn split_lines(buf: &str) -> Vec<&str> { /// Simplified text and some additional information gained from the input. #[derive(Debug, Default)] pub(crate) struct SimplifiedText { + /// The text itself. pub text: String, /// True if the message is forwarded. diff --git a/src/smtp/send.rs b/src/smtp/send.rs index 829e114b0..6f09d1346 100644 --- a/src/smtp/send.rs +++ b/src/smtp/send.rs @@ -4,13 +4,17 @@ use super::Smtp; use async_smtp::{EmailAddress, Envelope, SendableEmail, Transport}; use crate::config::Config; -use crate::constants::DEFAULT_MAX_SMTP_RCPT_TO; use crate::context::Context; use crate::events::EventType; use std::time::Duration; pub type Result = std::result::Result; +// if more recipients are needed in SMTP's `RCPT TO:` header, recipient-list is splitted to chunks. +// this does not affect MIME'e `To:` header. +// can be overwritten by the setting `max_smtp_rcpt_to` in provider-db. +pub(crate) const DEFAULT_MAX_SMTP_RCPT_TO: usize = 50; + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Envelope error: {}", _0)] diff --git a/src/socks.rs b/src/socks.rs index 89a9458c6..e7ea20730 100644 --- a/src/socks.rs +++ b/src/socks.rs @@ -4,10 +4,10 @@ use std::fmt; use std::pin::Pin; use std::time::Duration; -use anyhow::{Context as _, Result}; +use crate::net::connect_tcp; +use anyhow::Result; pub use async_smtp::ServerAddress; use tokio::net::{self, TcpStream}; -use tokio::time::timeout; use tokio_io_timeout::TimeoutStream; use crate::context::Context; @@ -59,14 +59,7 @@ impl Socks5Config { target_addr: impl net::ToSocketAddrs, timeout_val: Duration, ) -> Result>>>> { - let tcp_stream = timeout(timeout_val, TcpStream::connect(target_addr)) - .await - .context("connection timeout")? - .context("connection failure")?; - let mut timeout_stream = TimeoutStream::new(tcp_stream); - timeout_stream.set_write_timeout(Some(timeout_val)); - timeout_stream.set_read_timeout(Some(timeout_val)); - let timeout_stream = Box::pin(timeout_stream); + let tcp_stream = connect_tcp(target_addr, timeout_val).await?; let authentication_method = if let Some((username, password)) = self.user_password.as_ref() { @@ -78,8 +71,7 @@ impl Socks5Config { None }; let socks_stream = - Socks5Stream::use_stream(timeout_stream, authentication_method, Config::default()) - .await?; + Socks5Stream::use_stream(tcp_stream, authentication_method, Config::default()).await?; Ok(socks_stream) } diff --git a/src/sql.rs b/src/sql.rs index 92fa9bd4a..9d37c68ad 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -1,5 +1,7 @@ //! # SQLite wrapper. +#![allow(missing_docs)] + use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::path::Path; @@ -235,13 +237,13 @@ impl Sql { // When auto_vacuum is INCREMENTAL, it is possible to // use PRAGMA incremental_vacuum to return unused // database pages to the filesystem. - conn.pragma_update(None, "auto_vacuum", &"INCREMENTAL".to_string())?; + conn.pragma_update(None, "auto_vacuum", "INCREMENTAL".to_string())?; // journal_mode is persisted, it is sufficient to change it only for one handle. - conn.pragma_update(None, "journal_mode", &"WAL".to_string())?; + conn.pragma_update(None, "journal_mode", "WAL".to_string())?; // Default synchronous=FULL is much slower. NORMAL is sufficient for WAL mode. - conn.pragma_update(None, "synchronous", &"NORMAL".to_string())?; + conn.pragma_update(None, "synchronous", "NORMAL".to_string())?; Ok(()) })?; } @@ -477,7 +479,7 @@ impl Sql { let conn = self.get_conn().await?; tokio::task::block_in_place(move || { let mut exists = false; - conn.pragma(None, "table_info", &name.to_string(), |_row| { + conn.pragma(None, "table_info", name.to_string(), |_row| { // will only be executed if the info was found exists = true; Ok(()) @@ -494,7 +496,7 @@ impl Sql { let mut exists = false; // `PRAGMA table_info` returns one row per column, // each row containing 0=cid, 1=name, 2=type, 3=notnull, 4=dflt_value - conn.pragma(None, "table_info", &table_name.to_string(), |row| { + conn.pragma(None, "table_info", table_name.to_string(), |row| { let curr_name: String = row.get(1)?; if col_name == curr_name { exists = true; @@ -644,26 +646,26 @@ pub async fn housekeeping(context: &Context) -> Result<()> { if let Err(err) = remove_unused_files(context).await { warn!( context, - "Housekeeping: cannot remove unusued files: {}", err + "Housekeeping: cannot remove unusued files: {:#}", err ); } if let Err(err) = start_ephemeral_timers(context).await { warn!( context, - "Housekeeping: cannot start ephemeral timers: {}", err + "Housekeeping: cannot start ephemeral timers: {:#}", err ); } if let Err(err) = prune_tombstones(&context.sql).await { warn!( context, - "Housekeeping: Cannot prune message tombstones: {}", err + "Housekeeping: Cannot prune message tombstones: {:#}", err ); } if let Err(err) = deduplicate_peerstates(&context.sql).await { - warn!(context, "Failed to deduplicate peerstates: {}", err) + warn!(context, "Failed to deduplicate peerstates: {:#}", err) } context.schedule_quota_update().await?; diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 0b5a46da8..fde27051c 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -637,7 +637,11 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); verified_key_fingerprint TEXT DEFAULT '', UNIQUE (addr) -- Only one peerstate per address ); - INSERT OR IGNORE INTO new_acpeerstates SELECT * FROM acpeerstates; + INSERT OR IGNORE INTO new_acpeerstates SELECT + id, addr, last_seen, last_seen_autocrypt, public_key, prefer_encrypted, + gossip_timestamp, gossip_key, public_key_fingerprint, + gossip_key_fingerprint, verified_key, verified_key_fingerprint + FROM acpeerstates; DROP TABLE acpeerstates; ALTER TABLE new_acpeerstates RENAME TO acpeerstates; CREATE INDEX acpeerstates_index1 ON acpeerstates (addr); @@ -652,7 +656,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); if dbversion < 95 { sql.execute_migration( "CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\ - INSERT OR IGNORE INTO new_chats_contacts SELECT * FROM chats_contacts;\ + INSERT OR IGNORE INTO new_chats_contacts SELECT chat_id, contact_id FROM chats_contacts;\ DROP TABLE chats_contacts;\ ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\ CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\ @@ -660,6 +664,13 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); 95 ).await?; } + if dbversion < 96 { + sql.execute_migration( + "ALTER TABLE acpeerstates ADD COLUMN verifier TEXT DEFAULT '';", + 96, + ) + .await?; + } let new_version = sql .get_raw_config_int(VERSION_CFG) diff --git a/src/stock_str.rs b/src/stock_str.rs index e58de954d..2e93962d6 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -1,5 +1,7 @@ //! Module to work with translatable stock strings. +#![allow(missing_docs)] + use std::collections::HashMap; use std::sync::Arc; diff --git a/src/summary.rs b/src/summary.rs index 5f6a0537a..5b4d3f0bc 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -52,6 +52,8 @@ pub struct Summary { } impl Summary { + /// Constucts chatlist summary + /// from the provided message, chat and message author contact snapshots. pub async fn new( context: &Context, msg: &Message, diff --git a/src/test_utils.rs b/src/test_utils.rs index 43318edfc..f5bce1cf3 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -14,6 +14,7 @@ use chat::ChatItem; use once_cell::sync::Lazy; use rand::Rng; use tempfile::{tempdir, TempDir}; +use tokio::runtime::Handle; use tokio::sync::RwLock; use tokio::task; @@ -21,8 +22,8 @@ use crate::chat::{self, Chat, ChatId}; use crate::chatlist::Chatlist; use crate::config::Config; use crate::constants::Chattype; -use crate::constants::{DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER}; -use crate::contact::{Contact, ContactId, Modifier, Origin}; +use crate::constants::{DC_GCL_NO_SPECIALS, DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER}; +use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin}; use crate::context::Context; use crate::events::{Event, EventType, Events}; use crate::key::{self, DcKey, KeyPair, KeyPairUse}; @@ -244,6 +245,7 @@ impl TestContext { /// /// This is a shortcut which automatically calls [`TestContext::configure_alice`] after /// creating the context. + /// alice-email: alice@example.org pub async fn new_alice() -> Self { Self::builder().configure_alice().build().await } @@ -262,7 +264,6 @@ impl TestContext { Self::builder().configure_fiona().build().await } - #[allow(dead_code)] /// Print current chat state. pub async fn print_chats(&self) { println!("\n========== Chats of {}: ==========", self.name()); @@ -501,7 +502,7 @@ impl TestContext { /// Gets the most recent message over all chats. pub async fn get_last_msg(&self) -> Message { - let chats = Chatlist::try_load(&self.ctx, 0, None, None) + let chats = Chatlist::try_load(&self.ctx, DC_GCL_NO_SPECIALS, None, None) .await .expect("failed to load chatlist"); // 0 is correct in the next line (as opposed to `chats.len() - 1`, which would be the last element): @@ -522,13 +523,14 @@ impl TestContext { .await .unwrap_or_default() .unwrap_or_default(); - let addr = other.ctx.get_primary_self_addr().await.unwrap(); + 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, &name, addr, Origin::MailinglistAddress) .await - .unwrap(); + .expect("add_or_lookup"); match modified { Modifier::None => (), Modifier::Modified => warn!(&self.ctx, "Contact {} modified by TestContext", &addr), @@ -701,6 +703,19 @@ impl Deref for TestContext { } } +impl Drop for TestContext { + fn drop(&mut self) { + task::block_in_place(move || { + if let Ok(handle) = Handle::try_current() { + // Print the chats if runtime still exists. + handle.block_on(async move { + self.print_chats().await; + }); + } + }); + } +} + pub enum LogEvent { /// Logged event. Event(Event), @@ -1078,4 +1093,12 @@ mod tests { bob.ctx.emit_event(EventType::Info("there".into())); // panic!("Both fail"); } + + /// Checks that dropping the `TestContext` after the runtime does not panic, + /// e.g. that `TestContext::drop` does not assume the runtime still exists. + #[test] + fn test_new_test_context() { + let runtime = tokio::runtime::Runtime::new().expect("unable to create tokio runtime"); + runtime.block_on(TestContext::new()); + } } diff --git a/src/tools.rs b/src/tools.rs index ed04bc25a..e9c34f5e1 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -1,6 +1,8 @@ //! Some tools and enhancements to the used libraries, there should be //! no references to Context and other "larger" entities here. +#![allow(missing_docs)] + use core::cmp::{max, min}; use std::borrow::Cow; use std::fmt; @@ -271,10 +273,16 @@ async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time /// IDs generated by this function are 66 bit wide and are returned as 11 base64 characters. /// /// Additional information when used as a message-id or group-id: -/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as Gr..@ +/// - for OUTGOING messages this ID is written to the header as `Chat-Group-ID:` and is added to the message ID as `Gr..@` /// - for INCOMING messages, the ID is taken from the Chat-Group-ID-header or from the Message-ID in the In-Reply-To: or References:-Header /// - the group-id should be a string with the characters [a-zA-Z0-9\-_] pub(crate) fn create_id() -> String { + const URL_SAFE_ENGINE: base64::engine::fast_portable::FastPortable = + base64::engine::fast_portable::FastPortable::from( + &base64::alphabet::URL_SAFE, + base64::engine::fast_portable::NO_PAD, + ); + // ThreadRng implements CryptoRng trait and is supposed to be cryptographically secure. let mut rng = thread_rng(); @@ -283,7 +291,7 @@ pub(crate) fn create_id() -> String { rng.fill(&mut arr[..]); // Take 11 base64 characters containing 66 random bits. - base64::encode_config(arr, base64::URL_SAFE) + base64::encode_engine(arr, &URL_SAFE_ENGINE) .chars() .take(11) .collect() @@ -356,12 +364,10 @@ pub(crate) fn get_abs_path(context: &Context, path: impl AsRef) -> PathBuf } } -pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> u64 { +pub(crate) async fn get_filebytes(context: &Context, path: impl AsRef) -> Result { let path_abs = get_abs_path(context, &path); - match fs::metadata(&path_abs).await { - Ok(meta) => meta.len() as u64, - Err(_err) => 0, - } + let meta = fs::metadata(&path_abs).await?; + Ok(meta.len()) } pub(crate) async fn delete_file(context: &Context, path: impl AsRef) -> bool { @@ -492,7 +498,7 @@ pub fn open_file_std>( let p: PathBuf = path.as_ref().into(); let path_abs = get_abs_path(context, p); - match std::fs::File::open(&path_abs) { + match std::fs::File::open(path_abs) { Ok(bytes) => Ok(bytes), Err(err) => { warn!( @@ -591,11 +597,7 @@ impl rusqlite::types::ToSql for EmailAddress { /// Makes sure that a user input that is not supposed to contain newlines does not contain newlines. pub(crate) fn improve_single_line_input(input: &str) -> String { - input - .replace('\n', " ") - .replace('\r', " ") - .trim() - .to_string() + input.replace(['\n', '\r'], " ").trim().to_string() } pub(crate) trait IsNoneOrEmpty { @@ -1051,7 +1053,7 @@ DKIM Results: Passed=true, Works=true, Allow_Keychange=true"; .is_ok()); assert!(file_exist!(context, "$BLOBDIR/foobar")); assert!(!file_exist!(context, "$BLOBDIR/foobarx")); - assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await, 7); + assert_eq!(get_filebytes(context, "$BLOBDIR/foobar").await.unwrap(), 7); let abs_path = context .get_blobdir() diff --git a/src/webxdc.rs b/src/webxdc.rs index aba2eb850..0433ab15c 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -30,6 +30,7 @@ use crate::{chat, EventType}; /// In the future, that may be useful to avoid new Webxdc being loaded on old Delta Chats. const WEBXDC_API_VERSION: u32 = 1; +/// Suffix used to recognize webxdc files. pub const WEBXDC_SUFFIX: &str = "xdc"; const WEBXDC_DEFAULT_ICON: &str = "__webxdc__/default-icon.png"; @@ -54,20 +55,44 @@ const WEBXDC_RECEIVING_LIMIT: u64 = 4194304; #[derive(Debug, Deserialize)] #[non_exhaustive] struct WebxdcManifest { + /// Webxdc name, used on icons or page titles. name: Option, + + /// Minimum API version required to run this webxdc. min_api: Option, + + /// Optional URL of webxdc source code. source_code_url: Option, + + /// If the webxdc requests network access. request_internet_access: Option, } /// Parsed information from WebxdcManifest and fallbacks. #[derive(Debug, Serialize)] pub struct WebxdcInfo { + /// The name of the app. + /// Defaults to filename if not set in the manifest. pub name: String, + + /// Filename of the app icon. pub icon: String, + + /// If the webxdc represents a document and allows to edit it, + /// this is the document name. + /// Otherwise an empty string. pub document: String, + + /// Short description of the webxdc state. + /// For example, "7 votes". pub summary: String, + + /// URL of webxdc source code or an empty string. pub source_code_url: String, + + /// If the webxdc is allowed to access the network. + /// It should request access, be encrypted + /// and sent to self for this. pub internet_access: bool, } diff --git a/test-data/message/thunderbird_encrypted_unsigned.eml b/test-data/message/thunderbird_encrypted_unsigned.eml new file mode 100644 index 000000000..924edaced --- /dev/null +++ b/test-data/message/thunderbird_encrypted_unsigned.eml @@ -0,0 +1,44 @@ +Message-ID: <1e87b947-d7be-1ebd-e374-e22ebaa5e00a@example.net> +Date: Fri, 23 Dec 2022 13:00:00 +0000 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.6.1 +Content-Language: en-US +To: Alice +From: Bob +Subject: ... +Content-Type: multipart/encrypted; + protocol="application/pgp-encrypted"; + boundary="------------LXsQ1GfDj60OyJAdRFaJ9eSx" + +This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156) +--------------LXsQ1GfDj60OyJAdRFaJ9eSx +Content-Type: application/pgp-encrypted +Content-Description: PGP/MIME version identification + +Version: 1 + +--------------LXsQ1GfDj60OyJAdRFaJ9eSx +Content-Type: application/octet-stream; name="encrypted.asc" +Content-Description: OpenPGP encrypted message +Content-Disposition: inline; filename="encrypted.asc" + +-----BEGIN PGP MESSAGE----- + +wV4D5tq63hTeebASAQdAgKVFSZ8p8RyNRongu8xc0qnEl54jbOTjrow1rYHECFUw +44bDFHvaSjFYMFcFmumYZQ+hFcjaoIEv78Wx6GmEGTFzFcs8mBKUGbRDFeLJebbU +0sE8AcETwB3nrhR4WvUYlmqt87EAJfigMYX3Cmod0Pz9cFPdE13NIoOotZXEcxHq +aGefenHJUYF1/FX9iuGRMt/qo41stlSJ6Z575mxUKnHexJVnvoDgFLTwS2dGTNlt +0VBk7NOeaCmbxm8u4uJ3kt2vPG7ViYsSvHMGdYciIkGMsyHpJPzrOHzyoXZuuea1 +t8QTZym3FhHH9gbTRCnfnIX60G1Qu9hSMRAQandA6J2to9IahmlahFP9XoUin3fK +ikeCHoNs9kTFFXarU0q99O6byhQqwSehoMhx4BLEFXvMp49jD9LzHUGJmR3Pdcqb +lBgU9mPYWflfiQ8wj2Awyj94+YR3ovaOuc75LE+JylJ9BLk3axZou2HI6hDCoZJg +XQkF46JMPV4NArOtPxP1N5/Gvo9TGvH7H4LdMhsI1Wc8Lfiks6UdcGXUblAlEfcN +rQfywPIH4n+H8cTqk1v2ON3OsD9sFluggWUHEWLP3Eqtr1RO2YQURv+N+pcqvLyq +kPIP9JQ5rfSMjbRNBmN5RReflomYcq9Dt8iobMWXt2fokiyJueaRwZSst2d/pG6H +oYqzzxM7DXnxaJvZELGwJ2tGlQPL5JtSZL+jgL+Zd7+Z7czuRLLoVqf0Q6tgQPKE +s8cpgxVDW0hp5T3ukNvL03SxK+v+dqBPFLd9FYcxMA== +=Wl0m +-----END PGP MESSAGE----- + +--------------LXsQ1GfDj60OyJAdRFaJ9eSx-- diff --git a/test-data/message/thunderbird_signed_unencrypted.eml b/test-data/message/thunderbird_signed_unencrypted.eml new file mode 100644 index 000000000..ec9646050 --- /dev/null +++ b/test-data/message/thunderbird_signed_unencrypted.eml @@ -0,0 +1,56 @@ +From - Thu, 15 Dec 2022 14:45:17 GMT +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 00000000 +Message-ID: +Date: Thu, 15 Dec 2022 11:45:16 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.5.1 +Content-Language: en-US +To: bob@example.net +From: Alice +Subject: test message 15:53 +X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; + attachmentreminder=0; deliveryformat=0 +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; + boundary="------------iX39J1p7DOgblwacjo0e7jX7" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--------------iX39J1p7DOgblwacjo0e7jX7 +Content-Type: multipart/mixed; boundary="------------WD4DG7TcI4p4lbzyM4toRaDw"; + protected-headers="v1" +From: Alice +To: bob@example.net +Message-ID: +Subject: test message 15:53 + +--------------WD4DG7TcI4p4lbzyM4toRaDw +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: base64 + +DQo= + +--------------WD4DG7TcI4p4lbzyM4toRaDw-- + +--------------iX39J1p7DOgblwacjo0e7jX7 +Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="OpenPGP_signature" + +-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEEFKs/ZfwnS721+naMJfAHJFnkeuIFAmObMvwFAwAAAAAACgkQJfAHJFnkeuLM +TgwAnAADX93HE5vXmuBcAbRN2HKIwMzBtRtUF4FNPKchffUvvhSNpHkW2jW7A4hOHNgVSDQdqIUn ++62NgkaKrT1bZqozOZNHXMECHtKBwXWTkIAVqcBdvscCztVIgGby56OPnzZ5y09BsRaqqE5AhDgN +wGCLa6ipu5FYSF6+KzdO0GIPMY5aGRgVhtl4N01v4S3+r/Yu60MkN87nd15Eaqsrs60P9RmKJTt4 +hDie35kKvHnPzLNs8+xLfqPuO/P7ZbPQgkgCwMAMsMDRUYOv+k5c/bL3PKiOENuDpQ7dkKJ2OzSn +nTcg8qhDf17vWe26C/QBhFiGEsrHNBQ1KW5by+cqjIUBJgXElFnPl35S5L3fn6JHZLcz6q+wQuJu +vGT1mJuP//jLFkMHSexukFIVXzn41rWPLd05rBqMgwRcOHMIyzE9zaO1aa8MF2TirPaZ5lH9rx/y +9DCU/d2sqbbYt8TGqj4hM3pqg5K22eq4KT1W7y8+28I5QfjZumLLrHBdYTnR +=6JTB +-----END PGP SIGNATURE----- + +--------------iX39J1p7DOgblwacjo0e7jX7-- diff --git a/test-data/message/thunderbird_with_autocrypt_unencrypted.eml b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml new file mode 100644 index 000000000..201044751 --- /dev/null +++ b/test-data/message/thunderbird_with_autocrypt_unencrypted.eml @@ -0,0 +1,142 @@ +From - Wed, 14 Dec 2022 18:53:03 GMT +X-Mozilla-Status: 0801 +X-Mozilla-Status2: 00000000 +Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org> +Date: Wed, 14 Dec 2022 15:53:03 -0300 +MIME-Version: 1.0 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 + Thunderbird/102.5.1 +Content-Language: en-US +To: bob@example.net +From: Alice +Subject: test message 15:52 +Autocrypt: addr=alice@example.org; keydata= + xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E/jFiKZWj + 1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37mdw+DKs0YvNIlc+A + RjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsrtS2JgJ+tLSYUvNJeMJXm/cDL + XKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJF0De11/7G62dHNHuhmtgRLsTN4Q372Q9 + KNdYEFLHaN91jEzyD/+aHNskATxtcGhppI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yf + VAyA69t5fctQRb4+bTwL+sS9KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiL + vYUfdNJstAqvLf04mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3E + q8e6TbOY7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT + AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcFFQgJCgsF + FgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nnYq5pOuHGUndZ7jYK + cOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdtHoulEG4RPGVboDaY9tuMOL3/ + GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJ + x15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQl + nfISJ17GBLmH1YxmPPZ3CRHC6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJ + YskyNndtv0iaNRT7YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ + 8myMwA+ybfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw + eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVFuPGrSdRU + 06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXFpgtZUNV3iGOSOcSE + LldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6FdKYxVd4NBVH9abZ7t8Tm4qC + urZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBgU3q5W+wrtZ56kI9mxJec62KHpyLZ0rTE + xEAeVbChUJOo11vUtJfTrDhI6lhqyr72o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTS + UxOz60xNggEfDVtfgfjBZrBbiHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h + 8l019MYmGadpQgbuA4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfu + g2fuRf258Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY + AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXwByRZ5Hri + EOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxCjWrnUDvvJEwP1W3j + UXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ9eLjEM++rbOD4vWbYXRwaDiH + LetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvgPxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYT + XhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+bQw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajj + Wy7b9TuT38t1HArv4m/LyVuBHiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pK + MGDpk/1NVuMnIHJESRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAa + j4mkQQvMU0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa + /qMLjKwBpKEd/w== +X-Mozilla-Draft-Info: internal/draft; vcard=0; receipt=0; DSN=0; uuencode=0; + attachmentreminder=0; deliveryformat=0 +X-Identity-Key: id3 +Fcc: imap://alice%40example.org@in.example.org/Sent +Content-Type: multipart/signed; micalg=pgp-sha256; + protocol="application/pgp-signature"; + boundary="------------x6XEHrf0vHmVgEo6f9bMGGUy" + +This is an OpenPGP/MIME signed message (RFC 4880 and 3156) +--------------x6XEHrf0vHmVgEo6f9bMGGUy +Content-Type: multipart/mixed; boundary="------------pePWGfS6inyAJsaJRFnx5r9s"; + protected-headers="v1" +From: Alice +To: bob@example.net +Message-ID: <87d75c7e-0f52-1335-e437-af605c09f954@example.org> +Subject: test message 15:52 + +--------------pePWGfS6inyAJsaJRFnx5r9s +Content-Type: multipart/mixed; boundary="------------bG3L0s709hFHGhT5ybFZLKLf" + +--------------bG3L0s709hFHGhT5ybFZLKLf +Content-Type: text/plain; charset=UTF-8; format=flowed +Content-Transfer-Encoding: base64 + +DQo= +--------------bG3L0s709hFHGhT5ybFZLKLf +Content-Type: application/pgp-keys; name="OpenPGP_0x25F0072459E47AE2.asc" +Content-Disposition: attachment; filename="OpenPGP_0x25F0072459E47AE2.asc" +Content-Description: OpenPGP public key +Content-Transfer-Encoding: quoted-printable + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +xsDNBGOaGzQBDADCFtBNMHRDJQRkd2tNm7CJm1Yo3Y5r3qP6v0FSwP1BIHbiIf0E +/jFiKZWj1uL68J2mGUuUu+Qi4ovf1l9/QQYzg/DCaLZxlbc0LKu2LXcpUL5DPu37 +mdw+DKs0YvNIlc+ARjyFUwd3rsZN3k58inf1mYzKuKU6NpbdXULbOEYwnVEwzQsr +tS2JgJ+tLSYUvNJeMJXm/cDLXKJSApAyvVVdxxteG8uWcDqWV/HcXuopXLILf3yJ +F0De11/7G62dHNHuhmtgRLsTN4Q372Q9KNdYEFLHaN91jEzyD/+aHNskATxtcGhp +pI8OQsU3NzNgHyd8Smzx5oTyZ/6NdhYoh0pKB8yfVAyA69t5fctQRb4+bTwL+sS9 +KDobQOvcyOMUSccDfUhsWMghwsMCwU4Sz9hIY6dCAfroDAiLvYUfdNJstAqvLf04 +mZtMmkI7Q2BYLETEgu4KQzQHRQekmOE/3EaSiojNa4ZTVURMdJ9U+I3Eq8e6TbOY +7Xa4V8krAt/F2wMAEQEAAc0ZQWxpY2UgPGFsaWNlQGV4YW1wbGUub3JnPsLBDQQT +AQgANxYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs1BQm7+B4AAhsDBAsJCAcF +FQgJCgsFFgIDAQAACgkQJfAHJFnkeuKQ2wwAgDgiCI6bz9PjqE1GoDcy/xQdy+nn +Yq5pOuHGUndZ7jYKcOqM8LDEaG7GgrFsbs9vGhTA1fyqncM41pB7SmwQ7zBVaMdt +HoulEG4RPGVboDaY9tuMOL3/GVxFbovVHyU5Lr1euryNh/0JvMITY0UHaEY1k1M7 +izYUMyFu8I1ODZ9Iws2trUyU3Omw/sTJx15zzCsK8Aq+r3JmB+Q33SFSgWr/YWH0 +dQVIQ0I5iLN2q14oucmLBaKc9EXdRLiu8S8lLSQlnfISJ17GBLmH1YxmPPZ3CRHC +6iEKCLR6G9wzhsTPNdK7dRCYR5wTI27RVPLBcSnCKAeTopAJYskyNndtv0iaNRT7 +YLOfhrsBAofSjuLegR04CNiqBHtYQ3LO3WKhJ7riRcQ/Ksv0wYkmj1gJ8myMwA+y +bfYrpNqO4devnCvE3Eo5gzeYbvYU2Z17n9y6HAOG9/Tm/daiGEP2ni6iwV0kqLzw +eC48R1D75T66PxX/jQooujrTph8+K3ckV/q+zsDNBGOaGzUBDADV+DGgKxvCpfVF +uPGrSdRU06dxowdKOKavO6WGMvN3g/+CFrIsjUFy4S0Soo5ARnLh23i49ZSjacXF +pgtZUNV3iGOSOcSELldLtZk5BV9w/ATqqgu4/LVdNA9rm+o197bIeSQCRTnY/QV6 +FdKYxVd4NBVH9abZ7t8Tm4qCurZj56MjPCg3fqT+Q6sjxH+nKBrs8s8iCJkYhGBg +U3q5W+wrtZ56kI9mxJec62KHpyLZ0rTExEAeVbChUJOo11vUtJfTrDhI6lhqyr72 +o/A6bY1OV7WzkxtiBRl35eewQ+RDLJ4yxaNj/XTSUxOz60xNggEfDVtfgfjBZrBb +iHXqf8iKVV1ZPGm0ycvXZGYFw2zXLI2PwevhQCm+t4Ywty1h8l019MYmGadpQgbu +A4ZippuzOSzSGMQ+S4uYEzeeymR9ksxVSXn90HEzqC7LdHCcd2IO6rfug2fuRf25 +8Adfuoh3s8YUlWyXjEHLXKo9SRgGMfGs7qgCOL/ReAwFPtKACvEAEQEAAcLA/AQY +AQgAJhYhBBSrP2X8J0u9tfp2jCXwByRZ5HriBQJjmhs2BQm7+B4AAhsMAAoJECXw +ByRZ5HriEOkMALtq4DVYX8RfoPdU0Dt6y+yDj1NALv5GefvHbgfuaVT8PaOP0gxC +jWrnUDvvJEwP1W3jUXYqDwKP42hiGWsnXk2hbgXbplArgP3H987x7c8bu1wIAmkJ +9eLjEM++rbOD4vWbYXRwaDiHLetFJ5tGHDAIfL48NYpz2o3XZ3/O7WdTZphsAcvg +PxTC+zU7WkbUl2SQlj0/qwsoD+qe9RYTXhVXR7q7sjcGB4TpeqzRT7YKVLoVNq+b +Qw2lUX4W561gAYbZvVo/XByfDCoxmkxwuMlSmajjWy7b9TuT38t1HArv4m/LyVuB +HiikX0/MUNBeSSIiKDvTL6NdHTjnZM6ptZvdvW3+ou6ET0pKMGDpk/1NVuMnIHJE +SRg/SSFV6sElgq38k9wAT2oUqLcYvYI07nHmnuciaGygkCcGt+l2PvAaj4mkQQvM +U0cNRDBybk5aKi820oGIJjT7e+5RnD2mYZQdOAbQhDVCHvrfS1I60bsHT1MHqyAa +/qMLjKwBpKEd/w=3D=3D +=3DE1VA +-----END PGP PUBLIC KEY BLOCK----- + +--------------bG3L0s709hFHGhT5ybFZLKLf-- + +--------------pePWGfS6inyAJsaJRFnx5r9s-- + +--------------x6XEHrf0vHmVgEo6f9bMGGUy +Content-Type: application/pgp-signature; name="OpenPGP_signature.asc" +Content-Description: OpenPGP digital signature +Content-Disposition: attachment; filename="OpenPGP_signature" + +-----BEGIN PGP SIGNATURE----- + +wsD5BAABCAAjFiEEFKs/ZfwnS721+naMJfAHJFnkeuIFAmOaG48FAwAAAAAACgkQJfAHJFnkeuJx +1Av/SkGIP18ql7cImI4/t49MvZdIWNuqyKuHZr+7hCPDq0i3muKuy04e8AsGvhHRS8/aSSFkkCgf +OM5JYwHjOVj7DLTGSfbGM9GGpbu4fP6wa+rCm/WHgRr2H/T4ggy6jNv4rBOMcSNXhpO+J/28Zjoi +47Dl1eH6B8HyiwqHSPixRqWAf0d8dIp5S7Wf4asb+cFA+rM/7UlZqidJP5ihtHA3A6C1SNRnMBk/ +g3ABR45srubkgXu5QN4PsUalE0N4I00aCgR6WiPggJE2Zf1kslj5M7az3Az+dp0apRilxrlP4J4Y +KxEzim2X8tJbqvq8G7295NcNDH3YCx3sOT8utXM5NL9how+4c2iylD2m7Oczz3bv0TYDU/4ksWmO +zwiMm+47+45UNLWjQ2sGTpui6nXFC7ZuGxzKjUrbpfvkFDIeFgO1XVw812YDoMHKMaEzr8fDhoKD +LD14JUpwC47XsDlB4ZAMizcCYiK3MvTk1w/5I5ijzJmMiMIpPNxzYxIm82F3 +=wlaI +-----END PGP SIGNATURE----- + +--------------x6XEHrf0vHmVgEo6f9bMGGUy--