mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +03:00
Compare commits
1 Commits
v1.159.1
...
sk/create_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cadc5d850 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report something that isn't working.
|
||||
title: ''
|
||||
assignees: ''
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
This is the chatmail core's bug report tracker.
|
||||
For Delta Chat feature requests and support, please go to the forum: https://support.delta.chat
|
||||
Please fill out as much of this form as you can (leaving out stuff that is not applicable is ok).
|
||||
-->
|
||||
|
||||
- Operating System (Linux/Mac/Windows/iOS/Android):
|
||||
- Core Version:
|
||||
- Client Version:
|
||||
|
||||
## Expected behavior
|
||||
|
||||
*What did you try to achieve?*
|
||||
|
||||
## Actual behavior
|
||||
|
||||
*What happened instead?*
|
||||
|
||||
### Steps to reproduce the problem
|
||||
|
||||
1.
|
||||
2.
|
||||
|
||||
### Screenshots
|
||||
|
||||
### Logs
|
||||
|
||||
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
|
||||
|
||||
217
CHANGELOG.md
217
CHANGELOG.md
@@ -1,218 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [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
|
||||
|
||||
- deltachat-rpc-client: Accept `Account` as `Account.create_contact()` argument.
|
||||
- Rust: Add `ContactId.set_name()`.
|
||||
- JSON-RPC: Rename parameter name in `get_webxdc_href` to `info_msg_id` to reduce confusion potential ([#6681](https://github.com/chatmail/core/pull/6681)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Nicer configuration error ([#6684](https://github.com/chatmail/core/pull/6684)).
|
||||
- securejoin: Do not create 1:1 chat on Alice's side until `vc-request-with-auth`.
|
||||
- Understandable error message when accounts.lock can't be locked ([#6695](https://github.com/chatmail/core/pull/6695)).
|
||||
- Simplify e2ee decision logic, remove majority vote.
|
||||
- Stop saving txt_raw.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not fail to send the message if some keys are missing.
|
||||
- Synchronize contact name changes.
|
||||
- Move group name timestamp update up in create_send_msg_jobs().
|
||||
- Fixes for transport JSON-RPC ([#6680](https://github.com/chatmail/core/pull/6680)).
|
||||
|
||||
### Build system
|
||||
|
||||
- deltachat-rpc-client: Move development dependencies from tox.ini to pyproject.toml.
|
||||
- Update resolve-conf from 0.7.0 to 0.7.1.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Do not convert SQL arguments to `String` unnecessarily.
|
||||
- Factor out `update_chat_names()`.
|
||||
- Use `created_timestamp()` instead of duplicating its code ([#6692](https://github.com/chatmail/core/pull/6692)).
|
||||
- Use `chat_id.get_timestamp()` instead of duplicating its code ([#6691](https://github.com/chatmail/core/pull/6691)).
|
||||
- Move `mark_recipients_as_verified()` call out of `has_verified_encryption()`.
|
||||
- Move `proxy_config` out of `ConfiguredLoginParam` ([#6712](https://github.com/chatmail/core/pull/6712)).
|
||||
|
||||
### Tests
|
||||
|
||||
- Use vCard in TestContext.add_or_lookup_contact().
|
||||
- Remove test_group_with_removed_message_id.
|
||||
- Use add_or_lookup_email_contact() in get_chat().
|
||||
- Use add_or_lookup_email_contact in test_setup_contact_ex.
|
||||
- Use vCards more in Python tests.
|
||||
- Use TestContextManager in more tests.
|
||||
- Use vCards to create contacts in more Rust tests.
|
||||
- Set chat name multiple times in a row.
|
||||
- Online test for renaming the group multiple times.
|
||||
|
||||
## [1.157.3] - 2025-03-19
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add `copy_to_blob_dir` api ([#6660](https://github.com/chatmail/core/pull/6660)).
|
||||
- Add "delete_for_all" function in json-rpc ([#6672](https://github.com/chatmail/core/pull/6672)).
|
||||
- Sketch add_transport_from_qr(), add_transport(), list_transports(), delete_transport() APIs ([#6589](https://github.com/chatmail/core/pull/6589)).
|
||||
|
||||
### Build system
|
||||
|
||||
- Remove websocket support from deltachat-jsonrpc.
|
||||
- Remove encoded-words dependency.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Never send empty `To:` header ([#6663](https://github.com/chatmail/core/pull/6663)).
|
||||
- Use protected `Date` header for signed messages.
|
||||
- Fix setting up a profile and immediately transferring to a second device ([#6657](https://github.com/chatmail/core/pull/6657)).
|
||||
- Don't SMTP-send self-only messages if DeleteServerAfter is "immediate" ([#6661](https://github.com/chatmail/core/pull/6661)).
|
||||
- Use protected `Date` with protected Autocrypt.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Bump uuid from 1.12.1 to 1.15.1.
|
||||
- Update `strum` dependency.
|
||||
|
||||
### Refactor
|
||||
|
||||
- deltachat-rpc-client: Use wait_for_event() type argument.
|
||||
|
||||
### Tests
|
||||
|
||||
- Avoid creating contacts in `test_sync_{accept,block}_before_first_msg()`.
|
||||
- Fix `test_no_old_msg_is_fresh` flakiness.
|
||||
|
||||
## [1.157.2] - 2025-03-15
|
||||
|
||||
### Fixes
|
||||
@@ -6228,7 +6015,3 @@ https://github.com/chatmail/core/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.157.0]: https://github.com/chatmail/core/compare/v1.156.3..v1.157.0
|
||||
[1.157.1]: https://github.com/chatmail/core/compare/v1.157.0..v1.157.1
|
||||
[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
|
||||
|
||||
427
Cargo.lock
generated
427
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.159.1"
|
||||
version = "1.157.2"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.82"
|
||||
rust-version = "1.81"
|
||||
repository = "https://github.com/chatmail/core"
|
||||
|
||||
[profile.dev]
|
||||
@@ -41,7 +41,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
anyhow = { workspace = true }
|
||||
async-broadcast = "0.7.2"
|
||||
async-channel = { workspace = true }
|
||||
async-imap = { version = "0.10.4", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-imap = { version = "0.10.3", default-features = false, features = ["runtime-tokio", "compress"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.10", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.17", default-features = false, features = ["deflate", "tokio-fs"] }
|
||||
@@ -57,11 +57,11 @@ futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hickory-resolver = "=0.25.0-alpha.5"
|
||||
http-body-util = "0.1.3"
|
||||
http-body-util = "0.1.2"
|
||||
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"] }
|
||||
hyper-util = "0.1.10"
|
||||
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"
|
||||
@@ -72,6 +72,7 @@ mime = "0.3.17"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
parking_lot = "0.12"
|
||||
percent-encoding = "2.3"
|
||||
pgp = { version = "0.15.0", default-features = false }
|
||||
@@ -96,7 +97,7 @@ smallvec = "1.14.0"
|
||||
strum = "0.27"
|
||||
strum_macros = "0.27"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.2"
|
||||
textwrap = "0.16.1"
|
||||
thiserror = { workspace = true }
|
||||
tokio-io-timeout = "1.2.0"
|
||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
||||
@@ -108,7 +109,7 @@ toml = "0.8"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
webpki-roots = "0.26.8"
|
||||
blake3 = "1.8.0"
|
||||
blake3 = "1.6.1"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||
@@ -184,16 +185,17 @@ log = "0.4"
|
||||
mailparse = "0.16.1"
|
||||
nu-ansi-term = "0.46"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.20.2"
|
||||
rand = "0.8"
|
||||
regex = "1.10"
|
||||
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"
|
||||
tokio-util = "0.7.13"
|
||||
tracing-subscriber = "0.3"
|
||||
yerpc = "0.6.2"
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
|
||||
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -11,31 +11,9 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
The chatmail core library implements low-level network and encryption protocols,
|
||||
integrated by many chat bots and higher level applications,
|
||||
allowing to securely participate in the globally scaled e-mail server network.
|
||||
We provide reproducibly-built `deltachat-rpc-server` static binaries
|
||||
that offer a stdio-based high-level JSON-RPC API for instant messaging purposes.
|
||||
|
||||
The following protocols are handled without requiring API users to know much about them:
|
||||
|
||||
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
||||
|
||||
- robust [SMTP](https://github.com/chatmail/async-imap)
|
||||
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
||||
|
||||
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
||||
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
||||
|
||||
- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp)
|
||||
and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io)
|
||||
|
||||
- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and
|
||||
[webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime).
|
||||
|
||||
- a simulation- and real-world tested [P2P group membership
|
||||
protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership).
|
||||
|
||||
<p align="center">
|
||||
The core library for Delta Chat, written in Rust
|
||||
</p>
|
||||
|
||||
## Installing Rust and Cargo
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ license = "MPL-2.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
||||
|
||||
@@ -29,14 +29,202 @@
|
||||
|
||||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::bail;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, NaiveDateTime};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
mod vcard;
|
||||
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
||||
#[derive(Debug)]
|
||||
/// A Contact, as represented in a VCard.
|
||||
pub struct VcardContact {
|
||||
/// The email address, vcard property `email`
|
||||
pub addr: String,
|
||||
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
|
||||
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
|
||||
pub authname: String,
|
||||
/// The contact's public PGP key in Base64, vcard property `key`
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<i64>,
|
||||
}
|
||||
|
||||
impl VcardContact {
|
||||
/// Returns the contact's display name.
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self.authname.is_empty() {
|
||||
false => &self.authname,
|
||||
true => &self.addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
///
|
||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
||||
let timestamp = *c.timestamp.as_ref().ok()?;
|
||||
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:{addr}\r\n\
|
||||
FN:{display_name}\r\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
}
|
||||
res += "END:VCARD\r\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Parses `VcardContact`s from a given `&str`.
|
||||
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
let start_of_s = s.get(..prefix.len())?;
|
||||
|
||||
if start_of_s.eq_ignore_ascii_case(prefix) {
|
||||
s.get(prefix.len()..)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
|
||||
let remainder = remove_prefix(s, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
|
||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
||||
// This could be improved in the future, but for now, the parsing is good enough.
|
||||
let (params, value) = remainder.split_once(':')?;
|
||||
// In the example from above, `params` is now `;TYPE=work`
|
||||
// and `value` is now `alice@example.com`
|
||||
|
||||
if params
|
||||
.chars()
|
||||
.next()
|
||||
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
||||
.is_some()
|
||||
{
|
||||
// `s` started with `property`, but the next character after it was not punctuation,
|
||||
// so this line's property is actually something else
|
||||
return None;
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
|
||||
// ISO.8601, but fails to parse any of the examples given.
|
||||
// So, instead just parse using a format string.
|
||||
|
||||
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
|
||||
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
|
||||
Ok(datetime) => datetime.timestamp(),
|
||||
// Parses 19961022T140000.
|
||||
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
|
||||
Ok(datetime) => datetime
|
||||
.and_local_timezone(chrono::offset::Local)
|
||||
.single()
|
||||
.context("Could not apply local timezone to parsed date and time")?
|
||||
.timestamp(),
|
||||
Err(_) => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
// Skip to the start of the vcard:
|
||||
for line in lines.by_ref() {
|
||||
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut display_name = None;
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for mut line in lines.by_ref() {
|
||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
||||
// Remove the group name, if the group is called "item1".
|
||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
||||
//
|
||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
||||
line = remainder;
|
||||
}
|
||||
|
||||
if let Some(email) = vcard_property(line, "email") {
|
||||
addr.get_or_insert(email);
|
||||
} else if let Some(name) = vcard_property(line, "fn") {
|
||||
display_name.get_or_insert(name);
|
||||
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
|
||||
.or_else(|| remove_prefix(line, "KEY;PREF=1:data:application/pgp-keys;base64,"))
|
||||
{
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
|
||||
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
|
||||
{
|
||||
photo.get_or_insert(p);
|
||||
} else if let Some(rev) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
timestamp: datetime
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contacts
|
||||
}
|
||||
|
||||
/// Valid contact address.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -88,8 +276,7 @@ impl rusqlite::types::ToSql for ContactAddress {
|
||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
||||
/// - Removes the name if it is equal to the address by setting it to ""
|
||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
||||
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
||||
(
|
||||
if name.is_empty() {
|
||||
@@ -291,8 +478,148 @@ impl rusqlite::types::ToSql for EmailAddress {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::TimeZone;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vcard_thunderbird() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'Alice Mueller'
|
||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'bobzzz@freenet.de'
|
||||
EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].authname, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert!(contacts[1].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_simple_example() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Alice Wonderland
|
||||
N:Wonderland;Alice;;;Ms.
|
||||
GENDER:W
|
||||
EMAIL;TYPE=work:alice@example.com
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_trailing_newline() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\r
|
||||
VERSION:4.0\r
|
||||
FN:Alice Wonderland\r
|
||||
N:Wonderland;Alice;;;Ms.\r
|
||||
GENDER:W\r
|
||||
EMAIL;TYPE=work:alice@example.com\r
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
|
||||
REV:20240418T184242Z\r
|
||||
END:VCARD\r
|
||||
\r",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_and_parse_vcard() {
|
||||
let contacts = [
|
||||
VcardContact {
|
||||
addr: "alice@example.org".to_string(),
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
addr: "bob@example.com".to_string(),
|
||||
authname: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
let items = [
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:bob@example.com\r\n\
|
||||
FN:bob@example.com\r\n\
|
||||
REV:19700101T000000Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
];
|
||||
let mut expected = "".to_string();
|
||||
for len in 0..=contacts.len() {
|
||||
let contacts = &contacts[0..len];
|
||||
let vcard = make_vcard(contacts);
|
||||
if len > 0 {
|
||||
expected += items[len - 1];
|
||||
}
|
||||
assert_eq!(vcard, expected);
|
||||
let parsed = parse_vcard(&vcard);
|
||||
assert_eq!(parsed.len(), contacts.len());
|
||||
for i in 0..parsed.len() {
|
||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||
assert_eq!(parsed[i].authname, contacts[i].authname);
|
||||
assert_eq!(parsed[i].key, contacts[i].key);
|
||||
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||
assert_eq!(
|
||||
parsed[i].timestamp.as_ref().unwrap(),
|
||||
contacts[i].timestamp.as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contact_address() -> Result<()> {
|
||||
let alice_addr = "alice@example.org";
|
||||
@@ -339,6 +666,112 @@ mod tests {
|
||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_android() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
TEL;CELL:+1-234-567-890
|
||||
EMAIL;HOME:bob@example.org
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Alice;;;
|
||||
FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].authname, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_local_datetime() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Alice Wonderland\n\
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
REV:20240418T184242\n\
|
||||
END:VCARD",
|
||||
);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(
|
||||
*contacts[0].timestamp.as_ref().unwrap(),
|
||||
chrono::offset::Local
|
||||
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
||||
.unwrap()
|
||||
.timestamp()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
||||
// This one is Android-like.
|
||||
let vcard0 = "BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
EMAIL;HOME:bob@example.org
|
||||
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
||||
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
|
||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
||||
|
||||
END:VCARD
|
||||
";
|
||||
// This one is DOS-like.
|
||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
||||
for vcard in [vcard0, vcard1.as_str()] {
|
||||
let contacts = parse_vcard(vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protonmail_vcard() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN;PREF=1:Alice Wonderland
|
||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
ITEM1.X-PM-ENCRYPT:true
|
||||
ITEM1.X-PM-SIGN:true
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_name() {
|
||||
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use chrono::DateTime;
|
||||
use chrono::NaiveDateTime;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::sanitize_name_and_addr;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A Contact, as represented in a VCard.
|
||||
pub struct VcardContact {
|
||||
/// The email address, vcard property `email`
|
||||
pub addr: String,
|
||||
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
|
||||
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
|
||||
pub authname: String,
|
||||
/// The contact's public PGP key in Base64, vcard property `key`
|
||||
pub key: Option<String>,
|
||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
||||
pub profile_image: Option<String>,
|
||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
||||
pub timestamp: Result<i64>,
|
||||
}
|
||||
|
||||
impl VcardContact {
|
||||
/// Returns the contact's display name.
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self.authname.is_empty() {
|
||||
false => &self.authname,
|
||||
true => &self.addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vCard containing given contacts.
|
||||
///
|
||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
||||
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
||||
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
||||
let timestamp = *c.timestamp.as_ref().ok()?;
|
||||
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
||||
}
|
||||
|
||||
let mut res = "".to_string();
|
||||
for c in contacts {
|
||||
let addr = &c.addr;
|
||||
let display_name = c.display_name();
|
||||
res += &format!(
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:{addr}\r\n\
|
||||
FN:{display_name}\r\n"
|
||||
);
|
||||
if let Some(key) = &c.key {
|
||||
res += &format!("KEY:data:application/pgp-keys;base64,{key}\r\n");
|
||||
}
|
||||
if let Some(profile_image) = &c.profile_image {
|
||||
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\r\n");
|
||||
}
|
||||
if let Some(timestamp) = format_timestamp(c) {
|
||||
res += &format!("REV:{timestamp}\r\n");
|
||||
}
|
||||
res += "END:VCARD\r\n";
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Parses `VcardContact`s from a given `&str`.
|
||||
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
let start_of_s = s.get(..prefix.len())?;
|
||||
|
||||
if start_of_s.eq_ignore_ascii_case(prefix) {
|
||||
s.get(prefix.len()..)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Returns (parameters, value) tuple.
|
||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
||||
let remainder = remove_prefix(line, property)?;
|
||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
||||
|
||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
||||
// This could be improved in the future, but for now, the parsing is good enough.
|
||||
let (mut params, value) = remainder.split_once(':')?;
|
||||
// In the example from above, `params` is now `;TYPE=work`
|
||||
// and `value` is now `alice@example.com`
|
||||
|
||||
if params
|
||||
.chars()
|
||||
.next()
|
||||
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
||||
.is_some()
|
||||
{
|
||||
// `s` started with `property`, but the next character after it was not punctuation,
|
||||
// so this line's property is actually something else
|
||||
return None;
|
||||
}
|
||||
if let Some(p) = remove_prefix(params, ";") {
|
||||
params = p;
|
||||
}
|
||||
if let Some(p) = remove_prefix(params, "PREF=1") {
|
||||
params = p;
|
||||
}
|
||||
Some((params, value))
|
||||
}
|
||||
fn base64_key(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "key")?;
|
||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:application/pgp-keys;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:application/pgp-keys;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn base64_photo(line: &str) -> Option<&str> {
|
||||
let (params, value) = vcard_property(line, "photo")?;
|
||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG")
|
||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG")
|
||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64")
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
if let Some(value) = remove_prefix(value, "data:image/jpeg;base64,")
|
||||
.or_else(|| remove_prefix(value, r"data:image/jpeg;base64\,"))
|
||||
{
|
||||
return Some(value);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
||||
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
|
||||
// ISO.8601, but fails to parse any of the examples given.
|
||||
// So, instead just parse using a format string.
|
||||
|
||||
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
|
||||
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
|
||||
Ok(datetime) => datetime.timestamp(),
|
||||
// Parses 19961022T140000.
|
||||
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
|
||||
Ok(datetime) => datetime
|
||||
.and_local_timezone(chrono::offset::Local)
|
||||
.single()
|
||||
.context("Could not apply local timezone to parsed date and time")?
|
||||
.timestamp(),
|
||||
Err(_) => return Err(e.into()),
|
||||
},
|
||||
};
|
||||
Ok(timestamp)
|
||||
}
|
||||
|
||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
||||
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
||||
|
||||
let mut lines = unfolded_lines.lines().peekable();
|
||||
let mut contacts = Vec::new();
|
||||
|
||||
while lines.peek().is_some() {
|
||||
// Skip to the start of the vcard:
|
||||
for line in lines.by_ref() {
|
||||
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut display_name = None;
|
||||
let mut addr = None;
|
||||
let mut key = None;
|
||||
let mut photo = None;
|
||||
let mut datetime = None;
|
||||
|
||||
for mut line in lines.by_ref() {
|
||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
||||
// Remove the group name, if the group is called "item1".
|
||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
||||
//
|
||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
||||
line = remainder;
|
||||
}
|
||||
|
||||
if let Some((_params, email)) = vcard_property(line, "email") {
|
||||
addr.get_or_insert(email);
|
||||
} else if let Some((_params, name)) = vcard_property(line, "fn") {
|
||||
display_name.get_or_insert(name);
|
||||
} else if let Some(k) = base64_key(line) {
|
||||
key.get_or_insert(k);
|
||||
} else if let Some(p) = base64_photo(line) {
|
||||
photo.get_or_insert(p);
|
||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
||||
datetime.get_or_insert(rev);
|
||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
||||
let (authname, addr) =
|
||||
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
|
||||
|
||||
contacts.push(VcardContact {
|
||||
authname,
|
||||
addr,
|
||||
key: key.map(|s| s.to_string()),
|
||||
profile_image: photo.map(|s| s.to_string()),
|
||||
timestamp: datetime
|
||||
.context("No timestamp in vcard")
|
||||
.and_then(parse_datetime),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contacts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod vcard_tests;
|
||||
@@ -1,274 +0,0 @@
|
||||
use chrono::TimeZone as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vcard_thunderbird() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'Alice Mueller'
|
||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:'bobzzz@freenet.de'
|
||||
EMAIL;PREF=1:bobzzz@freenet.de
|
||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
||||
END:VCARD
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
||||
assert_eq!(contacts[1].authname, "".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
assert!(contacts[1].timestamp.is_err());
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_simple_example() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN:Alice Wonderland
|
||||
N:Wonderland;Alice;;;Ms.
|
||||
GENDER:W
|
||||
EMAIL;TYPE=work:alice@example.com
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
||||
REV:20240418T184242Z
|
||||
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_trailing_newline() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\r
|
||||
VERSION:4.0\r
|
||||
FN:Alice Wonderland\r
|
||||
N:Wonderland;Alice;;;Ms.\r
|
||||
GENDER:W\r
|
||||
EMAIL;TYPE=work:alice@example.com\r
|
||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
|
||||
REV:20240418T184242Z\r
|
||||
END:VCARD\r
|
||||
\r",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_and_parse_vcard() {
|
||||
let contacts = [
|
||||
VcardContact {
|
||||
addr: "alice@example.org".to_string(),
|
||||
authname: "Alice Wonderland".to_string(),
|
||||
key: Some("[base64-data]".to_string()),
|
||||
profile_image: Some("image in Base64".to_string()),
|
||||
timestamp: Ok(1713465762),
|
||||
},
|
||||
VcardContact {
|
||||
addr: "bob@example.com".to_string(),
|
||||
authname: "".to_string(),
|
||||
key: None,
|
||||
profile_image: None,
|
||||
timestamp: Ok(0),
|
||||
},
|
||||
];
|
||||
let items = [
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:alice@example.org\r\n\
|
||||
FN:Alice Wonderland\r\n\
|
||||
KEY:data:application/pgp-keys;base64,[base64-data]\r\n\
|
||||
PHOTO:data:image/jpeg;base64,image in Base64\r\n\
|
||||
REV:20240418T184242Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
"BEGIN:VCARD\r\n\
|
||||
VERSION:4.0\r\n\
|
||||
EMAIL:bob@example.com\r\n\
|
||||
FN:bob@example.com\r\n\
|
||||
REV:19700101T000000Z\r\n\
|
||||
END:VCARD\r\n",
|
||||
];
|
||||
let mut expected = "".to_string();
|
||||
for len in 0..=contacts.len() {
|
||||
let contacts = &contacts[0..len];
|
||||
let vcard = make_vcard(contacts);
|
||||
if len > 0 {
|
||||
expected += items[len - 1];
|
||||
}
|
||||
assert_eq!(vcard, expected);
|
||||
let parsed = parse_vcard(&vcard);
|
||||
assert_eq!(parsed.len(), contacts.len());
|
||||
for i in 0..parsed.len() {
|
||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
||||
assert_eq!(parsed[i].authname, contacts[i].authname);
|
||||
assert_eq!(parsed[i].key, contacts[i].key);
|
||||
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
||||
assert_eq!(
|
||||
parsed[i].timestamp.as_ref().unwrap(),
|
||||
contacts[i].timestamp.as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_android() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
TEL;CELL:+1-234-567-890
|
||||
EMAIL;HOME:bob@example.org
|
||||
END:VCARD
|
||||
BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Alice;;;
|
||||
FN:Alice
|
||||
EMAIL;HOME:alice@example.org
|
||||
END:VCARD
|
||||
",
|
||||
);
|
||||
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
|
||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[1].authname, "Alice".to_string());
|
||||
assert_eq!(contacts[1].key, None);
|
||||
assert_eq!(contacts[1].profile_image, None);
|
||||
|
||||
assert_eq!(contacts.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_local_datetime() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD\n\
|
||||
VERSION:4.0\n\
|
||||
FN:Alice Wonderland\n\
|
||||
EMAIL;TYPE=work:alice@example.org\n\
|
||||
REV:20240418T184242\n\
|
||||
END:VCARD",
|
||||
);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
||||
assert_eq!(
|
||||
*contacts[0].timestamp.as_ref().unwrap(),
|
||||
chrono::offset::Local
|
||||
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
||||
.unwrap()
|
||||
.timestamp()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vcard_with_base64_avatar() {
|
||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
||||
// This one is Android-like.
|
||||
let vcard0 = "BEGIN:VCARD
|
||||
VERSION:2.1
|
||||
N:;Bob;;;
|
||||
FN:Bob
|
||||
EMAIL;HOME:bob@example.org
|
||||
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
||||
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
|
||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
||||
|
||||
END:VCARD
|
||||
";
|
||||
// This one is DOS-like.
|
||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
||||
for vcard in [vcard0, vcard1.as_str()] {
|
||||
let contacts = parse_vcard(vcard);
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
||||
assert_eq!(contacts[0].key, None);
|
||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_protonmail_vcard() {
|
||||
let contacts = parse_vcard(
|
||||
"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN;PREF=1:Alice Wonderland
|
||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
ITEM1.X-PM-ENCRYPT:true
|
||||
ITEM1.X-PM-SIGN:true
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image, None);
|
||||
}
|
||||
|
||||
/// Proton at some point slightly changed the format of their vcards
|
||||
#[test]
|
||||
fn test_protonmail_vcard2() {
|
||||
let contacts = parse_vcard(
|
||||
r"BEGIN:VCARD
|
||||
VERSION:4.0
|
||||
FN;PREF=1:Alice
|
||||
PHOTO;PREF=1:data:image/jpeg;base64,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z
|
||||
REV:Invalid Date
|
||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
||||
KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
|
||||
UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||
END:VCARD",
|
||||
);
|
||||
|
||||
assert_eq!(contacts.len(), 1);
|
||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
||||
assert_eq!(&contacts[0].authname, "Alice");
|
||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
||||
assert!(contacts[0].timestamp.is_err());
|
||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.159.1"
|
||||
version = "1.157.2"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -24,6 +24,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
yerpc = { workspace = true, features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2162,29 +2162,6 @@ uint32_t dc_create_contact (dc_context_t* context, const char*
|
||||
int dc_add_address_book (dc_context_t* context, const char* addr_book);
|
||||
|
||||
|
||||
/**
|
||||
* Make a vCard.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param contact_id The ID of the contact to make the vCard of.
|
||||
* @return vCard, must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_make_vcard (dc_context_t* context, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Import a vCard.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param vcard vCard contents.
|
||||
* @return Returns the IDs of the contacts in the order they appear in the vCard.
|
||||
* Must be dc_array_unref()'d after usage.
|
||||
*/
|
||||
dc_array_t* dc_import_vcard (dc_context_t* context, const char* vcard);
|
||||
|
||||
|
||||
/**
|
||||
* Returns known and unblocked contacts.
|
||||
*
|
||||
@@ -4506,11 +4483,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* UIs can display e.g. an icon based upon the type.
|
||||
*
|
||||
* Currently, the following types are defined:
|
||||
* - DC_INFO_GROUP_NAME_CHANGED (2) - "Group name changd from OLD to BY by CONTACT"
|
||||
* - DC_INFO_GROUP_IMAGE_CHANGED (3) - "Group image changd by CONTACT"
|
||||
* - DC_INFO_MEMBER_ADDED_TO_GROUP (4) - "Member CONTACT added by OTHER_CONTACT"
|
||||
* - DC_INFO_MEMBER_REMOVED_FROM_GROUP (5) - "Member CONTACT removed by OTHER_CONTACT"
|
||||
* - DC_INFO_EPHEMERAL_TIMER_CHANGED (10) - "Disappearing messages CHANGED_TO by CONTACT"
|
||||
* - DC_INFO_PROTECTION_ENABLED (11) - Info-message for "Chat is now protected"
|
||||
* - DC_INFO_PROTECTION_DISABLED (12) - Info-message for "Chat is no longer protected"
|
||||
* - DC_INFO_INVALID_UNENCRYPTED_MAIL (13) - Info-message for "Provider requires end-to-end encryption which is not setup yet",
|
||||
@@ -4518,10 +4490,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
|
||||
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
|
||||
*
|
||||
* For the messages that refer to a CONTACT,
|
||||
* dc_msg_get_info_contact_id() returns the contact ID.
|
||||
* The UI should open the contact's profile when tapping the info message.
|
||||
*
|
||||
* Even when you display an icon,
|
||||
* you should still display the text of the informational message using dc_msg_get_text()
|
||||
*
|
||||
@@ -4534,29 +4502,6 @@ int dc_msg_is_info (const dc_msg_t* msg);
|
||||
int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Return the contact ID of the profile to open when tapping the info message.
|
||||
*
|
||||
* - For DC_INFO_MEMBER_ADDED_TO_GROUP and DC_INFO_MEMBER_REMOVED_FROM_GROUP,
|
||||
* this is the contact being added/removed.
|
||||
* The contact that did the adding/removal is usually only a tap away
|
||||
* (as introducer and/or atop of the memberlist),
|
||||
* and usually more known anyways.
|
||||
* - For DC_INFO_GROUP_NAME_CHANGED, DC_INFO_GROUP_IMAGE_CHANGED and DC_INFO_EPHEMERAL_TIMER_CHANGED
|
||||
* this is the contact who did the change.
|
||||
*
|
||||
* No need to check additionally for dc_msg_get_info_type(),
|
||||
* unless you e.g. want to show the info message in another style.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return If the info message refers to a contact,
|
||||
* this contact ID or DC_CONTACT_ID_SELF is returned.
|
||||
* Otherwise 0.
|
||||
*/
|
||||
uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
|
||||
|
||||
|
||||
// DC_INFO* uses the same values as SystemMessage in rust-land
|
||||
#define DC_INFO_UNKNOWN 0
|
||||
#define DC_INFO_GROUP_NAME_CHANGED 2
|
||||
@@ -6912,12 +6857,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Autocrypt Setup Message"
|
||||
///
|
||||
/// @deprecated 2025-04
|
||||
/// Used in subjects of outgoing Autocrypt Setup Messages.
|
||||
#define DC_STR_AC_SETUP_MSG_SUBJECT 42
|
||||
|
||||
/// "This is the Autocrypt Setup Message, open it in a compatible client to use your setup"
|
||||
///
|
||||
/// @deprecated 2025-04
|
||||
/// Used as message text of outgoing Autocrypt Setup Messages.
|
||||
#define DC_STR_AC_SETUP_MSG_BODY 43
|
||||
|
||||
/// "Cannot login as %1$s."
|
||||
@@ -7592,13 +7537,8 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Could not yet establish guaranteed end-to-end encryption, but you may already send a message."
|
||||
///
|
||||
/// @deprecated 2025-03
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "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
|
||||
#define DC_STR_SECUREJOIN_WAIT_TIMEOUT 191
|
||||
|
||||
/// "Contact". Deprecated, currently unused.
|
||||
#define DC_STR_CONTACT 200
|
||||
|
||||
@@ -18,7 +18,7 @@ use std::future::Future;
|
||||
use std::ops::Deref;
|
||||
use std::ptr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::Context as _;
|
||||
@@ -38,6 +38,7 @@ use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -67,8 +68,7 @@ const DC_GCM_INFO_ONLY: u32 = 0x02;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
static RT: LazyLock<Runtime> =
|
||||
LazyLock::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
where
|
||||
@@ -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
|
||||
@@ -2170,48 +2170,6 @@ pub unsafe extern "C" fn dc_add_address_book(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_make_vcard(
|
||||
context: *mut dc_context_t,
|
||||
contact_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_make_vcard()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
|
||||
block_on(contact::make_vcard(ctx, &[contact_id]))
|
||||
.unwrap_or_log_default(ctx, "dc_make_vcard failed")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_import_vcard(
|
||||
context: *mut dc_context_t,
|
||||
vcard: *const libc::c_char,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() || vcard.is_null() {
|
||||
eprintln!("ignoring careless call to dc_import_vcard()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(contact::import_vcard(ctx, &to_string_lossy(vcard)))
|
||||
.context("dc_import_vcard failed")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(contact_ids) => Box::into_raw(Box::new(dc_array_t::from(
|
||||
contact_ids
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
))),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_contacts(
|
||||
context: *mut dc_context_t,
|
||||
@@ -3772,20 +3730,6 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
|
||||
ffi_msg.message.get_info_type() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_info_contact_id(msg: *mut dc_msg_t) -> u32 {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_info_contact_id()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
block_on(ffi_msg.message.get_info_contact_id(context))
|
||||
.unwrap_or_default()
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.159.1"
|
||||
version = "1.157.2"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
|
||||
@@ -39,7 +39,6 @@ use deltachat::{imex, info};
|
||||
use sanitize_filename::is_sanitized;
|
||||
use tokio::fs;
|
||||
use tokio::sync::{watch, Mutex, RwLock};
|
||||
use types::login_param::EnteredLoginParam;
|
||||
use walkdir::WalkDir;
|
||||
use yerpc::rpc;
|
||||
|
||||
@@ -227,9 +226,8 @@ impl CommandApi {
|
||||
/// Get a list of all configured accounts.
|
||||
async fn get_all_accounts(&self) -> Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
let accounts_lock = self.accounts.read().await;
|
||||
for id in accounts_lock.get_all() {
|
||||
let context_option = accounts_lock.get_account(id);
|
||||
for id in self.accounts.read().await.get_all() {
|
||||
let context_option = self.accounts.read().await.get_account(id);
|
||||
if let Some(ctx) = context_option {
|
||||
accounts.push(Account::from_context(&ctx, id).await?)
|
||||
}
|
||||
@@ -327,12 +325,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))
|
||||
}
|
||||
|
||||
@@ -354,8 +348,8 @@ impl CommandApi {
|
||||
Ok(ctx.get_blobdir().to_str().map(|s| s.to_owned()))
|
||||
}
|
||||
|
||||
/// Copy file to blob dir.
|
||||
async fn copy_to_blob_dir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||
/// Copy file to blobdir.
|
||||
async fn copy_to_blobdir(&self, account_id: u32, path: String) -> Result<PathBuf> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let file = Path::new(&path);
|
||||
Ok(BlobObject::create_and_deduplicate(&ctx, file, file)?.to_abs_path())
|
||||
@@ -437,9 +431,6 @@ impl CommandApi {
|
||||
|
||||
/// Configures this account with the currently set parameters.
|
||||
/// Setup the credential config before calling this.
|
||||
///
|
||||
/// Deprecated as of 2025-02; use `add_transport_from_qr()`
|
||||
/// or `add_transport()` instead.
|
||||
async fn configure(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.stop_io().await;
|
||||
@@ -454,69 +445,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
/// and adds it as a transport.
|
||||
///
|
||||
/// If the email address is the same as an existing transport,
|
||||
/// then this existing account will be reconfigured instead of a new one being added.
|
||||
///
|
||||
/// This function stops and starts IO as needed.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `password`,
|
||||
/// and all the other settings will be autoconfigured.
|
||||
///
|
||||
/// During configuration, ConfigureProgress events are emitted;
|
||||
/// they indicate a successful configuration as well as errors
|
||||
/// and may be used to create a progress bar.
|
||||
/// This function will return after configuration is finished.
|
||||
///
|
||||
/// If configuration is successful,
|
||||
/// the working server parameters will be saved
|
||||
/// and used for connecting to the server.
|
||||
/// The parameters entered by the user will be saved separately
|
||||
/// so that they can be prefilled when the user opens the server-configuration screen again.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Self::is_configured()] to check whether there is
|
||||
/// at least one working transport.
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// 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_transport(&self, account_id: u32, param: EnteredLoginParam) -> Result<()> {
|
||||
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_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_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?;
|
||||
let res = ctx
|
||||
.list_transports()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|t| t.into())
|
||||
.collect();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
async fn delete_transport(&self, account_id: u32, addr: String) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
ctx.delete_transport(&addr).await
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
async fn stop_ongoing_process(&self, account_id: u32) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
@@ -1542,7 +1470,6 @@ impl CommandApi {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
async fn change_contact_name(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1551,7 +1478,9 @@ impl CommandApi {
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
contact_id.set_name(&ctx, &name).await?;
|
||||
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
Contact::create(&ctx, &name, addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1951,9 +1880,13 @@ impl CommandApi {
|
||||
|
||||
/// Get href from a WebxdcInfoMessage which might include a hash holding
|
||||
/// information about a specific position or state in a webxdc app (optional)
|
||||
async fn get_webxdc_href(&self, account_id: u32, info_msg_id: u32) -> Result<Option<String>> {
|
||||
async fn get_webxdc_href(
|
||||
&self,
|
||||
account_id: u32,
|
||||
instance_msg_id: u32,
|
||||
) -> Result<Option<String>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(info_msg_id)).await?;
|
||||
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
|
||||
Ok(message.get_webxdc_href())
|
||||
}
|
||||
|
||||
@@ -2276,37 +2209,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,
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::login_param as dc;
|
||||
use serde::Deserialize;
|
||||
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 {
|
||||
/// Email address.
|
||||
pub addr: String,
|
||||
|
||||
/// Password.
|
||||
pub password: String,
|
||||
|
||||
/// Imap server hostname or IP address.
|
||||
pub imap_server: Option<String>,
|
||||
|
||||
/// Imap server port.
|
||||
pub imap_port: Option<u16>,
|
||||
|
||||
/// Imap socket security.
|
||||
pub imap_security: Option<Socket>,
|
||||
|
||||
/// Imap username.
|
||||
pub imap_user: Option<String>,
|
||||
|
||||
/// SMTP server hostname or IP address.
|
||||
pub smtp_server: Option<String>,
|
||||
|
||||
/// SMTP server port.
|
||||
pub smtp_port: Option<u16>,
|
||||
|
||||
/// SMTP socket security.
|
||||
pub smtp_security: Option<Socket>,
|
||||
|
||||
/// SMTP username.
|
||||
pub smtp_user: Option<String>,
|
||||
|
||||
/// SMTP Password.
|
||||
///
|
||||
/// Only needs to be specified if different than IMAP password.
|
||||
pub smtp_password: Option<String>,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames.
|
||||
/// Default: Automatic
|
||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore).
|
||||
/// Default: false
|
||||
pub oauth2: Option<bool>,
|
||||
}
|
||||
|
||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
||||
let imap_security: Socket = param.imap.security.into();
|
||||
let smtp_security: Socket = param.smtp.security.into();
|
||||
let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into();
|
||||
Self {
|
||||
addr: param.addr,
|
||||
password: param.imap.password,
|
||||
imap_server: param.imap.server.into_option(),
|
||||
imap_port: param.imap.port.into_option(),
|
||||
imap_security: imap_security.into_option(),
|
||||
imap_user: param.imap.user.into_option(),
|
||||
smtp_server: param.smtp.server.into_option(),
|
||||
smtp_port: param.smtp.port.into_option(),
|
||||
smtp_security: smtp_security.into_option(),
|
||||
smtp_user: param.smtp.user.into_option(),
|
||||
smtp_password: param.smtp.password.into_option(),
|
||||
certificate_checks: certificate_checks.into_option(),
|
||||
oauth2: param.oauth2.into_option(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
||||
Ok(Self {
|
||||
addr: param.addr,
|
||||
imap: dc::EnteredServerLoginParam {
|
||||
server: param.imap_server.unwrap_or_default(),
|
||||
port: param.imap_port.unwrap_or_default(),
|
||||
security: param.imap_security.unwrap_or_default().into(),
|
||||
user: param.imap_user.unwrap_or_default(),
|
||||
password: param.password,
|
||||
},
|
||||
smtp: dc::EnteredServerLoginParam {
|
||||
server: param.smtp_server.unwrap_or_default(),
|
||||
port: param.smtp_port.unwrap_or_default(),
|
||||
security: param.smtp_security.unwrap_or_default().into(),
|
||||
user: param.smtp_user.unwrap_or_default(),
|
||||
password: param.smtp_password.unwrap_or_default(),
|
||||
},
|
||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
||||
oauth2: param.oauth2.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Socket {
|
||||
/// Unspecified socket security, select automatically.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
/// TLS connection.
|
||||
Ssl,
|
||||
|
||||
/// STARTTLS connection.
|
||||
Starttls,
|
||||
|
||||
/// No TLS, plaintext connection.
|
||||
Plain,
|
||||
}
|
||||
|
||||
impl From<dc::Socket> for Socket {
|
||||
fn from(value: dc::Socket) -> Self {
|
||||
match value {
|
||||
dc::Socket::Automatic => Self::Automatic,
|
||||
dc::Socket::Ssl => Self::Ssl,
|
||||
dc::Socket::Starttls => Self::Starttls,
|
||||
dc::Socket::Plain => Self::Plain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Socket> for dc::Socket {
|
||||
fn from(value: Socket) -> Self {
|
||||
match value {
|
||||
Socket::Automatic => Self::Automatic,
|
||||
Socket::Ssl => Self::Ssl,
|
||||
Socket::Starttls => Self::Starttls,
|
||||
Socket::Plain => Self::Plain,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum EnteredCertificateChecks {
|
||||
/// `Automatic` means that provider database setting should be taken.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// check certificates strictly.
|
||||
#[default]
|
||||
Automatic,
|
||||
|
||||
/// Ensure that TLS certificate is valid for the server hostname.
|
||||
Strict,
|
||||
|
||||
/// Accept certificates that are expired, self-signed
|
||||
/// or otherwise not valid for the server hostname.
|
||||
AcceptInvalidCertificates,
|
||||
}
|
||||
|
||||
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
|
||||
fn from(value: dc::EnteredCertificateChecks) -> Self {
|
||||
match value {
|
||||
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||
dc::EnteredCertificateChecks::Strict => Self::Strict,
|
||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
|
||||
Self::AcceptInvalidCertificates
|
||||
}
|
||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
|
||||
Self::AcceptInvalidCertificates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
|
||||
fn from(value: EnteredCertificateChecks) -> Self {
|
||||
match value {
|
||||
EnteredCertificateChecks::Automatic => Self::Automatic,
|
||||
EnteredCertificateChecks::Strict => Self::Strict,
|
||||
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoOption<T> {
|
||||
fn into_option(self) -> Option<T>;
|
||||
}
|
||||
impl<T> IntoOption<T> for T
|
||||
where
|
||||
T: Default + std::cmp::PartialEq,
|
||||
{
|
||||
fn into_option(self) -> Option<T> {
|
||||
if self == T::default() {
|
||||
None
|
||||
} else {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
@@ -680,6 +673,7 @@ pub struct MessageReadReceipt {
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageInfo {
|
||||
rawtext: String,
|
||||
ephemeral_timer: EphemeralTimer,
|
||||
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||
ephemeral_timestamp: Option<i64>,
|
||||
@@ -692,6 +686,7 @@ pub struct MessageInfo {
|
||||
impl MessageInfo {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let rawtext = msg_id.rawtext(context).await?;
|
||||
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||
deltachat::ephemeral::Timer::Disabled => None,
|
||||
@@ -704,6 +699,7 @@ impl MessageInfo {
|
||||
let hop_info = msg_id.hop_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
rawtext,
|
||||
ephemeral_timer,
|
||||
ephemeral_timestamp,
|
||||
error: message.error(),
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod contact;
|
||||
pub mod events;
|
||||
pub mod http;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
pub mod message;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.159.1"
|
||||
"version": "1.157.2"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { assert, expect } from "chai";
|
||||
import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js";
|
||||
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
|
||||
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
||||
|
||||
const EVENT_TIMEOUT = 20000;
|
||||
@@ -80,8 +80,11 @@ describe("online tests", function () {
|
||||
}
|
||||
this.timeout(15000);
|
||||
|
||||
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
||||
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
|
||||
@@ -98,18 +101,20 @@ describe("online tests", function () {
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
expect(message.text).equal("Hello");
|
||||
expect(message.showPadlock).equal(true);
|
||||
});
|
||||
|
||||
it("send and receive text message roundtrip", async function () {
|
||||
it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
|
||||
if (!accountsConfigured) {
|
||||
this.skip();
|
||||
}
|
||||
this.timeout(10000);
|
||||
|
||||
// send message from A to B
|
||||
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
||||
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
||||
const contactId = await dc.rpc.createContact(
|
||||
accountId1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.159.1"
|
||||
version = "1.157.2"
|
||||
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.1"
|
||||
version = "1.157.2"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -70,11 +70,3 @@ line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"imap-tools",
|
||||
"pytest",
|
||||
"pytest-timeout",
|
||||
"pytest-xdist",
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
@@ -26,12 +28,9 @@ class Account:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.manager.rpc
|
||||
|
||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
||||
def wait_for_event(self) -> AttrDict:
|
||||
"""Wait until the next event and return it."""
|
||||
while True:
|
||||
next_event = AttrDict(self._rpc.wait_for_event(self.id))
|
||||
if event_type is None or next_event.kind == event_type:
|
||||
return next_event
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
def clear_all_events(self):
|
||||
"""Removes all queued-up events for a given account. Useful for tests."""
|
||||
@@ -42,14 +41,14 @@ class Account:
|
||||
self._rpc.remove_account(self.id)
|
||||
|
||||
def clone(self) -> "Account":
|
||||
"""Clone given account.
|
||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature."""
|
||||
future = self._rpc.provide_backup.future(self.id)
|
||||
qr = self._rpc.get_backup_qr(self.id)
|
||||
new_account = self.manager.add_account()
|
||||
new_account._rpc.get_backup(new_account.id, qr)
|
||||
future()
|
||||
return new_account
|
||||
"""Clone given account."""
|
||||
with TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
self.export_backup(tmp_path)
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
new_account = self.manager.add_account()
|
||||
new_account.import_backup(files[0])
|
||||
return new_account
|
||||
|
||||
def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
@@ -110,17 +109,15 @@ class Account:
|
||||
"""Configure an account."""
|
||||
yield self._rpc.configure.future(self.id)
|
||||
|
||||
@futuremethod
|
||||
def add_transport(self, params):
|
||||
"""Add a new transport."""
|
||||
yield self._rpc.add_transport.future(self.id, params)
|
||||
|
||||
def bring_online(self):
|
||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
||||
self.start_io()
|
||||
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
|
||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -128,15 +125,9 @@ class Account:
|
||||
with that e-mail address, it is unblocked and its display
|
||||
name is updated if specified.
|
||||
|
||||
:param obj: email-address, contact id or account.
|
||||
:param obj: email-address or contact id.
|
||||
:param name: (optional) display name for this contact.
|
||||
"""
|
||||
if isinstance(obj, Account):
|
||||
vcard = obj.self_contact.make_vcard()
|
||||
[contact] = self.import_vcard(vcard)
|
||||
if name:
|
||||
contact.set_name(name)
|
||||
return contact
|
||||
if isinstance(obj, int):
|
||||
obj = Contact(self, obj)
|
||||
if isinstance(obj, Contact):
|
||||
@@ -157,8 +148,9 @@ class Account:
|
||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
||||
|
||||
def create_chat(self, account: "Account") -> Chat:
|
||||
"""Create a 1:1 chat with another account."""
|
||||
return self.create_contact(account).create_chat()
|
||||
vcard = account.self_contact.make_vcard()
|
||||
[contact] = self.import_vcard(vcard)
|
||||
return contact.create_chat()
|
||||
|
||||
def get_device_chat(self) -> Chat:
|
||||
"""Return device chat."""
|
||||
@@ -342,15 +334,24 @@ class Account:
|
||||
|
||||
def wait_for_incoming_msg_event(self):
|
||||
"""Wait for incoming message event and return it."""
|
||||
return self.wait_for_event(EventType.INCOMING_MSG)
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
return event
|
||||
|
||||
def wait_for_msgs_changed_event(self):
|
||||
"""Wait for messages changed event and return it."""
|
||||
return self.wait_for_event(EventType.MSGS_CHANGED)
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.MSGS_CHANGED:
|
||||
return event
|
||||
|
||||
def wait_for_msgs_noticed_event(self):
|
||||
"""Wait for messages noticed event and return it."""
|
||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
return event
|
||||
|
||||
def wait_for_incoming_msg(self):
|
||||
"""Wait for incoming message and return it.
|
||||
@@ -371,7 +372,10 @@ class Account:
|
||||
break
|
||||
|
||||
def wait_for_reactions_changed(self):
|
||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
||||
while True:
|
||||
event = self.wait_for_event()
|
||||
if event.kind == EventType.REACTIONS_CHANGED:
|
||||
return event
|
||||
|
||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -64,10 +64,6 @@ class Message:
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
def get_info(self) -> str:
|
||||
"""Return message info."""
|
||||
return self._rpc.get_message_info(self.account.id, self.id)
|
||||
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import random
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import py
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message
|
||||
@@ -12,6 +11,14 @@ from ._utils import futuremethod
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
def get_temp_credentials() -> dict:
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
password = f"{username}${username}"
|
||||
addr = f"{username}@{domain}"
|
||||
return {"email": addr, "password": password}
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
@@ -24,25 +31,26 @@ class ACFactory:
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
def get_credentials(self) -> (str, str):
|
||||
domain = os.getenv("CHATMAIL_DOMAIN")
|
||||
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6))
|
||||
return f"{username}@{domain}", f"{username}${username}"
|
||||
def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = get_temp_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
account.set_config("addr", credentials["email"])
|
||||
account.set_config("mail_pw", credentials["password"])
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
@futuremethod
|
||||
def new_configured_account(self):
|
||||
addr, password = self.get_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
params = {"addr": addr, "password": password}
|
||||
yield account.add_transport.future(params)
|
||||
|
||||
account = self.new_preconfigured_account()
|
||||
yield account.configure.future()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
def new_configured_bot(self) -> Bot:
|
||||
addr, password = self.get_credentials()
|
||||
credentials = get_temp_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(addr, password)
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
@futuremethod
|
||||
@@ -79,7 +87,7 @@ class ACFactory:
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account)
|
||||
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = from_account.create_group(group)
|
||||
to_chat.add_contact(to_contact)
|
||||
@@ -116,50 +124,3 @@ def rpc(tmp_path) -> AsyncGenerator:
|
||||
@pytest.fixture
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
"""Test data."""
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
for path in reversed(py.path.local(__file__).parts()):
|
||||
datadir = path.join("test-data")
|
||||
if datadir.isdir():
|
||||
self.path = datadir
|
||||
return
|
||||
raise Exception("Data path cannot be found")
|
||||
|
||||
def get_path(self, bn):
|
||||
"""return path of file or None if it doesn't exist."""
|
||||
fn = os.path.join(self.path, *bn.split("/"))
|
||||
assert os.path.exists(fn)
|
||||
return fn
|
||||
|
||||
def read_path(self, bn, mode="r"):
|
||||
fn = self.get_path(bn)
|
||||
if fn is not None:
|
||||
with open(fn, mode) as f:
|
||||
return f.read()
|
||||
return None
|
||||
|
||||
return Data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log():
|
||||
"""Log printer fixture."""
|
||||
|
||||
class Printer:
|
||||
def section(self, msg: str) -> None:
|
||||
print()
|
||||
print("=" * 10, msg, "=" * 10)
|
||||
|
||||
def step(self, msg: str) -> None:
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
|
||||
def indent(self, msg: str) -> None:
|
||||
print(" " + msg)
|
||||
|
||||
return Printer()
|
||||
|
||||
@@ -13,11 +13,10 @@ def test_event_on_configuration(acfactory: ACFactory) -> None:
|
||||
Test if ACCOUNTS_ITEM_CHANGED event is emitted on configure
|
||||
"""
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account = acfactory.new_preconfigured_account()
|
||||
account.clear_all_events()
|
||||
assert not account.is_configured()
|
||||
future = account.add_transport.future({"addr": addr, "password": password})
|
||||
future = account.configure.future()
|
||||
while True:
|
||||
event = account.wait_for_event()
|
||||
if event.kind == EventType.ACCOUNTS_ITEM_CHANGED:
|
||||
|
||||
@@ -48,7 +48,8 @@ def test_delivery_status(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
alice.clear_all_events()
|
||||
@@ -118,7 +119,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
"""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
@@ -148,7 +150,8 @@ def test_download_on_demand(acfactory: ACFactory) -> None:
|
||||
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("hi")
|
||||
|
||||
|
||||
@@ -175,11 +175,17 @@ def test_no_duplicate_messages(acfactory, path_to_webxdc):
|
||||
|
||||
threading.Thread(target=thread_run, daemon=True).start()
|
||||
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
n = int(bytes(event.data).decode())
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
n = int(bytes(event.data).decode())
|
||||
break
|
||||
|
||||
event = ac2.wait_for_event(EventType.WEBXDC_REALTIME_DATA)
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
while 1:
|
||||
event = ac2.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_DATA:
|
||||
assert int(bytes(event.data).decode()) > n
|
||||
break
|
||||
|
||||
|
||||
def test_no_reordering(acfactory, path_to_webxdc):
|
||||
@@ -223,5 +229,8 @@ def test_advertisement_after_chatting(acfactory, path_to_webxdc):
|
||||
ac2_hello_msg_snapshot.chat.accept()
|
||||
|
||||
ac2_webxdc_msg.send_webxdc_realtime_advertisement()
|
||||
event = ac1.wait_for_event(EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED)
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
while 1:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED:
|
||||
assert event.msg_id == ac1_webxdc_msg.id
|
||||
break
|
||||
|
||||
@@ -21,7 +21,6 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.bring_online()
|
||||
|
||||
setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
msg = wait_for_autocrypt_setup_message(alice2)
|
||||
@@ -35,12 +34,10 @@ def test_autocrypt_setup_message_key_transfer(acfactory):
|
||||
|
||||
def test_ac_setup_message_twice(acfactory):
|
||||
alice1 = acfactory.get_online_account()
|
||||
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.set_config("addr", alice1.get_config("addr"))
|
||||
alice2.set_config("mail_pw", alice1.get_config("mail_pw"))
|
||||
alice2.configure()
|
||||
alice2.bring_online()
|
||||
|
||||
# Send the first Autocrypt Setup Message and ignore it.
|
||||
_setup_code = alice1.initiate_autocrypt_key_transfer()
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
from imap_tools import AND
|
||||
|
||||
from deltachat_rpc_client import EventType
|
||||
from deltachat_rpc_client.const import MessageState
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, log, direct_imap):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
log.section("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_chat(ac2)
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.get_snapshot().is_forwarded
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
assert self_addr not in ev.msg
|
||||
assert other_addr in ev.msg
|
||||
|
||||
log.section("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
log.section("send out message with bcc to ourselves")
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ev = ac1.wait_for_event(EventType.SMTP_MESSAGE_SENT)
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone.wait_for_event(EventType.MSGS_CHANGED)
|
||||
assert ac1_clone.get_message_by_id(ev_msg.msg_id).get_snapshot().text == msg_out.get_snapshot().text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.msg
|
||||
assert self_addr in ev.msg
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.INFO and event.msg.endswith("Marked messages 1 in folder INBOX as seen."):
|
||||
break
|
||||
|
||||
# Check that the message is marked as seen on IMAP.
|
||||
ac1_direct_imap = direct_imap(ac1)
|
||||
ac1_direct_imap.connect()
|
||||
ac1_direct_imap.select_folder("Inbox")
|
||||
assert len(list(ac1_direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_multidevice_sync_seen(acfactory, log):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_clone = ac1.clone()
|
||||
ac1_clone.bring_online()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
log.section("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1.wait_for_incoming_msg()
|
||||
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
||||
assert ac1_chat.get_fresh_message_count() == 1
|
||||
assert ac1_clone_chat.get_fresh_message_count() == 1
|
||||
assert ac1_message.get_snapshot().state == MessageState.IN_FRESH
|
||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
log.section("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
log.section("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
||||
assert ev.chat_id == ac1_clone_chat.id
|
||||
|
||||
log.section("Send an ephemeral message from ac2 to ac1")
|
||||
ac2_chat.set_ephemeral_timer(60)
|
||||
ac1.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
|
||||
ac1.wait_for_incoming_msg()
|
||||
ac1_clone.wait_for_event(EventType.CHAT_EPHEMERAL_TIMER_MODIFIED)
|
||||
ac1_clone.wait_for_incoming_msg()
|
||||
|
||||
ac2_chat.send_text("Foobar")
|
||||
ac1_message = ac1.wait_for_incoming_msg()
|
||||
ac1_clone_message = ac1_clone.wait_for_incoming_msg()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_info()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_info()
|
||||
|
||||
ac1_message.mark_seen()
|
||||
assert "Expires: " in ac1_message.get_info()
|
||||
ev = ac1_clone.wait_for_event(EventType.MSGS_NOTICED)
|
||||
assert ev.chat_id == ac1_clone_chat.id
|
||||
assert ac1_clone_message.get_snapshot().state == MessageState.IN_SEEN
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_info()
|
||||
@@ -76,11 +76,17 @@ def test_qr_securejoin(acfactory, protect):
|
||||
bob.secure_join(qr_code)
|
||||
|
||||
# Alice deletes "vg-request".
|
||||
alice.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = alice.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
alice.wait_for_securejoin_inviter_success()
|
||||
# Bob deletes "vg-auth-required", Alice deletes "vg-request-with-auth".
|
||||
for ac in [alice, bob]:
|
||||
ac.wait_for_event(EventType.IMAP_MESSAGE_DELETED)
|
||||
while True:
|
||||
event = ac.wait_for_event()
|
||||
if event["kind"] == "ImapMessageDeleted":
|
||||
break
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
# Test that Alice verified Bob's profile.
|
||||
@@ -89,7 +95,7 @@ def test_qr_securejoin(acfactory, protect):
|
||||
assert alice_contact_bob_snapshot.is_verified
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected == protect
|
||||
|
||||
# Test that Bob verified Alice's profile.
|
||||
@@ -117,7 +123,8 @@ def test_qr_securejoin_contact_request(acfactory) -> None:
|
||||
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -154,8 +161,11 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
logging.info("Alice creates a verified group")
|
||||
group = alice.create_group("Group", protect=True)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie, "Charlie")
|
||||
bob_addr = bob.get_config("addr")
|
||||
charlie_addr = charlie.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
|
||||
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.add_contact(alice_contact_charlie)
|
||||
@@ -182,7 +192,7 @@ def test_qr_readreceipt(acfactory) -> None:
|
||||
charlie_snapshot = charlie_message.get_snapshot()
|
||||
assert charlie_snapshot.text == "Hi from Bob!"
|
||||
|
||||
bob_contact_charlie = bob.create_contact(charlie, "Charlie")
|
||||
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
|
||||
assert not bob.get_chat_by_contact(bob_contact_charlie)
|
||||
|
||||
logging.info("Charlie reads Bob's message")
|
||||
@@ -453,12 +463,12 @@ def test_qr_new_group_unblocked(acfactory):
|
||||
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="AEAP is disabled for now")
|
||||
def test_aeap_flow_verified(acfactory):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
# ac1new is only used to get a new address.
|
||||
ac1new = acfactory.new_preconfigured_account()
|
||||
|
||||
logging.info("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group("hello", protect=True)
|
||||
@@ -478,8 +488,8 @@ def test_aeap_flow_verified(acfactory):
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
logging.info("changing email account")
|
||||
ac1.set_config("addr", addr)
|
||||
ac1.set_config("mail_pw", password)
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.stop_io()
|
||||
ac1.configure()
|
||||
ac1.start_io()
|
||||
@@ -492,9 +502,11 @@ def test_aeap_flow_verified(acfactory):
|
||||
msg_in_2_snapshot = msg_in_2.get_snapshot()
|
||||
assert msg_in_2_snapshot.text == msg_out.text
|
||||
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().get_snapshot().address == addr
|
||||
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
|
||||
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
|
||||
assert addr in [contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()]
|
||||
assert ac1new.get_config("addr") in [
|
||||
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
|
||||
]
|
||||
|
||||
|
||||
def test_gossip_verification(acfactory) -> None:
|
||||
@@ -510,9 +522,9 @@ def test_gossip_verification(acfactory) -> None:
|
||||
bob.secure_join(qr_code)
|
||||
bob.wait_for_securejoin_joiner_success()
|
||||
|
||||
bob_contact_alice = bob.create_contact(alice, "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol, "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice, "Alice")
|
||||
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
|
||||
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
|
||||
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
|
||||
|
||||
logging.info("Bob creates an Autocrypt group")
|
||||
bob_group_chat = bob.create_group("Autocrypt Group")
|
||||
@@ -563,7 +575,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
|
||||
# ac1 waits for member added message and creates a QR code.
|
||||
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
ac1_qr_code = snapshot.chat.get_qr_code()
|
||||
|
||||
# ac2 verifies ac1
|
||||
@@ -572,7 +584,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
ac2.wait_for_securejoin_joiner_success()
|
||||
|
||||
# ac1 is verified for ac2.
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1, "")
|
||||
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
|
||||
assert ac2_contact_ac1.get_snapshot().is_verified
|
||||
|
||||
# ac1 resetups the account.
|
||||
@@ -587,7 +599,7 @@ def test_securejoin_after_contact_resetup(acfactory) -> None:
|
||||
# header sent by old ac1.
|
||||
while True:
|
||||
# ac1 sends a message to ac2.
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2, "")
|
||||
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
|
||||
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
|
||||
ac1_chat_ac2.send_text("Hello!")
|
||||
|
||||
@@ -646,7 +658,7 @@ def test_withdraw_securejoin_qr(acfactory):
|
||||
alice.clear_all_events()
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
assert snapshot.text == "Member Me added by {}.".format(alice.get_config("addr"))
|
||||
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
|
||||
assert snapshot.chat.get_basic_snapshot().is_protected
|
||||
bob_chat.leave()
|
||||
|
||||
|
||||
@@ -61,79 +61,61 @@ def test_acfactory(acfactory) -> None:
|
||||
|
||||
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapSecurity": "starttls",
|
||||
"smtpSecurity": "starttls",
|
||||
},
|
||||
)
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
account.set_config("mail_security", "2")
|
||||
account.set_config("send_security", "2")
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
def test_configure_ip(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
ip_address = socket.gethostbyname(addr.rsplit("@")[-1])
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
domain = account.get_config("addr").rsplit("@")[-1]
|
||||
ip_address = socket.gethostbyname(domain)
|
||||
|
||||
# This should fail TLS check.
|
||||
account.set_config("mail_server", ip_address)
|
||||
with pytest.raises(JsonRpcError):
|
||||
account.add_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
# This should fail TLS check.
|
||||
"imapServer": ip_address,
|
||||
},
|
||||
)
|
||||
account.configure()
|
||||
|
||||
|
||||
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_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapPort": 443,
|
||||
"smtpPort": 443,
|
||||
},
|
||||
)
|
||||
assert account.is_configured()
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
account.set_config("mail_port", "443")
|
||||
account.set_config("send_port", "443")
|
||||
|
||||
account.configure()
|
||||
|
||||
|
||||
def test_list_transports(acfactory) -> None:
|
||||
addr, password = acfactory.get_credentials()
|
||||
account = acfactory.get_unconfigured_account()
|
||||
account.add_transport(
|
||||
{
|
||||
"addr": addr,
|
||||
"password": password,
|
||||
"imapUser": addr,
|
||||
},
|
||||
)
|
||||
transports = account._rpc.list_transports(account.id)
|
||||
assert len(transports) == 1
|
||||
params = transports[0]
|
||||
assert params["addr"] == addr
|
||||
assert params["password"] == password
|
||||
assert params["imapUser"] == addr
|
||||
def test_configure_username(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
addr = account.get_config("addr")
|
||||
account.set_config("mail_user", addr)
|
||||
account.configure()
|
||||
|
||||
assert account.get_config("configured_mail_user") == addr
|
||||
|
||||
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
@@ -192,7 +174,8 @@ def test_account(acfactory) -> None:
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -258,7 +241,7 @@ def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
assert repr(alice_contact_bob)
|
||||
@@ -275,7 +258,8 @@ def test_contact(acfactory) -> None:
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -303,37 +287,13 @@ def test_message(acfactory) -> None:
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
def test_selfavatar_sync(acfactory, data, log) -> None:
|
||||
alice = acfactory.get_online_account()
|
||||
|
||||
log.section("Alice adds a second device")
|
||||
alice2 = alice.clone()
|
||||
|
||||
log.section("Second device goes online")
|
||||
alice2.start_io()
|
||||
|
||||
log.section("First device changes avatar")
|
||||
image = data.get_path("image/avatar1000x1000.jpg")
|
||||
alice.set_config("selfavatar", image)
|
||||
avatar_config = alice.get_config("selfavatar")
|
||||
avatar_hash = os.path.basename(avatar_config)
|
||||
print("Info: avatar hash is ", avatar_hash)
|
||||
|
||||
log.section("First device receives avatar change")
|
||||
alice2.wait_for_event(EventType.SELFAVATAR_CHANGED)
|
||||
avatar_config2 = alice2.get_config("selfavatar")
|
||||
avatar_hash2 = os.path.basename(avatar_config2)
|
||||
print("Info: avatar hash on second device is ", avatar_hash2)
|
||||
assert avatar_hash == avatar_hash2
|
||||
assert avatar_config != avatar_config2
|
||||
|
||||
|
||||
def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.start_io()
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
@@ -345,12 +305,20 @@ def test_reaction_seen_on_another_dev(acfactory) -> None:
|
||||
snapshot.chat.accept()
|
||||
message.send_reaction("😎")
|
||||
for a in [alice, alice2]:
|
||||
a.wait_for_event(EventType.INCOMING_REACTION)
|
||||
while True:
|
||||
event = a.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_REACTION:
|
||||
break
|
||||
|
||||
alice2.clear_all_events()
|
||||
alice_chat_bob.mark_noticed()
|
||||
chat_id = alice2.wait_for_event(EventType.MSGS_NOTICED).chat_id
|
||||
alice2_chat_bob = alice2.create_chat(bob)
|
||||
while True:
|
||||
event = alice2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
chat_id = event.chat_id
|
||||
break
|
||||
alice2_contact_bob = alice2.get_contact_by_addr(bob_addr)
|
||||
alice2_chat_bob = alice2_contact_bob.create_chat()
|
||||
assert chat_id == alice2_chat_bob.id
|
||||
|
||||
|
||||
@@ -358,19 +326,24 @@ def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
# Alice becomes a bot.
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
break
|
||||
|
||||
|
||||
def test_bot(acfactory) -> None:
|
||||
@@ -417,11 +390,9 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
addr, password = acfactory.get_credentials()
|
||||
bot = acfactory.get_unconfigured_account()
|
||||
bot = acfactory.new_preconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.add_transport({"addr": addr, "password": password})
|
||||
assert bot.is_configured()
|
||||
bot.configure()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not bot.wait_next_messages()
|
||||
@@ -430,7 +401,8 @@ def test_wait_next_messages(acfactory) -> None:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
|
||||
alice_contact_bot = alice.create_contact(bot, "Bot")
|
||||
bot_addr = bot.get_config("addr")
|
||||
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
@@ -454,7 +426,9 @@ def test_import_export_backup(acfactory, tmp_path) -> None:
|
||||
def test_import_export_keys(acfactory, tmp_path) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_chat_bob = alice.create_chat(bob)
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello Bob!")
|
||||
|
||||
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
|
||||
@@ -504,7 +478,9 @@ def test_provider_info(rpc) -> None:
|
||||
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
|
||||
# Bob creates chat manually so chat with Alice is accepted.
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
@@ -528,7 +504,10 @@ def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
|
||||
|
||||
# Alice reads Bob's message.
|
||||
message.mark_seen()
|
||||
bob.wait_for_event(EventType.MSG_READ)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.MSG_READ:
|
||||
break
|
||||
|
||||
# Bob sends a message to Alice, it should also be encrypted.
|
||||
bob_chat_alice.send_text("Hi Alice!")
|
||||
@@ -600,13 +579,9 @@ def test_reactions_for_a_reordering_move(acfactory, direct_imap):
|
||||
messages they refer to and thus dropped.
|
||||
"""
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
addr, password = acfactory.get_credentials()
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.add_transport({"addr": addr, "password": password})
|
||||
ac2 = acfactory.new_preconfigured_account()
|
||||
ac2.configure()
|
||||
ac2.set_config("mvbox_move", "1")
|
||||
assert ac2.is_configured()
|
||||
|
||||
ac2.bring_online()
|
||||
chat1 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac2.stop_io()
|
||||
@@ -650,7 +625,9 @@ def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
|
||||
chat.send_text("Hello Alice!")
|
||||
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
|
||||
|
||||
contact = alice.create_contact(account)
|
||||
contact_addr = account.get_config("addr")
|
||||
contact = alice.create_contact(contact_addr, "")
|
||||
|
||||
alice_group.add_contact(contact)
|
||||
|
||||
if n_accounts == 2:
|
||||
@@ -700,7 +677,10 @@ def test_markseen_contact_request(acfactory):
|
||||
assert message2.get_snapshot().state == MessageState.IN_FRESH
|
||||
|
||||
message.mark_seen()
|
||||
bob2.wait_for_event(EventType.MSGS_NOTICED)
|
||||
while True:
|
||||
event = bob2.wait_for_event()
|
||||
if event.kind == EventType.MSGS_NOTICED:
|
||||
break
|
||||
assert message2.get_snapshot().state == MessageState.IN_SEEN
|
||||
|
||||
|
||||
@@ -748,7 +728,10 @@ def test_no_old_msg_is_fresh(acfactory):
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 1
|
||||
assert len(list(ac1.get_fresh_messages())) == 1
|
||||
|
||||
ac1.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
||||
while True:
|
||||
event = ac1.wait_for_event()
|
||||
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
|
||||
logging.info("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'")
|
||||
ac1_clone_chat.send_text("Hi back")
|
||||
@@ -757,71 +740,3 @@ def test_no_old_msg_is_fresh(acfactory):
|
||||
assert ev.chat_id == first_msg.get_snapshot().chat_id
|
||||
assert ac1.create_chat(ac2).get_fresh_message_count() == 0
|
||||
assert len(list(ac1.get_fresh_messages())) == 0
|
||||
|
||||
|
||||
def test_rename_synchronization(acfactory):
|
||||
"""Test synchronization of contact renaming."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
alice2 = alice.clone()
|
||||
alice2.bring_online()
|
||||
|
||||
bob.set_config("displayname", "Bob")
|
||||
bob.create_chat(alice).send_text("Hello!")
|
||||
alice_msg = alice.wait_for_incoming_msg().get_snapshot()
|
||||
alice2_msg = alice2.wait_for_incoming_msg().get_snapshot()
|
||||
|
||||
assert alice2_msg.sender.get_snapshot().display_name == "Bob"
|
||||
alice_msg.sender.set_name("Bobby")
|
||||
alice2.wait_for_event(EventType.CONTACTS_CHANGED)
|
||||
assert alice2_msg.sender.get_snapshot().display_name == "Bobby"
|
||||
|
||||
|
||||
def test_rename_group(acfactory):
|
||||
"""Test renaming the group."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_group = alice.create_group("Test group")
|
||||
alice_contact_bob = alice.create_contact(bob)
|
||||
alice_group.add_contact(alice_contact_bob)
|
||||
alice_group.send_text("Hello!")
|
||||
|
||||
bob_msg = bob.wait_for_incoming_msg()
|
||||
bob_chat = bob_msg.get_snapshot().chat
|
||||
assert bob_chat.get_basic_snapshot().name == "Test group"
|
||||
|
||||
for name in ["Baz", "Foo bar", "Xyzzy"]:
|
||||
alice_group.set_name(name)
|
||||
bob.wait_for_incoming_msg_event()
|
||||
assert bob_chat.get_basic_snapshot().name == name
|
||||
|
||||
|
||||
def test_get_all_accounts_deadlock(rpc):
|
||||
"""Regression test for get_all_accounts deadlock."""
|
||||
for _ in range(100):
|
||||
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()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
def test_vcard(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_contact_charlie = alice.create_contact("charlie@example.org", "Charlie")
|
||||
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
event = bob.wait_for_incoming_msg_event()
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
while True:
|
||||
event = bob.wait_for_event()
|
||||
if event.kind == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
|
||||
webxdc_info = message.get_webxdc_info()
|
||||
assert webxdc_info == {
|
||||
@@ -44,7 +51,8 @@ def test_webxdc(acfactory) -> None:
|
||||
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
alice_contact_bob = alice.create_contact(bob, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
|
||||
@@ -12,8 +12,11 @@ setenv =
|
||||
RUST_MIN_STACK=8388608
|
||||
passenv =
|
||||
CHATMAIL_DOMAIN
|
||||
dependency_groups =
|
||||
dev
|
||||
deps =
|
||||
pytest
|
||||
pytest-timeout
|
||||
pytest-xdist
|
||||
imap-tools
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
@@ -21,7 +24,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.1"
|
||||
version = "1.157.2"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "index.d.ts",
|
||||
"version": "1.159.1"
|
||||
"version": "1.157.2"
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ async fn main() {
|
||||
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||
// until the user presses enter."
|
||||
if let Err(error) = &r {
|
||||
log::error!("Error: {error:#}.")
|
||||
log::error!("Fatal error: {error:#}.")
|
||||
}
|
||||
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ ignore = [
|
||||
# Please keep this list alphabetically sorted.
|
||||
skip = [
|
||||
{ name = "async-channel", version = "1.9.0" },
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "base64", version = "0.21.7" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "core-foundation", version = "0.9.4" },
|
||||
{ name = "event-listener", version = "2.5.3" },
|
||||
@@ -31,11 +33,11 @@ skip = [
|
||||
{ name = "getrandom", version = "0.2.12" },
|
||||
{ name = "heck", version = "0.4.1" },
|
||||
{ name = "http", version = "0.2.12" },
|
||||
{ name = "linux-raw-sys", version = "0.4.14" },
|
||||
{ name = "loom", version = "0.5.6" },
|
||||
{ name = "netlink-packet-route", version = "0.17.1" },
|
||||
{ name = "nix", version = "0.26.4" },
|
||||
{ name = "nix", version = "0.27.1" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
{ name = "rand_chacha", version = "0.3.1" },
|
||||
{ name = "rand_core", version = "0.6.4" },
|
||||
{ name = "rand", version = "0.8.5" },
|
||||
@@ -43,13 +45,13 @@ skip = [
|
||||
{ name = "regex-automata", version = "0.1.10" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rtnetlink", version = "0.13.1" },
|
||||
{ name = "rustix", version = "0.38.44" },
|
||||
{ name = "security-framework", version = "2.11.1" },
|
||||
{ name = "strum_macros", version = "0.26.2" },
|
||||
{ name = "strum", version = "0.26.2" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
{ name = "thiserror-impl", version = "1.0.69" },
|
||||
{ name = "thiserror", version = "1.0.69" },
|
||||
{ name = "unicode-width", version = "0.1.11" },
|
||||
{ name = "wasi", version = "0.11.0+wasi-snapshot-preview1" },
|
||||
{ name = "windows" },
|
||||
{ name = "windows_aarch64_gnullvm" },
|
||||
|
||||
@@ -584,9 +584,6 @@
|
||||
cargo-nextest
|
||||
perl # needed to build vendored OpenSSL
|
||||
git-cliff
|
||||
(python3.withPackages (pypkgs: with pypkgs; [
|
||||
tox
|
||||
]))
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_chat = ac1.qr_setup_contact(botproc.qr)
|
||||
ac1._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
bot_chat = bot_contact.create_chat()
|
||||
bot_chat.send_text("hello")
|
||||
|
||||
lp.sec("waiting for the reply message from the bot to arrive")
|
||||
@@ -48,9 +48,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_chat = ac1.qr_setup_contact(botproc.qr)
|
||||
ac1._evtracker.wait_securejoin_joiner_progress(1000)
|
||||
bot_contact = bot_chat.get_contacts()[0]
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
ch.add_contact(bot_contact)
|
||||
ch.send_text("hello")
|
||||
@@ -62,7 +60,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
)
|
||||
|
||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
||||
contact3 = ac1.create_contact(ac2)
|
||||
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
||||
ch.add_contact(contact3)
|
||||
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
version = "1.159.1"
|
||||
version = "1.157.2"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.8"
|
||||
|
||||
@@ -55,8 +55,6 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
ac = Account(args.db)
|
||||
qr = ac.get_setup_contact_qr()
|
||||
print(qr)
|
||||
|
||||
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
|
||||
|
||||
|
||||
@@ -280,12 +280,6 @@ class Account:
|
||||
:param name: (optional) display name for this contact
|
||||
:returns: :class:`deltachat.contact.Contact` instance.
|
||||
"""
|
||||
if isinstance(obj, Account):
|
||||
if not obj.is_configured():
|
||||
raise ValueError("Can only add configured accounts as contacts")
|
||||
assert name is None
|
||||
vcard = obj.get_self_contact().make_vcard()
|
||||
return self.import_vcard(vcard)[0]
|
||||
(name, addr) = self.get_contact_addr_and_name(obj, name)
|
||||
name_c = as_dc_charpointer(name)
|
||||
addr_c = as_dc_charpointer(addr)
|
||||
@@ -374,11 +368,6 @@ class Account:
|
||||
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def import_vcard(self, vcard):
|
||||
"""Import a vCard and return an array of contacts."""
|
||||
dc_array = ffi.gc(lib.dc_import_vcard(self._dc_context, as_dc_charpointer(vcard)), lib.dc_array_unref)
|
||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
||||
|
||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
||||
"""yield all fresh messages from all chats."""
|
||||
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
|
||||
|
||||
@@ -90,14 +90,6 @@ class Contact:
|
||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||
return from_optional_dc_charpointer(dc_res)
|
||||
|
||||
def make_vcard(self) -> str:
|
||||
"""Make a contact vCard.
|
||||
|
||||
:returns: vCard
|
||||
"""
|
||||
dc_context = self.account._dc_context
|
||||
return from_dc_charpointer(lib.dc_make_vcard(dc_context, self.id))
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Get contact status.
|
||||
|
||||
@@ -649,9 +649,6 @@ class BotProcess:
|
||||
|
||||
def __init__(self, popen, addr) -> None:
|
||||
self.popen = popen
|
||||
|
||||
# The first thing the bot prints to stdout is an invite link.
|
||||
self.qr = self.popen.stdout.readline()
|
||||
self.addr = addr
|
||||
|
||||
# we read stdout as quickly as we can in a thread and make
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
import time
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
@@ -16,6 +17,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 +47,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()
|
||||
@@ -187,6 +196,118 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [False, True])
|
||||
def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
|
||||
This way, we can already offer them some email addresses they can write to.
|
||||
|
||||
Also, the newest existing emails from each folder are fetched during onboarding.
|
||||
|
||||
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
|
||||
|
||||
def assert_folders_configured(ac):
|
||||
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
|
||||
So, check that they are still configured:"""
|
||||
assert ac.get_config("configured_sentbox_folder") == "Sent"
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
# We need to reconfigure to find the new "Sent" folder.
|
||||
# `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
|
||||
# would also find the "Sent" folder, but it would be too late:
|
||||
# The sentbox thread, started by `start_io()`, would have seen that there is no
|
||||
# ConfiguredSentboxFolder and do nothing.
|
||||
acfactory._acsetup.start_configure(ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message text")
|
||||
|
||||
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
|
||||
if mvbox_move:
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
else:
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("create a cloned ac1 and fetch contact history during configure")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
ac1_clone.start_io()
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that ac2 contact was fetched during configure")
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that messages changed events arrive for the correct message")
|
||||
msg = ac1_clone._evtracker.wait_next_messages_changed()
|
||||
assert msg.text == "message text"
|
||||
assert_folders_configured(ac1)
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
|
||||
def test_fetch_existing_msgs_group_and_single(acfactory, lp):
|
||||
"""There was a bug concerning fetch-existing-msgs:
|
||||
|
||||
A sent a message to you, adding you to a group. This created a contact request.
|
||||
You wrote a message to A, creating a chat.
|
||||
...but the group stayed blocked.
|
||||
So, after fetch-existing-msgs you have one contact request and one chat with the same person.
|
||||
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("receive a message")
|
||||
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
|
||||
|
||||
# wait until the bcc_self message arrives
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
lp.sec("Clone online account and let it fetch the existing messages")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
|
||||
ac1_clone.start_io()
|
||||
ac1_clone._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
chats = ac1_clone.get_chats()
|
||||
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
|
||||
group_chat = [c for c in chats if c.get_name() == "group name"][0]
|
||||
assert group_chat.is_group()
|
||||
(private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
|
||||
assert not private_chat.is_group()
|
||||
|
||||
group_messages = group_chat.get_messages()
|
||||
assert len(group_messages) == 1
|
||||
assert group_messages[0].text == "incoming, unencrypted group message"
|
||||
private_messages = private_chat.get_messages()
|
||||
# We can't decrypt the message in this chat, so the chat is empty:
|
||||
assert len(private_messages) == 0
|
||||
|
||||
|
||||
def test_undecipherable_group(acfactory, lp):
|
||||
"""Test how group messages that cannot be decrypted are
|
||||
handled.
|
||||
@@ -323,6 +444,63 @@ def test_ephemeral_timer(acfactory, lp):
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
def test_multidevice_sync_seen(acfactory, lp):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert ac1_chat.count_fresh_messages() == 1
|
||||
assert ac1_clone_chat.count_fresh_messages() == 1
|
||||
assert ac1_message.is_in_fresh
|
||||
assert ac1_clone_message.is_in_fresh
|
||||
|
||||
lp.sec("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
|
||||
lp.sec("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
|
||||
lp.sec("Send an ephemeral message from ac2 to ac1")
|
||||
ac2_chat.set_ephemeral_timer(60)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1_clone._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2_chat.send_text("Foobar")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
assert "Expires: " in ac1_message.get_message_info()
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
|
||||
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
|
||||
@@ -54,6 +54,57 @@ def test_configure_unref(tmp_path):
|
||||
lib.dc_context_unref(dc_context)
|
||||
|
||||
|
||||
def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
# test if sent messages are copied to it via BCC.
|
||||
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
self_addr = ac1.get_config("addr")
|
||||
other_addr = ac2.get_config("addr")
|
||||
|
||||
lp.sec("send out message without bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "0")
|
||||
msg_out = chat.send_text("message1")
|
||||
assert not msg_out.is_forwarded()
|
||||
|
||||
# wait for send out (no BCC)
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
# make sure we are not sending message to ourselves
|
||||
assert self_addr not in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
|
||||
lp.sec("ac1: setting bcc_self=1")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
msg_out = chat.send_text("message2")
|
||||
|
||||
# wait for send out (BCC)
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_SMTP_MESSAGE_SENT")
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
# Second client receives only second message, but not the first.
|
||||
ev_msg = ac1_clone._evtracker.wait_next_messages_changed()
|
||||
assert ev_msg.text == msg_out.text
|
||||
|
||||
# now make sure we are sending message to ourselves too
|
||||
assert self_addr in ev.data2
|
||||
assert other_addr in ev.data2
|
||||
|
||||
# BCC-self messages are marked as seen by the sender device.
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||
|
||||
# Check that the message is marked as seen on IMAP.
|
||||
ac1.direct_imap.select_folder("Inbox")
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -436,6 +487,26 @@ def test_forward_messages(acfactory, lp):
|
||||
assert not chat3.get_messages()
|
||||
|
||||
|
||||
def test_forward_encrypted_to_unencrypted(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
chat = acfactory.get_protected_chat(ac1, ac2)
|
||||
|
||||
lp.sec("ac1: send encrypted message to ac2")
|
||||
txt = "This should be encrypted"
|
||||
chat.send_text(txt)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == txt
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: forward message to ac3 unencrypted")
|
||||
unencrypted_chat = ac2.create_chat(ac3)
|
||||
msg_id = msg.id
|
||||
msg2 = unencrypted_chat.send_msg(msg)
|
||||
assert msg2 == msg
|
||||
assert msg.id != msg_id
|
||||
assert not msg.is_encrypted()
|
||||
|
||||
|
||||
def test_forward_own_message(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
@@ -798,6 +869,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 +884,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)
|
||||
|
||||
@@ -819,7 +931,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
" wrapped using format=flowed and unwrapped on the receiver"
|
||||
)
|
||||
msg_out = chat.send_text(text1)
|
||||
assert msg_out.is_encrypted()
|
||||
assert not msg_out.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive multi-line non-unicode message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
@@ -828,7 +940,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
|
||||
lp.sec("sending multi-line unicode text message from ac1 to ac2")
|
||||
text2 = "äalis\nthis is ßßÄ"
|
||||
msg_out = chat.send_text(text2)
|
||||
assert msg_out.is_encrypted()
|
||||
assert not msg_out.is_encrypted()
|
||||
|
||||
lp.sec("wait for ac2 to receive multi-line unicode message")
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
@@ -1234,7 +1346,7 @@ def test_qr_email_capitalization(acfactory, lp):
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me added by {}.".format(ac3.get_config("addr"))
|
||||
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a verified group via a QR code")
|
||||
@@ -1333,7 +1445,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
|
||||
lp.sec("ac1: add address2")
|
||||
# note that if the above create_chat() would not
|
||||
# happen we would not receive a proper member_added event
|
||||
contact2 = chat.add_contact(ac3)
|
||||
contact2 = chat.add_contact(ac3_addr)
|
||||
ev = in_list.get()
|
||||
assert ev.action == "chat-modified"
|
||||
ev = in_list.get()
|
||||
@@ -1773,7 +1885,9 @@ def test_name_changes(acfactory):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("displayname", "Account 1")
|
||||
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
# Similar to acfactory.get_accepted_chat, but without setting the contact name.
|
||||
ac2.create_contact(ac1.get_config("addr")).create_chat()
|
||||
chat12 = ac1.create_contact(ac2.get_config("addr")).create_chat()
|
||||
contact = None
|
||||
|
||||
def update_name():
|
||||
@@ -1938,6 +2052,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-12
|
||||
2025-03-15
|
||||
@@ -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
|
||||
|
||||
@@ -215,7 +215,7 @@ if __name__ == "__main__":
|
||||
" Config, ConfigDefault, Oauth2Authorizer, Provider, ProviderOptions, Server, Status,\n"
|
||||
"};\n"
|
||||
"use std::collections::HashMap;\n\n"
|
||||
"use std::sync::LazyLock;\n\n"
|
||||
"use once_cell::sync::Lazy;\n\n"
|
||||
)
|
||||
|
||||
process_dir(Path(sys.argv[1]))
|
||||
@@ -224,7 +224,7 @@ if __name__ == "__main__":
|
||||
out_all += out_domains
|
||||
out_all += "];\n\n"
|
||||
|
||||
out_all += "pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> = LazyLock::new(|| HashMap::from([\n"
|
||||
out_all += "pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| HashMap::from([\n"
|
||||
out_all += out_ids
|
||||
out_all += "]));\n\n"
|
||||
|
||||
@@ -233,8 +233,8 @@ if __name__ == "__main__":
|
||||
else:
|
||||
now = datetime.datetime.fromisoformat(sys.argv[2])
|
||||
out_all += (
|
||||
"pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> = "
|
||||
"LazyLock::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
"pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> = "
|
||||
"Lazy::new(|| chrono::NaiveDate::from_ymd_opt("
|
||||
+ str(now.year)
|
||||
+ ", "
|
||||
+ str(now.month)
|
||||
|
||||
90
spec.md
90
spec.md
@@ -1,10 +1,10 @@
|
||||
# Chatmail Specification
|
||||
# chat-mail specification
|
||||
|
||||
Version: 0.36.0
|
||||
Version: 0.35.0
|
||||
Status: In-progress
|
||||
Format: [Semantic Line Breaks](https://sembr.org/)
|
||||
|
||||
This document roughly describes how chatmail
|
||||
This document roughly describes how chat-mail
|
||||
apps use the standard e-mail system
|
||||
to implement typical messenger functions.
|
||||
|
||||
@@ -18,8 +18,6 @@ to implement typical messenger functions.
|
||||
- [Add and remove members](#add-and-remove-members)
|
||||
- [Change group name](#change-group-name)
|
||||
- [Set group image](#set-group-image)
|
||||
- [Request editing](#request-editing)
|
||||
- [Request deletion](#request-deletion)
|
||||
- [Set profile image](#set-profile-image)
|
||||
- [Locations](#locations)
|
||||
- [User locations](#user-locations)
|
||||
@@ -306,84 +304,6 @@ To save data, it is RECOMMENDED
|
||||
to add a `Chat-Group-Avatar` only on image changes.
|
||||
|
||||
|
||||
# Request editing
|
||||
|
||||
To request recipients to edit the text of an already sent message,
|
||||
the messenger MUST set the header `Chat-Edit`
|
||||
with value set to the message-id of the message to edit
|
||||
and the body to the new message text.
|
||||
|
||||
The body MAY be prefixed by a quote
|
||||
and the emoji "✏️" directly before the new text.
|
||||
Both MUST be skipped by the recipient.
|
||||
|
||||
Receiving messengers MUST look up the message-id from `Chat-Edit`,
|
||||
replace the text and MAY indicate the edit in the UI.
|
||||
|
||||
The new message text MUST NOT be empty.
|
||||
It is not possible to edit images or other attachments, including HTML messages.
|
||||
However, they can be deleted for everyone.
|
||||
|
||||
Example:
|
||||
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Message-ID: 00001@domain
|
||||
Content-Type: text/plain
|
||||
|
||||
Hello wordl!
|
||||
|
||||
The typo from the message above can be fixed by the following message:
|
||||
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Chat-Edit: 00001@domain
|
||||
In-Reply-To: 00001@domain
|
||||
Message-ID: 00002@domain
|
||||
Content-Type: text/plain
|
||||
|
||||
On 2025-03-27, sender@domain wrote:
|
||||
> Hello wordl!
|
||||
|
||||
✏️Hello world!
|
||||
|
||||
|
||||
# Request deletion
|
||||
|
||||
To request recipient to delete a message,
|
||||
the messenger MUST set the header `Chat-Delete`
|
||||
with the value set to the message-id of the message to delete.
|
||||
|
||||
Receiving messengers MUST look up the message-id, delete the corresponding message
|
||||
and MAY indicating the deletion in the UI.
|
||||
|
||||
The sender MUST set the body to any, non-empty text.
|
||||
The receiver MUST ignore the body.
|
||||
|
||||
Example:
|
||||
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Message-ID: 00003@domain
|
||||
Content-Type: text/plain
|
||||
|
||||
reminder for my pin: 1234
|
||||
|
||||
The message above can be requested for deletion by the following message:
|
||||
|
||||
From: sender@domain
|
||||
To: rcpt@domain
|
||||
Chat-Version: 1.0
|
||||
Chat-Delete: 00003@domain
|
||||
Message-ID: 00004@domain
|
||||
Content-Type: text/plain
|
||||
|
||||
foo
|
||||
|
||||
|
||||
# Set profile image
|
||||
|
||||
A user MAY have a profile-image that MAY be distributed to their contacts.
|
||||
@@ -455,7 +375,7 @@ eg. forwarded from a normal MUA.
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<kml xmlns="http://www.opengis.net/kml/2.2">
|
||||
<Document addr="foo@domain">
|
||||
<Document addr="ndh@deltachat.de">
|
||||
<Placemark>
|
||||
<Timestamp><when>2020-01-11T20:40:19Z</when></Timestamp>
|
||||
<Point><coordinates accuracy="1.2">1.234,5.678</coordinates></Point>
|
||||
@@ -622,4 +542,4 @@ We define the effective date of a message
|
||||
as the sending time of the message as indicated by its Date header,
|
||||
or the time of first receipt if that date is in the future or unavailable.
|
||||
|
||||
Copyright © Chatmail contributors.
|
||||
Copyright © 2017-2021 Delta Chat contributors.
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -73,7 +73,9 @@ impl Accounts {
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists(), "{:?} does not exist", config_file);
|
||||
|
||||
let config = Config::from_file(config_file, writable).await?;
|
||||
let config = Config::from_file(config_file, writable)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let push_subscriber = PushSubscriber::new();
|
||||
@@ -458,9 +460,7 @@ impl Config {
|
||||
rx.await?;
|
||||
Ok(())
|
||||
});
|
||||
if locked_rx.await.is_err() {
|
||||
bail!("Delta Chat is already running. To use Delta Chat, you must first close the existing Delta Chat process, or restart your device. (accounts.lock file is already locked)");
|
||||
};
|
||||
locked_rx.await?;
|
||||
Ok(Some(lock_task))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
@@ -107,8 +107,7 @@ fn remove_comments(header: &str) -> Cow<'_, str> {
|
||||
// In Pomsky, this is:
|
||||
// "(" Codepoint* lazy ")"
|
||||
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
|
||||
static RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
|
||||
RE.replace_all(header, " ")
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
187
src/chat.rs
187
src/chat.rs
@@ -130,7 +130,8 @@ pub(crate) enum CantSendReason {
|
||||
/// Not a member of the chat.
|
||||
NotAMember,
|
||||
|
||||
/// Temporary state for 1:1 chats while SecureJoin is in progress.
|
||||
/// Temporary state for 1:1 chats while SecureJoin is in progress, after a timeout sending
|
||||
/// messages (incl. unencrypted if we don't yet know the contact's pubkey) is allowed.
|
||||
SecurejoinWait,
|
||||
}
|
||||
|
||||
@@ -581,18 +582,7 @@ impl ChatId {
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled,
|
||||
};
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&text,
|
||||
cmd,
|
||||
timestamp_sort,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1090,8 +1080,7 @@ impl ChatId {
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
/// Returns timestamp of the latest message in the chat,
|
||||
/// including hidden messages or a draft if there is one.
|
||||
/// Returns timestamp of the latest message in the chat.
|
||||
pub(crate) async fn get_timestamp(self, context: &Context) -> Result<Option<i64>> {
|
||||
let timestamp = context
|
||||
.sql
|
||||
@@ -1376,10 +1365,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 +1715,13 @@ impl Chat {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
let reason = SecurejoinWait;
|
||||
if !skip_fn(&reason) {
|
||||
let (can_write, _) = self
|
||||
if !skip_fn(&reason)
|
||||
&& self
|
||||
.check_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await?;
|
||||
if !can_write {
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
.await?
|
||||
> 0
|
||||
{
|
||||
return Ok(Some(reason));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -1713,32 +1733,28 @@ impl Chat {
|
||||
Ok(self.why_cant_send(context).await?.is_none())
|
||||
}
|
||||
|
||||
/// Returns if the chat can be sent to
|
||||
/// and the remaining timeout for the 1:1 chat in-progress SecureJoin.
|
||||
/// Returns the remaining timeout for the 1:1 chat in-progress SecureJoin.
|
||||
///
|
||||
/// 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`].
|
||||
/// If the timeout has expired, notifies the user that sending messages is possible. See also
|
||||
/// [`CantSendReason::SecurejoinWait`].
|
||||
pub(crate) async fn check_securejoin_wait(
|
||||
&self,
|
||||
context: &Context,
|
||||
timeout: u64,
|
||||
) -> Result<(bool, u64)> {
|
||||
) -> Result<u64> {
|
||||
if self.typ != Chattype::Single || self.protected != ProtectionStatus::Unprotected {
|
||||
return Ok((true, 0));
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// chat is single and unprotected:
|
||||
// get last info message of type SecurejoinWait or SecurejoinWaitTimeout
|
||||
let (mut param_wait, mut param_timeout) = (Params::new(), Params::new());
|
||||
param_wait.set_cmd(SystemMessage::SecurejoinWait);
|
||||
param_timeout.set_cmd(SystemMessage::SecurejoinWaitTimeout);
|
||||
let (param_wait, param_timeout) = (param_wait.to_string(), param_timeout.to_string());
|
||||
let (mut param0, mut param1) = (Params::new(), Params::new());
|
||||
param0.set_cmd(SystemMessage::SecurejoinWait);
|
||||
param1.set_cmd(SystemMessage::SecurejoinWaitTimeout);
|
||||
let (param0, param1) = (param0.to_string(), param1.to_string());
|
||||
let Some((param, ts_sort, ts_start)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT param, timestamp, timestamp_sent FROM msgs WHERE id=\
|
||||
(SELECT MAX(id) FROM msgs WHERE chat_id=? AND param IN (?, ?))",
|
||||
(self.id, ¶m_wait, ¶m_timeout),
|
||||
(self.id, ¶m0, ¶m1),
|
||||
|row| {
|
||||
let param: String = row.get(0)?;
|
||||
let ts_sort: i64 = row.get(1)?;
|
||||
@@ -1748,13 +1764,11 @@ impl Chat {
|
||||
)
|
||||
.await?
|
||||
else {
|
||||
return Ok((true, 0));
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
if param == param_timeout {
|
||||
return Ok((false, 0));
|
||||
if param == param1 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let now = time();
|
||||
// Don't await SecureJoin if the clock was set back.
|
||||
if ts_start <= now {
|
||||
@@ -1762,14 +1776,13 @@ impl Chat {
|
||||
.saturating_add(timeout.try_into()?)
|
||||
.saturating_sub(now);
|
||||
if timeout > 0 {
|
||||
return Ok((false, timeout as u64));
|
||||
return Ok(timeout as u64);
|
||||
}
|
||||
}
|
||||
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self.id,
|
||||
&stock_str::securejoin_takes_longer(context).await,
|
||||
&stock_str::securejoin_wait_timeout(context).await,
|
||||
SystemMessage::SecurejoinWaitTimeout,
|
||||
// Use the sort timestamp of the "please wait" message, this way the added message is
|
||||
// never sorted below the protection message if the SecureJoin finishes in parallel.
|
||||
@@ -1777,11 +1790,10 @@ impl Chat {
|
||||
Some(now),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((false, 0))
|
||||
context.emit_event(EventType::ChatModified(self.id));
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
/// Checks if the user is part of a chat
|
||||
@@ -1886,6 +1898,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
|
||||
@@ -1976,7 +1989,13 @@ impl Chat {
|
||||
if let Some(member_list_timestamp) = self.param.get_i64(Param::MemberListTimestamp) {
|
||||
Ok(member_list_timestamp)
|
||||
} else {
|
||||
Ok(self.id.created_timestamp(context).await?)
|
||||
let creation_timestamp: i64 = context
|
||||
.sql
|
||||
.query_get_value("SELECT created_timestamp FROM chats WHERE id=?", (self.id,))
|
||||
.await
|
||||
.context("SQL error querying created_timestamp")?
|
||||
.context("Chat not found")?;
|
||||
Ok(creation_timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2008,8 +2027,6 @@ impl Chat {
|
||||
let mut to_id = 0;
|
||||
let mut location_id = 0;
|
||||
|
||||
let new_rfc724_mid = create_outgoing_rfc724_mid();
|
||||
|
||||
if self.typ == Chattype::Single {
|
||||
if let Some(id) = context
|
||||
.sql
|
||||
@@ -2031,9 +2048,7 @@ impl Chat {
|
||||
&& self.param.get_int(Param::Unpromoted).unwrap_or_default() == 1
|
||||
{
|
||||
msg.param.set_int(Param::AttachGroupImage, 1);
|
||||
self.param
|
||||
.remove(Param::Unpromoted)
|
||||
.set_i64(Param::GroupNameTimestamp, timestamp);
|
||||
self.param.remove(Param::Unpromoted);
|
||||
self.update_param(context).await?;
|
||||
// TODO: Remove this compat code needed because Core <= v1.143:
|
||||
// - doesn't accept synchronization of QR code tokens for unpromoted groups, so we also
|
||||
@@ -2106,7 +2121,7 @@ impl Chat {
|
||||
if references_vec.is_empty() {
|
||||
// As a fallback, use our Message-ID,
|
||||
// same as in the case of top-level message.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
new_references = msg.rfc724_mid.clone();
|
||||
} else {
|
||||
new_references = references_vec.join(" ");
|
||||
}
|
||||
@@ -2116,8 +2131,9 @@ impl Chat {
|
||||
// This allows us to identify replies to our message even if
|
||||
// email server such as Outlook changes `Message-ID:` header.
|
||||
// MUAs usually keep the first Message-ID in `References:` header unchanged.
|
||||
new_references = new_rfc724_mid.clone();
|
||||
new_references = msg.rfc724_mid.clone();
|
||||
}
|
||||
info!(context, "new references: {new_references:?}");
|
||||
|
||||
// add independent location to database
|
||||
if msg.param.exists(Param::SetLatitude) {
|
||||
@@ -2184,7 +2200,6 @@ impl Chat {
|
||||
|
||||
msg.chat_id = self.id;
|
||||
msg.from_id = ContactId::SELF;
|
||||
msg.rfc724_mid = new_rfc724_mid;
|
||||
msg.timestamp_sort = timestamp;
|
||||
|
||||
// add message to the database
|
||||
@@ -2428,6 +2443,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,
|
||||
|
||||
@@ -2584,7 +2602,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 {
|
||||
@@ -2989,12 +3007,6 @@ async fn prepare_send_msg(
|
||||
///
|
||||
/// The caller has to interrupt SMTP loop or otherwise process new rows.
|
||||
pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -> Result<Vec<i64>> {
|
||||
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
|
||||
msg.chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let needs_encryption = msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default();
|
||||
let mimefactory = MimeFactory::from_msg(context, msg.clone()).await?;
|
||||
let attach_selfavatar = mimefactory.attach_selfavatar;
|
||||
@@ -3015,10 +3027,10 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
// disabled by default is fine.
|
||||
//
|
||||
// `from` must be the last addr, see `receive_imf_inner()` why.
|
||||
recipients.retain(|x| x.to_lowercase() != lowercase_from);
|
||||
if (context.get_config_bool(Config::BccSelf).await?
|
||||
|| msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage)
|
||||
&& (context.get_config_delete_server_after().await? != Some(0) || !recipients.is_empty())
|
||||
if context.get_config_bool(Config::BccSelf).await?
|
||||
&& !recipients
|
||||
.iter()
|
||||
.any(|x| x.to_lowercase() == lowercase_from)
|
||||
{
|
||||
recipients.push(from);
|
||||
}
|
||||
@@ -3040,6 +3052,11 @@ pub(crate) async fn create_send_msg_jobs(context: &Context, msg: &mut Message) -
|
||||
msg.state = MessageState::OutDelivered;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if msg.param.get_cmd() == SystemMessage::GroupNameChanged {
|
||||
msg.chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, msg.timestamp_sort)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let rendered_msg = match mimefactory.render(context).await {
|
||||
Ok(res) => Ok(res),
|
||||
@@ -3066,6 +3083,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:#}.");
|
||||
@@ -3880,9 +3901,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
|
||||
let sync_qr_code_tokens;
|
||||
if from_handshake && chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 1 {
|
||||
chat.param
|
||||
.remove(Param::Unpromoted)
|
||||
.set_i64(Param::GroupNameTimestamp, smeared_time(context));
|
||||
chat.param.remove(Param::Unpromoted);
|
||||
chat.update_param(context).await?;
|
||||
sync_qr_code_tokens = true;
|
||||
} else {
|
||||
@@ -3925,8 +3944,6 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact_addr);
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
msg.param
|
||||
.set_int(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
|
||||
send_msg(context, chat_id, &mut msg).await?;
|
||||
|
||||
sync = Nosync;
|
||||
@@ -4124,8 +4141,6 @@ pub async fn remove_contact_from_chat(
|
||||
}
|
||||
msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr().to_lowercase());
|
||||
msg.param
|
||||
.set(Param::ContactAddedRemoved, contact.id.to_u32() as i32);
|
||||
let res = send_msg(context, chat_id, &mut msg).await;
|
||||
if contact_id == ContactId::SELF {
|
||||
res?;
|
||||
@@ -4343,7 +4358,6 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.param.remove(Param::IsEdited);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
// do not leak data as group names; a default subject is generated by mimefactory
|
||||
@@ -4353,6 +4367,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
let new_msg_id = chat
|
||||
.prepare_msg_raw(context, &mut msg, None, curr_timestamp)
|
||||
.await?;
|
||||
|
||||
msg.rfc724_mid = create_outgoing_rfc724_mid();
|
||||
curr_timestamp += 1;
|
||||
if !create_send_msg_jobs(context, &mut msg).await?.is_empty() {
|
||||
context.scheduler.interrupt_smtp().await;
|
||||
@@ -4404,7 +4420,7 @@ pub(crate) async fn save_copy_in_self_talk(
|
||||
bail!("message already saved.");
|
||||
}
|
||||
|
||||
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, \
|
||||
let copy_fields = "from_id, to_id, timestamp_sent, timestamp_rcvd, type, txt, txt_raw, \
|
||||
mime_modified, mime_headers, mime_compressed, mime_in_reply_to, subject, msgrmsg";
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -4598,7 +4614,17 @@ pub async fn add_device_msg_with_importance(
|
||||
// makes sure, the added message is the last one,
|
||||
// even if the date is wrong (useful esp. when warning about bad dates)
|
||||
let mut timestamp_sort = timestamp_sent;
|
||||
if let Some(last_msg_time) = chat_id.get_timestamp(context).await? {
|
||||
if let Some(last_msg_time) = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT MAX(timestamp)
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
HAVING COUNT(*) > 0",
|
||||
(chat_id,),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if timestamp_sort <= last_msg_time {
|
||||
timestamp_sort = last_msg_time + 1;
|
||||
}
|
||||
@@ -4725,17 +4751,13 @@ pub(crate) async fn add_info_msg_with_cmd(
|
||||
timestamp_sent_rcvd: Option<i64>,
|
||||
parent: Option<&Message>,
|
||||
from_id: Option<ContactId>,
|
||||
added_removed_id: Option<ContactId>,
|
||||
) -> Result<MsgId> {
|
||||
let rfc724_mid = create_outgoing_rfc724_mid();
|
||||
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
|
||||
|
||||
let mut param = Params::new();
|
||||
if cmd != SystemMessage::Unknown {
|
||||
param.set_cmd(cmd);
|
||||
}
|
||||
if let Some(contact_id) = added_removed_id {
|
||||
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
|
||||
param.set_cmd(cmd)
|
||||
}
|
||||
|
||||
let row_id =
|
||||
@@ -4783,7 +4805,6 @@ pub(crate) async fn add_info_msg(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use super::*;
|
||||
use crate::chatlist::get_archived_cnt;
|
||||
use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::imex::{has_backup, imex, ImexMode};
|
||||
use crate::message::{delete_msgs, MessengerMessage};
|
||||
@@ -299,11 +298,9 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
// Create contact for Bob on the Alice side with name "robert".
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
alice_bob_contact_id.set_name(&alice, "robert").await?;
|
||||
let alice_bob_contact_id = Contact::create(&alice, "robert", "bob@example.net").await?;
|
||||
|
||||
// Set Bob authname to "Bob" and send it to Alice.
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
@@ -323,35 +320,42 @@ async fn test_member_add_remove() -> Result<()> {
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_fiona_contact_id).await?;
|
||||
alice
|
||||
let sent = alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
assert!(sent.payload.contains("Hi! I created a group."));
|
||||
|
||||
// Alice adds Bob to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I added member Bob (bob@example.net)."));
|
||||
// Locally set name "robert" should not leak.
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
"You added member robert."
|
||||
"You added member robert (bob@example.net)."
|
||||
);
|
||||
|
||||
// Alice removes Bob from the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent
|
||||
.payload
|
||||
.contains("I removed member Bob (bob@example.net)."));
|
||||
assert!(!sent.payload.contains("robert"));
|
||||
assert_eq!(
|
||||
sent.load_from_db().await.get_text(),
|
||||
"You removed member robert."
|
||||
"You removed member robert (bob@example.net)."
|
||||
);
|
||||
|
||||
// Alice leaves the chat.
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
assert!(sent.payload.contains("I left the group."));
|
||||
assert_eq!(sent.load_from_db().await.get_text(), "You left the group.");
|
||||
|
||||
Ok(())
|
||||
@@ -364,12 +368,10 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", "fiona@example.net").await?;
|
||||
let alice_claire_contact_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
|
||||
// Create and promote a group.
|
||||
let alice_chat_id =
|
||||
@@ -384,8 +386,8 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Alice adds Charlie to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_charlie_contact_id).await?;
|
||||
// Alice adds Claire to the chat.
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_claire_contact_id).await?;
|
||||
let alice_sent_add_msg = alice.pop_sent_msg().await;
|
||||
|
||||
// Bob leaves the chat.
|
||||
@@ -414,7 +416,7 @@ async fn test_parallel_member_remove() -> Result<()> {
|
||||
// Test that remove message is rewritten.
|
||||
assert_eq!(
|
||||
bob_received_remove_msg.get_text(),
|
||||
"Member Me removed by alice@example.org."
|
||||
"Member Me (bob@example.net) removed by alice@example.org."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -426,10 +428,11 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(&fiona).await;
|
||||
let alice_bob_contact_id =
|
||||
Contact::create(&alice, "Bob", &bob.get_config(Config::Addr).await?.unwrap()).await?;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(&alice, "Fiona", fiona_addr).await?;
|
||||
let bob_fiona_contact_id = Contact::create(&bob, "Fiona", fiona_addr).await?;
|
||||
let alice_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
@@ -471,9 +474,8 @@ async fn test_msg_with_implicit_member_removed() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let a1 = tcm.alice().await;
|
||||
let a2 = tcm.alice().await;
|
||||
let a1 = TestContext::new_alice().await;
|
||||
let a2 = TestContext::new_alice().await;
|
||||
a1.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
// create group and sync it to the second device
|
||||
@@ -497,9 +499,8 @@ async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
assert_eq!(get_chat_contacts(&a2, a2_chat_id).await?.len(), 1);
|
||||
|
||||
// add a member to the group
|
||||
let bob = tcm.bob().await;
|
||||
let bob_id = a1.add_or_lookup_contact_id(&bob).await;
|
||||
add_contact_to_chat(&a1, a1_chat_id, bob_id).await?;
|
||||
let bob = Contact::create(&a1, "", "bob@example.org").await?;
|
||||
add_contact_to_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
@@ -523,19 +524,11 @@ async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
assert!(a2_msg.is_system_message());
|
||||
assert_eq!(a1_msg.get_info_type(), SystemMessage::GroupNameChanged);
|
||||
assert_eq!(a2_msg.get_info_type(), SystemMessage::GroupNameChanged);
|
||||
assert_eq!(
|
||||
a1_msg.get_info_contact_id(&a1).await?,
|
||||
Some(ContactId::SELF)
|
||||
);
|
||||
assert_eq!(
|
||||
a2_msg.get_info_contact_id(&a2).await?,
|
||||
Some(ContactId::SELF)
|
||||
);
|
||||
assert_eq!(Chat::load_from_db(&a1, a1_chat_id).await?.name, "bar");
|
||||
assert_eq!(Chat::load_from_db(&a2, a2_chat_id).await?.name, "bar");
|
||||
|
||||
// remove member from group
|
||||
remove_contact_from_chat(&a1, a1_chat_id, bob_id).await?;
|
||||
remove_contact_from_chat(&a1, a1_chat_id, bob).await?;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
@@ -562,18 +555,13 @@ async fn test_modify_chat_multi_device() -> Result<()> {
|
||||
async fn test_modify_chat_disordered() -> Result<()> {
|
||||
let _n = TimeShiftFalsePositiveNote;
|
||||
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
// Alice creates a group with Bob, Charlie and Fiona and then removes Charlie and Fiona
|
||||
// Alice creates a group with Bob, Claire and Daisy and then removes Claire and Daisy
|
||||
// (time shift is needed as otherwise smeared time from Alice looks to Bob like messages from the future which are all set to "now" then)
|
||||
let alice = tcm.alice().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
|
||||
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
@@ -581,21 +569,21 @@ async fn test_modify_chat_disordered() -> Result<()> {
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let add1 = alice.pop_sent_msg().await;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, charlie_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let add2 = alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_millis(1100));
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let add3 = alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_millis(1100));
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove1 = alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_millis(1100));
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let remove2 = alice.pop_sent_msg().await;
|
||||
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
@@ -646,27 +634,24 @@ async fn test_modify_chat_lost() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&fiona).await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@foo.de").await?;
|
||||
let daisy_id = Contact::create(&alice, "", "daisy@bar.de").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?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, fiona_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_millis(1100));
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, charlie_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
let remove1 = alice.pop_sent_msg().await;
|
||||
SystemTime::shift(Duration::from_millis(1100));
|
||||
|
||||
remove_contact_from_chat(&alice, alice_chat_id, fiona_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, daisy_id).await?;
|
||||
let remove2 = alice.pop_sent_msg().await;
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
@@ -689,13 +674,12 @@ async fn test_modify_chat_lost() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_leave_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Create group chat with Bob.
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let bob_contact = alice.add_or_lookup_contact(&bob).await.id;
|
||||
let bob_contact = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_contact).await?;
|
||||
|
||||
// Alice sends first message to group.
|
||||
@@ -1408,52 +1392,20 @@ async fn test_pinned_after_new_msgs() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_chat_name() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foo")
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
|
||||
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
|
||||
"foo"
|
||||
);
|
||||
|
||||
set_chat_name(alice, chat_id, "bar").await.unwrap();
|
||||
set_chat_name(&t, chat_id, "bar").await.unwrap();
|
||||
assert_eq!(
|
||||
Chat::load_from_db(alice, chat_id).await.unwrap().get_name(),
|
||||
Chat::load_from_db(&t, chat_id).await.unwrap().get_name(),
|
||||
"bar"
|
||||
);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, chat_id, bob_contact_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let sent_msg = alice.send_text(chat_id, "Hi").await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
let bob_chat_id = received_msg.chat_id;
|
||||
|
||||
for new_name in [
|
||||
"Baz",
|
||||
"xyzzy",
|
||||
"Quux",
|
||||
"another name",
|
||||
"something different",
|
||||
] {
|
||||
set_chat_name(alice, chat_id, new_name).await.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let received_msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(received_msg.chat_id, bob_chat_id);
|
||||
assert_eq!(
|
||||
Chat::load_from_db(bob, bob_chat_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.get_name(),
|
||||
new_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -1584,7 +1536,6 @@ async fn test_add_info_msg_with_cmd() -> Result<()> {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -1664,6 +1615,58 @@ async fn test_lookup_self_by_contact_id() {
|
||||
assert_eq!(chat.blocked, Blocked::Not);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_group_with_removed_message_id() -> Result<()> {
|
||||
// Alice creates a group with Bob, sends a message to bob
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_bob_contact = alice.add_or_lookup_contact(&bob).await;
|
||||
let contact_id = alice_bob_contact.id;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
let alice_chat = Chat::load_from_db(&alice, alice_chat_id).await?;
|
||||
|
||||
add_contact_to_chat(&alice, alice_chat_id, contact_id).await?;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?;
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
assert_eq!(msg.match_indices("Message-ID: <").count(), 2);
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
assert_eq!(msg.match_indices("References: <").count(), 1);
|
||||
|
||||
// Bob receives this message, he may detect group by `References:`- or `Chat-Group:`-header
|
||||
receive_imf(&bob, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = bob.get_last_msg().await;
|
||||
|
||||
let bob_chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(bob_chat.grpid, alice_chat.grpid);
|
||||
|
||||
// Bob accepts contact request.
|
||||
bob_chat.id.unblock(&bob).await?;
|
||||
|
||||
// Bob answers - simulate a normal MUA by not setting `Chat-*`-headers;
|
||||
// moreover, Bob's SMTP-server also replaces the `Message-ID:`-header
|
||||
send_text_msg(&bob, bob_chat.id, "ho!".to_string()).await?;
|
||||
let sent_msg = bob.pop_sent_msg().await;
|
||||
let msg = sent_msg.payload();
|
||||
let msg = msg.replace("Message-ID: <", "Message-ID: <X.X");
|
||||
let msg = msg.replace("Chat-", "XXXX-");
|
||||
assert_eq!(msg.match_indices("Chat-").count(), 0);
|
||||
|
||||
// Alice receives this message - she can still detect the group by the `References:`-header
|
||||
receive_imf(&alice, msg.as_bytes(), false).await.unwrap();
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(msg.text, "ho!".to_string());
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_marknoticed_chat() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -2028,22 +2031,20 @@ async fn test_forward() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_info_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
let chat_id1 = create_group_chat(alice, ProtectionStatus::Unprotected, "a").await?;
|
||||
send_text_msg(alice, chat_id1, "msg one".to_string()).await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
add_contact_to_chat(alice, chat_id1, bob_id).await?;
|
||||
let msg1 = alice.get_last_msg_in(chat_id1).await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a").await?;
|
||||
send_text_msg(&t, chat_id1, "msg one".to_string()).await?;
|
||||
let bob_id = Contact::create(&t, "", "bob@example.net").await?;
|
||||
add_contact_to_chat(&t, chat_id1, bob_id).await?;
|
||||
let msg1 = t.get_last_msg_in(chat_id1).await;
|
||||
assert!(msg1.is_info());
|
||||
assert!(msg1.get_text().contains("bob@example.net"));
|
||||
|
||||
let chat_id2 = ChatId::create_for_contact(alice, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(alice, chat_id2).await?.len(), 0);
|
||||
forward_msgs(alice, &[msg1.id], chat_id2).await?;
|
||||
let msg2 = alice.get_last_msg_in(chat_id2).await;
|
||||
let chat_id2 = ChatId::create_for_contact(&t, bob_id).await?;
|
||||
assert_eq!(get_chat_msgs(&t, chat_id2).await?.len(), 0);
|
||||
forward_msgs(&t, &[msg1.id], chat_id2).await?;
|
||||
let msg2 = t.get_last_msg_in(chat_id2).await;
|
||||
assert!(!msg2.is_info()); // forwarded info-messages lose their info-state
|
||||
assert_eq!(msg2.get_info_type(), SystemMessage::Unknown);
|
||||
assert_ne!(msg2.from_id, ContactId::INFO);
|
||||
@@ -2090,10 +2091,8 @@ async fn test_forward_quote() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_group() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
@@ -2101,12 +2100,12 @@ async fn test_forward_group() -> Result<()> {
|
||||
// Alice creates a group with Bob.
|
||||
let alice_group_chat_id =
|
||||
create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let charlie_id = alice.add_or_lookup_contact_id(&charlie).await;
|
||||
let bob_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "Claire", "claire@example.net").await?;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, charlie_id).await?;
|
||||
add_contact_to_chat(&alice, alice_group_chat_id, claire_id).await?;
|
||||
let sent_group_msg = alice
|
||||
.send_text(alice_group_chat_id, "Hi Bob and Charlie")
|
||||
.send_text(alice_group_chat_id, "Hi Bob and Claire")
|
||||
.await;
|
||||
let bob_group_chat_id = bob.recv_msg(&sent_group_msg).await.chat_id;
|
||||
|
||||
@@ -2294,29 +2293,6 @@ async fn test_forward_from_saved_to_saved() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forward_encrypted_to_unencrypted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let txt = "This should be encrypted";
|
||||
let sent = alice.send_text(alice.create_chat(bob).await.id, txt).await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, txt);
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let unencrypted_chat = bob.create_email_chat(charlie).await;
|
||||
forward_msgs(bob, &[msg.id], unencrypted_chat.id).await?;
|
||||
let msg2 = bob.get_last_msg().await;
|
||||
assert_eq!(msg2.text, txt);
|
||||
assert_ne!(msg.id, msg2.id);
|
||||
assert!(!msg2.get_showpadlock());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_from_saved_to_saved_failing() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -2338,13 +2314,11 @@ async fn test_save_from_saved_to_saved_failing() -> Result<()> {
|
||||
async fn test_resend_own_message() -> Result<()> {
|
||||
// Alice creates group with Bob and sends an initial message
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let fiona = TestContext::new_fiona().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
alice.add_or_lookup_contact_id(&bob).await,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
@@ -2353,7 +2327,7 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
alice.add_or_lookup_contact_id(&fiona).await,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
@@ -2397,13 +2371,15 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id).await?.len(), 2);
|
||||
|
||||
// Fiona does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
fiona.recv_msg(&sent2).await;
|
||||
let msg = fiona.recv_msg(&sent3).await;
|
||||
// Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
let claire = TestContext::new().await;
|
||||
claire.configure_addr("claire@example.org").await;
|
||||
claire.recv_msg(&sent2).await;
|
||||
let msg = claire.recv_msg(&sent3).await;
|
||||
assert_eq!(msg.get_text(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&fiona, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&fiona, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&fiona, msg.get_from_id()).await?;
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
assert!(sent1_ts_sent < msg.timestamp_sent);
|
||||
|
||||
@@ -2412,15 +2388,19 @@ async fn test_resend_own_message() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_foreign_message_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent1 = alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let msg = bob.recv_msg(&sent1).await;
|
||||
assert!(resend_msgs(bob, &[msg.id]).await.is_err());
|
||||
assert!(resend_msgs(&bob, &[msg.id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2464,23 +2444,24 @@ async fn test_resend_opportunistically_encryption() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_resend_info_message_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
|
||||
let alice_grp = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(alice, alice_grp, alice.add_or_lookup_contact_id(bob).await).await?;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
alice.send_text(alice_grp, "alice->bob").await;
|
||||
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_grp,
|
||||
alice.add_or_lookup_contact_id(charlie).await,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
assert!(resend_msgs(alice, &[sent2.sender_msg_id]).await.is_err());
|
||||
assert!(resend_msgs(&alice, &[sent2.sender_msg_id]).await.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2516,7 +2497,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 +2515,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,16 +2529,11 @@ 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
|
||||
.get_header(HeaderDef::ChatGroupMemberTimestamps)
|
||||
.is_none());
|
||||
assert!(msg.get_header(HeaderDef::AutocryptGossip).is_none());
|
||||
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");
|
||||
assert!(msg.get_showpadlock());
|
||||
assert!(!msg.get_showpadlock()); // avoid leaking recipients in encryption data
|
||||
let chat = Chat::load_from_db(&bob, msg.chat_id).await?;
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_ne!(chat.id, chat_bob.id);
|
||||
@@ -2890,7 +2863,12 @@ async fn test_blob_renaming() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(&alice, chat_id, alice.add_or_lookup_contact_id(&bob).await).await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
chat_id,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
let file = alice.get_blobdir().join("harmless_file.\u{202e}txt.exe");
|
||||
fs::write(&file, "aaa").await?;
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
@@ -2922,7 +2900,7 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
let a0b_chat_id = alice0.recv_msg(&sent_msg).await.chat_id;
|
||||
alice1.recv_msg(&sent_msg).await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact_id(&bob).await;
|
||||
let a0b_contact_id = alice0.add_or_lookup_contact(&bob).await.id;
|
||||
|
||||
assert_eq!(alice1.get_chat(&bob).await.blocked, Blocked::Request);
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
@@ -2977,13 +2955,12 @@ async fn test_sync_blocked() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
let bob = &tcm.bob().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let ba_chat = bob.create_chat(alice0).await;
|
||||
let sent_msg = bob.send_text(ba_chat.id, "hi").await;
|
||||
@@ -2997,7 +2974,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
a0b_chat_id.accept(alice0).await?;
|
||||
let a0b_contact = Contact::get_by_id(alice0, a0b_contact_id).await?;
|
||||
assert_eq!(a0b_contact.origin, Origin::CreateChat);
|
||||
assert_eq!(alice0.get_chat(bob).await.blocked, Blocked::Not);
|
||||
assert_eq!(alice0.get_chat(&bob).await.blocked, Blocked::Not);
|
||||
|
||||
sync(alice0, alice1).await;
|
||||
let alice1_contacts = Contact::get_all(alice1, 0, None).await?;
|
||||
@@ -3006,7 +2983,7 @@ async fn test_sync_accept_before_first_msg() -> Result<()> {
|
||||
let a1b_contact = Contact::get_by_id(alice1, a1b_contact_id).await?;
|
||||
assert_eq!(a1b_contact.get_addr(), "bob@example.net");
|
||||
assert_eq!(a1b_contact.origin, Origin::CreateChat);
|
||||
let a1b_chat = alice1.get_chat(bob).await;
|
||||
let a1b_chat = alice1.get_chat(&bob).await;
|
||||
assert_eq!(a1b_chat.blocked, Blocked::Not);
|
||||
let chats = Chatlist::try_load(alice1, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
@@ -3142,9 +3119,8 @@ async fn test_sync_adhoc_grp() -> Result<()> {
|
||||
/// - That sync messages don't unarchive the self-chat.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_visibility() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3196,9 +3172,8 @@ async fn test_sync_device_messages_visibility() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_muted() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3232,9 +3207,8 @@ async fn test_sync_muted() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_broadcast() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice0 = &tcm.alice().await;
|
||||
let alice1 = &tcm.alice().await;
|
||||
let alice0 = &TestContext::new_alice().await;
|
||||
let alice1 = &TestContext::new_alice().await;
|
||||
for a in [alice0, alice1] {
|
||||
a.set_config_bool(Config::SyncMsgs, true).await?;
|
||||
}
|
||||
@@ -3370,128 +3344,6 @@ async fn test_do_not_overwrite_draft() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_info_contact_id() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice2 = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
async fn pop_recv_and_check(
|
||||
alice: &TestContext,
|
||||
alice2: &TestContext,
|
||||
bob: &TestContext,
|
||||
expected_type: SystemMessage,
|
||||
expected_alice_id: ContactId,
|
||||
expected_bob_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(alice, sent_msg.sender_msg_id).await?;
|
||||
assert_eq!(msg.get_info_type(), expected_type);
|
||||
assert_eq!(
|
||||
msg.get_info_contact_id(alice).await?,
|
||||
Some(expected_alice_id)
|
||||
);
|
||||
|
||||
let msg = alice2.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_info_type(), expected_type);
|
||||
assert_eq!(
|
||||
msg.get_info_contact_id(alice2).await?,
|
||||
Some(expected_alice_id)
|
||||
);
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.get_info_type(), expected_type);
|
||||
assert_eq!(msg.get_info_contact_id(bob).await?, Some(expected_bob_id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Alice creates group, Bob receives group
|
||||
let alice_chat_id = alice
|
||||
.create_group_with_members(ProtectionStatus::Unprotected, "play", &[bob])
|
||||
.await;
|
||||
let sent_msg1 = alice.send_text(alice_chat_id, "moin").await;
|
||||
|
||||
let msg = bob.recv_msg(&sent_msg1).await;
|
||||
let bob_alice_id = msg.from_id;
|
||||
assert!(!bob_alice_id.is_special());
|
||||
|
||||
// Alice does group changes, Bob receives them
|
||||
set_chat_name(alice, alice_chat_id, "games").await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::GroupNameChanged,
|
||||
ContactId::SELF,
|
||||
bob_alice_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let file = alice.get_blobdir().join("avatar.png");
|
||||
let bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
set_chat_profile_image(alice, alice_chat_id, file.to_str().unwrap()).await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::GroupImageChanged,
|
||||
ContactId::SELF,
|
||||
bob_alice_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
alice_chat_id
|
||||
.set_ephemeral_timer(alice, Timer::Enabled { duration: 60 })
|
||||
.await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::EphemeralTimerChanged,
|
||||
ContactId::SELF,
|
||||
bob_alice_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let fiona_id = alice.add_or_lookup_contact_id(&tcm.fiona().await).await; // contexts are in sync, fiona_id is same everywhere
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
fiona_id,
|
||||
fiona_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
pop_recv_and_check(
|
||||
alice,
|
||||
alice2,
|
||||
bob,
|
||||
SystemMessage::MemberRemovedFromGroup,
|
||||
fiona_id,
|
||||
fiona_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// When fiona_id is deleted, get_info_contact_id() returns None.
|
||||
// We raw delete in db as Contact::delete() leaves a tombstone (which is great as the tap works longer then)
|
||||
alice
|
||||
.sql
|
||||
.execute("DELETE FROM contacts WHERE id=?", (fiona_id,))
|
||||
.await?;
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::MemberRemovedFromGroup);
|
||||
assert!(msg.get_info_contact_id(alice).await?.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test group consistency.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_add_member_bug() -> Result<()> {
|
||||
@@ -3499,10 +3351,9 @@ async fn test_add_member_bug() -> Result<()> {
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
// Create a group.
|
||||
let alice_chat_id =
|
||||
@@ -3546,8 +3397,7 @@ async fn test_past_members() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3559,7 +3409,8 @@ async fn test_past_members() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
let add_message = alice.pop_sent_msg().await;
|
||||
@@ -3578,7 +3429,8 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3590,8 +3442,7 @@ async fn non_member_cannot_modify_member_list() -> Result<()> {
|
||||
let bob_chat_id = bob_received_msg.get_chat_id();
|
||||
bob_chat_id.accept(bob).await?;
|
||||
|
||||
let fiona = &tcm.fiona().await;
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
let bob_fiona_contact_id = Contact::create(bob, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
// Alice removes Bob and Bob adds Fiona at the same time.
|
||||
remove_contact_from_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
@@ -3613,10 +3464,11 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3632,6 +3484,10 @@ async fn unpromoted_group_no_tombstones() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
let sent = alice.send_text(alice_chat_id, "Hello group!").await;
|
||||
let payload = sent.payload();
|
||||
assert_eq!(payload.contains("Hello group!"), true);
|
||||
assert_eq!(payload.contains(&bob_addr), true);
|
||||
assert_eq!(payload.contains(fiona_addr), false);
|
||||
|
||||
let bob_msg = bob.recv_msg(&sent).await;
|
||||
let bob_chat_id = bob_msg.chat_id;
|
||||
@@ -3647,8 +3503,8 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice_fiona_contact_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let fiona_addr = "fiona@example.net";
|
||||
let alice_fiona_contact_id = Contact::create(alice, "Fiona", fiona_addr).await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3663,10 +3519,12 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
let add_message = alice.pop_sent_msg().await;
|
||||
assert_eq!(add_message.payload.contains(fiona_addr), false);
|
||||
let bob_add_message = bob.recv_msg(&add_message).await;
|
||||
let bob_chat_id = bob_add_message.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
@@ -3678,15 +3536,11 @@ async fn test_expire_past_members_after_60_days() -> Result<()> {
|
||||
/// Test that past members are ordered by the timestamp of their removal.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_past_members_order() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let t = &tcm.alice().await;
|
||||
let t = &TestContext::new_alice().await;
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_contact_id = t.add_or_lookup_contact_id(&bob).await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let charlie_contact_id = t.add_or_lookup_contact_id(&charlie).await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let fiona_contact_id = t.add_or_lookup_contact_id(&fiona).await;
|
||||
let bob_contact_id = Contact::create(t, "Bob", "bob@example.net").await?;
|
||||
let charlie_contact_id = Contact::create(t, "Charlie", "charlie@example.org").await?;
|
||||
let fiona_contact_id = Contact::create(t, "Fiona", "fiona@example.net").await?;
|
||||
|
||||
let chat_id = create_group_chat(t, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
add_contact_to_chat(t, chat_id, bob_contact_id).await?;
|
||||
@@ -3744,11 +3598,13 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_charlie_contact_id = alice.add_or_lookup_contact_id(charlie).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
|
||||
let charlie_addr = "charlie@example.com";
|
||||
let alice_charlie_contact_id = Contact::create(alice, "Charlie", charlie_addr).await?;
|
||||
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group chat").await?;
|
||||
@@ -3770,6 +3626,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 1);
|
||||
|
||||
let remove_message = alice.pop_sent_msg().await;
|
||||
assert_eq!(remove_message.payload.contains(charlie_addr), true);
|
||||
bob.recv_msg(&remove_message).await;
|
||||
|
||||
// 60 days pass.
|
||||
@@ -3778,7 +3635,8 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
assert_eq!(get_past_chat_contacts(alice, alice_chat_id).await?.len(), 0);
|
||||
|
||||
// Bob adds Fiona to the chat.
|
||||
let bob_fiona_contact_id = bob.add_or_lookup_contact_id(fiona).await;
|
||||
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_fiona_contact_id = Contact::create(bob, "Fiona", &fiona_addr).await?;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_fiona_contact_id).await?;
|
||||
|
||||
let add_message = bob.pop_sent_msg().await;
|
||||
@@ -3842,7 +3700,7 @@ async fn test_restore_backup_after_60_days() -> Result<()> {
|
||||
// Charlie is not part of the chat.
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_past_chat_contacts(bob, bob_chat_id).await?.len(), 0);
|
||||
let bob_charlie_contact_id = bob.add_or_lookup_contact_id(charlie).await;
|
||||
let bob_charlie_contact_id = Contact::create(bob, "Charlie", charlie_addr).await?;
|
||||
assert!(!is_contact_in_chat(bob, bob_chat_id, bob_charlie_contact_id).await?);
|
||||
|
||||
assert_eq!(get_chat_contacts(fiona, fiona_chat_id).await?.len(), 3);
|
||||
@@ -3884,7 +3742,6 @@ async fn test_send_edit_request() -> Result<()> {
|
||||
bob.recv_msg_opt(&sent2).await;
|
||||
let test = Message::load_from_db(bob, bob_msg.id).await?;
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
assert!(test.is_edited());
|
||||
|
||||
// alice has another device, and sees the correction also there
|
||||
let alice2 = tcm.alice().await;
|
||||
@@ -3894,12 +3751,6 @@ async fn test_send_edit_request() -> Result<()> {
|
||||
alice2.recv_msg_opt(&sent2).await;
|
||||
let test = Message::load_from_db(&alice2, alice2_msg.id).await?;
|
||||
assert_eq!(test.text, "Text me on Delta.Chat");
|
||||
assert!(test.is_edited());
|
||||
|
||||
// Alice forwards the edited message, the new message shouldn't have the "edited" mark.
|
||||
forward_msgs(&alice2, &[test.id], test.chat_id).await?;
|
||||
let forwarded = alice2.get_last_msg().await;
|
||||
assert!(!forwarded.is_edited());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # Chat list module.
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use std::sync::LazyLock;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
|
||||
use crate::constants::{
|
||||
@@ -17,8 +17,8 @@ use crate::summary::Summary;
|
||||
use crate::tools::IsNoneOrEmpty;
|
||||
|
||||
/// Regex to find out if a query should filter by unread messages.
|
||||
pub static IS_UNREAD_FILTER: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
|
||||
pub static IS_UNREAD_FILTER: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
|
||||
|
||||
/// An object representing a single chatlist in memory.
|
||||
///
|
||||
@@ -492,20 +492,19 @@ mod tests {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_try_load() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
|
||||
let t = TestContext::new_bob().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
|
||||
let chat_id3 = create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// check that the chatlist starts with the most recent message
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 3);
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
|
||||
assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
|
||||
@@ -521,49 +520,51 @@ mod tests {
|
||||
// 2s here.
|
||||
for chat_id in &[chat_id1, chat_id3, chat_id2] {
|
||||
let mut msg = Message::new_text("hello".to_string());
|
||||
chat_id.set_draft(bob, Some(&mut msg)).await.unwrap();
|
||||
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
|
||||
}
|
||||
|
||||
let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
|
||||
|
||||
// check chatlist query and archive functionality
|
||||
let chats = Chatlist::try_load(bob, 0, Some("b"), None).await.unwrap();
|
||||
let chats = Chatlist::try_load(&t, 0, Some("b"), None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
// receive a message from alice
|
||||
let alice = &tcm.alice().await;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "alice chat")
|
||||
.await
|
||||
.unwrap();
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(bob).await,
|
||||
Contact::create(&alice, "bob", "bob@example.net")
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
send_text_msg(alice, alice_chat_id, "hi".into())
|
||||
send_text_msg(&alice, alice_chat_id, "hi".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let chats = Chatlist::try_load(bob, 0, Some("is:unread"), None)
|
||||
t.recv_msg(&sent_msg).await;
|
||||
let chats = Chatlist::try_load(&t, 0, Some("is:unread"), None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 0);
|
||||
|
||||
chat_id1
|
||||
.set_visibility(bob, ChatVisibility::Archived)
|
||||
.set_visibility(&t, ChatVisibility::Archived)
|
||||
.await
|
||||
.ok();
|
||||
let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
let chats = Chatlist::try_load(&t, DC_GCL_ARCHIVED_ONLY, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
|
||||
@@ -182,6 +182,12 @@ pub enum Config {
|
||||
#[strum(props(default = "0"))] // also change MediaQuality.default() on changes
|
||||
MediaQuality,
|
||||
|
||||
/// If set to "1", on the first time `start_io()` is called after configuring,
|
||||
/// the newest existing messages are fetched.
|
||||
/// Existing recipients are added to the contact database regardless of this setting.
|
||||
#[strum(props(default = "0"))]
|
||||
FetchExistingMsgs,
|
||||
|
||||
/// If set to "1", then existing messages are considered to be already fetched.
|
||||
/// This flag is reset after successful configuration.
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -475,10 +481,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,6 +707,7 @@ impl Context {
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::FetchExistingMsgs
|
||||
| Config::DeleteToTrash
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
|
||||
144
src/configure.rs
144
src/configure.rs
@@ -28,15 +28,13 @@ use crate::constants::NON_ALPHANUMERIC_WITHOUT_DOT;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::log::LogExt;
|
||||
pub use crate::login_param::EnteredLoginParam;
|
||||
use crate::login_param::{
|
||||
ConfiguredCertificateChecks, ConfiguredLoginParam, ConfiguredServerLoginParam,
|
||||
ConnectionCandidate, EnteredCertificateChecks, ProxyConfig,
|
||||
ConnectionCandidate, EnteredCertificateChecks, EnteredLoginParam,
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
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;
|
||||
@@ -66,59 +64,8 @@ impl Context {
|
||||
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_transport()` instead.
|
||||
/// Configures this account with the currently set parameters.
|
||||
pub async fn configure(&self) -> Result<()> {
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
self.add_transport_inner(¶m).await
|
||||
}
|
||||
|
||||
/// Configures a new email account using the provided parameters
|
||||
/// and adds it as a transport.
|
||||
///
|
||||
/// If the email address is the same as an existing transport,
|
||||
/// then this existing account will be reconfigured instead of a new one being added.
|
||||
///
|
||||
/// This function stops and starts IO as needed.
|
||||
///
|
||||
/// Usually it will be enough to only set `addr` and `imap.password`,
|
||||
/// and all the other settings will be autoconfigured.
|
||||
///
|
||||
/// During configuration, ConfigureProgress events are emitted;
|
||||
/// they indicate a successful configuration as well as errors
|
||||
/// and may be used to create a progress bar.
|
||||
/// This function will return after configuration is finished.
|
||||
///
|
||||
/// If configuration is successful,
|
||||
/// the working server parameters will be saved
|
||||
/// and used for connecting to the server.
|
||||
/// The parameters entered by the user will be saved separately
|
||||
/// so that they can be prefilled when the user opens the server-configuration screen again.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Self::is_configured()] to check whether there is
|
||||
/// at least one working transport.
|
||||
/// - [Self::add_transport_from_qr()] to add a transport
|
||||
/// 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_transport(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
let result = self.add_transport_inner(param).await;
|
||||
if result.is_err() {
|
||||
if let Ok(true) = self.is_configured().await {
|
||||
self.start_io().await;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_transport_inner(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
ensure!(
|
||||
!self.scheduler.is_running().await,
|
||||
"cannot configure, already running"
|
||||
@@ -127,77 +74,42 @@ impl Context {
|
||||
self.sql.is_open().await,
|
||||
"cannot configure, database not opened."
|
||||
);
|
||||
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!");
|
||||
}
|
||||
let cancel_channel = self.alloc_ongoing().await?;
|
||||
|
||||
let res = self
|
||||
.inner_configure(param)
|
||||
.inner_configure()
|
||||
.race(cancel_channel.recv().map(|_| Err(format_err!("Cancelled"))))
|
||||
.await;
|
||||
|
||||
self.free_ongoing().await;
|
||||
|
||||
if let Err(err) = res.as_ref() {
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
let error_msg = stock_str::configuration_failed(self, &format!("{err:#}")).await;
|
||||
progress!(self, 0, Some(error_msg.clone()));
|
||||
bail!(error_msg);
|
||||
progress!(
|
||||
self,
|
||||
0,
|
||||
Some(
|
||||
stock_str::configuration_failed(
|
||||
self,
|
||||
// We are using Anyhow's .context() and to show the
|
||||
// inner error, too, we need the {:#}:
|
||||
&format!("{err:#}"),
|
||||
)
|
||||
.await
|
||||
)
|
||||
);
|
||||
} else {
|
||||
param.save(self).await?;
|
||||
progress!(self, 1000);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// Adds a new email account as a transport
|
||||
/// using the server encoded in the QR code.
|
||||
/// See [Self::add_transport].
|
||||
pub async fn add_transport_from_qr(&self, qr: &str) -> Result<()> {
|
||||
self.stop_io().await;
|
||||
|
||||
let result = async move {
|
||||
set_account_from_qr(self, qr).await?;
|
||||
self.configure().await?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
if let Ok(true) = self.is_configured().await {
|
||||
self.start_io().await;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
self.start_io().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the list of all email accounts that are used as a transport in the current profile.
|
||||
/// 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 param = EnteredLoginParam::load(self).await?;
|
||||
|
||||
Ok(vec![param])
|
||||
}
|
||||
|
||||
/// Removes the transport with the specified email address
|
||||
/// (i.e. [EnteredLoginParam::addr]).
|
||||
#[expect(clippy::unused_async)]
|
||||
pub async fn delete_transport(&self, _addr: &str) -> Result<()> {
|
||||
bail!("Adding and removing additional transports is not supported yet. Check back in a few months!")
|
||||
}
|
||||
|
||||
async fn inner_configure(&self, param: &EnteredLoginParam) -> Result<()> {
|
||||
async fn inner_configure(&self) -> Result<()> {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let param = EnteredLoginParam::load(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let configured_param = configure(self, param).await?;
|
||||
let configured_param = configure(self, ¶m).await?;
|
||||
self.set_config_internal(Config::NotifyAboutWrongPw, Some("1"))
|
||||
.await?;
|
||||
on_configure_completed(self, configured_param, old_addr).await?;
|
||||
@@ -273,7 +185,8 @@ async fn get_configured_param(
|
||||
param.smtp.password.clone()
|
||||
};
|
||||
|
||||
let proxy_enabled = ctx.get_config_bool(Config::ProxyEnabled).await?;
|
||||
let proxy_config = param.proxy_config.clone();
|
||||
let proxy_enabled = proxy_config.is_some();
|
||||
|
||||
let mut addr = param.addr.clone();
|
||||
if param.oauth2 {
|
||||
@@ -432,6 +345,7 @@ async fn get_configured_param(
|
||||
.collect(),
|
||||
smtp_user: param.smtp.user.clone(),
|
||||
smtp_password,
|
||||
proxy_config: param.proxy_config.clone(),
|
||||
provider,
|
||||
certificate_checks: match param.certificate_checks {
|
||||
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
|
||||
@@ -453,8 +367,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let update_device_chats_handle = task::spawn(async move { ctx2.update_device_chats().await });
|
||||
|
||||
let configured_param = get_configured_param(ctx, param).await?;
|
||||
let proxy_config = ProxyConfig::load(ctx).await?;
|
||||
let strict_tls = configured_param.strict_tls(proxy_config.is_some());
|
||||
let strict_tls = configured_param.strict_tls();
|
||||
|
||||
progress!(ctx, 550);
|
||||
|
||||
@@ -464,15 +377,15 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let smtp_param = configured_param.smtp.clone();
|
||||
let smtp_password = configured_param.smtp_password.clone();
|
||||
let smtp_addr = configured_param.addr.clone();
|
||||
let proxy_config = configured_param.proxy_config.clone();
|
||||
|
||||
let proxy_config2 = proxy_config.clone();
|
||||
let smtp_config_task = task::spawn(async move {
|
||||
let mut smtp = Smtp::new();
|
||||
smtp.connect(
|
||||
&context_smtp,
|
||||
&smtp_param,
|
||||
&smtp_password,
|
||||
&proxy_config2,
|
||||
&proxy_config,
|
||||
&smtp_addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
@@ -490,7 +403,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let mut imap = Imap::new(
|
||||
configured_param.imap.clone(),
|
||||
configured_param.imap_password.clone(),
|
||||
proxy_config,
|
||||
configured_param.proxy_config.clone(),
|
||||
&configured_param.addr,
|
||||
strict_tls,
|
||||
configured_param.oauth2,
|
||||
@@ -499,10 +412,7 @@ async fn configure(ctx: &Context, param: &EnteredLoginParam) -> Result<Configure
|
||||
let configuring = true;
|
||||
let mut imap_session = match imap.connect(ctx, configuring).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => bail!(
|
||||
"{}",
|
||||
nicer_configuration_error(ctx, format!("{err:#}")).await
|
||||
),
|
||||
Err(err) => bail!("{}", nicer_configuration_error(ctx, err.to_string()).await),
|
||||
};
|
||||
|
||||
progress!(ctx, 850);
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::chat::ChatId;
|
||||
|
||||
pub static DC_VERSION_STR: LazyLock<String> =
|
||||
LazyLock::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
pub static DC_VERSION_STR: Lazy<String> = Lazy::new(|| env!("CARGO_PKG_VERSION").to_string());
|
||||
|
||||
/// Set of characters to percent-encode in email addresses and names.
|
||||
pub(crate) const NON_ALPHANUMERIC_WITHOUT_DOT: &AsciiSet = &NON_ALPHANUMERIC.remove(b'.');
|
||||
@@ -180,6 +178,9 @@ pub const DC_LP_AUTH_NORMAL: i32 = 0x4;
|
||||
/// if none of these flags are set, the default is chosen
|
||||
pub const DC_LP_AUTH_FLAGS: i32 = DC_LP_AUTH_OAUTH2 | DC_LP_AUTH_NORMAL;
|
||||
|
||||
/// How many existing messages shall be fetched after configuration.
|
||||
pub(crate) const DC_FETCH_EXISTING_MSGS_COUNT: i64 = 100;
|
||||
|
||||
// max. weight of images to send w/o recoding
|
||||
pub const BALANCED_IMAGE_BYTES: usize = 500_000;
|
||||
pub const WORSE_IMAGE_BYTES: usize = 130_000;
|
||||
@@ -220,19 +221,6 @@ pub(crate) const SECUREJOIN_WAIT_TIMEOUT: u64 = 15;
|
||||
// Newer Delta Chats will remove the prefix as needed.
|
||||
pub(crate) const EDITED_PREFIX: &str = "✏️";
|
||||
|
||||
// Strings needed to render the Autocrypt Setup Message.
|
||||
// Left untranslated as not being supported/recommended workflow and as translations would require deep knowledge.
|
||||
pub(crate) const ASM_SUBJECT: &str = "Autocrypt Setup Message";
|
||||
pub(crate) const ASM_BODY: &str = "This is the Autocrypt Setup Message \
|
||||
used to transfer your end-to-end setup between clients.
|
||||
|
||||
To decrypt and use your setup, \
|
||||
open the message in an Autocrypt-compliant client \
|
||||
and enter the setup code presented on the generating device.
|
||||
|
||||
If you see this message in a chatmail client (Delta Chat, Arcane Chat, Delta Touch ...), \
|
||||
use \"Settings / Add Second Device\" instead.";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
293
src/contact.rs
293
src/contact.rs
@@ -95,50 +95,6 @@ impl ContactId {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Sets display name for existing contact.
|
||||
///
|
||||
/// Display name may be an empty string,
|
||||
/// in which case the name displayed in the UI
|
||||
/// for this contact will switch to the
|
||||
/// contact's authorized name.
|
||||
pub async fn set_name(self, context: &Context, name: &str) -> Result<()> {
|
||||
let addr = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let is_changed = transaction.execute(
|
||||
"UPDATE contacts SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(name, self),
|
||||
)? > 0;
|
||||
if is_changed {
|
||||
update_chat_names(context, transaction, self)?;
|
||||
let addr = transaction.query_row(
|
||||
"SELECT addr FROM contacts WHERE id=?",
|
||||
(self,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
Ok(addr)
|
||||
},
|
||||
)?;
|
||||
Ok(Some(addr))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(addr) = addr {
|
||||
chat::sync(
|
||||
context,
|
||||
chat::SyncId::ContactAddr(addr.to_string()),
|
||||
chat::SyncAction::Rename(name.to_string()),
|
||||
)
|
||||
.await
|
||||
.log_err(context)
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark contact as bot.
|
||||
pub(crate) async fn mark_bot(&self, context: &Context, is_bot: bool) -> Result<()> {
|
||||
context
|
||||
@@ -887,48 +843,44 @@ impl Contact {
|
||||
|
||||
let mut update_addr = false;
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.transaction(|transaction| {
|
||||
let row = transaction
|
||||
.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
let row_id = context.sql.transaction(|transaction| {
|
||||
let row = transaction.query_row(
|
||||
"SELECT id, name, addr, origin, authname
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE",
|
||||
(addr,),
|
||||
|row| {
|
||||
let row_id: u32 = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
let row_addr: String = row.get(2)?;
|
||||
let row_origin: Origin = row.get(3)?;
|
||||
let row_authname: String = row.get(4)?;
|
||||
[addr.to_string()],
|
||||
|row| {
|
||||
let row_id: isize = row.get(0)?;
|
||||
let row_name: String = row.get(1)?;
|
||||
let row_addr: String = row.get(2)?;
|
||||
let row_origin: Origin = row.get(3)?;
|
||||
let row_authname: String = row.get(4)?;
|
||||
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
},
|
||||
)
|
||||
.optional()?;
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
}).optional()?;
|
||||
|
||||
let row_id;
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname = !manual
|
||||
&& name != row_authname
|
||||
&& !name.is_empty()
|
||||
&& (origin >= row_origin
|
||||
|| origin == Origin::IncomingUnknownFrom
|
||||
|| row_authname.is_empty());
|
||||
let row_id;
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = row {
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname = !manual
|
||||
&& name != row_authname
|
||||
&& !name.is_empty()
|
||||
&& (origin >= row_origin
|
||||
|| origin == Origin::IncomingUnknownFrom
|
||||
|| row_authname.is_empty());
|
||||
|
||||
row_id = id;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
let new_name = if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_name
|
||||
};
|
||||
row_id = u32::try_from(id)?;
|
||||
if origin >= row_origin && addr.as_ref() != row_addr {
|
||||
update_addr = true;
|
||||
}
|
||||
if update_name || update_authname || update_addr || origin > row_origin {
|
||||
let new_name = if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
row_name
|
||||
};
|
||||
|
||||
transaction.execute(
|
||||
transaction
|
||||
.execute(
|
||||
"UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;",
|
||||
(
|
||||
new_name,
|
||||
@@ -947,38 +899,88 @@ impl Contact {
|
||||
} else {
|
||||
row_authname
|
||||
},
|
||||
row_id,
|
||||
row_id
|
||||
),
|
||||
)?;
|
||||
|
||||
if update_name || update_authname {
|
||||
let contact_id = ContactId::new(row_id);
|
||||
update_chat_names(context, transaction, contact_id)?;
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
}
|
||||
} else {
|
||||
let update_name = manual;
|
||||
let update_authname = !manual;
|
||||
if update_name || update_authname {
|
||||
// Update the contact name also if it is used as a group name.
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
(Chattype::Single, isize::try_from(row_id)?),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
).optional()?;
|
||||
|
||||
transaction.execute(
|
||||
if let Some(chat_id) = chat_id {
|
||||
let contact_id = ContactId::new(row_id);
|
||||
let (addr, name, authname) =
|
||||
transaction.query_row(
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let authname: String = row.get(2)?;
|
||||
Ok((addr, name, authname))
|
||||
})?;
|
||||
|
||||
let chat_name = if !name.is_empty() {
|
||||
name
|
||||
} else if !authname.is_empty() {
|
||||
authname
|
||||
} else {
|
||||
addr
|
||||
};
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(chat_name, chat_id))?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
sth_modified = Modifier::Modified;
|
||||
}
|
||||
} else {
|
||||
let update_name = manual;
|
||||
let update_authname = !manual;
|
||||
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO contacts (name, addr, origin, authname)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
(
|
||||
if update_name { &name } else { "" },
|
||||
(
|
||||
if update_name {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
&addr,
|
||||
origin,
|
||||
if update_authname { &name } else { "" },
|
||||
if update_authname {
|
||||
name.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
),
|
||||
)?;
|
||||
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
}
|
||||
Ok(row_id)
|
||||
})
|
||||
.await?;
|
||||
sth_modified = Modifier::Created;
|
||||
row_id = u32::try_from(transaction.last_insert_rowid())?;
|
||||
info!(context, "Added contact id={row_id} addr={addr}.");
|
||||
}
|
||||
Ok(row_id)
|
||||
}).await?;
|
||||
|
||||
let contact_id = ContactId::new(row_id);
|
||||
|
||||
@@ -1277,16 +1279,9 @@ impl Contact {
|
||||
.map(|k| k.dc_fingerprint().to_string())
|
||||
.unwrap_or_default();
|
||||
if addr < peerstate.addr {
|
||||
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
@@ -1294,18 +1289,11 @@ impl Contact {
|
||||
} else {
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
contact.get_display_name(),
|
||||
&peerstate.addr,
|
||||
&fingerprint_other_verified,
|
||||
&fingerprint_other_unverified,
|
||||
);
|
||||
cat_fingerprint(
|
||||
&mut ret,
|
||||
&stock_str::self_msg(context).await,
|
||||
&addr,
|
||||
&fingerprint_self,
|
||||
"",
|
||||
);
|
||||
cat_fingerprint(&mut ret, &addr, &fingerprint_self, "");
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
@@ -1406,13 +1394,16 @@ impl Contact {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
/// Get authorized name or address.
|
||||
/// Get a summary of authorized name and address.
|
||||
///
|
||||
/// The returned string is either "Name (email@domain.com)" or just
|
||||
/// "email@domain.com" if the name is unset.
|
||||
///
|
||||
/// This string is suitable for sending over email
|
||||
/// as it does not leak the locally set name.
|
||||
pub(crate) fn get_authname_or_addr(&self) -> String {
|
||||
pub fn get_authname_n_addr(&self) -> String {
|
||||
if !self.authname.is_empty() {
|
||||
(&self.authname).into()
|
||||
format!("{} ({})", self.authname, self.addr)
|
||||
} else {
|
||||
(&self.addr).into()
|
||||
}
|
||||
@@ -1615,60 +1606,6 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates the names of the chats which use the contact name.
|
||||
//
|
||||
// This is one of the few duplicated data, however, getting the chat list is easier this way.
|
||||
fn update_chat_names(
|
||||
context: &Context,
|
||||
transaction: &rusqlite::Connection,
|
||||
contact_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let chat_id: Option<ChatId> = transaction.query_row(
|
||||
"SELECT id FROM chats WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?)",
|
||||
(Chattype::Single, contact_id),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
Ok(chat_id)
|
||||
}
|
||||
).optional()?;
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
let (addr, name, authname) = transaction.query_row(
|
||||
"SELECT addr, name, authname
|
||||
FROM contacts
|
||||
WHERE id=?",
|
||||
(contact_id,),
|
||||
|row| {
|
||||
let addr: String = row.get(0)?;
|
||||
let name: String = row.get(1)?;
|
||||
let authname: String = row.get(2)?;
|
||||
Ok((addr, name, authname))
|
||||
},
|
||||
)?;
|
||||
|
||||
let chat_name = if !name.is_empty() {
|
||||
name
|
||||
} else if !authname.is_empty() {
|
||||
authname
|
||||
} else {
|
||||
addr
|
||||
};
|
||||
|
||||
let count = transaction.execute(
|
||||
"UPDATE chats SET name=?1 WHERE id=?2 AND name!=?1",
|
||||
(chat_name, chat_id),
|
||||
)?;
|
||||
|
||||
if count > 0 {
|
||||
// Chat name updated
|
||||
context.emit_event(EventType::ChatModified(chat_id));
|
||||
chatlist_events::emit_chatlist_items_changed_for_contact(context, contact_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn set_blocked(
|
||||
context: &Context,
|
||||
sync: sync::Sync,
|
||||
@@ -1859,14 +1796,12 @@ pub(crate) async fn update_last_seen(
|
||||
|
||||
fn cat_fingerprint(
|
||||
ret: &mut String,
|
||||
name: &str,
|
||||
addr: &str,
|
||||
fingerprint_verified: &str,
|
||||
fingerprint_unverified: &str,
|
||||
) {
|
||||
*ret += &format!(
|
||||
"\n\n{} ({}):\n{}",
|
||||
name,
|
||||
"\n\n{}:\n{}",
|
||||
addr,
|
||||
if !fingerprint_verified.is_empty() {
|
||||
fingerprint_verified
|
||||
@@ -1878,7 +1813,7 @@ fn cat_fingerprint(
|
||||
&& !fingerprint_unverified.is_empty()
|
||||
&& fingerprint_verified != fingerprint_unverified
|
||||
{
|
||||
*ret += &format!("\n\n{name} (alternative):\n{fingerprint_unverified}");
|
||||
*ret += &format!("\n\n{addr} (alternative):\n{fingerprint_unverified}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -763,11 +763,11 @@ async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
"End-to-end encryption preferred.
|
||||
Fingerprints:
|
||||
|
||||
Me (alice@example.org):
|
||||
alice@example.org:
|
||||
2E6F A2CB 23B5 32D7 2863
|
||||
4B58 64B0 8F61 A9ED 9443
|
||||
|
||||
Bob (bob@example.net):
|
||||
bob@example.net:
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
@@ -1050,9 +1050,8 @@ async fn test_sync_create() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_make_n_import_vcard() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = &TestContext::new_alice().await;
|
||||
let bob = &TestContext::new_bob().await;
|
||||
bob.set_config(Config::Displayname, Some("Bob")).await?;
|
||||
let avatar_path = bob.dir.path().join("avatar.png");
|
||||
let avatar_bytes = include_bytes!("../../test-data/image/avatar64x64.png");
|
||||
|
||||
@@ -903,6 +903,12 @@ impl Context {
|
||||
}
|
||||
|
||||
res.insert("secondary_addrs", secondary_addrs);
|
||||
res.insert(
|
||||
"fetch_existing_msgs",
|
||||
self.get_config_int(Config::FetchExistingMsgs)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"fetched_existing_msgs",
|
||||
self.get_config_bool(Config::FetchedExistingMsgs)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
//! A module to remove HTML tags from the email text
|
||||
|
||||
use std::io::BufRead;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use quick_xml::{
|
||||
events::{BytesEnd, BytesStart, BytesText},
|
||||
Reader,
|
||||
@@ -176,8 +176,7 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
|
||||
static LINE_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
|
||||
|
||||
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|
||||
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
|
||||
|
||||
@@ -220,6 +220,7 @@ impl Session {
|
||||
vec![uid],
|
||||
&uid_message_ids,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
if last_uid.is_none() {
|
||||
@@ -368,6 +369,7 @@ mod tests {
|
||||
header.as_bytes(),
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
@@ -383,6 +385,7 @@ mod tests {
|
||||
format!("{header}\n\n100k text...").as_bytes(),
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = t.get_last_msg().await;
|
||||
@@ -417,6 +420,7 @@ mod tests {
|
||||
Content-Type: text/plain",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
@@ -453,6 +457,7 @@ mod tests {
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
Some(sent2.payload().len() as u32),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
@@ -468,6 +473,7 @@ mod tests {
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
@@ -511,7 +517,15 @@ mod tests {
|
||||
";
|
||||
|
||||
// not downloading the mdn results in an placeholder
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, Some(raw.len() as u32)).await?;
|
||||
receive_imf_from_inbox(
|
||||
&bob,
|
||||
"bar@example.org",
|
||||
raw,
|
||||
false,
|
||||
Some(raw.len() as u32),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 1);
|
||||
@@ -519,7 +533,7 @@ mod tests {
|
||||
|
||||
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
|
||||
// (usually mdn are too small for not being downloaded directly)
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None).await?;
|
||||
receive_imf_from_inbox(&bob, "bar@example.org", raw, false, None, false).await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id).await?.len(), 0);
|
||||
assert!(Message::load_from_db_optional(&bob, msg.id)
|
||||
.await?
|
||||
|
||||
126
src/e2ee.rs
126
src/e2ee.rs
@@ -1,9 +1,8 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use mail_builder::mime::MimePart;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
@@ -43,76 +42,79 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
/// Determines if we can and should encrypt.
|
||||
///
|
||||
/// `e2ee_guaranteed` should be set to true for replies to encrypted messages (as required by
|
||||
/// Autocrypt Level 1, version 1.1) and for messages sent in protected groups.
|
||||
///
|
||||
/// Returns an error if `e2ee_guaranteed` is true, but one or more keys are missing.
|
||||
pub(crate) async fn should_encrypt(
|
||||
&self,
|
||||
context: &Context,
|
||||
e2ee_guaranteed: bool,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<bool> {
|
||||
let is_chatmail = context.is_chatmail().await?;
|
||||
for (peerstate, _addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
// For chatmail we ignore the encryption preference,
|
||||
// because we can either send encrypted or not at all.
|
||||
if is_chatmail || peerstate.prefer_encrypt != EncryptPreference::Reset {
|
||||
continue;
|
||||
let mut prefer_encrypt_count = 1;
|
||||
for (peerstate, addr) in peerstates {
|
||||
match peerstate {
|
||||
Some(peerstate) => {
|
||||
if match peerstate.prefer_encrypt {
|
||||
EncryptPreference::Reset => is_chatmail,
|
||||
EncryptPreference::NoPreference | EncryptPreference::Mutual => true,
|
||||
} {
|
||||
prefer_encrypt_count += 1;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let msg = format!("Peerstate for {addr:?} missing, cannot encrypt");
|
||||
if e2ee_guaranteed {
|
||||
return Err(format_err!("{msg}"));
|
||||
} else {
|
||||
info!(context, "{msg}.");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
|
||||
// Count number of recipients, including self.
|
||||
// This does not depend on whether we send a copy to self or not.
|
||||
let recipients_count = peerstates.len() + 1;
|
||||
|
||||
Ok(e2ee_guaranteed || 2 * prefer_encrypt_count > recipients_count)
|
||||
}
|
||||
|
||||
/// Constructs a vector of public keys for given peerstates.
|
||||
///
|
||||
/// In addition returns the set of recipient addresses
|
||||
/// for which there is no key available.
|
||||
///
|
||||
/// Returns an error if there are recipients
|
||||
/// other than self, but no recipient keys are available.
|
||||
pub(crate) fn encryption_keyring(
|
||||
&self,
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
context: &Context,
|
||||
verified: bool,
|
||||
peerstates: &[(Option<Peerstate>, String)],
|
||||
) -> Result<(Vec<SignedPublicKey>, BTreeSet<String>)> {
|
||||
// Encrypt to self unconditionally,
|
||||
// even for a single-device setup.
|
||||
let mut keyring = vec![self.public_key.clone()];
|
||||
let mut missing_key_addresses = BTreeSet::new();
|
||||
|
||||
if peerstates.is_empty() {
|
||||
return Ok((keyring, missing_key_addresses));
|
||||
}
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
peerstates: Vec<(Option<Peerstate>, String)>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let mut keyring: Vec<SignedPublicKey> = Vec::new();
|
||||
|
||||
let mut verifier_addresses: Vec<&str> = Vec::new();
|
||||
|
||||
for (peerstate, addr) in peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let Some(key) = peerstate.clone().take_key(verified) {
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
} else {
|
||||
warn!(context, "Encryption key for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
} else {
|
||||
warn!(context, "Peerstate for {addr} is missing.");
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
}
|
||||
for (peerstate, addr) in peerstates
|
||||
.iter()
|
||||
.filter_map(|(state, addr)| state.clone().map(|s| (s, addr)))
|
||||
{
|
||||
let key = peerstate
|
||||
.take_key(verified)
|
||||
.with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?;
|
||||
keyring.push(key);
|
||||
verifier_addresses.push(addr);
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
!keyring.is_empty(),
|
||||
"At least our own key is in the keyring"
|
||||
);
|
||||
if keyring.len() <= 1 {
|
||||
bail!("No recipient keys are available, cannot encrypt");
|
||||
}
|
||||
// Encrypt to self.
|
||||
keyring.push(self.public_key.clone());
|
||||
|
||||
// Encrypt to secondary verified keys
|
||||
// if we also encrypt to the introducer ("verifier") of the key.
|
||||
if verified {
|
||||
for (peerstate, _addr) in peerstates {
|
||||
for (peerstate, _addr) in &peerstates {
|
||||
if let Some(peerstate) = peerstate {
|
||||
if let (Some(key), Some(verifier)) = (
|
||||
peerstate.secondary_verified_key.as_ref(),
|
||||
@@ -126,17 +128,6 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
Ok((keyring, missing_key_addresses))
|
||||
}
|
||||
|
||||
/// Tries to encrypt the passed in `mail`.
|
||||
pub async fn encrypt(
|
||||
self,
|
||||
context: &Context,
|
||||
keyring: Vec<SignedPublicKey>,
|
||||
mail_to_encrypt: MimePart<'static>,
|
||||
compress: bool,
|
||||
) -> Result<String> {
|
||||
let sign_key = load_self_secret_key(context).await?;
|
||||
|
||||
let mut raw_message = Vec::new();
|
||||
@@ -333,17 +324,22 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
let encrypt_helper = EncryptHelper::new(&t).await.unwrap();
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::NoPreference);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
// Own preference is `Mutual` and we have the peer's key.
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Reset);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
|
||||
let ps = new_peerstates(EncryptPreference::Mutual);
|
||||
assert!(encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
|
||||
// test with missing peerstate
|
||||
let ps = vec![(None, "bob@foo.bar".to_string())];
|
||||
assert!(!encrypt_helper.should_encrypt(&t, &ps).await?);
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).await.is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::chat::{
|
||||
};
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_CHAT_ID_ARCHIVED_LINK;
|
||||
use crate::contact::Contact;
|
||||
use crate::download::DownloadState;
|
||||
use crate::location;
|
||||
use crate::message::markseen_msgs;
|
||||
@@ -790,7 +791,7 @@ async fn test_ephemeral_timer_non_member() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
|
||||
let alice_chat_id =
|
||||
create_group_chat(alice, ProtectionStatus::Unprotected, "Group name").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
|
||||
43
src/html.rs
43
src/html.rs
@@ -285,7 +285,7 @@ mod tests {
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{MessengerMessage, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_htmlparse_plain_unspecified() {
|
||||
@@ -442,25 +442,24 @@ test some special html-characters as < > and & but also " and &#x
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding() {
|
||||
// alice receives a non-delta html-message
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let chat = alice
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(alice, raw, false).await.unwrap();
|
||||
receive_imf(&alice, raw, false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
assert_ne!(msg.get_from_id(), ContactId::SELF);
|
||||
assert_eq!(msg.is_dc_message, MessengerMessage::No);
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// alice: create chat with bob and forward received html-message there
|
||||
let chat = alice.create_chat_with_contact("", "bob@example.net").await;
|
||||
forward_msgs(alice, &[msg.get_id()], chat.get_id())
|
||||
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
@@ -469,11 +468,11 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
|
||||
// bob: check that bob also got the html-part of the forwarded message
|
||||
let bob = &tcm.bob().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat = bob.create_chat_with_contact("", "alice@example.org").await;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(chat.id, msg.chat_id);
|
||||
@@ -482,7 +481,7 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
@@ -520,11 +519,10 @@ test some special html-characters as < > and & but also " and &#x
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_html_forwarding_encrypted() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
// Alice receives a non-delta html-message
|
||||
// (`ShowEmails=AcceptedContacts` lets Alice actually receive non-delta messages for known
|
||||
// contacts, the contact is marked as known by creating a chat using `chat_with_contact()`)
|
||||
let alice = &tcm.alice().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("1"))
|
||||
.await
|
||||
@@ -533,19 +531,19 @@ test some special html-characters as < > and & but also " and &#x
|
||||
.create_chat_with_contact("", "sender@testrun.org")
|
||||
.await;
|
||||
let raw = include_bytes!("../test-data/message/text_alt_plain_html.eml");
|
||||
receive_imf(alice, raw, false).await.unwrap();
|
||||
receive_imf(&alice, raw, false).await.unwrap();
|
||||
let msg = alice.get_last_msg_in(chat.get_id()).await;
|
||||
|
||||
// forward the message to saved-messages,
|
||||
// this will encrypt the message as new_alice() has set up keys
|
||||
let chat = alice.get_self_chat().await;
|
||||
forward_msgs(alice, &[msg.get_id()], chat.get_id())
|
||||
forward_msgs(&alice, &[msg.get_id()], chat.get_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = alice.pop_sent_msg().await;
|
||||
|
||||
// receive the message on another device
|
||||
let alice = &tcm.alice().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
alice
|
||||
.set_config(Config::ShowEmails, Some("0"))
|
||||
.await
|
||||
@@ -558,39 +556,38 @@ test some special html-characters as < > and & but also " and &#x
|
||||
assert!(msg.is_forwarded());
|
||||
assert!(msg.get_text().contains("this is plain"));
|
||||
assert!(msg.has_html());
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("this is <b>html</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_html() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// alice sends a message with html-part to bob
|
||||
let chat_id = alice.create_chat(bob).await.id;
|
||||
let chat_id = alice.create_chat(&bob).await.id;
|
||||
let mut msg = Message::new_text("plain text".to_string());
|
||||
msg.set_html(Some("<b>html</b> text".to_string()));
|
||||
assert!(msg.mime_modified);
|
||||
chat::send_msg(alice, chat_id, &mut msg).await.unwrap();
|
||||
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
|
||||
|
||||
// check the message is written correctly to alice's db
|
||||
let msg = alice.get_last_msg_in(chat_id).await;
|
||||
assert_eq!(msg.get_text(), "plain text");
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(alice).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
|
||||
// let bob receive the message
|
||||
let chat_id = bob.create_chat(alice).await.id;
|
||||
let chat_id = bob.create_chat(&alice).await.id;
|
||||
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(msg.chat_id, chat_id);
|
||||
assert_eq!(msg.get_text(), "plain text");
|
||||
assert!(!msg.is_forwarded());
|
||||
assert!(msg.mime_modified);
|
||||
let html = msg.get_id().get_html(bob).await.unwrap().unwrap();
|
||||
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
|
||||
assert!(html.contains("<b>html</b> text"));
|
||||
}
|
||||
|
||||
|
||||
49
src/imap.rs
49
src/imap.rs
@@ -271,14 +271,12 @@ impl Imap {
|
||||
let param = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
let strict_tls = param.strict_tls(proxy_config.is_some());
|
||||
let imap = Self::new(
|
||||
param.imap.clone(),
|
||||
param.imap_password.clone(),
|
||||
proxy_config,
|
||||
param.proxy_config.clone(),
|
||||
¶m.addr,
|
||||
strict_tls,
|
||||
param.strict_tls(),
|
||||
param.oauth2,
|
||||
idle_interrupt_receiver,
|
||||
);
|
||||
@@ -350,11 +348,10 @@ impl Imap {
|
||||
connection_candidate,
|
||||
)
|
||||
.await
|
||||
.context("IMAP failed to connect")
|
||||
{
|
||||
Ok(client) => client,
|
||||
Err(err) => {
|
||||
warn!(context, "{err:#}.");
|
||||
warn!(context, "IMAP failed to connect: {err:#}.");
|
||||
first_error.get_or_insert(err);
|
||||
continue;
|
||||
}
|
||||
@@ -507,7 +504,7 @@ impl Imap {
|
||||
}
|
||||
|
||||
let msgs_fetched = self
|
||||
.fetch_new_messages(context, session, watch_folder, folder_meaning)
|
||||
.fetch_new_messages(context, session, watch_folder, folder_meaning, false)
|
||||
.await
|
||||
.context("fetch_new_messages")?;
|
||||
if msgs_fetched && context.get_config_delete_device_after().await?.is_some() {
|
||||
@@ -535,6 +532,7 @@ impl Imap {
|
||||
session: &mut Session,
|
||||
folder: &str,
|
||||
folder_meaning: FolderMeaning,
|
||||
fetch_existing_msgs: bool,
|
||||
) -> Result<bool> {
|
||||
if should_ignore_folder(context, folder, folder_meaning).await? {
|
||||
info!(context, "Not fetching from {folder:?}.");
|
||||
@@ -551,7 +549,7 @@ impl Imap {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !session.new_mail {
|
||||
if !session.new_mail && !fetch_existing_msgs {
|
||||
info!(context, "No new emails in folder {folder:?}.");
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -560,7 +558,14 @@ impl Imap {
|
||||
let uid_validity = get_uidvalidity(context, folder).await?;
|
||||
let old_uid_next = get_uid_next(context, folder).await?;
|
||||
|
||||
let msgs = session.prefetch(old_uid_next).await.context("prefetch")?;
|
||||
let msgs = if fetch_existing_msgs {
|
||||
session
|
||||
.prefetch_existing_msgs()
|
||||
.await
|
||||
.context("prefetch_existing_msgs")?
|
||||
} else {
|
||||
session.prefetch(old_uid_next).await.context("prefetch")?
|
||||
};
|
||||
let read_cnt = msgs.len();
|
||||
|
||||
let download_limit = context.download_limit().await?;
|
||||
@@ -713,6 +718,7 @@ impl Imap {
|
||||
uids_fetch_in_batch.split_off(0),
|
||||
&uid_message_ids,
|
||||
fetch_partially,
|
||||
fetch_existing_msgs,
|
||||
)
|
||||
.await
|
||||
.context("fetch_many_msgs")?;
|
||||
@@ -777,6 +783,28 @@ impl Imap {
|
||||
.await
|
||||
.context("failed to get recipients from the inbox")?;
|
||||
|
||||
if context.get_config_bool(Config::FetchExistingMsgs).await? {
|
||||
for meaning in [
|
||||
FolderMeaning::Mvbox,
|
||||
FolderMeaning::Inbox,
|
||||
FolderMeaning::Sent,
|
||||
] {
|
||||
let config = match meaning.to_config() {
|
||||
Some(c) => c,
|
||||
None => continue,
|
||||
};
|
||||
if let Some(folder) = context.get_config(config).await? {
|
||||
info!(
|
||||
context,
|
||||
"Fetching existing messages from folder {folder:?}."
|
||||
);
|
||||
self.fetch_new_messages(context, session, &folder, meaning, true)
|
||||
.await
|
||||
.context("could not fetch existing messages")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(context, "Done fetching existing messages.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1303,6 +1331,7 @@ impl Session {
|
||||
/// Returns the last UID fetched successfully and the info about each downloaded message.
|
||||
/// If the message is incorrect or there is a failure to write a message to the database,
|
||||
/// it is skipped and the error is logged.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) async fn fetch_many_msgs(
|
||||
&mut self,
|
||||
context: &Context,
|
||||
@@ -1311,6 +1340,7 @@ impl Session {
|
||||
request_uids: Vec<u32>,
|
||||
uid_message_ids: &BTreeMap<u32, String>,
|
||||
fetch_partially: bool,
|
||||
fetching_existing_messages: bool,
|
||||
) -> Result<(Option<u32>, Vec<ReceivedMsg>)> {
|
||||
let mut last_uid = None;
|
||||
let mut received_msgs = Vec::new();
|
||||
@@ -1444,6 +1474,7 @@ impl Session {
|
||||
body,
|
||||
is_seen,
|
||||
partial,
|
||||
fetching_existing_messages,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cmp;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
@@ -6,6 +7,7 @@ use async_imap::types::Mailbox;
|
||||
use async_imap::Session as ImapSession;
|
||||
use futures::TryStreamExt;
|
||||
|
||||
use crate::constants::DC_FETCH_EXISTING_MSGS_COUNT;
|
||||
use crate::imap::capabilities::Capabilities;
|
||||
use crate::net::session::SessionStream;
|
||||
|
||||
@@ -141,4 +143,33 @@ impl Session {
|
||||
|
||||
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
|
||||
}
|
||||
|
||||
/// Like prefetch(), but not for new messages but existing ones (the DC_FETCH_EXISTING_MSGS_COUNT newest messages)
|
||||
pub(crate) async fn prefetch_existing_msgs(
|
||||
&mut self,
|
||||
) -> Result<Vec<(u32, async_imap::types::Fetch)>> {
|
||||
let exists: i64 = {
|
||||
let mailbox = self.selected_mailbox.as_ref().context("no mailbox")?;
|
||||
mailbox.exists.into()
|
||||
};
|
||||
|
||||
// Fetch last DC_FETCH_EXISTING_MSGS_COUNT (100) messages.
|
||||
// Sequence numbers are sequential. If there are 1000 messages in the inbox,
|
||||
// we can fetch the sequence numbers 900-1000 and get the last 100 messages.
|
||||
let first = cmp::max(1, exists - DC_FETCH_EXISTING_MSGS_COUNT + 1);
|
||||
let set = format!("{first}:{exists}");
|
||||
let mut list = self
|
||||
.fetch(&set, PREFETCH_FLAGS)
|
||||
.await
|
||||
.context("IMAP Could not fetch")?;
|
||||
|
||||
let mut msgs = BTreeMap::new();
|
||||
while let Some(msg) = list.try_next().await? {
|
||||
if let Some(msg_uid) = msg.uid {
|
||||
msgs.insert((msg.internal_date(), msg_uid), msg);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(msgs.into_iter().map(|((_, uid), msg)| (uid, msg)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use anyhow::{bail, ensure, Result};
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::{self, ChatId};
|
||||
use crate::config::Config;
|
||||
use crate::constants::{ASM_BODY, ASM_SUBJECT};
|
||||
use crate::contact::ContactId;
|
||||
use crate::context::Context;
|
||||
use crate::imex::set_self_key;
|
||||
@@ -15,6 +14,7 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::Param;
|
||||
use crate::pgp;
|
||||
use crate::stock_str;
|
||||
use crate::tools::open_file_std;
|
||||
|
||||
/// Initiates key transfer via Autocrypt Setup Message.
|
||||
@@ -39,18 +39,18 @@ pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.param.set(Param::File, setup_file_blob.as_name());
|
||||
msg.param
|
||||
.set(Param::Filename, "autocrypt-setup-message.html");
|
||||
msg.subject = ASM_SUBJECT.to_owned();
|
||||
msg.subject = stock_str::ac_setup_msg_subject(context).await;
|
||||
msg.param
|
||||
.set(Param::MimeType, "application/autocrypt-setup");
|
||||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
|
||||
// Enable BCC-self, because transferring a key
|
||||
// means we have a multi-device setup.
|
||||
context.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
Ok(setup_code)
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
|
||||
);
|
||||
let pgp_msg = encr.replace("-----BEGIN PGP MESSAGE-----", &replacement);
|
||||
|
||||
let msg_subj = ASM_SUBJECT;
|
||||
let msg_body = ASM_BODY.to_string();
|
||||
let msg_subj = stock_str::ac_setup_msg_subject(context).await;
|
||||
let msg_body = stock_str::ac_setup_msg_body(context).await;
|
||||
let msg_body_html = msg_body.replace('\r', "").replace('\n', "<br>");
|
||||
Ok(format!(
|
||||
concat!(
|
||||
@@ -187,6 +187,7 @@ mod tests {
|
||||
|
||||
use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE};
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::stock_str::StockMessage;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use ::pgp::armor::BlockType;
|
||||
|
||||
@@ -212,9 +213,12 @@ mod tests {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_render_setup_file_newline_replace() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_stock_translation(StockMessage::AcSetupMsgBody, "hello\r\nthere".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = render_setup_file(&t, "pw").await.unwrap();
|
||||
println!("{}", &msg);
|
||||
assert!(msg.contains("<p>This is the Autocrypt Setup Message used to transfer your end-to-end setup between clients.<br>"));
|
||||
assert!(msg.contains("<p>hello<br>there</p>"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
|
||||
@@ -45,7 +45,7 @@ use crate::message::Message;
|
||||
use crate::qr::Qr;
|
||||
use crate::stock_str::backup_transfer_msg_body;
|
||||
use crate::tools::{create_id, time, TempPathGuard};
|
||||
use crate::{e2ee, EventType};
|
||||
use crate::EventType;
|
||||
|
||||
use super::{export_backup_stream, export_database, import_backup_stream, DBFILE_BACKUP_NAME};
|
||||
|
||||
@@ -109,11 +109,6 @@ impl BackupProvider {
|
||||
.parent()
|
||||
.context("Context dir not found")?;
|
||||
|
||||
// before we export, make sure the private key exists
|
||||
e2ee::ensure_secret_key_exists(context)
|
||||
.await
|
||||
.context("Cannot create private key or private key not available")?;
|
||||
|
||||
let dbfile = context_dir.join(DBFILE_BACKUP_NAME);
|
||||
if fs::metadata(&dbfile).await.is_ok() {
|
||||
fs::remove_file(&dbfile).await?;
|
||||
|
||||
16
src/key.rs
16
src/key.rs
@@ -456,13 +456,15 @@ impl std::str::FromStr for Fingerprint {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::sync::Arc;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::*;
|
||||
use crate::config::Config;
|
||||
use crate::test_utils::{alice_keypair, TestContext};
|
||||
|
||||
static KEYPAIR: LazyLock<KeyPair> = LazyLock::new(alice_keypair);
|
||||
static KEYPAIR: Lazy<KeyPair> = Lazy::new(alice_keypair);
|
||||
|
||||
#[test]
|
||||
fn test_from_armored_string() {
|
||||
@@ -612,6 +614,16 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD
|
||||
assert_eq!(key, key2);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_existing() {
|
||||
let alice = alice_keypair();
|
||||
let t = TestContext::new_alice().await;
|
||||
let pubkey = load_self_public_key(&t).await.unwrap();
|
||||
assert_eq!(alice.public, pubkey);
|
||||
let seckey = load_self_secret_key(&t).await.unwrap();
|
||||
assert_eq!(alice.secret, seckey);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_load_self_generate_public() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -68,7 +68,7 @@ mod imap;
|
||||
pub mod imex;
|
||||
pub mod key;
|
||||
pub mod location;
|
||||
pub mod login_param;
|
||||
mod login_param;
|
||||
pub mod message;
|
||||
mod mimefactory;
|
||||
pub mod mimeparser;
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::fmt;
|
||||
|
||||
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;
|
||||
@@ -12,11 +11,9 @@ use crate::configure::server_params::{expand_param_vector, ServerParams};
|
||||
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::{Protocol, Provider, UsernamePattern};
|
||||
use crate::net::proxy::ProxyConfig;
|
||||
use crate::provider::{Protocol, Provider, Socket, UsernamePattern};
|
||||
use crate::sql::Sql;
|
||||
use crate::tools::ToOption;
|
||||
|
||||
/// User-entered setting for certificate checks.
|
||||
///
|
||||
@@ -47,7 +44,7 @@ pub enum EnteredCertificateChecks {
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub(crate) enum ConfiguredCertificateChecks {
|
||||
pub enum ConfiguredCertificateChecks {
|
||||
/// Use configuration from the provider database.
|
||||
/// If there is no provider database setting for certificate checks,
|
||||
/// accept invalid certificates.
|
||||
@@ -119,13 +116,15 @@ pub struct EnteredLoginParam {
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: EnteredCertificateChecks,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore)
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
impl EnteredLoginParam {
|
||||
/// Loads entered account settings.
|
||||
pub(crate) async fn load(context: &Context) -> Result<Self> {
|
||||
pub async fn load(context: &Context) -> Result<Self> {
|
||||
let addr = context
|
||||
.get_config(Config::Addr)
|
||||
.await?
|
||||
@@ -197,6 +196,8 @@ impl EnteredLoginParam {
|
||||
.unwrap_or_default();
|
||||
let oauth2 = matches!(server_flags & DC_LP_AUTH_FLAGS, DC_LP_AUTH_OAUTH2);
|
||||
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(EnteredLoginParam {
|
||||
addr,
|
||||
imap: EnteredServerLoginParam {
|
||||
@@ -214,71 +215,10 @@ impl EnteredLoginParam {
|
||||
password: send_pw,
|
||||
},
|
||||
certificate_checks,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves entered account settings,
|
||||
/// so that they can be prefilled if the user wants to configure the server again.
|
||||
pub(crate) async fn save(&self, context: &Context) -> Result<()> {
|
||||
context.set_config(Config::Addr, Some(&self.addr)).await?;
|
||||
|
||||
context
|
||||
.set_config(Config::MailServer, self.imap.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPort, self.imap.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::MailSecurity,
|
||||
self.imap.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailUser, self.imap.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::MailPw, self.imap.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(Config::SendServer, self.smtp.server.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPort, self.smtp.port.to_option().as_deref())
|
||||
.await?;
|
||||
context
|
||||
.set_config(
|
||||
Config::SendSecurity,
|
||||
self.smtp.security.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendUser, self.smtp.user.to_option())
|
||||
.await?;
|
||||
context
|
||||
.set_config(Config::SendPw, self.smtp.password.to_option())
|
||||
.await?;
|
||||
|
||||
context
|
||||
.set_config(
|
||||
Config::ImapCertificateChecks,
|
||||
self.certificate_checks.to_i32().to_option().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let server_flags = if self.oauth2 {
|
||||
Some(DC_LP_AUTH_OAUTH2.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
context
|
||||
.set_config(Config::ServerFlags, server_flags.as_deref())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EnteredLoginParam {
|
||||
@@ -379,7 +319,7 @@ impl TryFrom<Socket> for ConnectionSecurity {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct ConfiguredServerLoginParam {
|
||||
pub struct ConfiguredServerLoginParam {
|
||||
pub connection: ConnectionCandidate,
|
||||
|
||||
/// Username.
|
||||
@@ -417,7 +357,7 @@ pub(crate) async fn prioritize_server_login_params(
|
||||
/// Login parameters saved to the database
|
||||
/// after successful configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ConfiguredLoginParam {
|
||||
pub struct ConfiguredLoginParam {
|
||||
/// `From:` address that was used at the time of configuration.
|
||||
pub addr: String,
|
||||
|
||||
@@ -441,13 +381,15 @@ pub(crate) struct ConfiguredLoginParam {
|
||||
|
||||
pub smtp_password: String,
|
||||
|
||||
/// Proxy configuration.
|
||||
pub proxy_config: Option<ProxyConfig>,
|
||||
|
||||
pub provider: Option<&'static Provider>,
|
||||
|
||||
/// TLS options: whether to allow invalid certificates and/or
|
||||
/// invalid hostnames
|
||||
pub certificate_checks: ConfiguredCertificateChecks,
|
||||
|
||||
/// If true, login via OAUTH2 (not recommended anymore)
|
||||
pub oauth2: bool,
|
||||
}
|
||||
|
||||
@@ -486,7 +428,7 @@ impl ConfiguredLoginParam {
|
||||
/// Load configured account settings from the database.
|
||||
///
|
||||
/// Returns `None` if account is not configured.
|
||||
pub(crate) async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
pub async fn load(context: &Context) -> Result<Option<Self>> {
|
||||
if !context.get_config_bool(Config::Configured).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -739,6 +681,8 @@ impl ConfiguredLoginParam {
|
||||
}];
|
||||
}
|
||||
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
|
||||
Ok(Some(ConfiguredLoginParam {
|
||||
addr,
|
||||
imap,
|
||||
@@ -749,12 +693,13 @@ impl ConfiguredLoginParam {
|
||||
smtp_password: send_pw,
|
||||
certificate_checks,
|
||||
provider,
|
||||
proxy_config,
|
||||
oauth2,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Save this loginparam to the database.
|
||||
pub(crate) async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
|
||||
pub async fn save_as_configured_params(&self, context: &Context) -> Result<()> {
|
||||
context.set_primary_self_addr(&self.addr).await?;
|
||||
|
||||
context
|
||||
@@ -831,11 +776,11 @@ impl ConfiguredLoginParam {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn strict_tls(&self, connected_through_proxy: bool) -> bool {
|
||||
pub fn strict_tls(&self) -> bool {
|
||||
let provider_strict_tls = self.provider.map(|provider| provider.opt.strict_tls);
|
||||
match self.certificate_checks {
|
||||
ConfiguredCertificateChecks::OldAutomatic => {
|
||||
provider_strict_tls.unwrap_or(connected_through_proxy)
|
||||
provider_strict_tls.unwrap_or(self.proxy_config.is_some())
|
||||
}
|
||||
ConfiguredCertificateChecks::Automatic => provider_strict_tls.unwrap_or(true),
|
||||
ConfiguredCertificateChecks::Strict => true,
|
||||
@@ -894,42 +839,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_entered_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let param = EnteredLoginParam {
|
||||
addr: "alice@example.org".to_string(),
|
||||
imap: EnteredServerLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 0,
|
||||
security: Socket::Starttls,
|
||||
user: "".to_string(),
|
||||
password: "foobar".to_string(),
|
||||
},
|
||||
smtp: EnteredServerLoginParam {
|
||||
server: "".to_string(),
|
||||
port: 2947,
|
||||
security: Socket::default(),
|
||||
user: "".to_string(),
|
||||
password: "".to_string(),
|
||||
},
|
||||
certificate_checks: Default::default(),
|
||||
oauth2: false,
|
||||
};
|
||||
param.save(&t).await?;
|
||||
assert_eq!(
|
||||
t.get_config(Config::Addr).await?.unwrap(),
|
||||
"alice@example.org"
|
||||
);
|
||||
assert_eq!(t.get_config(Config::MailPw).await?.unwrap(), "foobar");
|
||||
assert_eq!(t.get_config(Config::SendPw).await?, None);
|
||||
assert_eq!(t.get_config_int(Config::SendPort).await?, 2947);
|
||||
|
||||
assert_eq!(EnteredLoginParam::load(&t).await?, param);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_save_load_login_param() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
@@ -956,6 +865,8 @@ mod tests {
|
||||
}],
|
||||
smtp_user: "".to_string(),
|
||||
smtp_password: "bar".to_string(),
|
||||
// proxy_config is not saved by `save_to_database`, using default value
|
||||
proxy_config: None,
|
||||
provider: None,
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
@@ -1058,6 +969,7 @@ mod tests {
|
||||
],
|
||||
smtp_user: "alice@posteo.de".to_string(),
|
||||
smtp_password: "foobarbaz".to_string(),
|
||||
proxy_config: None,
|
||||
provider: get_provider_by_id("posteo"),
|
||||
certificate_checks: ConfiguredCertificateChecks::Strict,
|
||||
oauth2: false,
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::chat::{send_msg, Chat, ChatId, ChatIdBlocked, ChatVisibility};
|
||||
use crate::chatlist_events;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_MSG_ID_LAST_SPECIAL,
|
||||
Blocked, Chattype, VideochatType, DC_CHAT_ID_TRASH, DC_DESIRED_TEXT_LEN, DC_MSG_ID_LAST_SPECIAL,
|
||||
};
|
||||
use crate::contact::{self, Contact, ContactId};
|
||||
use crate::context::Context;
|
||||
@@ -33,9 +33,10 @@ use crate::reaction::get_msg_reactions;
|
||||
use crate::sql;
|
||||
use crate::summary::Summary;
|
||||
use crate::sync::SyncData;
|
||||
use crate::tools::create_outgoing_rfc724_mid;
|
||||
use crate::tools::{
|
||||
buf_compress, buf_decompress, get_filebytes, get_filemeta, gm2local_offset, read_file,
|
||||
sanitize_filename, time, timestamp_to_str,
|
||||
sanitize_filename, time, timestamp_to_str, truncate,
|
||||
};
|
||||
|
||||
/// Message ID, including reserved IDs.
|
||||
@@ -174,6 +175,15 @@ impl MsgId {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns raw text of a message, used for message info
|
||||
pub async fn rawtext(self, context: &Context) -> Result<String> {
|
||||
Ok(context
|
||||
.sql
|
||||
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?", (self,))
|
||||
.await?
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Returns server foldernames and UIDs of a message, used for message info
|
||||
pub async fn get_info_server_urls(
|
||||
context: &Context,
|
||||
@@ -210,9 +220,12 @@ impl MsgId {
|
||||
/// Returns detailed message information in a multi-line text form.
|
||||
pub async fn get_info(self, context: &Context) -> Result<String> {
|
||||
let msg = Message::load_from_db(context, self).await?;
|
||||
let rawtxt: String = self.rawtext(context).await?;
|
||||
|
||||
let mut ret = String::new();
|
||||
|
||||
let rawtxt = truncate(rawtxt.trim(), DC_DESIRED_TEXT_LEN);
|
||||
|
||||
let fts = timestamp_to_str(msg.get_timestamp());
|
||||
ret += &format!("Sent: {fts}");
|
||||
|
||||
@@ -323,6 +336,9 @@ impl MsgId {
|
||||
if duration != 0 {
|
||||
ret += &format!("Duration: {duration} ms\n",);
|
||||
}
|
||||
if !rawtxt.is_empty() {
|
||||
ret += &format!("\n{rawtxt}\n");
|
||||
}
|
||||
if !msg.rfc724_mid.is_empty() {
|
||||
ret += &format!("\nMessage-ID: {}", msg.rfc724_mid);
|
||||
|
||||
@@ -470,6 +486,7 @@ impl Message {
|
||||
pub fn new(viewtype: Viewtype) -> Self {
|
||||
Message {
|
||||
viewtype,
|
||||
rfc724_mid: create_outgoing_rfc724_mid(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -479,6 +496,7 @@ impl Message {
|
||||
Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text,
|
||||
rfc724_mid: create_outgoing_rfc724_mid(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -936,51 +954,6 @@ impl Message {
|
||||
self.param.get_cmd()
|
||||
}
|
||||
|
||||
/// Return the contact ID of the profile to open when tapping the info message.
|
||||
pub async fn get_info_contact_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
match self.param.get_cmd() {
|
||||
SystemMessage::GroupNameChanged
|
||||
| SystemMessage::GroupImageChanged
|
||||
| SystemMessage::EphemeralTimerChanged => {
|
||||
if self.from_id != ContactId::INFO {
|
||||
Ok(Some(self.from_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
SystemMessage::MemberAddedToGroup | SystemMessage::MemberRemovedFromGroup => {
|
||||
if let Some(contact_i32) = self.param.get_int(Param::ContactAddedRemoved) {
|
||||
let contact_id = ContactId::new(contact_i32.try_into()?);
|
||||
if contact_id == ContactId::SELF
|
||||
|| Contact::real_exists_by_id(context, contact_id).await?
|
||||
{
|
||||
Ok(Some(contact_id))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
SystemMessage::AutocryptSetupMessage
|
||||
| SystemMessage::SecurejoinMessage
|
||||
| SystemMessage::LocationStreamingEnabled
|
||||
| SystemMessage::LocationOnly
|
||||
| SystemMessage::ChatProtectionEnabled
|
||||
| SystemMessage::ChatProtectionDisabled
|
||||
| SystemMessage::InvalidUnencryptedMail
|
||||
| SystemMessage::SecurejoinWait
|
||||
| SystemMessage::SecurejoinWaitTimeout
|
||||
| SystemMessage::MultiDeviceSync
|
||||
| SystemMessage::WebxdcStatusUpdate
|
||||
| SystemMessage::WebxdcInfoMessage
|
||||
| SystemMessage::IrohNodeAddr
|
||||
| SystemMessage::Unknown => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the message is a system message.
|
||||
pub fn is_system_message(&self) -> bool {
|
||||
let cmd = self.param.get_cmd();
|
||||
@@ -1762,10 +1735,6 @@ pub async fn delete_msgs_ex(
|
||||
!delete_for_all || msg.from_id == ContactId::SELF,
|
||||
"Can delete only own messages for others"
|
||||
);
|
||||
ensure!(
|
||||
!delete_for_all || msg.get_showpadlock(),
|
||||
"Cannot request deletion of unencrypted message for others"
|
||||
);
|
||||
|
||||
modified_chat_ids.insert(msg.chat_id);
|
||||
deleted_rfc724_mid.push(msg.rfc724_mid.clone());
|
||||
@@ -2192,9 +2161,11 @@ pub(crate) async fn get_by_rfc724_mids(
|
||||
let mut latest = None;
|
||||
for id in mids.iter().rev() {
|
||||
let Some((msg_id, _)) = rfc724_mid_exists(context, id).await? else {
|
||||
info!(context, "continue msgid");
|
||||
continue;
|
||||
};
|
||||
let Some(msg) = Message::load_from_db_optional(context, msg_id).await? else {
|
||||
info!(context, "continue msg");
|
||||
continue;
|
||||
};
|
||||
if msg.download_state == DownloadState::Done {
|
||||
|
||||
@@ -147,8 +147,6 @@ async fn test_quote() {
|
||||
|
||||
let mut msg = Message::new_text("Quoted message".to_string());
|
||||
|
||||
// Send message, so it gets a Message-Id.
|
||||
assert!(msg.rfc724_mid.is_empty());
|
||||
let msg_id = chat::send_msg(ctx, chat.id, &mut msg).await.unwrap();
|
||||
let msg = Message::load_from_db(ctx, msg_id).await.unwrap();
|
||||
assert!(!msg.rfc724_mid.is_empty());
|
||||
@@ -358,6 +356,7 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
let sent1 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg1 = bob.recv_msg(&sent1).await;
|
||||
let bob_chat_id = msg1.chat_id;
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let sent2 = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
let msg2 = bob.recv_msg(&sent2).await;
|
||||
assert_eq!(msg1.chat_id, msg2.chat_id);
|
||||
@@ -380,9 +379,11 @@ async fn test_markseen_msgs() -> Result<()> {
|
||||
|
||||
// bob sends to alice,
|
||||
// alice knows bob and messages appear in normal chat
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let msg1 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let msg2 = alice
|
||||
.recv_msg(&bob.send_msg(bob_chat_id, &mut msg).await)
|
||||
.await;
|
||||
|
||||
@@ -13,17 +13,14 @@ 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;
|
||||
use crate::constants::ASM_SUBJECT;
|
||||
use crate::constants::{Chattype, DC_FROM_HANDSHAKE};
|
||||
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};
|
||||
@@ -89,15 +86,13 @@ pub struct MimeFactory {
|
||||
/// Vector of pairs of recipient name and address that goes into the `To` field.
|
||||
///
|
||||
/// The list of actual message recipient addresses may be different,
|
||||
/// e.g. if members are hidden for broadcast lists
|
||||
/// or if the keys for some recipients are missing
|
||||
/// and encrypted message cannot be sent to them.
|
||||
/// e.g. if members are hidden for broadcast lists.
|
||||
to: Vec<(String, String)>,
|
||||
|
||||
/// 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 +128,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
|
||||
@@ -188,6 +184,9 @@ impl MimeFactory {
|
||||
let mut req_mdn = false;
|
||||
|
||||
if chat.is_self_talk() {
|
||||
if msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||||
recipients.push(from_addr.to_string());
|
||||
}
|
||||
to.push((from_displayname.to_string(), from_addr.to_string()));
|
||||
} else if chat.is_mailing_list() {
|
||||
let list_post = chat
|
||||
@@ -417,10 +416,12 @@ impl MimeFactory {
|
||||
|
||||
fn should_force_plaintext(&self) -> bool {
|
||||
match &self.loaded {
|
||||
Loaded::Message { msg, .. } => msg
|
||||
.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default(),
|
||||
Loaded::Message { chat, msg } => {
|
||||
msg.param
|
||||
.get_bool(Param::ForcePlaintext)
|
||||
.unwrap_or_default()
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
}
|
||||
Loaded::Mdn { .. } => false,
|
||||
}
|
||||
}
|
||||
@@ -434,6 +435,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,9 +783,13 @@ 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?);
|
||||
&& encrypt_helper
|
||||
.should_encrypt(context, e2ee_guaranteed, &peerstates)
|
||||
.await?;
|
||||
let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
|
||||
msg.param.get_cmd() == SystemMessage::SecurejoinMessage
|
||||
} else {
|
||||
@@ -920,78 +954,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.
|
||||
@@ -1013,23 +985,14 @@ impl MimeFactory {
|
||||
Loaded::Mdn { .. } => true,
|
||||
};
|
||||
|
||||
let (encryption_keyring, missing_key_addresses) =
|
||||
encrypt_helper.encryption_keyring(context, verified, &peerstates)?;
|
||||
|
||||
// XXX: additional newline is needed
|
||||
// to pass filtermail at
|
||||
// <https://github.com/deltachat/chatmail/blob/4d915f9800435bf13057d41af8d708abd34dbfa8/chatmaild/src/chatmaild/filtermail.py#L84-L86>
|
||||
let encrypted = encrypt_helper
|
||||
.encrypt(context, encryption_keyring, message, compress)
|
||||
.encrypt(context, verified, message, peerstates, compress)
|
||||
.await?
|
||||
+ "\n";
|
||||
|
||||
// Remove recipients for which the key is missing.
|
||||
if !missing_key_addresses.is_empty() {
|
||||
self.recipients
|
||||
.retain(|addr| !missing_key_addresses.contains(addr));
|
||||
}
|
||||
|
||||
// Set the appropriate Content-Type for the outer message
|
||||
MimePart::new(
|
||||
"multipart/encrypted; protocol=\"application/pgp-encrypted\"",
|
||||
@@ -1135,6 +1098,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,
|
||||
@@ -1335,7 +1299,7 @@ impl MimeFactory {
|
||||
mail_builder::headers::raw::Raw::new("v1").into(),
|
||||
));
|
||||
|
||||
placeholdertext = Some(ASM_SUBJECT.to_string());
|
||||
placeholdertext = Some(stock_str::ac_setup_msg_body(context).await);
|
||||
}
|
||||
SystemMessage::SecurejoinMessage => {
|
||||
let step = msg.param.get(Param::Arg).unwrap_or_default();
|
||||
|
||||
@@ -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");
|
||||
@@ -728,6 +727,7 @@ async fn test_selfavatar_unencrypted_signed() {
|
||||
.is_some());
|
||||
|
||||
// if another message is sent, that one must not contain the avatar
|
||||
let mut msg = Message::new_text("this is the text!".to_string());
|
||||
let sent_msg = t.send_msg(chat.id, &mut msg).await;
|
||||
let mut payload = sent_msg.payload().splitn(4, "\r\n\r\n");
|
||||
|
||||
@@ -773,24 +773,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 +797,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");
|
||||
|
||||
@@ -345,13 +345,6 @@ impl MimeMessage {
|
||||
}
|
||||
|
||||
decrypted_msg = Some(msg);
|
||||
|
||||
timestamp_sent = Self::get_timestamp_sent(
|
||||
&decrypted_mail.headers,
|
||||
timestamp_sent,
|
||||
timestamp_rcvd,
|
||||
);
|
||||
|
||||
if let Some(protected_aheader_value) = decrypted_mail
|
||||
.headers
|
||||
.get_header_value(HeaderDef::Autocrypt)
|
||||
@@ -423,6 +416,8 @@ impl MimeMessage {
|
||||
content
|
||||
});
|
||||
if let (Ok(mail), true) = (mail, encrypted) {
|
||||
timestamp_sent =
|
||||
Self::get_timestamp_sent(&mail.headers, timestamp_sent, timestamp_rcvd);
|
||||
if !signatures.is_empty() {
|
||||
// Remove unsigned opportunistically protected headers from messages considered
|
||||
// Autocrypt-encrypted / displayed with padlock.
|
||||
|
||||
@@ -44,13 +44,13 @@ use anyhow::{Context as _, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
use tokio::net::lookup_host;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use super::load_connection_timestamp;
|
||||
use crate::context::Context;
|
||||
use crate::tools::time;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// Inserts entry into DNS cache
|
||||
/// or updates existing one with a new timestamp.
|
||||
@@ -90,8 +90,8 @@ pub(crate) async fn prune_dns_cache(context: &Context) -> Result<()> {
|
||||
/// <https://docs.rs/tokio/1.40.0/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use>
|
||||
/// and
|
||||
/// <https://stackoverflow.com/questions/63712823/why-do-i-get-a-deadlock-when-using-tokio-with-a-stdsyncmutex>.
|
||||
static LOOKUP_HOST_CACHE: LazyLock<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
|
||||
LazyLock::new(Default::default);
|
||||
static LOOKUP_HOST_CACHE: Lazy<parking_lot::RwLock<HashMap<String, Vec<IpAddr>>>> =
|
||||
Lazy::new(Default::default);
|
||||
|
||||
/// Wrapper for `lookup_host` that returns IP addresses.
|
||||
async fn lookup_ips(host: impl tokio::net::ToSocketAddrs) -> Result<impl Iterator<Item = IpAddr>> {
|
||||
@@ -229,7 +229,7 @@ pub(crate) async fn update_connect_timestamp(
|
||||
///
|
||||
/// See <https://support.delta.chat/t/no-dns-resolution-result/2778> and
|
||||
/// <https://github.com/deltachat/deltachat-core-rust/issues/4920> for reasons.
|
||||
static DNS_PRELOAD: LazyLock<HashMap<&'static str, Vec<IpAddr>>> = LazyLock::new(|| {
|
||||
static DNS_PRELOAD: Lazy<HashMap<&'static str, Vec<IpAddr>>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"mail.sangham.net",
|
||||
|
||||
@@ -154,21 +154,18 @@ impl Socks5Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the proxy through which all traffic
|
||||
/// (except for iroh p2p connections)
|
||||
/// will be sent.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ProxyConfig {
|
||||
/// HTTP proxy.
|
||||
// HTTP proxy.
|
||||
Http(HttpConfig),
|
||||
|
||||
/// HTTPS proxy.
|
||||
// HTTPS proxy.
|
||||
Https(HttpConfig),
|
||||
|
||||
/// SOCKS5 proxy.
|
||||
// SOCKS5 proxy.
|
||||
Socks5(Socks5Config),
|
||||
|
||||
/// Shadowsocks proxy.
|
||||
// Shadowsocks proxy.
|
||||
Shadowsocks(ShadowsocksConfig),
|
||||
}
|
||||
|
||||
@@ -249,7 +246,7 @@ where
|
||||
|
||||
impl ProxyConfig {
|
||||
/// Creates a new proxy configuration by parsing given proxy URL.
|
||||
pub fn from_url(url: &str) -> Result<Self> {
|
||||
pub(crate) fn from_url(url: &str) -> Result<Self> {
|
||||
let url = Url::parse(url).context("Cannot parse proxy URL")?;
|
||||
match url.scheme() {
|
||||
"http" => {
|
||||
@@ -308,7 +305,7 @@ impl ProxyConfig {
|
||||
///
|
||||
/// This function can be used to normalize proxy URL
|
||||
/// by parsing it and serializing back.
|
||||
pub fn to_url(&self) -> String {
|
||||
pub(crate) fn to_url(&self) -> String {
|
||||
match self {
|
||||
Self::Http(http_config) => http_config.to_url("http"),
|
||||
Self::Https(http_config) => http_config.to_url("https"),
|
||||
@@ -394,7 +391,7 @@ impl ProxyConfig {
|
||||
|
||||
/// If `load_dns_cache` is true, loads cached DNS resolution results.
|
||||
/// Use this only if the connection is going to be protected with TLS checks.
|
||||
pub(crate) async fn connect(
|
||||
pub async fn connect(
|
||||
&self,
|
||||
context: &Context,
|
||||
target_host: &str,
|
||||
|
||||
@@ -215,9 +215,6 @@ pub enum Param {
|
||||
|
||||
/// For messages: Message text was edited.
|
||||
IsEdited = b'L',
|
||||
|
||||
/// For info messages: Contact ID in added or removed to a group.
|
||||
ContactAddedRemoved = b'5',
|
||||
}
|
||||
|
||||
/// An object for handling key=value parameter lists.
|
||||
|
||||
@@ -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.
|
||||
@@ -726,7 +748,6 @@ impl Peerstate {
|
||||
Some(timestamp),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
128
src/pgp.rs
128
src/pgp.rs
@@ -1,6 +1,7 @@
|
||||
//! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp).
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
@@ -13,8 +14,8 @@ use pgp::composed::{
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, StringToKey};
|
||||
use rand::thread_rng;
|
||||
use pgp::types::{CompressionAlgorithm, PublicKeyTrait, SignatureBytes, StringToKey};
|
||||
use rand::{thread_rng, CryptoRng, Rng};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
@@ -30,6 +31,95 @@ const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AE
|
||||
/// Preferred cryptographic hash.
|
||||
const HASH_ALGORITHM: HashAlgorithm = HashAlgorithm::SHA2_256;
|
||||
|
||||
/// A wrapper for rPGP public key types
|
||||
#[derive(Debug)]
|
||||
enum SignedPublicKeyOrSubkey<'a> {
|
||||
Key(&'a SignedPublicKey),
|
||||
Subkey(&'a SignedPublicSubKey),
|
||||
}
|
||||
|
||||
impl PublicKeyTrait for SignedPublicKeyOrSubkey<'_> {
|
||||
fn version(&self) -> pgp::types::KeyVersion {
|
||||
match self {
|
||||
Self::Key(k) => k.version(),
|
||||
Self::Subkey(k) => k.version(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fingerprint(&self) -> pgp::types::Fingerprint {
|
||||
match self {
|
||||
Self::Key(k) => k.fingerprint(),
|
||||
Self::Subkey(k) => k.fingerprint(),
|
||||
}
|
||||
}
|
||||
|
||||
fn key_id(&self) -> pgp::types::KeyId {
|
||||
match self {
|
||||
Self::Key(k) => k.key_id(),
|
||||
Self::Subkey(k) => k.key_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> pgp::crypto::public_key::PublicKeyAlgorithm {
|
||||
match self {
|
||||
Self::Key(k) => k.algorithm(),
|
||||
Self::Subkey(k) => k.algorithm(),
|
||||
}
|
||||
}
|
||||
|
||||
fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
|
||||
match self {
|
||||
Self::Key(k) => k.created_at(),
|
||||
Self::Subkey(k) => k.created_at(),
|
||||
}
|
||||
}
|
||||
|
||||
fn expiration(&self) -> Option<u16> {
|
||||
match self {
|
||||
Self::Key(k) => k.expiration(),
|
||||
Self::Subkey(k) => k.expiration(),
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_signature(
|
||||
&self,
|
||||
hash: HashAlgorithm,
|
||||
data: &[u8],
|
||||
sig: &SignatureBytes,
|
||||
) -> pgp::errors::Result<()> {
|
||||
match self {
|
||||
Self::Key(k) => k.verify_signature(hash, data, sig),
|
||||
Self::Subkey(k) => k.verify_signature(hash, data, sig),
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt<R: Rng + CryptoRng>(
|
||||
&self,
|
||||
rng: R,
|
||||
plain: &[u8],
|
||||
typ: pgp::types::EskType,
|
||||
) -> pgp::errors::Result<pgp::types::PkeskBytes> {
|
||||
match self {
|
||||
Self::Key(k) => k.encrypt(rng, plain, typ),
|
||||
Self::Subkey(k) => k.encrypt(rng, plain, typ),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_for_hashing(&self, writer: &mut impl io::Write) -> pgp::errors::Result<()> {
|
||||
match self {
|
||||
Self::Key(k) => k.serialize_for_hashing(writer),
|
||||
Self::Subkey(k) => k.serialize_for_hashing(writer),
|
||||
}
|
||||
}
|
||||
|
||||
fn public_params(&self) -> &pgp::types::PublicParams {
|
||||
match self {
|
||||
Self::Key(k) => k.public_params(),
|
||||
Self::Subkey(k) => k.public_params(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Split data from PGP Armored Data as defined in <https://tools.ietf.org/html/rfc4880#section-6.2>.
|
||||
///
|
||||
/// Returns (type, headers, base64 encoded body).
|
||||
@@ -146,15 +236,28 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
|
||||
Ok(key_pair)
|
||||
}
|
||||
|
||||
/// Selects a subkey of the public key to use for encryption.
|
||||
/// Select public key or subkey to use for encryption.
|
||||
///
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
/// First, tries to use subkeys. If none of the subkeys are suitable
|
||||
/// for encryption, tries to use primary key. Returns `None` if the public
|
||||
/// key cannot be used for encryption.
|
||||
///
|
||||
/// TODO: take key flags and expiration dates into account
|
||||
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<SignedPublicKeyOrSubkey> {
|
||||
key.public_subkeys
|
||||
.iter()
|
||||
.find(|subkey| subkey.is_encryption_key())
|
||||
.map_or_else(
|
||||
|| {
|
||||
// No usable subkey found, try primary key
|
||||
if key.is_encryption_key() {
|
||||
Some(SignedPublicKeyOrSubkey::Key(key))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
|subkey| Some(SignedPublicKeyOrSubkey::Subkey(subkey)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Encrypts `plain` text using `public_keys_for_encryption`
|
||||
@@ -169,10 +272,11 @@ pub async fn pk_encrypt(
|
||||
|
||||
Handle::current()
|
||||
.spawn_blocking(move || {
|
||||
let pkeys: Vec<&SignedPublicSubKey> = public_keys_for_encryption
|
||||
let pkeys: Vec<SignedPublicKeyOrSubkey> = public_keys_for_encryption
|
||||
.iter()
|
||||
.filter_map(select_pk_for_encryption)
|
||||
.collect();
|
||||
let pkeys_refs: Vec<&SignedPublicKeyOrSubkey> = pkeys.iter().collect();
|
||||
|
||||
let mut rng = thread_rng();
|
||||
|
||||
@@ -183,9 +287,13 @@ pub async fn pk_encrypt(
|
||||
} else {
|
||||
signed_msg
|
||||
};
|
||||
compressed_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
|
||||
compressed_msg.encrypt_to_keys_seipdv1(
|
||||
&mut rng,
|
||||
SYMMETRIC_KEY_ALGORITHM,
|
||||
&pkeys_refs,
|
||||
)?
|
||||
} else {
|
||||
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys)?
|
||||
lit_msg.encrypt_to_keys_seipdv1(&mut rng, SYMMETRIC_KEY_ALGORITHM, &pkeys_refs)?
|
||||
};
|
||||
|
||||
let encoded_msg = encrypted_msg.to_armored_string(Default::default())?;
|
||||
@@ -316,7 +424,7 @@ pub async fn symm_decrypt<T: std::io::Read + std::io::Seek>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::OnceCell;
|
||||
|
||||
use super::*;
|
||||
@@ -394,7 +502,7 @@ mod tests {
|
||||
static CLEARTEXT: &[u8] = b"This is a test";
|
||||
|
||||
/// Initialised [TestKeys] for tests.
|
||||
static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
|
||||
static KEYS: Lazy<TestKeys> = Lazy::new(TestKeys::new);
|
||||
|
||||
static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
|
||||
static CTEXT_UNSIGNED: OnceCell<String> = OnceCell::const_new();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Handle plain text together with some attributes.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::simplify::remove_message_footer;
|
||||
|
||||
@@ -25,10 +25,10 @@ impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());
|
||||
|
||||
static LINKIFY_URL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
||||
static LINKIFY_URL_RE: Lazy<regex::Regex> = Lazy::new(|| {
|
||||
regex::Regex::new(r"\b((http|https|ftp|ftps):[\w.,:;$/@!?&%\-~=#+]+)").unwrap()
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::provider::{
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
// 163.md: 163.com
|
||||
static P_163: Provider = Provider {
|
||||
@@ -2406,85 +2406,84 @@ pub(crate) static PROVIDER_DATA: [(&str, &Provider); 533] = [
|
||||
("zoho.com", &P_ZOHO),
|
||||
];
|
||||
|
||||
pub(crate) static PROVIDER_IDS: LazyLock<HashMap<&'static str, &'static Provider>> =
|
||||
LazyLock::new(|| {
|
||||
HashMap::from([
|
||||
("163", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun", &P_ALIYUN),
|
||||
("aol", &P_AOL),
|
||||
("arcor.de", &P_ARCOR_DE),
|
||||
("autistici.org", &P_AUTISTICI_ORG),
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
("espiv.net", &P_ESPIV_NET),
|
||||
("example.com", &P_EXAMPLE_COM),
|
||||
("fastmail", &P_FASTMAIL),
|
||||
("firemail.de", &P_FIREMAIL_DE),
|
||||
("five.chat", &P_FIVE_CHAT),
|
||||
("freenet.de", &P_FREENET_DE),
|
||||
("gmail", &P_GMAIL),
|
||||
("gmx.net", &P_GMX_NET),
|
||||
("hermes.radio", &P_HERMES_RADIO),
|
||||
("hey.com", &P_HEY_COM),
|
||||
("i.ua", &P_I_UA),
|
||||
("i3.net", &P_I3_NET),
|
||||
("icloud", &P_ICLOUD),
|
||||
("infomaniak.com", &P_INFOMANIAK_COM),
|
||||
("kolst.com", &P_KOLST_COM),
|
||||
("kontent.com", &P_KONTENT_COM),
|
||||
("mail.com", &P_MAIL_COM),
|
||||
("mail.de", &P_MAIL_DE),
|
||||
("mail.ru", &P_MAIL_RU),
|
||||
("mail2tor", &P_MAIL2TOR),
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("migadu", &P_MIGADU),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
("ouvaton.coop", &P_OUVATON_COOP),
|
||||
("posteo", &P_POSTEO),
|
||||
("protonmail", &P_PROTONMAIL),
|
||||
("purelymail.com", &P_PURELYMAIL_COM),
|
||||
("qq", &P_QQ),
|
||||
("rambler.ru", &P_RAMBLER_RU),
|
||||
("riseup.net", &P_RISEUP_NET),
|
||||
("rogers.com", &P_ROGERS_COM),
|
||||
("sonic", &P_SONIC),
|
||||
("stinpriza.net", &P_STINPRIZA_NET),
|
||||
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
|
||||
("systemli.org", &P_SYSTEMLI_ORG),
|
||||
("t-online", &P_T_ONLINE),
|
||||
("testrun", &P_TESTRUN),
|
||||
("tiscali.it", &P_TISCALI_IT),
|
||||
("tutanota", &P_TUTANOTA),
|
||||
("ukr.net", &P_UKR_NET),
|
||||
("undernet.uy", &P_UNDERNET_UY),
|
||||
("vfemail", &P_VFEMAIL),
|
||||
("vivaldi", &P_VIVALDI),
|
||||
("vk.com", &P_VK_COM),
|
||||
("vodafone.de", &P_VODAFONE_DE),
|
||||
("web.de", &P_WEB_DE),
|
||||
("wkpb.de", &P_WKPB_DE),
|
||||
("yahoo", &P_YAHOO),
|
||||
("yandex.ru", &P_YANDEX_RU),
|
||||
("yggmail", &P_YGGMAIL),
|
||||
("ziggo.nl", &P_ZIGGO_NL),
|
||||
("zoho", &P_ZOHO),
|
||||
])
|
||||
});
|
||||
pub(crate) static PROVIDER_IDS: Lazy<HashMap<&'static str, &'static Provider>> = Lazy::new(|| {
|
||||
HashMap::from([
|
||||
("163", &P_163),
|
||||
("aktivix.org", &P_AKTIVIX_ORG),
|
||||
("aliyun", &P_ALIYUN),
|
||||
("aol", &P_AOL),
|
||||
("arcor.de", &P_ARCOR_DE),
|
||||
("autistici.org", &P_AUTISTICI_ORG),
|
||||
("blindzeln.org", &P_BLINDZELN_ORG),
|
||||
("bluewin.ch", &P_BLUEWIN_CH),
|
||||
("buzon.uy", &P_BUZON_UY),
|
||||
("chello.at", &P_CHELLO_AT),
|
||||
("comcast", &P_COMCAST),
|
||||
("daleth.cafe", &P_DALETH_CAFE),
|
||||
("dismail.de", &P_DISMAIL_DE),
|
||||
("disroot", &P_DISROOT),
|
||||
("e.email", &P_E_EMAIL),
|
||||
("espiv.net", &P_ESPIV_NET),
|
||||
("example.com", &P_EXAMPLE_COM),
|
||||
("fastmail", &P_FASTMAIL),
|
||||
("firemail.de", &P_FIREMAIL_DE),
|
||||
("five.chat", &P_FIVE_CHAT),
|
||||
("freenet.de", &P_FREENET_DE),
|
||||
("gmail", &P_GMAIL),
|
||||
("gmx.net", &P_GMX_NET),
|
||||
("hermes.radio", &P_HERMES_RADIO),
|
||||
("hey.com", &P_HEY_COM),
|
||||
("i.ua", &P_I_UA),
|
||||
("i3.net", &P_I3_NET),
|
||||
("icloud", &P_ICLOUD),
|
||||
("infomaniak.com", &P_INFOMANIAK_COM),
|
||||
("kolst.com", &P_KOLST_COM),
|
||||
("kontent.com", &P_KONTENT_COM),
|
||||
("mail.com", &P_MAIL_COM),
|
||||
("mail.de", &P_MAIL_DE),
|
||||
("mail.ru", &P_MAIL_RU),
|
||||
("mail2tor", &P_MAIL2TOR),
|
||||
("mailbox.org", &P_MAILBOX_ORG),
|
||||
("mailo.com", &P_MAILO_COM),
|
||||
("mehl.cloud", &P_MEHL_CLOUD),
|
||||
("mehl.store", &P_MEHL_STORE),
|
||||
("migadu", &P_MIGADU),
|
||||
("nauta.cu", &P_NAUTA_CU),
|
||||
("naver", &P_NAVER),
|
||||
("nine.testrun.org", &P_NINE_TESTRUN_ORG),
|
||||
("nubo.coop", &P_NUBO_COOP),
|
||||
("outlook.com", &P_OUTLOOK_COM),
|
||||
("ouvaton.coop", &P_OUVATON_COOP),
|
||||
("posteo", &P_POSTEO),
|
||||
("protonmail", &P_PROTONMAIL),
|
||||
("purelymail.com", &P_PURELYMAIL_COM),
|
||||
("qq", &P_QQ),
|
||||
("rambler.ru", &P_RAMBLER_RU),
|
||||
("riseup.net", &P_RISEUP_NET),
|
||||
("rogers.com", &P_ROGERS_COM),
|
||||
("sonic", &P_SONIC),
|
||||
("stinpriza.net", &P_STINPRIZA_NET),
|
||||
("systemausfall.org", &P_SYSTEMAUSFALL_ORG),
|
||||
("systemli.org", &P_SYSTEMLI_ORG),
|
||||
("t-online", &P_T_ONLINE),
|
||||
("testrun", &P_TESTRUN),
|
||||
("tiscali.it", &P_TISCALI_IT),
|
||||
("tutanota", &P_TUTANOTA),
|
||||
("ukr.net", &P_UKR_NET),
|
||||
("undernet.uy", &P_UNDERNET_UY),
|
||||
("vfemail", &P_VFEMAIL),
|
||||
("vivaldi", &P_VIVALDI),
|
||||
("vk.com", &P_VK_COM),
|
||||
("vodafone.de", &P_VODAFONE_DE),
|
||||
("web.de", &P_WEB_DE),
|
||||
("wkpb.de", &P_WKPB_DE),
|
||||
("yahoo", &P_YAHOO),
|
||||
("yandex.ru", &P_YANDEX_RU),
|
||||
("yggmail", &P_YGGMAIL),
|
||||
("ziggo.nl", &P_ZIGGO_NL),
|
||||
("zoho", &P_ZOHO),
|
||||
])
|
||||
});
|
||||
|
||||
pub static _PROVIDER_UPDATED: LazyLock<chrono::NaiveDate> =
|
||||
LazyLock::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
|
||||
pub static _PROVIDER_UPDATED: Lazy<chrono::NaiveDate> =
|
||||
Lazy::new(|| chrono::NaiveDate::from_ymd_opt(2024, 9, 13).unwrap());
|
||||
|
||||
12
src/qr.rs
12
src/qr.rs
@@ -2,11 +2,11 @@
|
||||
|
||||
mod dclogin_scheme;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _, Result};
|
||||
pub use dclogin_scheme::LoginOptions;
|
||||
use deltachat_contact_tools::{addr_normalize, may_be_valid_addr, ContactAddress};
|
||||
use once_cell::sync::Lazy;
|
||||
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -695,7 +695,7 @@ struct CreateAccountErrorResponse {
|
||||
/// take a qr of the type DC_QR_ACCOUNT, parse it's parameters,
|
||||
/// download additional information from the contained url and set the parameters.
|
||||
/// on success, a configure::configure() should be able to log in to the account
|
||||
pub(crate) async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
async fn set_account_from_qr(context: &Context, qr: &str) -> Result<()> {
|
||||
let url_str = qr
|
||||
.get(DCACCOUNT_SCHEME.len()..)
|
||||
.context("Invalid DCACCOUNT scheme")?;
|
||||
@@ -915,10 +915,10 @@ async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
|
||||
Qr::from_address(context, name, &addr, None).await
|
||||
}
|
||||
|
||||
static VCARD_NAME_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: LazyLock<regex::Regex> =
|
||||
LazyLock::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
static VCARD_NAME_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"(?m)^N:([^;]*);([^;\n]*)").unwrap());
|
||||
static VCARD_EMAIL_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"(?m)^EMAIL([^:\n]*):([^;\n]*)").unwrap());
|
||||
|
||||
/// Extract address for the vcard scheme.
|
||||
///
|
||||
|
||||
@@ -731,9 +731,8 @@ Here's my footer -- bob@example.net"
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_reaction_summary() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
alice.set_config(Config::Displayname, Some("ALICE")).await?;
|
||||
bob.set_config(Config::Displayname, Some("BOB")).await?;
|
||||
let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
@@ -900,6 +899,7 @@ Here's my footer -- bob@example.net"
|
||||
msg_header.as_bytes(),
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
@@ -930,6 +930,7 @@ Here's my footer -- bob@example.net"
|
||||
msg_full.as_bytes(),
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::iter;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
@@ -10,6 +9,7 @@ use deltachat_contact_tools::{addr_cmp, may_be_valid_addr, sanitize_single_line,
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use mailparse::SingleInfo;
|
||||
use num_traits::FromPrimitive;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
@@ -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,
|
||||
@@ -99,11 +98,12 @@ pub async fn receive_imf(
|
||||
head.as_bytes(),
|
||||
seen,
|
||||
Some(imf_raw.len().try_into()?),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None).await
|
||||
receive_imf_from_inbox(context, &rfc724_mid, imf_raw, seen, None, false).await
|
||||
}
|
||||
|
||||
/// Emulates reception of a message from "INBOX".
|
||||
@@ -116,6 +116,7 @@ pub(crate) async fn receive_imf_from_inbox(
|
||||
imf_raw: &[u8],
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
fetching_existing_messages: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
receive_imf_inner(
|
||||
context,
|
||||
@@ -126,6 +127,7 @@ pub(crate) async fn receive_imf_from_inbox(
|
||||
imf_raw,
|
||||
seen,
|
||||
is_partial_download,
|
||||
fetching_existing_messages,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -169,6 +171,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
imf_raw: &[u8],
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
fetching_existing_messages: bool,
|
||||
) -> Result<Option<ReceivedMsg>> {
|
||||
if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
|
||||
info!(
|
||||
@@ -402,11 +405,8 @@ pub(crate) async fn receive_imf_inner(
|
||||
received_msg = None;
|
||||
}
|
||||
|
||||
let verified_encryption = has_verified_encryption(&mime_parser, from_id)?;
|
||||
|
||||
if verified_encryption == VerifiedEncryption::Verified {
|
||||
mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
|
||||
}
|
||||
let verified_encryption =
|
||||
has_verified_encryption(context, &mime_parser, from_id, &to_ids).await?;
|
||||
|
||||
if verified_encryption == VerifiedEncryption::Verified
|
||||
&& mime_parser.get_header(HeaderDef::ChatVerified).is_some()
|
||||
@@ -441,6 +441,7 @@ pub(crate) async fn receive_imf_inner(
|
||||
seen,
|
||||
is_partial_download,
|
||||
replace_msg_id,
|
||||
fetching_existing_messages,
|
||||
prevent_rename,
|
||||
verified_encryption,
|
||||
)
|
||||
@@ -453,25 +454,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?;
|
||||
}
|
||||
}
|
||||
@@ -717,10 +715,13 @@ async fn add_parts(
|
||||
seen: bool,
|
||||
is_partial_download: Option<u32>,
|
||||
mut replace_msg_id: Option<MsgId>,
|
||||
fetching_existing_messages: bool,
|
||||
prevent_rename: bool,
|
||||
verified_encryption: VerifiedEncryption,
|
||||
) -> Result<ReceivedMsg> {
|
||||
let is_bot = context.get_config_bool(Config::Bot).await?;
|
||||
// Bots handle existing messages the same way as new ones.
|
||||
let fetching_existing_messages = fetching_existing_messages && !is_bot;
|
||||
let rfc724_mid_orig = &mime_parser
|
||||
.get_rfc724_mid()
|
||||
.unwrap_or(rfc724_mid.to_string());
|
||||
@@ -763,10 +764,8 @@ async fn add_parts(
|
||||
let allow_creation;
|
||||
if mime_parser.is_system_message != SystemMessage::AutocryptSetupMessage
|
||||
&& is_dc_message == MessengerMessage::No
|
||||
&& !context.get_config_bool(Config::IsChatmail).await?
|
||||
{
|
||||
// the message is a classic email in a classic profile
|
||||
// (in chatmail profiles, we always show all messages, because shared dc-mua usage is not supported)
|
||||
// this message is a classic email not a chat-message nor a reply to one
|
||||
match show_emails {
|
||||
ShowEmails::Off => {
|
||||
info!(context, "Classical email not shown (TRASH).");
|
||||
@@ -1041,7 +1040,11 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
state = if seen || is_mdn || chat_id_blocked == Blocked::Yes || group_changes.silent
|
||||
state = if seen
|
||||
|| fetching_existing_messages
|
||||
|| is_mdn
|
||||
|| chat_id_blocked == Blocked::Yes
|
||||
|| group_changes.silent
|
||||
// No check for `hidden` because only reactions are such and they should be `InFresh`.
|
||||
{
|
||||
MessageState::InSeen
|
||||
@@ -1108,7 +1111,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if mime_parser.decrypting_failed {
|
||||
if mime_parser.decrypting_failed && !fetching_existing_messages {
|
||||
if chat_id.is_none() {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
} else {
|
||||
@@ -1234,6 +1237,12 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
if fetching_existing_messages && mime_parser.decrypting_failed {
|
||||
chat_id = Some(DC_CHAT_ID_TRASH);
|
||||
// We are only gathering old messages on first start. We do not want to add loads of non-decryptable messages to the chats.
|
||||
info!(context, "Existing non-decipherable message (TRASH).");
|
||||
}
|
||||
|
||||
if mime_parser.webxdc_status_update.is_some() && mime_parser.parts.len() == 1 {
|
||||
if let Some(part) = mime_parser.parts.first() {
|
||||
if part.typ == Viewtype::Text && part.msg.is_empty() {
|
||||
@@ -1439,13 +1448,13 @@ async fn add_parts(
|
||||
None => better_msg = Some(m),
|
||||
Some(_) => {
|
||||
if !m.is_empty() {
|
||||
group_changes.extra_msgs.push((m, is_system_message, None))
|
||||
group_changes.extra_msgs.push((m, is_system_message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (group_changes_msg, cmd, added_removed_id) in group_changes.extra_msgs {
|
||||
for (group_changes_msg, cmd) in group_changes.extra_msgs {
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
chat_id,
|
||||
@@ -1455,7 +1464,6 @@ async fn add_parts(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
added_removed_id,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1496,7 +1504,7 @@ async fn add_parts(
|
||||
while let Some(part) = parts.next() {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen;
|
||||
let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages;
|
||||
set_msg_reaction(
|
||||
context,
|
||||
mime_in_reply_to,
|
||||
@@ -1528,6 +1536,7 @@ async fn add_parts(
|
||||
}
|
||||
}
|
||||
|
||||
let mut txt_raw = "".to_string();
|
||||
let (msg, typ): (&str, Viewtype) = if let Some(better_msg) = &better_msg {
|
||||
if better_msg.is_empty() && is_partial_download.is_none() {
|
||||
chat_id = DC_CHAT_ID_TRASH;
|
||||
@@ -1539,13 +1548,14 @@ async fn add_parts(
|
||||
let part_is_empty =
|
||||
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
|
||||
if let Some(contact_id) = group_changes.added_removed_id {
|
||||
param.set(Param::ContactAddedRemoved, contact_id.to_u32().to_string());
|
||||
}
|
||||
|
||||
save_mime_modified |= mime_parser.is_mime_modified && !part_is_empty && !hidden;
|
||||
let save_mime_modified = save_mime_modified && parts.peek().is_none();
|
||||
|
||||
if part.typ == Viewtype::Text {
|
||||
let msg_raw = part.msg_raw.as_ref().cloned().unwrap_or_default();
|
||||
txt_raw = format!("{subject}\n\n{msg_raw}");
|
||||
}
|
||||
|
||||
let ephemeral_timestamp = if in_fresh {
|
||||
0
|
||||
} else {
|
||||
@@ -1572,7 +1582,7 @@ INSERT INTO msgs
|
||||
rfc724_mid, chat_id,
|
||||
from_id, to_id, timestamp, timestamp_sent,
|
||||
timestamp_rcvd, type, state, msgrmsg,
|
||||
txt, txt_normalized, subject, param, hidden,
|
||||
txt, txt_normalized, subject, txt_raw, param, hidden,
|
||||
bytes, mime_headers, mime_compressed, mime_in_reply_to,
|
||||
mime_references, mime_modified, error, ephemeral_timer,
|
||||
ephemeral_timestamp, download_state, hop_info
|
||||
@@ -1581,7 +1591,7 @@ INSERT INTO msgs
|
||||
?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, 1,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?
|
||||
@@ -1591,7 +1601,7 @@ SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, state=max(state,excluded.state), msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, txt_normalized=excluded.txt_normalized, subject=excluded.subject,
|
||||
param=excluded.param,
|
||||
txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
hidden=excluded.hidden,bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
|
||||
mime_references=excluded.mime_references, mime_modified=excluded.mime_modified, error=excluded.error, ephemeral_timer=excluded.ephemeral_timer,
|
||||
@@ -1613,6 +1623,8 @@ RETURNING id
|
||||
if trash || hidden { "" } else { msg },
|
||||
if trash || hidden { None } else { message::normalize_text(msg) },
|
||||
if trash || hidden { "" } else { &subject },
|
||||
// txt_raw might contain invalid utf8
|
||||
if trash || hidden { "" } else { &txt_raw },
|
||||
if trash {
|
||||
"".to_string()
|
||||
} else {
|
||||
@@ -2327,12 +2339,10 @@ struct GroupChangesInfo {
|
||||
/// Optional: A better message that should replace the original system message.
|
||||
/// If this is an empty string, the original system message should be trashed.
|
||||
better_msg: Option<String>,
|
||||
/// Added/removed contact `better_msg` refers to.
|
||||
added_removed_id: Option<ContactId>,
|
||||
/// If true, the user should not be notified about the group change.
|
||||
silent: bool,
|
||||
/// A list of additional group changes messages that should be shown in the chat.
|
||||
extra_msgs: Vec<(String, SystemMessage, Option<ContactId>)>,
|
||||
extra_msgs: Vec<(String, SystemMessage)>,
|
||||
}
|
||||
|
||||
/// Apply group member list, name, avatar and protection status changes from the MIME message.
|
||||
@@ -2438,16 +2448,16 @@ async fn apply_group_changes(
|
||||
.filter(|grpname| grpname.len() < 200)
|
||||
{
|
||||
let grpname = &sanitize_single_line(grpname);
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
|
||||
let chat_group_name_timestamp =
|
||||
chat.param.get_i64(Param::GroupNameTimestamp).unwrap_or(0);
|
||||
let group_name_timestamp = group_name_timestamp.unwrap_or(mime_parser.timestamp_sent);
|
||||
// To provide group name consistency, compare names if timestamps are equal.
|
||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, &chat.name)
|
||||
if (chat_group_name_timestamp, grpname) < (group_name_timestamp, old_name)
|
||||
&& chat_id
|
||||
.update_timestamp(context, Param::GroupNameTimestamp, group_name_timestamp)
|
||||
.await?
|
||||
&& grpname != &chat.name
|
||||
{
|
||||
info!(context, "Updating grpname for chat {chat_id}.");
|
||||
context
|
||||
@@ -2460,7 +2470,6 @@ async fn apply_group_changes(
|
||||
.get_header(HeaderDef::ChatGroupNameChanged)
|
||||
.is_some()
|
||||
{
|
||||
let old_name = &sanitize_single_line(old_name);
|
||||
better_msg.get_or_insert(
|
||||
stock_str::msg_grp_name(context, old_name, grpname, from_id).await,
|
||||
);
|
||||
@@ -2650,11 +2659,6 @@ async fn apply_group_changes(
|
||||
}
|
||||
Ok(GroupChangesInfo {
|
||||
better_msg,
|
||||
added_removed_id: if added_id.is_some() {
|
||||
added_id
|
||||
} else {
|
||||
removed_id
|
||||
},
|
||||
silent,
|
||||
extra_msgs: group_changes_msgs,
|
||||
})
|
||||
@@ -2666,7 +2670,7 @@ async fn group_changes_msgs(
|
||||
added_ids: &HashSet<ContactId>,
|
||||
removed_ids: &HashSet<ContactId>,
|
||||
chat_id: ChatId,
|
||||
) -> Result<Vec<(String, SystemMessage, Option<ContactId>)>> {
|
||||
) -> Result<Vec<(String, SystemMessage)>> {
|
||||
let mut group_changes_msgs = Vec::new();
|
||||
if !added_ids.is_empty() {
|
||||
warn!(
|
||||
@@ -2687,7 +2691,6 @@ async fn group_changes_msgs(
|
||||
stock_str::msg_add_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
SystemMessage::MemberAddedToGroup,
|
||||
Some(contact.id),
|
||||
));
|
||||
}
|
||||
for contact_id in removed_ids {
|
||||
@@ -2696,14 +2699,13 @@ async fn group_changes_msgs(
|
||||
stock_str::msg_del_member_local(context, contact.get_addr(), ContactId::UNDEFINED)
|
||||
.await,
|
||||
SystemMessage::MemberRemovedFromGroup,
|
||||
Some(contact.id),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(group_changes_msgs)
|
||||
}
|
||||
|
||||
static LIST_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
static LIST_ID_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(.+)<(.+)>$").unwrap());
|
||||
|
||||
fn mailinglist_header_listid(list_id_header: &str) -> Result<String> {
|
||||
Ok(match LIST_ID_REGEX.captures(list_id_header) {
|
||||
@@ -2811,8 +2813,8 @@ fn compute_mailinglist_name(
|
||||
// (as that part is much more visible, we assume, that names is shorter and comes more to the point,
|
||||
// than the sometimes longer part from ListId)
|
||||
let subject = mime_parser.get_subject().unwrap_or_default();
|
||||
static SUBJECT: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
|
||||
static SUBJECT: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^.{0,5}\[(.+?)\](\s*\[.+\])?").unwrap()); // remove square brackets around first name
|
||||
if let Some(cap) = SUBJECT.captures(&subject) {
|
||||
name = cap[1].to_string() + cap.get(2).map_or("", |m| m.as_str());
|
||||
}
|
||||
@@ -2839,8 +2841,8 @@ fn compute_mailinglist_name(
|
||||
// but strip some known, long hash prefixes
|
||||
if name.is_empty() {
|
||||
// 51231231231231231231231232869f58.xing.com -> xing.com
|
||||
static PREFIX_32_CHARS_HEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
||||
static PREFIX_32_CHARS_HEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"([0-9a-fA-F]{32})\.(.{6,})").unwrap());
|
||||
if let Some(cap) = PREFIX_32_CHARS_HEX
|
||||
.captures(listid)
|
||||
.and_then(|caps| caps.get(2))
|
||||
@@ -3057,12 +3059,23 @@ async fn update_verified_keys(
|
||||
/// Checks whether the message is allowed to appear in a protected chat.
|
||||
///
|
||||
/// This means that it is encrypted and signed with a verified key.
|
||||
fn has_verified_encryption(
|
||||
///
|
||||
/// Also propagates gossiped keys to verified if needed.
|
||||
async fn has_verified_encryption(
|
||||
context: &Context,
|
||||
mimeparser: &MimeMessage,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
) -> Result<VerifiedEncryption> {
|
||||
use VerifiedEncryption::*;
|
||||
|
||||
// We do not need to check if we are verified with ourself.
|
||||
let to_ids = to_ids
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|id| *id != ContactId::SELF)
|
||||
.collect::<Vec<ContactId>>();
|
||||
|
||||
if !mimeparser.was_encrypted() {
|
||||
return Ok(NotVerified("This message is not encrypted".to_string()));
|
||||
};
|
||||
@@ -3091,24 +3104,21 @@ fn has_verified_encryption(
|
||||
}
|
||||
}
|
||||
|
||||
mark_recipients_as_verified(context, from_id, to_ids, mimeparser).await?;
|
||||
Ok(Verified)
|
||||
}
|
||||
|
||||
async fn mark_recipients_as_verified(
|
||||
context: &Context,
|
||||
from_id: ContactId,
|
||||
to_ids: &[ContactId],
|
||||
to_ids: Vec<ContactId>,
|
||||
mimeparser: &MimeMessage,
|
||||
) -> Result<()> {
|
||||
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let contact = Contact::get_by_id(context, from_id).await?;
|
||||
for &id in to_ids {
|
||||
if id == ContactId::SELF {
|
||||
continue;
|
||||
}
|
||||
|
||||
for id in to_ids {
|
||||
let Some((to_addr, is_verified)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
@@ -3206,6 +3216,7 @@ async fn get_parent_message(
|
||||
if let Some(field) = references {
|
||||
mids.append(&mut parse_message_ids(field));
|
||||
}
|
||||
info!(context, "mids: {mids:?}");
|
||||
message::get_by_rfc724_mids(context, &mids).await
|
||||
}
|
||||
|
||||
|
||||
@@ -2218,17 +2218,6 @@ async fn test_no_smtp_job_for_self_chat() -> Result<()> {
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
|
||||
bob.set_config_bool(Config::BccSelf, true).await?;
|
||||
bob.set_config(Config::DeleteServerAfter, Some("1")).await?;
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_none());
|
||||
|
||||
bob.set_config(Config::DeleteServerAfter, None).await?;
|
||||
let mut msg = Message::new_text("Happy birthday to me".to_string());
|
||||
chat::send_msg(bob, chat_id, &mut msg).await?;
|
||||
assert!(bob.pop_sent_msg_opt(Duration::ZERO).await.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3313,7 +3302,6 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
let alice1 = tcm.alice().await;
|
||||
let alice2 = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
|
||||
// =============== Bob creates a group ===============
|
||||
let group_id = chat::create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
|
||||
@@ -3322,8 +3310,8 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> {
|
||||
time(),
|
||||
group_id,
|
||||
&[
|
||||
bob.add_or_lookup_contact_id(&alice1).await,
|
||||
bob.add_or_lookup_contact_id(&charlie).await,
|
||||
bob.add_or_lookup_contact(&alice1).await.id,
|
||||
Contact::create(&bob, "", "charlie@example.org").await?,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
@@ -3450,7 +3438,8 @@ async fn test_send_as_bot() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
alice.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let alice_bob_id = Contact::create(alice, "", &bob_addr).await?;
|
||||
let bob_chat_id = tcm.send_recv_accept(alice, bob, "hi").await.chat_id;
|
||||
let alice_chat_id = ChatId::lookup_by_contact(alice, alice_bob_id)
|
||||
.await?
|
||||
@@ -3466,6 +3455,46 @@ async fn test_send_as_bot() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_bot_recv_existing_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let bob = &tcm.bob().await;
|
||||
bob.set_config(Config::Bot, Some("1")).await.unwrap();
|
||||
bob.set_config(Config::FetchExistingMsgs, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
let fetching_existing_messages = true;
|
||||
let msg = receive_imf_from_inbox(
|
||||
bob,
|
||||
"first@example.org",
|
||||
b"From: Alice <alice@example.org>\n\
|
||||
To: Bob <bob@example.net>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <first@example.org>\n\
|
||||
Date: Sun, 14 Nov 2021 00:10:00 +0000\n\
|
||||
Content-Type: text/plain\n\
|
||||
\n\
|
||||
hello\n",
|
||||
false,
|
||||
None,
|
||||
fetching_existing_messages,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
let msg = Message::load_from_db(bob, msg.msg_ids[0]).await?;
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
let event = bob
|
||||
.evtracker
|
||||
.get_matching(|ev| matches!(ev, EventType::IncomingMsg { .. }))
|
||||
.await;
|
||||
let EventType::IncomingMsg { chat_id, msg_id } = event else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(chat_id, msg.chat_id);
|
||||
assert_eq!(msg_id, msg.id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_wrong_date_in_imf_section() {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4004,7 +4033,7 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let bob_addr = bob.get_config(Config::Addr).await?.unwrap();
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let bob_id = Contact::create(alice, "Bob", &bob_addr).await?;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
@@ -4026,39 +4055,38 @@ async fn test_unsigned_chat_group_hdr() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_sync_member_list_on_rejoin() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice = tcm.alice().await;
|
||||
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let claire_id = Contact::create(&alice, "", "claire@example.de").await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
|
||||
|
||||
// remove bob from chat
|
||||
remove_contact_from_chat(alice, alice_chat_id, bob_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let remove_bob = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&remove_bob).await;
|
||||
|
||||
// remove any other member
|
||||
remove_contact_from_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, claire_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// re-add bob
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
let add2 = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&add2).await;
|
||||
|
||||
// number of members in chat should have updated
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4070,7 +4098,8 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_bob_id =
|
||||
Contact::create(alice, "", &bob.get_config(Config::Addr).await?.unwrap()).await?;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
|
||||
// Alice creates a group chat. Bob accepts it.
|
||||
@@ -4113,101 +4142,107 @@ async fn test_ignore_outdated_membership_changes() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let charlie = &tcm.charlie().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(bob).await,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// alice adds a member
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(fiona).await,
|
||||
Contact::create(&alice, "fiona", "fiona@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Bob adds a member.
|
||||
let bob_charlie = bob.add_or_lookup_contact_id(charlie).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_charlie).await?;
|
||||
// bob adds a member.
|
||||
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bob didn't receive the addition of Fiona, but Alice mustn't remove Fiona from the members
|
||||
// list back. Instead, Bob must add Fiona from the next Alice's message to make their group
|
||||
// members view consistent.
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 4);
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
|
||||
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_charlie).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(alice, alice_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
send_text_msg(alice, alice_chat_id, "Finally add Fiona please".to_string()).await?;
|
||||
send_text_msg(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
"Finally add Fiona please".to_string(),
|
||||
)
|
||||
.await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delayed_removal_is_ignored() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice_bob = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_fiona = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice_fiona = Contact::create(&alice, "fiona", "fiona@example.net").await?;
|
||||
// create chat with three members
|
||||
add_to_chat_contacts_table(alice, time(), chat_id, &[alice_bob, alice_fiona]).await?;
|
||||
add_to_chat_contacts_table(
|
||||
&alice,
|
||||
time(),
|
||||
chat_id,
|
||||
&[
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
alice_fiona,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_text_msg(alice, chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Bob removes Fiona.
|
||||
let bob_contact_fiona = bob.add_or_lookup_contact_id(fiona).await;
|
||||
remove_contact_from_chat(bob, bob_chat_id, bob_contact_fiona).await?;
|
||||
let bob_contact_fiona = Contact::create(&bob, "fiona", "fiona@example.net").await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_contact_fiona).await?;
|
||||
let remove_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Bob adds new members Dom and Elena, but first addition message is lost.
|
||||
let dom = &tcm.dom().await;
|
||||
let elena = &tcm.elena().await;
|
||||
let bob_dom = bob.add_or_lookup_contact_id(dom).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_dom).await?;
|
||||
// Bob adds new members "blue" and "orange", but first addition message is lost.
|
||||
let bob_blue = Contact::create(&bob, "blue", "blue@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
let bob_elena = bob.add_or_lookup_contact_id(elena).await;
|
||||
add_contact_to_chat(bob, bob_chat_id, bob_elena).await?;
|
||||
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
|
||||
let add_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice only receives the second member addition,
|
||||
// but this results in addition of both members
|
||||
// and removal of Fiona.
|
||||
alice.recv_msg(&add_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 4);
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 4);
|
||||
|
||||
// Alice re-adds Fiona.
|
||||
add_contact_to_chat(alice, chat_id, alice_fiona).await?;
|
||||
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5);
|
||||
add_contact_to_chat(&alice, chat_id, alice_fiona).await?;
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
|
||||
|
||||
// Delayed removal of Fiona by Bob shouldn't remove her.
|
||||
alice.recv_msg(&remove_msg).await;
|
||||
assert_eq!(get_chat_contacts(alice, chat_id).await?.len(), 5);
|
||||
assert_eq!(get_chat_contacts(&alice, chat_id).await?.len(), 5);
|
||||
|
||||
alice
|
||||
.golden_test_chat(chat_id, "receive_imf_delayed_removal_is_ignored")
|
||||
@@ -4219,42 +4254,41 @@ async fn test_delayed_removal_is_ignored() -> Result<()> {
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_dont_readd_with_normal_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(bob).await,
|
||||
Contact::create(&alice, "bob", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
bob_chat_id.accept(bob).await?;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
|
||||
// Bob leaves, but Alice didn't receive Bob's leave message.
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 1);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 1);
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(fiona).await,
|
||||
Contact::create(&alice, "fiora", "fiora@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
|
||||
// Bob received a message from Alice, but this should not re-add him to the group.
|
||||
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
// Bob got an update that Fiona is added nevertheless.
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
// Bob got an update that fiora is added nevertheless.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4458,17 +4492,16 @@ async fn test_mua_can_readd() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_member_left_does_not_create_chat() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(bob).await,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
// Bob only received a message of Alice leaving the group.
|
||||
@@ -4478,7 +4511,7 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
|
||||
// especially the chats that were created due to "split group" bugs
|
||||
// which some members simply deleted and some members left,
|
||||
// recreating the chat for others.
|
||||
remove_contact_from_chat(alice, alice_chat_id, ContactId::SELF).await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, ContactId::SELF).await?;
|
||||
bob.recv_msg_trash(&alice.pop_sent_msg().await).await;
|
||||
|
||||
Ok(())
|
||||
@@ -4486,45 +4519,44 @@ async fn test_member_left_does_not_create_chat() -> Result<()> {
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_recreate_member_list_on_missing_add_of_self() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(
|
||||
alice,
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(bob).await,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
send_text_msg(alice, alice_chat_id, "second message".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "second message".to_string()).await?;
|
||||
|
||||
let bob_chat_id = bob.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
assert!(!bob_chat_id.is_special());
|
||||
|
||||
// Bob missed the message adding them, but must recreate the member list.
|
||||
assert_eq!(get_chat_contacts(bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
// But if Bob just left, they mustn't recreate the member list even after missing a message.
|
||||
bob_chat_id.accept(bob).await?;
|
||||
remove_contact_from_chat(bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob_chat_id.accept(&bob).await?;
|
||||
remove_contact_from_chat(&bob, bob_chat_id, ContactId::SELF).await?;
|
||||
bob.pop_sent_msg().await;
|
||||
send_text_msg(alice, alice_chat_id, "3rd message".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "3rd message".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
send_text_msg(alice, alice_chat_id, "4th message".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "4th message".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
// Even if some time passed, Bob must not be re-added back.
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
send_text_msg(alice, alice_chat_id, "5th message".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "5th message".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
send_text_msg(alice, alice_chat_id, "6th message".to_string()).await?;
|
||||
send_text_msg(&alice, alice_chat_id, "6th message".to_string()).await?;
|
||||
bob.recv_msg(&alice.pop_sent_msg().await).await;
|
||||
assert!(!is_contact_in_chat(bob, bob_chat_id, ContactId::SELF).await?);
|
||||
assert!(!is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4537,7 +4569,7 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(&bob).await,
|
||||
Contact::create(&alice, "bob", &bob.get_config(Config::Addr).await?.unwrap()).await?,
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
@@ -4547,7 +4579,12 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(&fiona).await,
|
||||
Contact::create(
|
||||
&alice,
|
||||
"fiona",
|
||||
&fiona.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
let fiona_chat_id = fiona.recv_msg(&alice.pop_sent_msg().await).await.chat_id;
|
||||
@@ -4559,7 +4596,12 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
// Bob missed the message adding fiona, but mustn't recreate the member list.
|
||||
assert_eq!(get_chat_contacts(&bob, bob_chat_id).await?.len(), 2);
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, ContactId::SELF).await?);
|
||||
let bob_alice_contact = bob.add_or_lookup_contact_id(&alice).await;
|
||||
let bob_alice_contact = Contact::create(
|
||||
&bob,
|
||||
"alice",
|
||||
&alice.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -4745,14 +4787,13 @@ async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
async fn test_partial_group_consistency() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let fiona = tcm.fiona().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
|
||||
let alice_chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(&alice, alice_chat_id, bob_id).await?;
|
||||
|
||||
send_text_msg(&alice, alice_chat_id, "populate".to_string()).await?;
|
||||
let add = alice.pop_sent_msg().await;
|
||||
let bob = tcm.bob().await;
|
||||
bob.recv_msg(&add).await;
|
||||
let bob_chat_id = bob.get_last_msg().await.chat_id;
|
||||
let contacts = get_chat_contacts(&bob, bob_chat_id).await?;
|
||||
@@ -4772,6 +4813,7 @@ Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
Some(100000),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
@@ -4786,7 +4828,7 @@ Chat-Group-Member-Added: charlie@example.com",
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_chat_id,
|
||||
alice.add_or_lookup_contact_id(&fiona).await,
|
||||
Contact::create(&alice, "fiona", "fiona@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4809,6 +4851,7 @@ Content-Type: text/plain
|
||||
Chat-Group-Member-Added: charlie@example.com",
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.context("no received message")?;
|
||||
@@ -4870,10 +4913,11 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
let fiona_addr = fiona.get_config(Config::Addr).await?.unwrap();
|
||||
mark_as_verified(alice, fiona).await;
|
||||
let alice_fiona_id = alice.add_or_lookup_contact(fiona).await.id;
|
||||
add_contact_to_chat(alice, group_id, alice_fiona_id).await?;
|
||||
|
||||
// The message is not sent to Bob,
|
||||
// but member is added to the chat locally anyway.
|
||||
assert!(add_contact_to_chat(alice, group_id, alice_fiona_id)
|
||||
.await
|
||||
.is_err());
|
||||
// Sending the message failed,
|
||||
// but member is added to the chat locally already.
|
||||
assert!(is_contact_in_chat(alice, group_id, alice_fiona_id).await?);
|
||||
let msg = alice.get_last_msg_in(group_id).await;
|
||||
assert!(msg.is_info());
|
||||
@@ -4882,6 +4926,10 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
stock_str::msg_add_member_local(alice, &fiona_addr, ContactId::SELF).await
|
||||
);
|
||||
|
||||
// Now the chat has a message "You added member fiona@example.net. [INFO] !!" (with error) that
|
||||
// may be confusing, but if the error is displayed in UIs, it's more or less ok. This is not a
|
||||
// normal scenario anyway.
|
||||
|
||||
remove_contact_from_chat(alice, group_id, alice_bob_id).await?;
|
||||
assert!(!is_contact_in_chat(alice, group_id, alice_bob_id).await?);
|
||||
let msg = alice.get_last_msg_in(group_id).await;
|
||||
@@ -4890,6 +4938,7 @@ async fn test_protected_group_add_remove_member_missing_key() -> Result<()> {
|
||||
msg.get_text(),
|
||||
stock_str::msg_del_member_local(alice, &bob_addr, ContactId::SELF,).await
|
||||
);
|
||||
assert!(msg.error().is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4986,9 +5035,8 @@ async fn test_unarchive_on_member_removal() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
let fiona = &tcm.fiona().await;
|
||||
let bob_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let fiona_id = alice.add_or_lookup_contact_id(fiona).await;
|
||||
let bob_id = Contact::create(alice, "", "bob@example.net").await?;
|
||||
let fiona_id = Contact::create(alice, "", "fiona@example.net").await?;
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "foos").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, bob_id).await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, fiona_id).await?;
|
||||
@@ -5091,11 +5139,11 @@ async fn test_references() -> Result<()> {
|
||||
alice.set_config_bool(Config::BccSelf, true).await?;
|
||||
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
let _sent = alice
|
||||
alice
|
||||
.send_text(alice_chat_id, "Hi! I created a group.")
|
||||
.await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(bob).await;
|
||||
let alice_bob_contact_id = Contact::create(alice, "Bob", "bob@example.net").await?;
|
||||
add_contact_to_chat(alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let bob_received_msg = bob.recv_msg(&sent).await;
|
||||
@@ -5357,16 +5405,14 @@ Hello!"
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_rename_chat_on_missing_message() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let charlie = tcm.charlie().await;
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_to_chat_contacts_table(
|
||||
&alice,
|
||||
time(),
|
||||
chat_id,
|
||||
&[alice.add_or_lookup_contact_id(&bob).await],
|
||||
&[Contact::create(&alice, "bob", "bob@example.net").await?],
|
||||
)
|
||||
.await?;
|
||||
send_text_msg(&alice, chat_id, "populate".to_string()).await?;
|
||||
@@ -5380,8 +5426,8 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
|
||||
bob.pop_sent_msg().await;
|
||||
|
||||
// Bob adds a new member.
|
||||
let bob_charlie = bob.add_or_lookup_contact_id(&charlie).await;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_charlie).await?;
|
||||
let bob_orange = Contact::create(&bob, "orange", "orange@example.net").await?;
|
||||
add_contact_to_chat(&bob, bob_chat_id, bob_orange).await?;
|
||||
let add_msg = bob.pop_sent_msg().await;
|
||||
|
||||
// Alice only receives the member addition.
|
||||
@@ -5391,29 +5437,6 @@ async fn test_rename_chat_on_missing_message() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_rename_chat_after_creating_invite() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
for populate_before_securejoin in [false, true] {
|
||||
let alice_chat_id = create_group_chat(alice, ProtectionStatus::Protected, "Group").await?;
|
||||
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(60));
|
||||
chat::set_chat_name(alice, alice_chat_id, "Renamed").await?;
|
||||
if populate_before_securejoin {
|
||||
send_text_msg(alice, alice_chat_id, "populate".to_string()).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
}
|
||||
|
||||
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
|
||||
let bob_chat = Chat::load_from_db(bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.get_name(), "Renamed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that creating a group
|
||||
/// is preferred over assigning message to existing
|
||||
/// chat based on `In-Reply-To` and `References`.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
//! DC release info.
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use std::sync::LazyLock;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
const DATE_STR: &str = include_str!("../release-date.in");
|
||||
|
||||
/// Last release date.
|
||||
pub static DATE: LazyLock<NaiveDate> =
|
||||
LazyLock::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap());
|
||||
pub static DATE: Lazy<NaiveDate> =
|
||||
Lazy::new(|| NaiveDate::parse_from_str(DATE_STR, "%Y-%m-%d").unwrap());
|
||||
|
||||
@@ -334,6 +334,12 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
|
||||
inviter_progress(context, contact_id, 300);
|
||||
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
if !join_vg {
|
||||
ChatId::create_for_contact(context, contact_id).await?;
|
||||
}
|
||||
|
||||
// Alice -> Bob
|
||||
send_alice_handshake_msg(
|
||||
context,
|
||||
@@ -429,11 +435,6 @@ pub(crate) async fn handle_securejoin_handshake(
|
||||
}
|
||||
contact_id.regossip_keys(context).await?;
|
||||
ContactId::scaleup_origin(context, &[contact_id], Origin::SecurejoinInvited).await?;
|
||||
// for setup-contact, make Alice's one-to-one chat with Bob visible
|
||||
// (secure-join-information are shown in the group chat)
|
||||
if !join_vg {
|
||||
ChatId::create_for_contact(context, contact_id).await?;
|
||||
}
|
||||
info!(context, "Auth verified.",);
|
||||
context.emit_event(EventType::ContactsChanged(Some(contact_id)));
|
||||
inviter_progress(context, contact_id, 600);
|
||||
|
||||
@@ -126,7 +126,6 @@ pub(super) async fn start_protocol(context: &Context, invite: QrInvite) -> Resul
|
||||
Some(ts_start),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
chat_id.spawn_securejoin_wait(context, constants::SECUREJOIN_WAIT_TIMEOUT);
|
||||
|
||||
@@ -129,7 +129,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
1
|
||||
);
|
||||
|
||||
let sent = alice.pop_sent_msg().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(), false);
|
||||
assert_eq!(bob_chat.can_send(&bob).await.unwrap(), true);
|
||||
}
|
||||
|
||||
// Step 4: Bob receives vc-auth-required, sends vc-request-with-auth
|
||||
@@ -208,7 +208,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.gossiped_keys
|
||||
.insert(alice_addr.to_string(), wrong_pubkey)
|
||||
.unwrap();
|
||||
let contact_bob = alice.add_or_lookup_email_contact(&bob).await;
|
||||
let contact_bob = alice.add_or_lookup_contact(&bob).await;
|
||||
let handshake_msg = handle_securejoin_handshake(&alice, &mut msg, contact_bob.id)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -318,7 +318,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
.check_securejoin_wait(&bob, constants::SECUREJOIN_WAIT_TIMEOUT)
|
||||
.await
|
||||
.unwrap(),
|
||||
(true, 0)
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(
|
||||
msg.get_text(),
|
||||
stock_str::securejoin_takes_longer(&bob).await
|
||||
stock_str::securejoin_wait_timeout(&bob).await
|
||||
);
|
||||
}
|
||||
let msg = get_chat_msg(&bob, bob_chat.get_id(), i.next().unwrap(), msg_cnt).await;
|
||||
@@ -715,7 +715,7 @@ async fn test_unknown_sender() -> Result<()> {
|
||||
|
||||
let sent = bob.send_text(bob_chat_id, "Hi hi!").await;
|
||||
|
||||
let alice_bob_contact_id = alice.add_or_lookup_contact_id(&bob).await;
|
||||
let alice_bob_contact_id = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
remove_contact_from_chat(&alice, alice_chat_id, alice_bob_contact_id).await?;
|
||||
alice.pop_sent_msg().await;
|
||||
|
||||
|
||||
@@ -90,14 +90,13 @@ impl Smtp {
|
||||
let lp = ConfiguredLoginParam::load(context)
|
||||
.await?
|
||||
.context("Not configured")?;
|
||||
let proxy_config = ProxyConfig::load(context).await?;
|
||||
self.connect(
|
||||
context,
|
||||
&lp.smtp,
|
||||
&lp.smtp_password,
|
||||
&proxy_config,
|
||||
&lp.proxy_config,
|
||||
&lp.addr,
|
||||
lp.strict_tls(proxy_config.is_some()),
|
||||
lp.strict_tls(),
|
||||
lp.oauth2,
|
||||
)
|
||||
.await
|
||||
@@ -439,7 +438,6 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user