mirror of
https://github.com/chatmail/core.git
synced 2026-04-14 03:57:19 +03:00
Compare commits
1 Commits
v1.159.3
...
link2xt/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
956d7009fb |
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@@ -20,22 +20,20 @@ permissions: {}
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUST_VERSION: 1.86.0
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
MSRV: 1.82.0
|
||||
|
||||
jobs:
|
||||
lint_rust:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.84.1
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
- name: Install rustfmt and clippy
|
||||
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
||||
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
- name: Run rustfmt
|
||||
@@ -93,36 +91,25 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: latest
|
||||
rust: 1.84.1
|
||||
- os: windows-latest
|
||||
rust: latest
|
||||
rust: 1.84.1
|
||||
- os: macos-latest
|
||||
rust: latest
|
||||
rust: 1.84.1
|
||||
|
||||
# Minimum Supported Rust Version
|
||||
# Minimum Supported Rust Version = 1.81.0
|
||||
- os: ubuntu-latest
|
||||
rust: minimum
|
||||
rust: 1.81.0
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- run:
|
||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
if: matrix.rust == 'minimum'
|
||||
- run:
|
||||
echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
if: matrix.rust == 'latest'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust ${{ matrix.rust }}
|
||||
run: rustup toolchain install --profile minimal $RUSTUP_TOOLCHAIN
|
||||
shell: bash
|
||||
- run: rustup override set $RUSTUP_TOOLCHAIN
|
||||
shell: bash
|
||||
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||
- run: rustup override set ${{ matrix.rust }}
|
||||
|
||||
- name: Cache rust cargo artifacts
|
||||
uses: swatinem/rust-cache@v2
|
||||
|
||||
@@ -9,7 +9,7 @@ permissions: {}
|
||||
jobs:
|
||||
pack-module:
|
||||
name: "Publish @deltachat/jsonrpc-client"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
166
CHANGELOG.md
166
CHANGELOG.md
@@ -1,167 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [1.159.3] - 2025-04-24
|
||||
|
||||
### CI
|
||||
|
||||
- Use `ubuntu-latest` runner for `@deltachat/jsonrpc-client` publishing.
|
||||
|
||||
## [1.159.2] - 2025-04-23
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow to send to chats after failed securejoin again ([#6817](https://github.com/chatmail/core/pull/6817)).
|
||||
- Parse login scheme in `add_transport_from_qr()` ([#6802](https://github.com/chatmail/core/pull/6802)).
|
||||
- Lowercase address in add_transport() ([#6805](https://github.com/chatmail/core/pull/6805)).
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Rename add_transport() -> add_or_update_transport() ([#6800](https://github.com/chatmail/core/pull/6800)).
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update yerpc to 0.6.4.
|
||||
- Clean up `deltachat-jsonrpc` dependencies.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Move logins into SQL table ([#6724](https://github.com/chatmail/core/pull/6724)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Check headers absense straightforwardly.
|
||||
- Fix mismatch between the contact and the account in securejoin tests.
|
||||
- Test that key of the recipient is gossiped in 1:1 chats.
|
||||
|
||||
## [1.159.1] - 2025-04-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.add_transport()`.
|
||||
- Add jsonrpc for info_contact_id.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update crossbeam-channel from 0.5.14 to 0.5.15.
|
||||
- Increase MSRV to 1.82.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Don't make ruff format quiet ([#6785](https://github.com/chatmail/core/pull/6785)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- MimeFactory.member_timestamps has the same order as To: rather than RCPT TO:.
|
||||
- Two JsonRPC doc improvements ([#6778](https://github.com/chatmail/core/pull/6778)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Improve error message when the user tries to do AEAP ([#6786](https://github.com/chatmail/core/pull/6786)).
|
||||
- Pass email and password via env in python-jsonrpc.
|
||||
- Track gossiping per (chat, fingerprint) pair.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Add missing ChatDeleted event to python jsonrpc client.
|
||||
- Never send Autocrypt-Gossip in broadcast lists.
|
||||
- Restart I/O when mvbox_move setting is changed.
|
||||
|
||||
### Tests
|
||||
|
||||
- Port test_delete_deltachat_folder to JSON-RPC.
|
||||
- Autocrypt-Gossip header isn't sent in broadcast messages.
|
||||
- Encrypt test_subject_in_group().
|
||||
- Encrypt test_remove_member_bcc.
|
||||
|
||||
## [1.159.0] - 2025-04-08
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add Message.get_info().
|
||||
- CFFI: Add `dc_make_vcard()` and `dc_import_vcard()`.
|
||||
- Add legacy Python bindings for `make_vcard` and `import_vcard`.
|
||||
|
||||
### CI
|
||||
|
||||
- Upgrade Rust from 1.84.1 to 1.86.0 ([#6784](https://github.com/chatmail/core/pull/6784)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add name resp. "Me" to contact encryption info ([#6720](https://github.com/chatmail/core/pull/6720)).
|
||||
- Get contact-id for info messages ([#6714](https://github.com/chatmail/core/pull/6714)).
|
||||
- No unencrypted chat when securejoin times out ([#6722](https://github.com/chatmail/core/pull/6722)).
|
||||
- Clear `Param::IsEdited` when forwarding a message.
|
||||
- Remove email address from 'add second device' qr code ([#6760](https://github.com/chatmail/core/pull/6760)).
|
||||
- Parse Proton Mail vCards again ([#6771](https://github.com/chatmail/core/pull/6771)).
|
||||
- Do not consider encrypting to the primary OpenPGP key.
|
||||
|
||||
### Fixes
|
||||
|
||||
- jsonrpc: Fix deadlock in get_all_accounts().
|
||||
- Set GroupNameTimestamp on group promotion ([#6729](https://github.com/chatmail/core/pull/6729)).
|
||||
- Encrypt broadcast lists.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update yerpc to 0.6.3.
|
||||
- cargo: Update textwrap from 0.16.1 to 0.16.2.
|
||||
- cargo: Bump uuid from 1.15.1 to 1.16.0.
|
||||
- cargo: Bump libc from 0.2.170 to 0.2.171.
|
||||
- cargo: Bump anyhow from 1.0.96 to 1.0.97.
|
||||
- cargo: Bump bytes from 1.10.0 to 1.10.1.
|
||||
- cargo: Bump once_cell from 1.20.3 to 1.21.3.
|
||||
- cargo: Bump thiserror from 2.0.11 to 2.0.12.
|
||||
- cargo: Bump pin-project from 1.1.9 to 1.1.10.
|
||||
- cargo: Bump hyper-util from 0.1.10 to 0.1.11.
|
||||
- cargo: Bump log from 0.4.26 to 0.4.27.
|
||||
- cargo: Bump tokio-util from 0.7.13 to 0.7.14.
|
||||
- cargo: Bump syn from 2.0.98 to 2.0.100.
|
||||
- cargo: Bump serde_json from 1.0.139 to 1.0.140.
|
||||
- cargo: Bump quote from 1.0.38 to 1.0.40.
|
||||
- cargo: Bump http-body-util from 0.1.2 to 0.1.3.
|
||||
- cargo: Bump openssl from 0.10.71 to 0.10.72.
|
||||
- cargo: Bump quick-xml from 0.37.2 to 0.37.4.
|
||||
- cargo: Bump blake3 from 1.6.1 to 1.8.0.
|
||||
- cargo: Bump tokio from 1.43.0 to 1.43.1 ([#6780](https://github.com/chatmail/core/pull/6780)).
|
||||
- Add issue template.
|
||||
- Add bug label on bug issue template.
|
||||
- cargo: Bump tokio from 1.43.0 to 1.44.1.
|
||||
- cargo: Bump fd-lock from 4.0.2 to 4.0.4.
|
||||
- Update async-smtp from 0.10.0 to 0.10.1.
|
||||
- Update async-imap from 0.10.3 to 0.10.4.
|
||||
- cargo: Bump tempfile from 3.14.0 to 3.19.1.
|
||||
- cargo: Bump image from 0.25.5 to 0.25.6.
|
||||
- cargo: Bump serde from 1.0.218 to 1.0.219.
|
||||
|
||||
### Other
|
||||
|
||||
- Add python and tox to flake.nix devshell ([#6233](https://github.com/chatmail/core/pull/6233))
|
||||
- Update spec wrt edit/delete, minor rewordings ([#6708](https://github.com/chatmail/core/pull/6708))
|
||||
- Update 'takes longer' fallback wording.
|
||||
- Handle classic emails as such only in classic profiles ([#6767](https://github.com/chatmail/core/pull/6767))
|
||||
- Move ASM strings to core, point to "Add Second Device" ([#6777](https://github.com/chatmail/core/pull/6777))
|
||||
|
||||
### Refactor
|
||||
|
||||
- Replace `once_cell::sync::Lazy` with `std::sync::LazyLock`.
|
||||
- Move vCard code to its own file ([#6776](https://github.com/chatmail/core/pull/6776)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Use encryption in more Rust tests.
|
||||
- Use encryption in all JSON-RPC online tests.
|
||||
- Encrypt legacy Python tests.
|
||||
- Send only encrypted messages in online JS tests.
|
||||
- Add APIs to create `dom@example.net` and `elena@example.net`.
|
||||
- Split public keys from secret keys in runtime.
|
||||
- Remove fetch_existing tests.
|
||||
- Port test_forward_encrypted_to_unencrypted from legacy Python to Rust.
|
||||
- Port test_one_account_send_bcc_setting from legacy Python to JSON-RPC.
|
||||
- Port test_multidevice_sync_seen to JSON-RPC.
|
||||
- Use QR codes to setup contact with test bots.
|
||||
- Remove flaky key::tests::test_load_self_existing test ([#6763](https://github.com/chatmail/core/pull/6763)).
|
||||
- Update blob hash in blob::blob_tests::test_selfavatar_outside_blobdir.
|
||||
|
||||
## [1.158.0] - 2025-03-29
|
||||
|
||||
### API-Changes
|
||||
@@ -6263,7 +6101,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.157.2]: https://github.com/chatmail/core/compare/v1.157.1..v1.157.2
|
||||
[1.157.3]: https://github.com/chatmail/core/compare/v1.157.2..v1.157.3
|
||||
[1.158.0]: https://github.com/chatmail/core/compare/v1.157.3..v1.158.0
|
||||
[1.159.0]: https://github.com/chatmail/core/compare/v1.158.0..v1.159.0
|
||||
[1.159.1]: https://github.com/chatmail/core/compare/v1.159.0..v1.159.1
|
||||
[1.159.2]: https://github.com/chatmail/core/compare/v1.159.1..v1.159.2
|
||||
[1.159.3]: https://github.com/chatmail/core/compare/v1.159.2..v1.159.3
|
||||
|
||||
51
Cargo.lock
generated
51
Cargo.lock
generated
@@ -1050,9 +1050,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
version = "0.5.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -1255,7 +1255,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-broadcast",
|
||||
@@ -1364,7 +1364,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 2.3.1",
|
||||
@@ -1372,6 +1372,7 @@ dependencies = [
|
||||
"deltachat",
|
||||
"deltachat-contact-tools",
|
||||
"futures",
|
||||
"log",
|
||||
"num-traits",
|
||||
"sanitize-filename",
|
||||
"schemars",
|
||||
@@ -1386,7 +1387,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1402,7 +1403,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1431,7 +1432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -2783,9 +2784,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.6"
|
||||
version = "0.25.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
|
||||
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@@ -5354,9 +5355,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -5372,9 +5373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
version = "1.0.218"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5491,9 +5492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shadowsocks"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1678a9acd37add020f89bfe05d45b9b8a6e8ad5d09f54ac2af3e0dcf0557b481"
|
||||
checksum = "ddafa3f54e58a651af9b5ce3170895aa2c970dc9a746bd22b197d2a6cd1b3635"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -5509,7 +5510,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"sendfd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5527,9 +5528,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shadowsocks-crypto"
|
||||
version = "0.5.8"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc77ecb3a97509d22751b76665894fcffad2d10df8758f4e3f20c92ccde6bf4f"
|
||||
checksum = "bda401a0ad32c82981d8862f2795713618de9bbf9768f03c17d9d145c6d805df"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
@@ -5539,7 +5540,7 @@ dependencies = [
|
||||
"chacha20poly1305",
|
||||
"hkdf",
|
||||
"md-5",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.0",
|
||||
"ring-compat",
|
||||
"sha1",
|
||||
]
|
||||
@@ -5874,14 +5875,14 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.19.1"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf"
|
||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix 1.0.5",
|
||||
"rustix 0.38.44",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -7290,9 +7291,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yerpc"
|
||||
version = "0.6.4"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dc24983fbe850227bfc1de89bf8cbfb3e2463afc322e0de2f155c4c23d06445"
|
||||
checksum = "0a3c86bc22116513ae8b72b96da2f4fc9de66110000864456d1bd5244c15de68"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel 1.9.0",
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.81"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -61,7 +61,7 @@ http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
hyper-util = "0.1.11"
|
||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.25.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
iroh-gossip = { version = "0.33", default-features = false, features = ["net"] }
|
||||
iroh = { version = "0.33", default-features = false }
|
||||
kamadak-exif = "0.6.1"
|
||||
@@ -91,7 +91,7 @@ serde_urlencoded = "0.7.1"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
shadowsocks = { version = "1.22.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
shadowsocks = { version = "1.23.0", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
||||
smallvec = "1.14.0"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
@@ -190,12 +190,12 @@ rusqlite = "0.32"
|
||||
sanitize-filename = "0.5"
|
||||
serde = "1.0"
|
||||
serde_json = "1"
|
||||
tempfile = "3.19.1"
|
||||
tempfile = "3.14.0"
|
||||
thiserror = "2"
|
||||
tokio = "1"
|
||||
tokio-util = "0.7.14"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.4"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -7595,7 +7595,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// @deprecated 2025-03
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "The contact must be online to proceed. This process will continue automatically in background."
|
||||
/// "That seems to take longer, maybe the contact or you are offline. However, the process continues in background, you can do something else…"
|
||||
///
|
||||
/// Used as info message.
|
||||
#define DC_STR_SECUREJOIN_TAKES_LONGER 192
|
||||
|
||||
@@ -536,7 +536,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::IncomingReaction { .. } => 2002,
|
||||
EventType::IncomingWebxdcNotify { .. } => 2003,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch => 2006,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
@@ -594,7 +594,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ConfigSynced { .. }
|
||||
| EventType::IncomingMsgBunch
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_)
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
@@ -669,7 +669,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatlistChanged
|
||||
@@ -771,7 +771,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::AccountsBackgroundFetchDone
|
||||
| EventType::ChatEphemeralTimerModified { .. }
|
||||
| EventType::ChatDeleted { .. }
|
||||
| EventType::IncomingMsgBunch
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ChatlistItemChanged { .. }
|
||||
| EventType::ChatlistChanged
|
||||
| EventType::AccountsChanged
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -13,7 +13,10 @@ deltachat-contact-tools = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
schemars = "0.8.22"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tempfile = { workspace = true }
|
||||
log = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
||||
@@ -24,8 +27,6 @@ base64 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
||||
tempfile = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -327,12 +327,8 @@ impl CommandApi {
|
||||
.get_config_bool(deltachat::config::Config::ProxyEnabled)
|
||||
.await?;
|
||||
|
||||
let provider_info = get_provider_info(
|
||||
&ctx,
|
||||
email.split('@').next_back().unwrap_or(""),
|
||||
proxy_enabled,
|
||||
)
|
||||
.await;
|
||||
let provider_info =
|
||||
get_provider_info(&ctx, email.split('@').last().unwrap_or(""), proxy_enabled).await;
|
||||
Ok(ProviderInfo::from_dc_type(provider_info))
|
||||
}
|
||||
|
||||
@@ -439,7 +435,7 @@ impl CommandApi {
|
||||
/// Setup the credential config before calling this.
|
||||
///
|
||||
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_or_update_transport()` instead.
|
||||
/// or `add_transport()` instead.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
@@ -483,30 +479,21 @@ impl CommandApi {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
async fn add_or_update_transport(
|
||||
&self,
|
||||
account_id: u32,
|
||||
param: EnteredLoginParam,
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.add_or_update_transport(&mut param.try_into()?).await
|
||||
}
|
||||
|
||||
/// Deprecated 2025-04. Alias for [Self::add_or_update_transport()].
|
||||
async fn add_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
|
||||
self.add_or_update_transport(account_id, param).await
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.add_transport(¶m.try_into()?).await
|
||||
}
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
/// using the server encoded in the QR code.
|
||||
/// See [Self::add_or_update_transport].
|
||||
/// See [Self::add_transport].
|
||||
async fn add_transport_from_qr(&self, account_id: u32, qr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.add_transport_from_qr(&qr).await
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// Use [Self::add_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
async fn list_transports(&self, account_id: u32) -> Result<Vec<EnteredLoginParam>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -2285,37 +2272,6 @@ impl CommandApi {
|
||||
|
||||
// mimics the old desktop call, will get replaced with something better in the composer rewrite,
|
||||
// the better version will just be sending the current draft, though there will be probably something similar with more options to this for the corner cases like setting a marker on the map
|
||||
/// Send a message to a chat.
|
||||
///
|
||||
/// This function returns after the message has been placed in the sending queue.
|
||||
/// This does not imply that the message was really sent out yet.
|
||||
/// However, from your view, you're done with the message.
|
||||
/// Sooner or later it will find its way.
|
||||
///
|
||||
/// **Attaching files:**
|
||||
///
|
||||
/// Pass the file path in the `file` parameter.
|
||||
/// If `file` is not in the blob directory yet,
|
||||
/// it will be copied into the blob directory.
|
||||
/// If you want, you can delete the file immediately after this function returns.
|
||||
///
|
||||
/// You can also write the attachment directly into the blob directory
|
||||
/// and then pass the path as the `file` parameter;
|
||||
/// this will prevent an unnecessary copying of the file.
|
||||
///
|
||||
/// In `filename`, you can pass the original name of the file,
|
||||
/// which will then be shown in the UI.
|
||||
/// in this case the current name of `file` on the filesystem will be ignored.
|
||||
///
|
||||
/// In order to deduplicate files that contain the same data,
|
||||
/// the file will be named `<hash>.<extension>`, e.g. `ce940175885d7b78f7b7e9f1396611f.jpg`.
|
||||
///
|
||||
/// NOTE:
|
||||
/// - This function will rename the file. To get the new file path, call `get_file()`.
|
||||
/// - The file must not be modified after this function was called.
|
||||
/// - Images etc. will NOT be recoded.
|
||||
/// In order to recode images,
|
||||
/// use `misc_set_draft` and pass `Image` as the viewtype.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn misc_send_msg(
|
||||
&self,
|
||||
|
||||
@@ -5,9 +5,7 @@ use serde::Serialize;
|
||||
use yerpc::TypeDef;
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
/// and all the other settings will be autoconfigured.
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnteredLoginParam {
|
||||
|
||||
@@ -70,9 +70,6 @@ pub struct MessageObject {
|
||||
/// when is_info is true this describes what type of system message it is
|
||||
system_message_type: SystemMessageType,
|
||||
|
||||
/// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped.
|
||||
info_contact_id: Option<u32>,
|
||||
|
||||
duration: i32,
|
||||
dimensions_height: i32,
|
||||
dimensions_width: i32,
|
||||
@@ -231,10 +228,6 @@ impl MessageObject {
|
||||
is_forwarded: message.is_forwarded(),
|
||||
is_bot: message.is_bot(),
|
||||
system_message_type: message.get_info_type().into(),
|
||||
info_contact_id: message
|
||||
.get_info_contact_id(context)
|
||||
.await?
|
||||
.map(|id| id.to_u32()),
|
||||
|
||||
duration: message.get_duration(),
|
||||
dimensions_height: message.get_height(),
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "1.158.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from threading import Thread
|
||||
@@ -90,8 +89,8 @@ def _run_cli(
|
||||
help="accounts folder (default: current working directory)",
|
||||
nargs="?",
|
||||
)
|
||||
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
|
||||
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
|
||||
parser.add_argument("--email", action="store", help="email address")
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
|
||||
@@ -110,17 +110,6 @@ class Account:
|
||||
"""Configure an account."""
|
||||
yield self._rpc.configure.future(self.id)
|
||||
|
||||
@futuremethod
|
||||
def add_or_update_transport(self, params):
|
||||
"""Add a new transport."""
|
||||
yield self._rpc.add_or_update_transport.future(self.id, params)
|
||||
|
||||
@futuremethod
|
||||
def list_transports(self):
|
||||
"""Returns the list of all email accounts that are used as a transport in the current profile."""
|
||||
transports = yield self._rpc.list_transports.future(self.id)
|
||||
return transports
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
|
||||
@@ -48,7 +48,6 @@ class EventType(str, Enum):
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_DELETED = "ChatDeleted"
|
||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||
CONTACTS_CHANGED = "ContactsChanged"
|
||||
LOCATION_CHANGED = "LocationChanged"
|
||||
|
||||
@@ -34,7 +34,7 @@ class ACFactory:
|
||||
addr, password = self.get_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
params = {"addr": addr, "password": password}
|
||||
yield account.add_or_update_transport.future(params)
|
||||
yield account._rpc.add_transport.future(account.id, params)
|
||||
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.add_or_update_transport.future({"addr": addr, "password": password})
|
||||
future = account._rpc.add_transport.future(account.id, {"addr": addr, "password": password})
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||
|
||||
@@ -63,7 +63,8 @@ def test_acfactory(acfactory) -> None:
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
@@ -74,36 +75,14 @@ def test_configure_starttls(acfactory) -> None:
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_lowercase_address(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
addr_upper = addr.upper()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
{
|
||||
"addr": addr_upper,
|
||||
"password": password,
|
||||
},
|
||||
)
|
||||
assert account.is_configured()
|
||||
assert addr_upper != addr
|
||||
assert account.get_config("configured_addr") == addr
|
||||
assert account.list_transports()[0]["addr"] == addr
|
||||
|
||||
for param in [
|
||||
account.get_info()["used_account_settings"],
|
||||
account.get_info()["entered_account_settings"],
|
||||
]:
|
||||
assert addr in param
|
||||
assert addr_upper not in param
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_or_update_transport(
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
@@ -117,7 +96,8 @@ def test_configure_alternative_port(acfactory) -> None:
|
||||
"""Test that configuration with alternative port 443 works."""
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
@@ -131,14 +111,15 @@ def test_configure_alternative_port(acfactory) -> None:
|
||||
def test_list_transports(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_or_update_transport(
|
||||
account._rpc.add_transport(
|
||||
account.id,
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapUser": addr,
|
||||
},
|
||||
)
|
||||
transports = account.list_transports()
|
||||
transports = account._rpc.list_transports(account.id)
|
||||
assert len(transports) == 1
|
||||
params = transports[0]
|
||||
assert params["addr"] == addr
|
||||
@@ -443,7 +424,7 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
bot = acfactory.get_unconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.add_or_update_transport({"addr": addr, "password": password})
|
||||
bot._rpc.add_transport(bot.id, {"addr": addr, "password": password})
|
||||
assert bot.is_configured()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
@@ -626,7 +607,7 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_or_update_transport({"addr": addr, "password": password})
|
||||
ac2._rpc.add_transport(ac2.id, {"addr": addr, "password": password})
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
@@ -736,11 +717,12 @@ def test_get_http_response(acfactory):
|
||||
|
||||
def test_configured_imap_certificate_checks(acfactory):
|
||||
alice = acfactory.new_configured_account()
|
||||
configured_certificate_checks = alice.get_config("configured_imap_certificate_checks")
|
||||
|
||||
# Certificate checks should be configured (not None)
|
||||
assert "cert_automatic" in alice.get_info().used_account_settings
|
||||
assert configured_certificate_checks
|
||||
|
||||
# "cert_old_automatic" is the value old Delta Chat core versions used
|
||||
# 0 is the value old Delta Chat core versions used
|
||||
# to mean user entered "imap_certificate_checks=0" (Automatic)
|
||||
# and configuration failed to use strict TLS checks
|
||||
# so it switched strict TLS checks off.
|
||||
@@ -751,7 +733,7 @@ def test_configured_imap_certificate_checks(acfactory):
|
||||
#
|
||||
# Core 1.142.4, 1.142.5 and 1.142.6 saved this value due to bug.
|
||||
# This test is a regression test to prevent this happening again.
|
||||
assert "cert_old_automatic" not in alice.get_info().used_account_settings
|
||||
assert configured_certificate_checks != "0"
|
||||
|
||||
|
||||
def test_no_old_msg_is_fresh(acfactory):
|
||||
@@ -823,27 +805,3 @@ def test_get_all_accounts_deadlock(rpc):
|
||||
all_accounts = rpc.get_all_accounts.future()
|
||||
rpc.add_account()
|
||||
all_accounts()
|
||||
|
||||
|
||||
def test_delete_deltachat_folder(acfactory, direct_imap):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.new_configured_account()
|
||||
ac1.set_config("mvbox_move", "1")
|
||||
ac1.bring_online()
|
||||
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.conn.folder.delete("DeltaChat")
|
||||
assert "DeltaChat" not in ac1_direct_imap.list_folders()
|
||||
|
||||
# Wait until new folder is created and UIDVALIDITY is updated.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.INFO and "uid/validity change folder DeltaChat" in event.msg:
|
||||
break
|
||||
|
||||
ac2 = acfactory.get_online_account()
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert "DeltaChat" in ac1_direct_imap.list_folders()
|
||||
|
||||
@@ -21,7 +21,7 @@ skip_install = True
|
||||
deps =
|
||||
ruff
|
||||
commands =
|
||||
ruff format --diff src/ examples/ tests/
|
||||
ruff format --quiet --diff src/ examples/ tests/
|
||||
ruff check src/ examples/ tests/
|
||||
|
||||
[pytest]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.159.3"
|
||||
"version": "1.158.0"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.159.3"
|
||||
version = "1.158.0"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -482,8 +482,12 @@ class ACFactory:
|
||||
addr = f"{acname}@offline.org"
|
||||
ac.update_config(
|
||||
{
|
||||
"configured_addr": addr,
|
||||
"addr": addr,
|
||||
"displayname": acname,
|
||||
"mail_pw": "123",
|
||||
"configured_addr": addr,
|
||||
"configured_mail_pw": "123",
|
||||
"configured": "1",
|
||||
},
|
||||
)
|
||||
self._preconfigure_key(ac)
|
||||
|
||||
@@ -16,6 +16,8 @@ class TestGroupStressTests:
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg1 = chat.send_text("hello")
|
||||
assert msg1.is_encrypted()
|
||||
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp > 0
|
||||
|
||||
assert chat.num_contacts() == 3 + 1
|
||||
|
||||
@@ -44,13 +46,19 @@ class TestGroupStressTests:
|
||||
assert to_remove.addr in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 3
|
||||
|
||||
# Receiving message about removed contact does not reset gossip
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: sending another message to the chat")
|
||||
chat.send_text("hello2")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello2"
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: adding fifth member to the chat")
|
||||
chat.add_contact(ac5)
|
||||
# Adding contact to chat resets gossiped_timestamp
|
||||
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
|
||||
|
||||
lp.sec("ac2: receiving system message about contact addition")
|
||||
sysmsg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -798,6 +798,12 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
msg3.mark_seen()
|
||||
assert not list(ac1.get_fresh_messages())
|
||||
|
||||
# Test that we do not gossip peer keys in 1-to-1 chat,
|
||||
# as it makes no sense to gossip to peers their own keys.
|
||||
# Gossip is only sent in encrypted messages,
|
||||
# and we sent encrypted msg_back right above.
|
||||
assert chat2b.get_summary()["gossiped_timestamp"] == 0
|
||||
|
||||
lp.sec("create group chat with two members, one of which has no encrypt state")
|
||||
chat = ac1.create_group_chat("encryption test")
|
||||
chat.add_contact(ac2)
|
||||
@@ -807,6 +813,41 @@ def test_send_and_receive_will_encrypt_decrypt(acfactory, lp):
|
||||
ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
|
||||
|
||||
def test_gossip_optimization(acfactory, lp):
|
||||
"""Test that gossip timestamp is updated when someone else sends gossip,
|
||||
so we don't have to send gossip ourselves.
|
||||
"""
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
acfactory.introduce_each_other([ac2, ac3])
|
||||
|
||||
lp.sec("ac1 creates a group chat with ac2")
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
# No Autocrypt gossip was sent yet.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == 0
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.text == "hi"
|
||||
|
||||
lp.sec("ac2 adds ac3 to the group")
|
||||
msg.chat.add_contact(ac3)
|
||||
|
||||
lp.sec("ac1 receives message from ac2 and updates gossip timestamp")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
# ac1 has updated the gossip timestamp even though no gossip was sent by ac1.
|
||||
# ac1 does not need to send gossip because ac2 already did it.
|
||||
gossiped_timestamp = msg.chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp == int(msg.time_sent.timestamp())
|
||||
|
||||
|
||||
def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1502,6 +1543,15 @@ def test_connectivity(acfactory, lp):
|
||||
assert len(msgs) == 2
|
||||
assert msgs[1].text == "Hi 2"
|
||||
|
||||
lp.sec("Test that the connectivity is NOT_CONNECTED if the password is wrong")
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
"""This is a regression test: Messages with \\Deleted flag were downloaded again and again,
|
||||
@@ -1929,6 +1979,23 @@ def test_scan_folders(acfactory, lp, folder, move, expected_destination):
|
||||
assert len(ac1.direct_imap.get_all_messages()) == 0
|
||||
|
||||
|
||||
def test_delete_deltachat_folder(acfactory):
|
||||
"""Test that DeltaChat folder is recreated if user deletes it manually."""
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.wait_configured(ac1)
|
||||
|
||||
ac1.direct_imap.conn.folder.delete("DeltaChat")
|
||||
assert "DeltaChat" not in ac1.direct_imap.list_folders()
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac2.create_chat(ac1).send_text("hello")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
|
||||
assert "DeltaChat" in ac1.direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_archived_muted_chat(acfactory, lp):
|
||||
"""If an archived and muted chat receives a new message, DC_EVENT_MSGS_CHANGED for
|
||||
DC_CHAT_ID_ARCHIVED_LINK must be generated if the chat had only seen messages previously.
|
||||
|
||||
@@ -45,7 +45,7 @@ deps =
|
||||
pygments
|
||||
restructuredtext_lint
|
||||
commands =
|
||||
ruff format --diff setup.py src/deltachat examples/ tests/
|
||||
ruff format --quiet --diff setup.py src/deltachat examples/ tests/
|
||||
ruff check src/deltachat tests/ examples/
|
||||
rst-lint --encoding 'utf-8' README.rst
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2025-04-24
|
||||
2025-03-29
|
||||
@@ -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.86.0
|
||||
RUST_VERSION=1.84.1
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu
|
||||
|
||||
@@ -174,7 +174,7 @@ async fn test_selfavatar_outside_blobdir() {
|
||||
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||
let avatar_path = Path::new(&avatar_blob);
|
||||
assert!(
|
||||
avatar_blob.ends_with("1e08d1c9398297c21dd3820f7db2324.jpg"),
|
||||
avatar_blob.ends_with("009161310a6afc319163e4bcabd23b9.jpg"),
|
||||
"The avatar filename should be its hash, put instead it's {avatar_blob}"
|
||||
);
|
||||
let scaled_avatar_size = file_size(avatar_path).await;
|
||||
|
||||
78
src/chat.rs
78
src/chat.rs
@@ -1376,10 +1376,41 @@ impl ChatId {
|
||||
}
|
||||
|
||||
pub(crate) async fn reset_gossiped_timestamp(self, context: &Context) -> Result<()> {
|
||||
self.set_gossiped_timestamp(context, 0).await
|
||||
}
|
||||
|
||||
/// Get timestamp of the last gossip sent in the chat.
|
||||
/// Zero return value means that gossip was never sent.
|
||||
pub async fn get_gossiped_timestamp(self, context: &Context) -> Result<i64> {
|
||||
let timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value("SELECT gossiped_timestamp FROM chats WHERE id=?;", (self,))
|
||||
.await?;
|
||||
Ok(timestamp.unwrap_or_default())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_gossiped_timestamp(
|
||||
self,
|
||||
context: &Context,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
ensure!(
|
||||
!self.is_special(),
|
||||
"can not set gossiped timestamp for special chats"
|
||||
);
|
||||
info!(
|
||||
context,
|
||||
"Set gossiped_timestamp for chat {} to {}.", self, timestamp,
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM gossip_timestamp WHERE chat_id=?", (self,))
|
||||
.execute(
|
||||
"UPDATE chats SET gossiped_timestamp=? WHERE id=?;",
|
||||
(timestamp, self),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1695,13 +1726,13 @@ impl Chat {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
let reason = SecurejoinWait;
|
||||
if !skip_fn(&reason)
|
||||
&& self
|
||||
if !skip_fn(&reason) {
|
||||
let (can_write, _) = self
|
||||
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await?
|
||||
> 0
|
||||
{
|
||||
return Ok(Some(reason));
|
||||
.await?;
|
||||
if !can_write {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1713,17 +1744,18 @@ impl Chat {
|
||||
Ok(self.why_cant_send(context).await?.is_none())
|
||||
}
|
||||
|
||||
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
|
||||
/// Returns if the chat can be sent to
|
||||
/// and the remaining timeout for the 1:1 chat in-progress SecureJoin.
|
||||
///
|
||||
/// If the timeout has expired, adds an info message with additional information.
|
||||
/// See also [`CantSendReason::SecurejoinWait`].
|
||||
/// If the timeout has expired, adds an info message with additional information;
|
||||
/// the chat still cannot be sent to in this case. See also [`CantSendReason::SecurejoinWait`].
|
||||
pub(crate) async fn check_securejoin_wait(
|
||||
&self,
|
||||
context: &Context,
|
||||
timeout: u64,
|
||||
) -> Result<u64> {
|
||||
) -> Result<(bool, u64)> {
|
||||
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
|
||||
return Ok(0);
|
||||
return Ok((true, 0));
|
||||
}
|
||||
|
||||
// chat is single and unprotected:
|
||||
@@ -1747,10 +1779,11 @@ impl Chat {
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok(0);
|
||||
return Ok((true, 0));
|
||||
};
|
||||
|
||||
if param == param_timeout {
|
||||
return Ok(0);
|
||||
return Ok((false, 0));
|
||||
}
|
||||
|
||||
let now = time();
|
||||
@@ -1760,9 +1793,10 @@ impl Chat {
|
||||
.saturating_add(timeout.try_into()?)
|
||||
.saturating_sub(now);
|
||||
if timeout > 0 {
|
||||
return Ok(timeout as u64);
|
||||
return Ok((false, timeout as u64));
|
||||
}
|
||||
}
|
||||
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self.id,
|
||||
@@ -1777,8 +1811,8 @@ impl Chat {
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
context.emit_event(EventType::ChatModified(self.id));
|
||||
Ok(0)
|
||||
|
||||
Ok((false, 0))
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
@@ -1883,6 +1917,7 @@ impl Chat {
|
||||
name: self.name.clone(),
|
||||
archived: self.visibility == ChatVisibility::Archived,
|
||||
param: self.param.to_string(),
|
||||
gossiped_timestamp: self.id.get_gossiped_timestamp(context).await?,
|
||||
is_sending_locations: self.is_sending_locations,
|
||||
color: self.get_color(context).await?,
|
||||
profile_image: self
|
||||
@@ -2425,6 +2460,9 @@ pub struct ChatInfo {
|
||||
/// This is the string-serialised version of `Params` currently.
|
||||
pub param: String,
|
||||
|
||||
/// Last time this client sent autocrypt gossip headers to this chat.
|
||||
pub gossiped_timestamp: i64,
|
||||
|
||||
/// Whether this chat is currently sending location-stream messages.
|
||||
pub is_sending_locations: bool,
|
||||
|
||||
@@ -2581,7 +2619,7 @@ pub(crate) async fn resume_securejoin_wait(context: &Context) -> Result<()> {
|
||||
|
||||
for chat_id in chat_ids {
|
||||
let chat = Chat::load_from_db(context, chat_id).await?;
|
||||
let timeout = chat
|
||||
let (_, timeout) = chat
|
||||
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await?;
|
||||
if timeout > 0 {
|
||||
@@ -3063,6 +3101,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
|
||||
let now = smeared_time(context);
|
||||
|
||||
if rendered_msg.is_gossiped {
|
||||
msg.chat_id.set_gossiped_timestamp(context, now).await?;
|
||||
}
|
||||
|
||||
if rendered_msg.last_added_location_id.is_some() {
|
||||
if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, now).await {
|
||||
error!(context, "Failed to set kml sent_timestamp: {err:#}.");
|
||||
|
||||
@@ -2516,7 +2516,6 @@ async fn test_broadcast() -> Result<()> {
|
||||
// create two context, send two messages so both know the other
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
send_text_msg(&alice, chat_alice.id, "hi!".to_string()).await?;
|
||||
@@ -2535,8 +2534,6 @@ async fn test_broadcast() -> Result<()> {
|
||||
get_chat_contacts(&alice, chat_bob.id).await?.pop().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
let fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
add_contact_to_chat(&alice, broadcast_id, fiona_contact_id).await?;
|
||||
set_chat_name(&alice, broadcast_id, "Broadcast list").await?;
|
||||
{
|
||||
let chat = Chat::load_from_db(&alice, broadcast_id).await?;
|
||||
@@ -2551,10 +2548,7 @@ async fn test_broadcast() -> Result<()> {
|
||||
|
||||
{
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = bob.parse_msg(&sent_msg).await;
|
||||
assert!(msg.was_encrypted());
|
||||
assert!(!msg.header_exists(HeaderDef::ChatGroupMemberTimestamps));
|
||||
assert!(!msg.header_exists(HeaderDef::AutocryptGossip));
|
||||
assert!(!sent_msg.payload.contains("Chat-Group-Member-Timestamps:"));
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_text(), "ola!");
|
||||
assert_eq!(msg.subject, "Broadcast list");
|
||||
@@ -4050,35 +4044,3 @@ async fn test_send_delete_request_no_encryption() -> Result<()> {
|
||||
assert_eq!(alice_chat.id.get_msg_cnt(alice).await?, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that in multi-device setup
|
||||
/// second device learns the key of a contact
|
||||
/// via Autocrypt-Gossip in 1:1 chats.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_oneone_gossip() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
tcm.section("Alice imports Bob's vCard and sends a message from the first device");
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
let sent_msg = alice.send_text(alice_chat.id, "Hello Bob!").await;
|
||||
|
||||
tcm.section("Alice receives a copy on second device");
|
||||
let rcvd_msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(rcvd_msg.get_showpadlock(), true);
|
||||
|
||||
tcm.section("Alice sends a message from the second device");
|
||||
let alice2_chat_id = rcvd_msg.chat_id;
|
||||
let sent_msg2 = alice2
|
||||
.send_text(alice2_chat_id, "Hello from second device!")
|
||||
.await;
|
||||
|
||||
tcm.section("Bob receives a message from the second device");
|
||||
let rcvd_msg2 = bob.recv_msg(&sent_msg2).await;
|
||||
assert_eq!(rcvd_msg2.get_showpadlock(), true);
|
||||
assert_eq!(rcvd_msg2.text, "Hello from second device!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::env;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::{addr_cmp, sanitize_single_line};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -13,12 +13,10 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::mimefactory::RECOMMENDED_FILE_SIZE;
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::sync::{self, Sync::*, SyncData};
|
||||
@@ -477,10 +475,7 @@ impl Config {
|
||||
|
||||
/// Whether the config option needs an IO scheduler restart to take effect.
|
||||
pub(crate) fn needs_io_restart(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Config::MvboxMove | Config::OnlyFetchMvbox | Config::SentboxWatch
|
||||
)
|
||||
matches!(self, Config::OnlyFetchMvbox | Config::SentboxWatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,22 +522,21 @@ impl Context {
|
||||
// Default values
|
||||
let val = match key {
|
||||
Config::BccSelf => match Box::pin(self.is_chatmail()).await? {
|
||||
false => Some("1".to_string()),
|
||||
true => Some("0".to_string()),
|
||||
false => Some("1"),
|
||||
true => Some("0"),
|
||||
},
|
||||
Config::ConfiguredInboxFolder => Some("INBOX".to_string()),
|
||||
Config::ConfiguredInboxFolder => Some("INBOX"),
|
||||
Config::DeleteServerAfter => {
|
||||
match !Box::pin(self.get_config_bool(Config::BccSelf)).await?
|
||||
&& Box::pin(self.is_chatmail()).await?
|
||||
{
|
||||
true => Some("1".to_string()),
|
||||
false => Some("0".to_string()),
|
||||
true => Some("1"),
|
||||
false => Some("0"),
|
||||
}
|
||||
}
|
||||
Config::Addr => self.get_config_opt(Config::ConfiguredAddr).await?,
|
||||
_ => key.get_str("default").map(|s| s.to_string()),
|
||||
_ => key.get_str("default"),
|
||||
};
|
||||
Ok(val)
|
||||
Ok(val.map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
/// Returns Some(T) if a value for the given key is set and was successfully parsed.
|
||||
@@ -808,19 +802,6 @@ impl Context {
|
||||
.set_raw_config(constants::DC_FOLDERS_CONFIGURED_KEY, None)
|
||||
.await?;
|
||||
}
|
||||
Config::ConfiguredAddr => {
|
||||
if self.is_configured().await? {
|
||||
bail!("Cannot change ConfiguredAddr");
|
||||
}
|
||||
if let Some(addr) = value {
|
||||
info!(self, "Creating a pseudo configured account which will not be able to send or receive messages. Only meant for tests!");
|
||||
ConfiguredLoginParam::from_json(&format!(
|
||||
r#"{{"addr":"{addr}","imap":[],"imap_user":"","imap_password":"","smtp":[],"smtp_user":"","smtp_password":"","certificate_checks":"Automatic","oauth2":false}}"#
|
||||
))?
|
||||
.save_to_transports_table(self, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
@@ -907,7 +888,6 @@ impl Context {
|
||||
/// primary address (if exists) as a secondary address.
|
||||
///
|
||||
/// This should only be used by test code and during configure.
|
||||
#[cfg(test)] // AEAP is disabled, but there are still tests for it
|
||||
pub(crate) async fn set_primary_self_addr(&self, primary_new: &str) -> Result<()> {
|
||||
self.quota.write().await.take();
|
||||
|
||||
@@ -921,8 +901,7 @@ impl Context {
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(primary_new))
|
||||
self.set_config_internal(Config::ConfiguredAddr, Some(primary_new))
|
||||
.await?;
|
||||
self.emit_event(EventType::ConnectivityChanged);
|
||||
Ok(())
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(crate) mod server_params;
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use auto_mozilla::moz_autoconfigure;
|
||||
use auto_outlook::outlk_autodiscover;
|
||||
use deltachat_contact_tools::{addr_normalize, EmailAddress};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use futures::FutureExt;
|
||||
use futures_lite::FutureExt as _;
|
||||
use percent_encoding::utf8_percent_encode;
|
||||
@@ -35,7 +35,8 @@ use crate::login_param::{
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::provider::{Protocol, Socket, UsernamePattern};
|
||||
use crate::qr::set_account_from_qr;
|
||||
use crate::smtp::Smtp;
|
||||
use crate::sync::Sync::*;
|
||||
use crate::tools::time;
|
||||
@@ -62,17 +63,17 @@ macro_rules! progress {
|
||||
impl Context {
|
||||
/// Checks if the context is already configured.
|
||||
pub async fn is_configured(&self) -> Result<bool> {
|
||||
self.sql.exists("SELECT COUNT(*) FROM transports", ()).await
|
||||
self.sql.get_raw_config_bool("configured").await
|
||||
}
|
||||
|
||||
/// Configures this account with the currently provided parameters.
|
||||
///
|
||||
/// Deprecated since 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_or_update_transport()` instead.
|
||||
/// or `add_transport()` instead.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
let mut param = EnteredLoginParam::load(self).await?;
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
self.add_transport_inner(&mut param).await
|
||||
self.add_transport_inner(¶m).await
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
@@ -104,7 +105,7 @@ impl Context {
|
||||
/// from a server encoded in a QR code.
|
||||
/// - [Self::list_transports()] to get a list of all configured transports.
|
||||
/// - [Self::delete_transport()] to remove a transport.
|
||||
pub async fn add_or_update_transport(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
pub async fn add_transport(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
if result.is_err() {
|
||||
@@ -117,7 +118,7 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_transport_inner(&self, param: &mut EnteredLoginParam) -> Result<()> {
|
||||
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
ensure!(
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
@@ -126,10 +127,9 @@ impl Context {
|
||||
self.sql.is_open().await,
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
param.addr = addr_normalize(¶m.addr);
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
if self.is_configured().await? && !addr_cmp(&old_addr.unwrap_or_default(), ¶m.addr) {
|
||||
bail!("Changing your email address is not supported right now. Check back in a few months!");
|
||||
bail!("Adding a new transport is not supported right now. Check back in a few months!");
|
||||
}
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
@@ -156,23 +156,12 @@ impl Context {
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
/// using the server encoded in the QR code.
|
||||
/// See [Self::add_or_update_transport].
|
||||
/// See [Self::add_transport].
|
||||
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
|
||||
// This code first sets the deprecated Config::Addr, Config::MailPw, etc.
|
||||
// and then calls configure(), which loads them again.
|
||||
// At some point, we will remove configure()
|
||||
// and then simplify the code
|
||||
// to directly create an EnteredLoginParam.
|
||||
let result = async move {
|
||||
match crate::qr::check_qr(self, qr).await? {
|
||||
crate::qr::Qr::Account { .. } => crate::qr::set_account_from_qr(self, qr).await?,
|
||||
crate::qr::Qr::Login { address, options } => {
|
||||
crate::qr::configure_from_login_qr(self, &address, options).await?
|
||||
}
|
||||
_ => bail!("QR code does not contain account"),
|
||||
}
|
||||
set_account_from_qr(self, qr).await?;
|
||||
self.configure().await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -189,24 +178,12 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// Use [Self::add_or_update_transport()] to add or change a transport
|
||||
/// Use [Self::add_transport()] to add or change a transport
|
||||
/// and [Self::delete_transport()] to delete a transport.
|
||||
pub async fn list_transports(&self) -> Result<Vec<EnteredLoginParam>> {
|
||||
let transports = self
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT entered_param FROM transports",
|
||||
(),
|
||||
|row| row.get::<_, String>(0),
|
||||
|rows| {
|
||||
rows.flatten()
|
||||
.map(|s| Ok(serde_json::from_str(&s)?))
|
||||
.collect::<Result<Vec<EnteredLoginParam>>>()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
Ok(transports)
|
||||
Ok(vec![param])
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
@@ -220,20 +197,20 @@ impl Context {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let provider = configure(self, param).await?;
|
||||
let configured_param = configure(self, param).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, provider, old_addr).await?;
|
||||
on_configure_completed(self, configured_param, old_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
provider: Option<&'static Provider>,
|
||||
param: ConfiguredLoginParam,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = provider {
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = provider.config_defaults {
|
||||
for def in config_defaults {
|
||||
if !context.config_exists(def.key).await? {
|
||||
@@ -469,7 +446,7 @@ async fn get_configured_param(
|
||||
Ok(configured_login_param)
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'static Provider>> {
|
||||
async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<ConfiguredLoginParam> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
let ctx2 = ctx.clone();
|
||||
@@ -579,11 +556,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
}
|
||||
}
|
||||
|
||||
let provider = configured_param.provider;
|
||||
configured_param
|
||||
.save_to_transports_table(ctx, param)
|
||||
.await?;
|
||||
|
||||
configured_param.save_as_configured_params(ctx).await?;
|
||||
ctx.set_config_internal(Config::ConfiguredTimestamp, Some(&time().to_string()))
|
||||
.await?;
|
||||
|
||||
@@ -599,7 +572,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Option<&'
|
||||
ctx.sql.set_raw_config_bool("configured", true).await?;
|
||||
ctx.emit_event(EventType::AccountsItemChanged);
|
||||
|
||||
Ok(provider)
|
||||
Ok(configured_param)
|
||||
}
|
||||
|
||||
/// Retrieve available autoconfigurations.
|
||||
|
||||
@@ -2,38 +2,26 @@
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, ensure, format_err, Context as _, Result};
|
||||
use deltachat_contact_tools::{addr_cmp, addr_normalize, EmailAddress};
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use num_traits::ToPrimitive as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::configure::server_params::{expand_param_vector, ServerParams};
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2};
|
||||
use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::context::Context;
|
||||
use crate::net::load_connection_timestamp;
|
||||
pub use crate::net::proxy::ProxyConfig;
|
||||
pub use crate::provider::Socket;
|
||||
use crate::provider::{get_provider_by_id, Protocol, Provider, UsernamePattern};
|
||||
use crate::provider::{Protocol, Provider, UsernamePattern};
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::ToOption;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
///
|
||||
/// Should be saved into `imap_certificate_checks` before running configuration.
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
Display,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[derive(Copy, Clone, Debug, Default, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum EnteredCertificateChecks {
|
||||
@@ -56,9 +44,7 @@ pub enum EnteredCertificateChecks {
|
||||
}
|
||||
|
||||
/// Values saved into `imap_certificate_checks`.
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub(crate) enum ConfiguredCertificateChecks {
|
||||
@@ -95,7 +81,7 @@ pub(crate) enum ConfiguredCertificateChecks {
|
||||
}
|
||||
|
||||
/// Login parameters for a single server, either IMAP or SMTP
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct EnteredServerLoginParam {
|
||||
/// Server hostname or IP address.
|
||||
pub server: String,
|
||||
@@ -118,7 +104,7 @@ pub struct EnteredServerLoginParam {
|
||||
}
|
||||
|
||||
/// Login parameters entered by the user.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct EnteredLoginParam {
|
||||
/// Email address.
|
||||
pub addr: String,
|
||||
@@ -465,22 +451,6 @@ pub(crate) struct ConfiguredLoginParam {
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
/// The representation of ConfiguredLoginParam in the database,
|
||||
/// saved as Json.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ConfiguredLoginParamJson {
|
||||
pub addr: String,
|
||||
pub imap: Vec<ConfiguredServerLoginParam>,
|
||||
pub imap_user: String,
|
||||
pub imap_password: String,
|
||||
pub smtp: Vec<ConfiguredServerLoginParam>,
|
||||
pub smtp_user: String,
|
||||
pub smtp_password: String,
|
||||
pub provider_id: Option<String>,
|
||||
pub certificate_checks: ConfiguredCertificateChecks,
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for ConfiguredLoginParam {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let addr = &self.addr;
|
||||
@@ -517,26 +487,6 @@ impl ConfiguredLoginParam {
|
||||
///
|
||||
/// Returns `None` if account is not configured.
|
||||
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
let Some(self_addr) = context.get_config(Config::ConfiguredAddr).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let json: Option<String> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT configured_param FROM transports WHERE addr=?",
|
||||
(&self_addr,),
|
||||
)
|
||||
.await?;
|
||||
if let Some(json) = json {
|
||||
Ok(Some(Self::from_json(&json)?))
|
||||
} else {
|
||||
bail!("Self address {self_addr} doesn't have a corresponding transport");
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads legacy configured param. Only used for tests and the migration.
|
||||
pub(crate) async fn load_legacy(context: &Context) -> Result<Option<Self>> {
|
||||
if !context.get_config_bool(Config::Configured).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -803,82 +753,84 @@ impl ConfiguredLoginParam {
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn save_to_transports_table(
|
||||
self,
|
||||
context: &Context,
|
||||
entered_param: &EnteredLoginParam,
|
||||
) -> Result<()> {
|
||||
let addr = addr_normalize(&self.addr);
|
||||
let configured_addr = context.get_config(Config::ConfiguredAddr).await?;
|
||||
if let Some(configured_addr) = configured_addr {
|
||||
ensure!(
|
||||
addr_cmp(&configured_addr, &addr,),
|
||||
"Adding a second transport is not supported right now."
|
||||
);
|
||||
}
|
||||
/// Save this loginparam to the database.
|
||||
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
|
||||
context.set_primary_self_addr(&self.addr).await?;
|
||||
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(
|
||||
Config::ConfiguredProvider.as_ref(),
|
||||
.set_config(
|
||||
Config::ConfiguredImapServers,
|
||||
Some(&serde_json::to_string(&self.imap)?),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::ConfiguredSmtpServers,
|
||||
Some(&serde_json::to_string(&self.smtp)?),
|
||||
)
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredMailUser, Some(&self.imap_user))
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredMailPw, Some(&self.imap_password))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(Config::ConfiguredSendUser, Some(&self.smtp_user))
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredSendPw, Some(&self.smtp_password))
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config_u32(
|
||||
Config::ConfiguredImapCertificateChecks,
|
||||
self.certificate_checks as u32,
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config_u32(
|
||||
Config::ConfiguredSmtpCertificateChecks,
|
||||
self.certificate_checks as u32,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Remove legacy settings.
|
||||
context
|
||||
.set_config(Config::ConfiguredMailServer, None)
|
||||
.await?;
|
||||
context.set_config(Config::ConfiguredMailPort, None).await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredMailSecurity, None)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredSendServer, None)
|
||||
.await?;
|
||||
context.set_config(Config::ConfiguredSendPort, None).await?;
|
||||
context
|
||||
.set_config(Config::ConfiguredSendSecurity, None)
|
||||
.await?;
|
||||
|
||||
let server_flags = match self.oauth2 {
|
||||
true => DC_LP_AUTH_OAUTH2,
|
||||
false => DC_LP_AUTH_NORMAL,
|
||||
};
|
||||
context
|
||||
.set_config_u32(Config::ConfiguredServerFlags, server_flags as u32)
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(
|
||||
Config::ConfiguredProvider,
|
||||
self.provider.map(|provider| provider.id),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (addr)
|
||||
DO UPDATE SET entered_param=excluded.entered_param, configured_param=excluded.configured_param",
|
||||
(
|
||||
self.addr.clone(),
|
||||
serde_json::to_string(entered_param)?,
|
||||
self.into_json()?,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn from_json(json: &str) -> Result<Self> {
|
||||
let json: ConfiguredLoginParamJson = serde_json::from_str(json)?;
|
||||
|
||||
let provider = json.provider_id.and_then(|id| get_provider_by_id(&id));
|
||||
|
||||
Ok(ConfiguredLoginParam {
|
||||
addr: json.addr,
|
||||
imap: json.imap,
|
||||
imap_user: json.imap_user,
|
||||
imap_password: json.imap_password,
|
||||
smtp: json.smtp,
|
||||
smtp_user: json.smtp_user,
|
||||
smtp_password: json.smtp_password,
|
||||
provider,
|
||||
certificate_checks: json.certificate_checks,
|
||||
oauth2: json.oauth2,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn into_json(self) -> Result<String> {
|
||||
let json = ConfiguredLoginParamJson {
|
||||
addr: self.addr,
|
||||
imap: self.imap,
|
||||
imap_user: self.imap_user,
|
||||
imap_password: self.imap_password,
|
||||
smtp: self.smtp,
|
||||
smtp_user: self.smtp_user,
|
||||
smtp_password: self.smtp_password,
|
||||
provider_id: self.provider.map(|p| p.id.to_string()),
|
||||
certificate_checks: self.certificate_checks,
|
||||
oauth2: self.oauth2,
|
||||
};
|
||||
Ok(serde_json::to_string(&json)?)
|
||||
}
|
||||
|
||||
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
|
||||
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
|
||||
match self.certificate_checks {
|
||||
@@ -896,10 +848,8 @@ impl ConfiguredLoginParam {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::log::LogExt as _;
|
||||
use crate::provider::get_provider_by_id;
|
||||
use crate::test_utils::TestContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_certificate_checks_display() {
|
||||
@@ -1011,32 +961,17 @@ mod tests {
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
param
|
||||
.clone()
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
let expected_param = r#"{"addr":"alice@example.org","imap":[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}],"imap_user":"","imap_password":"foo","smtp":[{"connection":{"host":"smtp.example.com","port":456,"security":"Tls"},"user":"alice@example.org"}],"smtp_user":"","smtp_password":"bar","provider_id":null,"certificate_checks":"Strict","oauth2":false}"#;
|
||||
param.save_as_configured_params(&t).await?;
|
||||
assert_eq!(
|
||||
t.sql
|
||||
.query_get_value::<String>("SELECT configured_param FROM transports", ())
|
||||
.await?
|
||||
.unwrap(),
|
||||
expected_param
|
||||
t.get_config(Config::ConfiguredImapServers).await?.unwrap(),
|
||||
r#"[{"connection":{"host":"imap.example.com","port":123,"security":"Starttls"},"user":"alice"}]"#
|
||||
);
|
||||
assert_eq!(t.is_configured().await?, true);
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(param, loaded);
|
||||
|
||||
// Legacy ConfiguredImapCertificateChecks config is ignored
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_ok());
|
||||
|
||||
// Test that we don't panic on unknown ConfiguredImapCertificateChecks values.
|
||||
let wrong_param = expected_param.replace("Strict", "Stricct");
|
||||
assert_ne!(expected_param, wrong_param);
|
||||
t.sql
|
||||
.execute("UPDATE transports SET configured_param=?", (wrong_param,))
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("999"))
|
||||
.await?;
|
||||
assert!(ConfiguredLoginParam::load(&t).await.is_err());
|
||||
|
||||
@@ -1054,8 +989,7 @@ mod tests {
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some("posteo"))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some("alice@posteo.at"))
|
||||
t.set_config(Config::ConfiguredAddr, Some("alice@posteo.at"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailServer, Some("posteo.de"))
|
||||
.await?;
|
||||
@@ -1129,68 +1063,12 @@ mod tests {
|
||||
oauth2: false,
|
||||
};
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list_legacy() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
//
|
||||
// There is at least one such provider in the provider database.
|
||||
let (domain, provider) = crate::provider::data::PROVIDER_DATA
|
||||
.iter()
|
||||
.find(|(_domain, provider)| provider.server.is_empty())
|
||||
.unwrap();
|
||||
|
||||
let t = TestContext::new().await;
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some(provider.id))
|
||||
.await?;
|
||||
t.sql
|
||||
.set_raw_config(Config::ConfiguredAddr.as_ref(), Some(&addr))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let loaded = ConfiguredLoginParam::load_legacy(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
migrate_configured_login_param(&t).await;
|
||||
|
||||
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn migrate_configured_login_param(t: &TestContext) {
|
||||
t.sql.execute("DROP TABLE transports;", ()).await.unwrap();
|
||||
t.sql.set_raw_config_int("dbversion", 130).await.unwrap();
|
||||
t.sql.run_migrations(t).await.log_err(t).ok();
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_empty_server_list() -> Result<()> {
|
||||
// Find a provider that does not have server list set.
|
||||
@@ -1205,41 +1083,25 @@ mod tests {
|
||||
|
||||
let addr = format!("alice@{domain}");
|
||||
|
||||
ConfiguredLoginParam {
|
||||
addr: addr.clone(),
|
||||
imap: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.clone(),
|
||||
}],
|
||||
imap_user: addr.clone(),
|
||||
imap_password: "foobarbaz".to_string(),
|
||||
smtp: vec![ConfiguredServerLoginParam {
|
||||
connection: ConnectionCandidate {
|
||||
host: "example.org".to_string(),
|
||||
port: 100,
|
||||
security: ConnectionSecurity::Tls,
|
||||
},
|
||||
user: addr.clone(),
|
||||
}],
|
||||
smtp_user: addr.clone(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
provider: Some(provider),
|
||||
certificate_checks: ConfiguredCertificateChecks::Automatic,
|
||||
oauth2: false,
|
||||
}
|
||||
.save_to_transports_table(&t, &EnteredLoginParam::default())
|
||||
.await?;
|
||||
t.set_config(Config::Configured, Some("1")).await?;
|
||||
t.set_config(Config::ConfiguredProvider, Some(provider.id))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredAddr, Some(&addr)).await?;
|
||||
t.set_config(Config::ConfiguredMailPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredImapCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredSendPw, Some("foobarbaz"))
|
||||
.await?;
|
||||
t.set_config(Config::ConfiguredSmtpCertificateChecks, Some("1"))
|
||||
.await?; // Strict
|
||||
t.set_config(Config::ConfiguredServerFlags, Some("0"))
|
||||
.await?;
|
||||
|
||||
let loaded = ConfiguredLoginParam::load(&t).await?.unwrap();
|
||||
assert_eq!(loaded.provider, Some(*provider));
|
||||
assert_eq!(loaded.imap.is_empty(), false);
|
||||
assert_eq!(loaded.smtp.is_empty(), false);
|
||||
assert_eq!(t.get_configured_provider().await?, Some(*provider));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use mail_builder::headers::HeaderType;
|
||||
use mail_builder::mime::MimePart;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, Chat};
|
||||
use crate::config::Config;
|
||||
@@ -23,7 +22,6 @@ use crate::contact::{Contact, ContactId, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::e2ee::EncryptHelper;
|
||||
use crate::ephemeral::Timer as EphemeralTimer;
|
||||
use crate::key::DcKey;
|
||||
use crate::location;
|
||||
use crate::message::{self, Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{is_hidden, SystemMessage};
|
||||
@@ -97,7 +95,7 @@ pub struct MimeFactory {
|
||||
/// Vector of pairs of past group member names and addresses.
|
||||
past_members: Vec<(String, String)>,
|
||||
|
||||
/// Timestamps of the members in the same order as in the `to`
|
||||
/// Timestamps of the members in the same order as in the `recipients`
|
||||
/// followed by `past_members`.
|
||||
///
|
||||
/// If this is not empty, its length
|
||||
@@ -133,6 +131,7 @@ pub struct RenderedEmail {
|
||||
pub message: String,
|
||||
// pub envelope: Envelope,
|
||||
pub is_encrypted: bool,
|
||||
pub is_gossiped: bool,
|
||||
pub last_added_location_id: Option<u32>,
|
||||
|
||||
/// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
|
||||
@@ -434,6 +433,35 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
async fn should_do_gossip(&self, context: &Context, multiple_recipients: bool) -> Result<bool> {
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
let cmd = msg.param.get_cmd();
|
||||
if cmd == SystemMessage::MemberAddedToGroup
|
||||
|| cmd == SystemMessage::SecurejoinMessage
|
||||
{
|
||||
Ok(true)
|
||||
} else if multiple_recipients {
|
||||
// beside key- and member-changes, force a periodic re-gossip.
|
||||
let gossiped_timestamp = chat.id.get_gossiped_timestamp(context).await?;
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
// Otherwise "smeared timestamps" may result in the condition
|
||||
// to fail even if the clock is monotonic.
|
||||
if gossip_period == 0 || time() >= gossiped_timestamp + gossip_period {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_attach_profile_data(msg: &Message) -> bool {
|
||||
msg.param.get_cmd() != SystemMessage::SecurejoinMessage || {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
@@ -753,6 +781,8 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
|
||||
let mut is_gossiped = false;
|
||||
|
||||
let peerstates = self.peerstates_for_recipients(context).await?;
|
||||
let is_encrypted = !self.should_force_plaintext()
|
||||
&& (e2ee_guaranteed || encrypt_helper.should_encrypt(context, &peerstates).await?);
|
||||
@@ -920,78 +950,16 @@ impl MimeFactory {
|
||||
// Add gossip headers in chats with multiple recipients
|
||||
let multiple_recipients =
|
||||
peerstates.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
|
||||
|
||||
let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
|
||||
let now = time();
|
||||
|
||||
match &self.loaded {
|
||||
Loaded::Message { chat, msg } => {
|
||||
if chat.typ != Chattype::Broadcast {
|
||||
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
||||
let Some(key) = peerstate.peek_key(verified) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
let cmd = msg.param.get_cmd();
|
||||
let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
|
||||
|| cmd == SystemMessage::SecurejoinMessage
|
||||
|| multiple_recipients && {
|
||||
let gossiped_timestamp: Option<i64> = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT timestamp
|
||||
FROM gossip_timestamp
|
||||
WHERE chat_id=? AND fingerprint=?",
|
||||
(chat.id, &fingerprint),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// `gossip_period == 0` is a special case for testing,
|
||||
// enabling gossip in every message.
|
||||
//
|
||||
// If current time is in the past compared to
|
||||
// `gossiped_timestamp`, we also gossip because
|
||||
// either the `gossiped_timestamp` or clock is wrong.
|
||||
gossip_period == 0
|
||||
|| gossiped_timestamp
|
||||
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
|
||||
};
|
||||
|
||||
if !should_do_gossip {
|
||||
continue;
|
||||
}
|
||||
|
||||
let header = Aheader::new(
|
||||
peerstate.addr.clone(),
|
||||
key.clone(),
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included.
|
||||
EncryptPreference::NoPreference,
|
||||
)
|
||||
.to_string();
|
||||
|
||||
message = message.header(
|
||||
"Autocrypt-Gossip",
|
||||
mail_builder::headers::raw::Raw::new(header),
|
||||
);
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (chat_id, fingerprint)
|
||||
DO UPDATE SET timestamp=excluded.timestamp",
|
||||
(chat.id, &fingerprint, now),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if self.should_do_gossip(context, multiple_recipients).await? {
|
||||
for peerstate in peerstates.iter().filter_map(|(state, _)| state.as_ref()) {
|
||||
if let Some(header) = peerstate.render_gossip_header(verified) {
|
||||
message = message.header(
|
||||
"Autocrypt-Gossip",
|
||||
mail_builder::headers::raw::Raw::new(header),
|
||||
);
|
||||
is_gossiped = true;
|
||||
}
|
||||
}
|
||||
Loaded::Mdn { .. } => {
|
||||
// Never gossip in MDNs.
|
||||
}
|
||||
}
|
||||
|
||||
// Set the appropriate Content-Type for the inner message.
|
||||
@@ -1135,6 +1103,7 @@ impl MimeFactory {
|
||||
message,
|
||||
// envelope: Envelope::new,
|
||||
is_encrypted,
|
||||
is_gossiped,
|
||||
last_added_location_id,
|
||||
sync_ids_to_delete: self.sync_ids_to_delete,
|
||||
rfc724_mid,
|
||||
|
||||
@@ -340,12 +340,11 @@ async fn test_subject_in_group() -> Result<()> {
|
||||
|
||||
// 6. Test that in a group, replies also take the quoted message's subject, while non-replies use the group title as subject
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let group_id = chat::create_group_chat(&t, chat::ProtectionStatus::Unprotected, "groupname") // TODO encodings, ä
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
|
||||
chat::add_contact_to_chat(&t, group_id, bob_contact_id).await?;
|
||||
let bob = Contact::create(&t, "", "bob@example.org").await?;
|
||||
chat::add_contact_to_chat(&t, group_id, bob).await?;
|
||||
|
||||
let subject = send_msg_get_subject(&t, group_id, None).await?;
|
||||
assert_eq!(subject, "groupname");
|
||||
@@ -773,24 +772,19 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
/// Test that removed member address does not go into the `To:` field.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_remove_member_bcc() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Alice creates a group with Bob and Claire and then removes Bob.
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
let charlie_contact = Contact::get_by_id(alice, charlie_id).await?;
|
||||
let charlie_addr = charlie_contact.get_addr();
|
||||
let claire_addr = "claire@foo.de";
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", claire_addr).await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, charlie_id).await?;
|
||||
send_text_msg(alice, alice_chat_id, "Creating a group".to_string()).await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "Creating a group".to_string()).await?;
|
||||
|
||||
remove_contact_from_chat(alice, alice_chat_id, charlie_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove = alice.pop_sent_msg().await;
|
||||
let remove_payload = remove.payload();
|
||||
let parsed = mailparse::parse_mail(remove_payload.as_bytes())?;
|
||||
@@ -802,8 +796,8 @@ async fn test_remove_member_bcc() -> Result<()> {
|
||||
for to_addr in to.iter() {
|
||||
match to_addr {
|
||||
mailparse::MailAddr::Single(ref info) => {
|
||||
// Addresses should be of existing members (Alice and Bob) and not Charlie.
|
||||
assert_ne!(info.addr, charlie_addr);
|
||||
// Addresses should be of existing members (Alice and Bob) and not Claire.
|
||||
assert_ne!(info.addr, claire_addr);
|
||||
}
|
||||
mailparse::MailAddr::Group(_) => {
|
||||
panic!("Group addresses are not expected here");
|
||||
@@ -893,7 +887,10 @@ async fn test_dont_remove_self() -> Result<()> {
|
||||
let mime_message = MimeMessage::from_bytes(alice, sent.payload.as_bytes(), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!mime_message.header_exists(HeaderDef::ChatGroupPastMembers));
|
||||
assert_eq!(
|
||||
mime_message.get_header(HeaderDef::ChatGroupPastMembers),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
mime_message.chat_group_member_timestamps().unwrap().len(),
|
||||
1 // There is a timestamp for Bob, not for Alice
|
||||
|
||||
@@ -57,10 +57,6 @@ pub(crate) struct MimeMessage {
|
||||
/// Message headers.
|
||||
headers: HashMap<String, String>,
|
||||
|
||||
#[cfg(test)]
|
||||
/// Names of removed (ignored) headers. Used by `header_exists()` needed for tests.
|
||||
headers_removed: HashSet<String>,
|
||||
|
||||
/// List of addresses from the `To` and `Cc` headers.
|
||||
///
|
||||
/// Addresses are normalized and lowercase.
|
||||
@@ -240,7 +236,6 @@ impl MimeMessage {
|
||||
let mut hop_info = parse_receive_headers(&mail.get_headers());
|
||||
|
||||
let mut headers = Default::default();
|
||||
let mut headers_removed = HashSet::<String>::new();
|
||||
let mut recipients = Default::default();
|
||||
let mut past_members = Default::default();
|
||||
let mut from = Default::default();
|
||||
@@ -258,12 +253,7 @@ impl MimeMessage {
|
||||
&mut chat_disposition_notification_to,
|
||||
&mail.headers,
|
||||
);
|
||||
headers.retain(|k, _| {
|
||||
!is_hidden(k) || {
|
||||
headers_removed.insert(k.clone());
|
||||
false
|
||||
}
|
||||
});
|
||||
headers.retain(|k, _| !is_hidden(k));
|
||||
|
||||
// Parse hidden headers.
|
||||
let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
|
||||
@@ -308,11 +298,9 @@ impl MimeMessage {
|
||||
// Overwrite Message-ID with X-Microsoft-Original-Message-ID.
|
||||
// However if we later find Message-ID in the protected part,
|
||||
// it will overwrite both.
|
||||
if let Some(microsoft_message_id) = remove_header(
|
||||
&mut headers,
|
||||
HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
|
||||
&mut headers_removed,
|
||||
) {
|
||||
if let Some(microsoft_message_id) =
|
||||
headers.remove(HeaderDef::XMicrosoftOriginalMessageId.get_headername())
|
||||
{
|
||||
headers.insert(
|
||||
HeaderDef::MessageId.get_headername().to_string(),
|
||||
microsoft_message_id,
|
||||
@@ -321,7 +309,7 @@ impl MimeMessage {
|
||||
|
||||
// Remove headers that are allowed _only_ in the encrypted+signed part. It's ok to leave
|
||||
// them in signed-only emails, but has no value currently.
|
||||
Self::remove_secured_headers(&mut headers, &mut headers_removed);
|
||||
Self::remove_secured_headers(&mut headers);
|
||||
|
||||
let mut from = from.context("No from in message")?;
|
||||
let private_keyring = load_self_secret_keyring(context).await?;
|
||||
@@ -454,7 +442,7 @@ impl MimeMessage {
|
||||
HeaderDef::ChatEdit,
|
||||
HeaderDef::ChatUserAvatar,
|
||||
] {
|
||||
remove_header(&mut headers, h.get_headername(), &mut headers_removed);
|
||||
headers.remove(h.get_headername());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,7 +506,7 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
if signatures.is_empty() {
|
||||
Self::remove_secured_headers(&mut headers, &mut headers_removed);
|
||||
Self::remove_secured_headers(&mut headers);
|
||||
|
||||
// If it is not a read receipt, degrade encryption.
|
||||
if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
|
||||
@@ -542,9 +530,6 @@ impl MimeMessage {
|
||||
let mut parser = MimeMessage {
|
||||
parts: Vec::new(),
|
||||
headers,
|
||||
#[cfg(test)]
|
||||
headers_removed,
|
||||
|
||||
recipients,
|
||||
past_members,
|
||||
list_post,
|
||||
@@ -946,16 +931,6 @@ impl MimeMessage {
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Returns whether the header exists in any part of the parsed message.
|
||||
///
|
||||
/// Use this to check for header absense. Header presense should be checked using
|
||||
/// `get_header(...).is_some()` as it also checks that the header isn't ignored.
|
||||
pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
|
||||
let hname = headerdef.get_headername();
|
||||
self.headers.contains_key(hname) || self.headers_removed.contains(hname)
|
||||
}
|
||||
|
||||
/// Returns `Chat-Group-ID` header value if it is a valid group ID.
|
||||
pub fn get_chat_group_id(&self) -> Option<&str> {
|
||||
self.get_header(HeaderDef::ChatGroupId)
|
||||
@@ -1551,17 +1526,14 @@ impl MimeMessage {
|
||||
.and_then(|msgid| parse_message_id(msgid).ok())
|
||||
}
|
||||
|
||||
fn remove_secured_headers(
|
||||
headers: &mut HashMap<String, String>,
|
||||
removed: &mut HashSet<String>,
|
||||
) {
|
||||
remove_header(headers, "secure-join-fingerprint", removed);
|
||||
remove_header(headers, "secure-join-auth", removed);
|
||||
remove_header(headers, "chat-verified", removed);
|
||||
remove_header(headers, "autocrypt-gossip", removed);
|
||||
fn remove_secured_headers(headers: &mut HashMap<String, String>) {
|
||||
headers.remove("secure-join-fingerprint");
|
||||
headers.remove("secure-join-auth");
|
||||
headers.remove("chat-verified");
|
||||
headers.remove("autocrypt-gossip");
|
||||
|
||||
// Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
|
||||
if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
|
||||
if let Some(secure_join) = headers.remove("secure-join") {
|
||||
if secure_join == "vc-request" || secure_join == "vg-request" {
|
||||
headers.insert("secure-join".to_string(), secure_join);
|
||||
}
|
||||
@@ -1889,19 +1861,6 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_header(
|
||||
headers: &mut HashMap<String, String>,
|
||||
key: &str,
|
||||
removed: &mut HashSet<String>,
|
||||
) -> Option<String> {
|
||||
if let Some((k, v)) = headers.remove_entry(key) {
|
||||
removed.insert(k);
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `Autocrypt-Gossip` headers from the email and applies them to peerstates.
|
||||
/// Params:
|
||||
/// from: The address which sent the message currently being parsed
|
||||
|
||||
@@ -405,6 +405,28 @@ impl Peerstate {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the contents of the `Autocrypt-Gossip` header for outgoing messages.
|
||||
pub fn render_gossip_header(&self, verified: bool) -> Option<String> {
|
||||
if let Some(key) = self.peek_key(verified) {
|
||||
let header = Aheader::new(
|
||||
self.addr.clone(),
|
||||
key.clone(), // TODO: avoid cloning
|
||||
// Autocrypt 1.1.0 specification says that
|
||||
// `prefer-encrypt` attribute SHOULD NOT be included,
|
||||
// but we include it anyway to propagate encryption
|
||||
// preference to new members in group chats.
|
||||
if self.last_seen_autocrypt > 0 {
|
||||
self.prefer_encrypt
|
||||
} else {
|
||||
EncryptPreference::NoPreference
|
||||
},
|
||||
);
|
||||
Some(header.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the peerstate into the contact public key.
|
||||
///
|
||||
/// Similar to [`Self::peek_key`], but consumes the peerstate and returns owned key.
|
||||
|
||||
@@ -5,7 +5,6 @@ pub(crate) mod data;
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use hickory_resolver::{config, Resolver, TokioResolver};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
@@ -38,19 +37,7 @@ pub enum Protocol {
|
||||
}
|
||||
|
||||
/// Socket security.
|
||||
#[derive(
|
||||
Debug,
|
||||
Default,
|
||||
Display,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Copy,
|
||||
Clone,
|
||||
FromPrimitive,
|
||||
ToPrimitive,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Default, Display, PartialEq, Eq, Copy, Clone, FromPrimitive, ToPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Socket {
|
||||
/// Unspecified socket security, select automatically.
|
||||
|
||||
@@ -10,7 +10,7 @@ use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress}
|
||||
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub(crate) use self::dclogin_scheme::configure_from_login_qr;
|
||||
use self::dclogin_scheme::configure_from_login_qr;
|
||||
use crate::chat::ChatIdBlocked;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Blocked;
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::ephemeral::{stock_ephemeral_timer_changed, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
use crate::imap::{markseen_on_imap_table, GENERATED_PREFIX};
|
||||
use crate::key::DcKey;
|
||||
use crate::log::LogExt;
|
||||
use crate::message::{
|
||||
self, rfc724_mid_exists, Message, MessageState, MessengerMessage, MsgId, Viewtype,
|
||||
@@ -453,25 +452,22 @@ pub(crate) async fn receive_imf_inner(
|
||||
}
|
||||
|
||||
// Update gossiped timestamp for the chat if someone else or our other device sent
|
||||
// Autocrypt-Gossip header to avoid sending Autocrypt-Gossip ourselves
|
||||
// Autocrypt-Gossip for all recipients in the chat to avoid sending Autocrypt-Gossip ourselves
|
||||
// and waste traffic.
|
||||
let chat_id = received_msg.chat_id;
|
||||
if !chat_id.is_special() {
|
||||
for gossiped_key in mime_parser.gossiped_keys.values() {
|
||||
context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
let fingerprint = gossiped_key.dc_fingerprint().hex();
|
||||
transaction.execute(
|
||||
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (chat_id, fingerprint)
|
||||
DO UPDATE SET timestamp=MAX(timestamp, excluded.timestamp)",
|
||||
(chat_id, &fingerprint, mime_parser.timestamp_sent),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
if !chat_id.is_special()
|
||||
&& mime_parser.recipients.iter().all(|recipient| {
|
||||
recipient.addr == mime_parser.from.addr
|
||||
|| mime_parser.gossiped_keys.contains_key(&recipient.addr)
|
||||
})
|
||||
{
|
||||
info!(
|
||||
context,
|
||||
"Received message contains Autocrypt-Gossip for all members of {chat_id}, updating timestamp."
|
||||
);
|
||||
if chat_id.get_gossiped_timestamp(context).await? < mime_parser.timestamp_sent {
|
||||
chat_id
|
||||
.set_gossiped_timestamp(context, mime_parser.timestamp_sent)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3113,6 +3113,7 @@ Message with references."#;
|
||||
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;
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vc-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Step 3: Alice receives vc-request, sends vc-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
@@ -149,7 +149,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
);
|
||||
if case == SetupContactCase::SecurejoinWaitTimeout {
|
||||
SystemTime::shift(Duration::from_secs(constants::SECUREJOIN_WAIT_TIMEOUT));
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), false);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
@@ -318,7 +318,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
0
|
||||
(true, 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ async fn test_setup_contact_bob_knows_alice() -> Result<()> {
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vc-contact-confirm
|
||||
bob.recv_msg_trash(&sent).await;
|
||||
@@ -523,7 +523,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
assert!(!msg.was_encrypted());
|
||||
assert_eq!(msg.get_header(HeaderDef::SecureJoin).unwrap(), "vg-request");
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinInvitenumber).is_some());
|
||||
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
|
||||
// Old Delta Chat core sent `Secure-Join-Group` header in `vg-request`,
|
||||
// but it was only used by Alice in `vg-request-with-auth`.
|
||||
@@ -531,7 +531,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
// and it is deprecated.
|
||||
// Now `Secure-Join-Group` header
|
||||
// is only sent in `vg-request-with-auth` for compatibility.
|
||||
assert!(!msg.header_exists(HeaderDef::SecureJoinGroup));
|
||||
assert!(msg.get_header(HeaderDef::SecureJoinGroup).is_none());
|
||||
|
||||
// Step 3: Alice receives vg-request, sends vg-auth-required
|
||||
alice.recv_msg_trash(&sent).await;
|
||||
@@ -606,7 +606,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
// Formally this message is auto-submitted, but as the member addition is a result of an
|
||||
// explicit user action, the Auto-Submitted header shouldn't be present. Otherwise it would
|
||||
// be strange to have it in "member-added" messages of verified groups only.
|
||||
assert!(!msg.header_exists(HeaderDef::AutoSubmitted));
|
||||
assert!(msg.get_header(HeaderDef::AutoSubmitted).is_none());
|
||||
// This is a two-member group, but Alice must Autocrypt-gossip to her other devices.
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_some());
|
||||
|
||||
@@ -636,7 +636,7 @@ async fn test_secure_join() -> Result<()> {
|
||||
.expect("Error looking up contact")
|
||||
.expect("Contact not found");
|
||||
let contact_alice = Contact::get_by_id(&bob.ctx, contact_alice_id).await?;
|
||||
assert_eq!(contact_alice.is_verified(&bob.ctx).await?, false);
|
||||
assert_eq!(contact_bob.is_verified(&bob.ctx).await?, false);
|
||||
|
||||
// Step 7: Bob receives vg-member-added
|
||||
bob.recv_msg(&sent).await;
|
||||
|
||||
@@ -5,11 +5,9 @@ use deltachat_contact_tools::EmailAddress;
|
||||
use rusqlite::OptionalExtension;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::configure::EnteredLoginParam;
|
||||
use crate::constants::ShowEmails;
|
||||
use crate::context::Context;
|
||||
use crate::imap;
|
||||
use crate::login_param::ConfiguredLoginParam;
|
||||
use crate::message::MsgId;
|
||||
use crate::provider::get_provider_by_domain;
|
||||
use crate::sql::Sql;
|
||||
@@ -1171,59 +1169,6 @@ CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 130)?;
|
||||
if dbversion < migration_version {
|
||||
sql.execute_migration(
|
||||
"
|
||||
CREATE TABLE gossip_timestamp (
|
||||
chat_id INTEGER NOT NULL,
|
||||
fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
|
||||
timestamp INTEGER NOT NULL,
|
||||
UNIQUE (chat_id, fingerprint)
|
||||
) STRICT;
|
||||
CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
|
||||
",
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
inc_and_check(&mut migration_version, 131)?;
|
||||
if dbversion < migration_version {
|
||||
let entered_param = EnteredLoginParam::load(context).await?;
|
||||
let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
|
||||
|
||||
sql.execute_migration_transaction(
|
||||
|transaction| {
|
||||
transaction.execute(
|
||||
"CREATE TABLE transports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
addr TEXT NOT NULL,
|
||||
entered_param TEXT NOT NULL,
|
||||
configured_param TEXT NOT NULL,
|
||||
UNIQUE(addr)
|
||||
)",
|
||||
(),
|
||||
)?;
|
||||
if let Some(configured_param) = configured_param {
|
||||
transaction.execute(
|
||||
"INSERT INTO transports (addr, entered_param, configured_param)
|
||||
VALUES (?, ?, ?)",
|
||||
(
|
||||
configured_param.addr.clone(),
|
||||
serde_json::to_string(&entered_param)?,
|
||||
configured_param.into_json()?,
|
||||
),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
},
|
||||
migration_version,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let new_version = sql
|
||||
.get_raw_config_int(VERSION_CFG)
|
||||
.await?
|
||||
@@ -1268,21 +1213,6 @@ impl Sql {
|
||||
}
|
||||
|
||||
async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
|
||||
self.execute_migration_transaction(
|
||||
|transaction| {
|
||||
transaction.execute_batch(query)?;
|
||||
Ok(())
|
||||
},
|
||||
version,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn execute_migration_transaction(
|
||||
&self,
|
||||
migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>,
|
||||
version: i32,
|
||||
) -> Result<()> {
|
||||
self.transaction(move |transaction| {
|
||||
let curr_version: String = transaction.query_row(
|
||||
"SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
|
||||
@@ -1292,7 +1222,7 @@ impl Sql {
|
||||
let curr_version: i32 = curr_version.parse()?;
|
||||
ensure!(curr_version < version, "Db version must be increased");
|
||||
Self::set_db_version_trans(transaction, version)?;
|
||||
migration(transaction)?;
|
||||
transaction.execute_batch(query)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ CREATE TABLE chats (
|
||||
grpid TEXT DEFAULT '',
|
||||
param TEXT DEFAULT '',
|
||||
archived INTEGER DEFAULT 0,
|
||||
gossiped_timestamp INTEGER DEFAULT 0, -- deprecated 2025-04-08, replaced with gossiped_timestamp table
|
||||
gossiped_timestamp INTEGER DEFAULT 0,
|
||||
locations_send_begin INTEGER DEFAULT 0,
|
||||
locations_send_until INTEGER DEFAULT 0,
|
||||
locations_last_sent INTEGER DEFAULT 0,
|
||||
|
||||
@@ -430,7 +430,7 @@ pub enum StockMessage {
|
||||
SecurejoinWait = 190,
|
||||
|
||||
#[strum(props(
|
||||
fallback = "The contact must be online to proceed.\n\nThis process will continue automatically in background."
|
||||
fallback = "That seems to take longer, maybe the contact or you are offline.\n\nHowever, the process continues in background, you can do something else…"
|
||||
))]
|
||||
SecurejoinTakesLonger = 192,
|
||||
}
|
||||
@@ -824,7 +824,7 @@ pub(crate) async fn securejoin_wait(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinWait).await
|
||||
}
|
||||
|
||||
/// Stock string: `The contact must be online to proceed. This process will continue automatically in background.`.
|
||||
/// Stock string: `That seems to take longer, maybe the contact or you are offline. However, the process continues in background, you can do something else…`.
|
||||
pub(crate) async fn securejoin_takes_longer(context: &Context) -> String {
|
||||
translated(context, StockMessage::SecurejoinTakesLonger).await
|
||||
}
|
||||
|
||||
@@ -477,11 +477,15 @@ impl TestContext {
|
||||
/// The context will be configured but the key will not be pre-generated so if a key is
|
||||
/// used the fingerprint will be different every time.
|
||||
pub async fn configure_addr(&self, addr: &str) {
|
||||
self.ctx.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
self.ctx
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.ctx
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
if let Some(name) = addr.split('@').next() {
|
||||
self.set_name(name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user